diff --git a/cmd/mithril/node/node.go b/cmd/mithril/node/node.go index 8d44dc22..9c8ef30a 100644 --- a/cmd/mithril/node/node.go +++ b/cmd/mithril/node/node.go @@ -2743,11 +2743,19 @@ postBootstrap: ParentChainedMerkleRoot: tip.AlpenglowChainedMerkleRoot, HasParentChainedMerkleRoot: tip.HasAlpenglowChainedMerkleRoot, ParentLastEntryHash: tip.LastEntryHash, + ParentLastBlockhash: tip.LastBlockhash, + ParentBlockHeight: tip.BlockHeight, + LatestEvictedBlockhash: tip.LatestEvictedBlockhash, EpochRewardsActive: tip.EpochRewardsActive, PrevNumSigs: tip.PrevNumSigs, PrevFeeGovernor: tip.PrevFeeGovernor, AcctsLtHash: tip.AcctsLtHash, Features: tip.Features, + BankSysvars: tip.BankSysvars, + EpochStakes: tip.EpochStakes, + TotalEpochStake: tip.TotalEpochStake, + NanosecondClockAccount: tip.NanosecondClockAccount, + HasNanosecondClockAccount: tip.HasNanosecondClockAccount, UnrootedRead: tip.UnrootedRead, TransactionStatuses: tip.TransactionStatuses, } diff --git a/pkg/blockprod/commit_block.go b/pkg/blockprod/commit_block.go index 9c2c8dc4..7333a3ed 100644 --- a/pkg/blockprod/commit_block.go +++ b/pkg/blockprod/commit_block.go @@ -3,21 +3,21 @@ package blockprod import ( b "github.com/Overclock-Validator/mithril/pkg/block" "github.com/Overclock-Validator/mithril/pkg/fees" - "github.com/Overclock-Validator/mithril/pkg/global" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/gagliardetto/solana-go" ) // LeaderBlockInput captures forged leader state for AccountsDB commit. type LeaderBlockInput struct { - Bank *WorkingBank - EpochSchedule *sealevel.SysvarEpochSchedule - ParentSlot uint64 - ParentBankhash solana.Hash - PrevNumSigs uint64 - PrevFeeGovernor *sealevel.FeeRateGovernor - EntryBlockhash solana.Hash - TxFeeAccumulator fees.TxFeeInfoAccumulator + Bank *WorkingBank + ParentSlot uint64 + ParentBankhash solana.Hash + ParentLastBlockhash solana.Hash + ParentBlockHeight uint64 + PrevNumSigs uint64 + PrevFeeGovernor *sealevel.FeeRateGovernor + EntryBlockhash solana.Hash + TxFeeAccumulator fees.TxFeeInfoAccumulator } func BuildLeaderBlock(in LeaderBlockInput) *b.Block { @@ -28,7 +28,7 @@ func BuildLeaderBlock(in LeaderBlockInput) *b.Block { ParentSlot: in.ParentSlot, Leader: bank.Leader(), Transactions: bank.ForgedTransactions(), - Epoch: in.EpochSchedule.GetEpoch(slot), + Epoch: bank.SlotCtx().Epoch, Features: bank.SlotCtx().Features, // Agave resets Bank.signature_count for every child bank. The parent // count is used only to derive this slot's fee governor; it is not part @@ -36,14 +36,14 @@ func BuildLeaderBlock(in LeaderBlockInput) *b.Block { NumSignatures: bank.NumSignatures(), PrevNumSignatures: in.PrevNumSigs, PrevFeeRateGovernor: in.PrevFeeGovernor, - LastBlockhash: global.LatestBlockHash(), + LastBlockhash: in.ParentLastBlockhash, Blockhash: in.EntryBlockhash, + BlockHeight: in.ParentBlockHeight + 1, } copy(block.ParentBankhash[:], in.ParentBankhash[:]) block.FeeRateGovernor = sealevel.NewFeeRateGovernorDerived(block.PrevFeeRateGovernor, block.PrevNumSignatures) if block.FeeRateGovernor.PrevLamportsPerSignature == 0 { block.FeeRateGovernor.PrevLamportsPerSignature = 5000 } - block.BlockHeight = global.BlockHeight() + 1 return block } diff --git a/pkg/blockprod/commit_block_test.go b/pkg/blockprod/commit_block_test.go index d9668c53..2aa031ad 100644 --- a/pkg/blockprod/commit_block_test.go +++ b/pkg/blockprod/commit_block_test.go @@ -19,15 +19,18 @@ func TestBuildLeaderBlockSignatureCountIsBankLocal(t *testing.T) { } block := BuildLeaderBlock(LeaderBlockInput{ - Bank: env.Bank, - EpochSchedule: &sealevel.SysvarEpochSchedule{SlotsPerEpoch: 54_000}, - ParentSlot: 41, - ParentBankhash: solana.Hash{1}, - PrevNumSigs: 1_750, - PrevFeeGovernor: &sealevel.FeeRateGovernor{LamportsPerSignature: 5_000}, - EntryBlockhash: solana.Hash{2}, + Bank: env.Bank, + ParentSlot: 41, + ParentBankhash: solana.Hash{1}, + ParentLastBlockhash: solana.Hash{3}, + ParentBlockHeight: 39, + PrevNumSigs: 1_750, + PrevFeeGovernor: &sealevel.FeeRateGovernor{LamportsPerSignature: 5_000}, + EntryBlockhash: solana.Hash{2}, }) require.Equal(t, uint64(1_750), block.PrevNumSignatures) require.Equal(t, uint64(5), block.NumSignatures) + require.Equal(t, solana.Hash{3}, solana.Hash(block.LastBlockhash)) + require.Equal(t, uint64(40), block.BlockHeight) } diff --git a/pkg/blockprod/leader.go b/pkg/blockprod/leader.go index c611850b..086afd61 100644 --- a/pkg/blockprod/leader.go +++ b/pkg/blockprod/leader.go @@ -999,21 +999,26 @@ func (l *LeaderLoop) finishActiveSlotLocked() { footerRewards = l.rewardCerts.BuildForLeaderSlot(slot) } - if l.accountsDb == nil || l.epochSchedule == nil || l.activeSess == nil { - l.failProductionWindowLocked(slot, leaderReasonFinalizationUnavailable, "accounts DB, epoch schedule, or broadcast session is unavailable") + if l.accountsDb == nil || l.activeSess == nil { + l.failProductionWindowLocked(slot, leaderReasonFinalizationUnavailable, "accounts DB or broadcast session is unavailable") l.abortActiveSlotLocked() return } + parentLastBlockhash := l.parentCtx.ParentLastBlockhash + if parentLastBlockhash == (solana.Hash{}) { + parentLastBlockhash = l.parentCtx.ParentLastEntryHash + } producedBlock := BuildLeaderBlock(LeaderBlockInput{ - Bank: l.activeBank, - EpochSchedule: l.epochSchedule, - ParentSlot: l.parentCtx.ParentSlot, - ParentBankhash: l.parentCtx.ParentBankhash, - PrevNumSigs: l.parentCtx.PrevNumSigs, - PrevFeeGovernor: l.parentCtx.PrevFeeGovernor, - EntryBlockhash: tickHash, - TxFeeAccumulator: l.activeBank.TxFeeAccumulator(), + Bank: l.activeBank, + ParentSlot: l.parentCtx.ParentSlot, + ParentBankhash: l.parentCtx.ParentBankhash, + ParentLastBlockhash: parentLastBlockhash, + ParentBlockHeight: l.parentCtx.ParentBlockHeight, + PrevNumSigs: l.parentCtx.PrevNumSigs, + PrevFeeGovernor: l.parentCtx.PrevFeeGovernor, + EntryBlockhash: tickHash, + TxFeeAccumulator: l.activeBank.TxFeeAccumulator(), }) producedBlock.SkipRewardCert = append([]byte(nil), footerRewards.Skip...) producedBlock.NotarRewardCert = append([]byte(nil), footerRewards.Notar...) @@ -1033,7 +1038,6 @@ func (l *LeaderLoop) finishActiveSlotLocked() { AcctsDb: l.accountsDb, SlotCtx: l.activeBank.SlotCtx(), Block: producedBlock, - EpochSchedule: l.epochSchedule, TxFeeAccumulator: l.activeBank.TxFeeAccumulator(), AlpenglowClock: l.alpenglowClock, AlpenglowShredVersion: l.shredVersion, @@ -1139,6 +1143,14 @@ func (l *LeaderLoop) startSlotLocked(slot uint64) error { if l.parentContext != nil { parentCtx = l.parentContext(slot) } + epochSchedule := l.epochSchedule + if parentCtx.BankSysvars != nil { + if bankEpochSchedule, ok := parentCtx.BankSysvars.EpochSchedule(); ok { + epochSchedule = &bankEpochSchedule + } else { + return fmt.Errorf("%w: EpochSchedule missing for replay parent slot %d", errParentNotReady, parentCtx.ParentSlot) + } + } parentSlot := parentCtx.ParentSlot if slot == 0 { parentSlot = 0 @@ -1152,7 +1164,7 @@ func (l *LeaderLoop) startSlotLocked(slot uint64) error { if slot > 0 && parentCtx.ParentBankhash == (solana.Hash{}) { return fmt.Errorf("%w: parent bankhash missing for slot %d", errParentNotReady, parentSlot) } - if slot > 0 && l.epochSchedule != nil && l.epochSchedule.GetEpoch(parentSlot) != l.epochSchedule.GetEpoch(slot) { + if slot > 0 && epochSchedule != nil && epochSchedule.GetEpoch(parentSlot) != epochSchedule.GetEpoch(slot) { return fmt.Errorf("%w: parent block is slot %d", errEpochTransitionProductionUnsupported, parentSlot) } if parentCtx.EpochRewardsActive { @@ -1164,6 +1176,14 @@ func (l *LeaderLoop) startSlotLocked(slot uint64) error { if slot > 0 && parentCtx.PrevFeeGovernor == nil { return fmt.Errorf("%w: parent fee rate governor missing for replay parent slot %d", errParentNotReady, parentSlot) } + if slot > 0 && l.accountsDb != nil && parentCtx.BankSysvars == nil { + return fmt.Errorf("%w: bank sysvar snapshot missing for replay parent slot %d", errParentNotReady, parentSlot) + } + if slot > 0 && l.accountsDb != nil { + if err := parentCtx.BankSysvars.ValidateForExecution(); err != nil { + return fmt.Errorf("%w: invalid bank sysvar snapshot for replay parent slot %d: %v", errParentNotReady, parentSlot, err) + } + } if slot > 0 && parentCtx.ReplayGeneration == 0 { return fmt.Errorf("%w: replay generation missing for parent slot %d", errParentNotReady, parentSlot) } @@ -1191,22 +1211,25 @@ func (l *LeaderLoop) startSlotLocked(slot uint64) error { Broadcaster: l.broadcaster, UserAgent: l.userAgent, }) - slotCtx, err := NewLeaderSlotCtx(slot, parentSlot, l.accountsDb, parentCtx, l.epochSchedule) + slotCtx, err := NewLeaderSlotCtx(slot, parentSlot, l.accountsDb, parentCtx, epochSchedule) if err != nil { return fmt.Errorf("new leader slot ctx: %w", err) } - if l.accountsDb != nil && l.epochSchedule != nil { + if l.accountsDb != nil && epochSchedule != nil { prepBlock := &b.Block{ Slot: slot, ParentSlot: parentSlot, - Epoch: l.epochSchedule.GetEpoch(slot), + Epoch: epochSchedule.GetEpoch(slot), ParentBankhash: parentCtx.ParentBankhash, } - if err := replay.PrepareLeaderSlotSysvars(slotCtx, prepBlock, l.epochSchedule, l.alpenglowClock); err != nil { + if err := replay.PrepareLeaderSlotSysvars(slotCtx, prepBlock, l.alpenglowClock); err != nil { return fmt.Errorf("prepare leader sysvars: %w", err) } } - startEntryHash := parentCtx.ParentLastEntryHash + startEntryHash := parentCtx.ParentLastBlockhash + if startEntryHash == (solana.Hash{}) { + startEntryHash = parentCtx.ParentLastEntryHash + } if startEntryHash == (solana.Hash{}) { startEntryHash = parentCtx.ParentBankhash } diff --git a/pkg/blockprod/slot_ctx.go b/pkg/blockprod/slot_ctx.go index 5f02529e..f0f3f4cb 100644 --- a/pkg/blockprod/slot_ctx.go +++ b/pkg/blockprod/slot_ctx.go @@ -1,12 +1,12 @@ package blockprod import ( + "fmt" "sync" "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/accountsdb" "github.com/Overclock-Validator/mithril/pkg/features" - "github.com/Overclock-Validator/mithril/pkg/global" "github.com/Overclock-Validator/mithril/pkg/lthash" "github.com/Overclock-Validator/mithril/pkg/replay" "github.com/Overclock-Validator/mithril/pkg/sealevel" @@ -25,11 +25,19 @@ type ParentContext struct { ParentChainedMerkleRoot solana.Hash HasParentChainedMerkleRoot bool ParentLastEntryHash solana.Hash + ParentLastBlockhash solana.Hash + ParentBlockHeight uint64 + LatestEvictedBlockhash [32]byte EpochRewardsActive bool PrevNumSigs uint64 // signatures processed in the parent bank; fee-governor input only PrevFeeGovernor *sealevel.FeeRateGovernor AcctsLtHash *lthash.LtHash Features *features.Features + BankSysvars *sealevel.BankSysvars + EpochStakes map[solana.PublicKey]uint64 // immutable; shared by banks in one epoch + TotalEpochStake uint64 + NanosecondClockAccount *accounts.Account + HasNanosecondClockAccount bool UnrootedRead sealevel.AccountReader TransactionStatuses *replay.TransactionStatusView } @@ -38,12 +46,22 @@ type ParentContext struct { func NewLeaderSlotCtx(slot, parentSlot uint64, acctsDb *accountsdb.AccountsDb, parent ParentContext, epochSchedule *sealevel.SysvarEpochSchedule) (*sealevel.SlotCtx, error) { feats := leaderFeatures(parent.Features) + if parent.BankSysvars != nil { + if bankEpochSchedule, ok := parent.BankSysvars.EpochSchedule(); ok { + epochSchedule = &bankEpochSchedule + } else { + return nil, fmt.Errorf("parent bank snapshot has no EpochSchedule sysvar") + } + } var epoch uint64 if epochSchedule != nil { epoch = epochSchedule.GetEpoch(slot) } - lastBlockhash := global.LatestBlockHash() + lastBlockhash := parent.ParentLastBlockhash + if lastBlockhash == (solana.Hash{}) { + lastBlockhash = parent.ParentLastEntryHash + } prevFee := parent.PrevFeeGovernor if prevFee == nil { prevFee = &sealevel.FeeRateGovernor{PrevLamportsPerSignature: 5000, LamportsPerSignature: 5000} @@ -54,19 +72,27 @@ func NewLeaderSlotCtx(slot, parentSlot uint64, acctsDb *accountsdb.AccountsDb, p } slotCtx := &sealevel.SlotCtx{ - Slot: slot, - ParentSlot: parentSlot, - Epoch: epoch, - Accounts: accounts.NewMemAccounts(), - ParentAccts: accounts.NewMemAccounts(), - AccountsDb: acctsDb, - UnrootedRead: parent.UnrootedRead, - Features: feats, - FeeRateGovernor: feeGovernor, - LastBlockhash: lastBlockhash, - AcctMapsMu: &sync.Mutex{}, - ModifiedAccts: make(map[solana.PublicKey]bool), - WritableAccts: make(map[solana.PublicKey]bool), + Slot: slot, + ParentSlot: parentSlot, + Epoch: epoch, + Accounts: accounts.NewMemAccounts(), + ParentAccts: accounts.NewMemAccounts(), + AccountsDb: acctsDb, + UnrootedRead: parent.UnrootedRead, + Features: feats, + FeeRateGovernor: feeGovernor, + LastBlockhash: lastBlockhash, + LatestEvictedBlockhash: parent.LatestEvictedBlockhash, + AcctMapsMu: &sync.Mutex{}, + ModifiedAccts: make(map[solana.PublicKey]bool), + WritableAccts: make(map[solana.PublicKey]bool), + VoteTimestampMu: &sync.Mutex{}, + VoteTimestamps: make(map[solana.PublicKey]sealevel.BlockTimestamp), + // EpochStakes is immutable for the epoch and is replaced, rather than + // mutated, at an epoch transition. Share it so leader creation does not + // copy the validator stake map every slot. + VoteAccts: parent.EpochStakes, + TotalEpochStake: parent.TotalEpochStake, // Signature count is bank-local. Parent.PrevNumSigs feeds the fee // governor above, while the new child bank starts at zero. NumSignatures: 0, @@ -74,11 +100,80 @@ func NewLeaderSlotCtx(slot, parentSlot uint64, acctsDb *accountsdb.AccountsDb, p if parent.AcctsLtHash != nil { slotCtx.AcctsLtHash = parent.AcctsLtHash.Clone() } + if slotCtx.VoteAccts == nil { + slotCtx.VoteAccts = make(map[solana.PublicKey]uint64) + } + + if parent.BankSysvars != nil { + childSysvars, err := parent.BankSysvars.Derive(slot) + if err != nil { + return nil, fmt.Errorf("derive bank sysvars for leader slot %d: %w", slot, err) + } + if err := parent.BankSysvars.RangeAccountViews(func(pubkey solana.PublicKey, acct *accounts.Account) error { + if acct == nil { + return nil + } + parentAcct := acct.Clone() + parentAcct.Key = pubkey + if err := slotCtx.ParentAccts.SetAccountWithoutLock(pubkey, parentAcct); err != nil { + return err + } + currentAcct := acct.Clone() + currentAcct.Key = pubkey + return slotCtx.SetAccount(pubkey, currentAcct) + }); err != nil { + return nil, fmt.Errorf("install parent bank sysvars for leader slot %d: %w", slot, err) + } + if err := sealevel.RangeBankSysvarAddresses(func(pubkey solana.PublicKey) error { + if _, present := parent.BankSysvars.AccountView(pubkey); present { + return nil + } + // Preserve known absence and prevent an ordinary account load from + // falling through to a newer replay generation. + tombstone := &accounts.Account{Key: pubkey} + if err := slotCtx.ParentAccts.SetAccountWithoutLock(pubkey, tombstone.Clone()); err != nil { + return fmt.Errorf("pin absent parent sysvar %s: %w", pubkey, err) + } + if err := slotCtx.SetAccount(pubkey, tombstone); err != nil { + return fmt.Errorf("install absent current sysvar %s: %w", pubkey, err) + } + return nil + }); err != nil { + return nil, err + } + if err := slotCtx.PublishBankSysvars(childSysvars); err != nil { + return nil, fmt.Errorf("publish bank sysvars for leader slot %d: %w", slot, err) + } + } - if sealevel.SysvarCache.Rent.Sysvar == nil { - rent := sealevel.NewDefaultRentSysvar() - sealevel.SysvarCache.Rent.Sysvar = &rent + nanoClockAddr := replay.NanosecondClockAccountAddr() + if parent.HasNanosecondClockAccount { + if parent.NanosecondClockAccount == nil { + return nil, fmt.Errorf("parent nanosecond clock marked present without an account") + } + parentNanoClock := parent.NanosecondClockAccount.Clone() + parentNanoClock.Key = nanoClockAddr + if err := slotCtx.ParentAccts.SetAccountWithoutLock(nanoClockAddr, parentNanoClock); err != nil { + return nil, fmt.Errorf("install parent nanosecond clock: %w", err) + } + currentNanoClock := parent.NanosecondClockAccount.Clone() + currentNanoClock.Key = nanoClockAddr + if err := slotCtx.SetAccount(nanoClockAddr, currentNanoClock); err != nil { + return nil, fmt.Errorf("install current nanosecond clock: %w", err) + } + } else if parent.BankSysvars != nil { + // BankSysvars being present makes absence explicit rather than unknown. + // Pin the zero before-value so footer creation cannot consult a mutable + // replay reader while calculating the child bank's LtHash delta. + absentNanoClock := &accounts.Account{Key: nanoClockAddr} + if err := slotCtx.ParentAccts.SetAccountWithoutLock(nanoClockAddr, absentNanoClock.Clone()); err != nil { + return nil, fmt.Errorf("pin absent parent nanosecond clock: %w", err) + } + if err := slotCtx.SetAccount(nanoClockAddr, absentNanoClock); err != nil { + return nil, fmt.Errorf("install absent current nanosecond clock: %w", err) + } } + return slotCtx, nil } diff --git a/pkg/blockprod/slot_ctx_test.go b/pkg/blockprod/slot_ctx_test.go index 22f8bb37..5fff4069 100644 --- a/pkg/blockprod/slot_ctx_test.go +++ b/pkg/blockprod/slot_ctx_test.go @@ -1,14 +1,38 @@ package blockprod import ( + "bytes" "testing" + "github.com/Overclock-Validator/mithril/pkg/accounts" + b "github.com/Overclock-Validator/mithril/pkg/block" "github.com/Overclock-Validator/mithril/pkg/features" "github.com/Overclock-Validator/mithril/pkg/lthash" + "github.com/Overclock-Validator/mithril/pkg/replay" "github.com/Overclock-Validator/mithril/pkg/sealevel" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" ) +type panicLeaderAccountReader struct{} + +func (panicLeaderAccountReader) GetAccount(uint64, solana.PublicKey) (*accounts.Account, error) { + panic("leader preparation consulted mutable parent account reader") +} + +func leaderTestEpochScheduleAccount(t *testing.T, schedule sealevel.SysvarEpochSchedule) *accounts.Account { + t.Helper() + var data bytes.Buffer + enc := bin.NewBinEncoder(&data) + require.NoError(t, enc.WriteUint64(schedule.SlotsPerEpoch, bin.LE)) + require.NoError(t, enc.WriteUint64(schedule.LeaderScheduleSlotOffset, bin.LE)) + require.NoError(t, enc.WriteBool(schedule.Warmup)) + require.NoError(t, enc.WriteUint64(schedule.FirstNormalEpoch, bin.LE)) + require.NoError(t, enc.WriteUint64(schedule.FirstNormalSlot, bin.LE)) + return &accounts.Account{Key: sealevel.SysvarEpochScheduleAddr, Lamports: 1, Data: data.Bytes()} +} + func TestNewLeaderSlotCtxInheritsAcctsLtHashAndFeatures(t *testing.T) { parentLtHash := new(lthash.LtHash).InitWithHash(make([]byte, 2048)) parentFeatures := features.NewFeaturesDefault() @@ -19,9 +43,13 @@ func TestNewLeaderSlotCtxInheritsAcctsLtHashAndFeatures(t *testing.T) { SlotsPerEpoch: 54000, } slotCtx, err := NewLeaderSlotCtx(100, 99, nil, ParentContext{ - PrevNumSigs: 42, - AcctsLtHash: parentLtHash, - Features: parentFeatures, + PrevNumSigs: 42, + AcctsLtHash: parentLtHash, + Features: parentFeatures, + ParentLastBlockhash: solana.Hash{7}, + LatestEvictedBlockhash: [32]byte{8}, + EpochStakes: map[solana.PublicKey]uint64{solana.PublicKey{9}: 10}, + TotalEpochStake: 10, }, epochSchedule) require.NoError(t, err) require.NotNil(t, slotCtx.AcctsLtHash) @@ -31,4 +59,144 @@ func TestNewLeaderSlotCtxInheritsAcctsLtHashAndFeatures(t *testing.T) { require.True(t, slotCtx.Features.IsActive(features.FormalizeLoadedTransactionDataSize)) require.Equal(t, uint64(0), slotCtx.NumSignatures) require.Equal(t, uint64(0), slotCtx.Epoch) // slot 100 with default schedule + require.Equal(t, [32]byte{7}, slotCtx.LastBlockhash) + require.Equal(t, [32]byte{8}, slotCtx.LatestEvictedBlockhash) + require.Equal(t, uint64(10), slotCtx.VoteAccts[solana.PublicKey{9}]) + require.Equal(t, uint64(10), slotCtx.TotalEpochStake) +} + +func TestNewLeaderSlotCtxInstallsPinnedBankState(t *testing.T) { + const parentSlot = uint64(99) + clock := sealevel.SysvarClock{Slot: parentSlot, UnixTimestamp: 1234} + clockAcct := &accounts.Account{ + Key: sealevel.SysvarClockAddr, + Lamports: 1, + Data: clock.MustMarshal(), + } + schedule := sealevel.SysvarEpochSchedule{SlotsPerEpoch: 54_000} + parentSysvars, err := sealevel.NewBankSysvars(parentSlot, clockAcct, leaderTestEpochScheduleAccount(t, schedule)) + require.NoError(t, err) + + slotCtx, err := NewLeaderSlotCtx(100, parentSlot, nil, ParentContext{ + BankSysvars: parentSysvars, + ParentLastBlockhash: solana.Hash{7}, + LatestEvictedBlockhash: [32]byte{8}, + }, &schedule) + require.NoError(t, err) + require.NotNil(t, slotCtx.BankSysvars()) + require.Equal(t, uint64(100), slotCtx.BankSysvars().Slot()) + gotClock, ok := slotCtx.BankSysvars().Clock() + require.True(t, ok) + require.Equal(t, clock, gotClock) + + currentClock, err := slotCtx.GetAccount(sealevel.SysvarClockAddr) + require.NoError(t, err) + parentClock, err := slotCtx.GetParentAccount(sealevel.SysvarClockAddr) + require.NoError(t, err) + require.Equal(t, clockAcct.Data, currentClock.Data) + require.Equal(t, clockAcct.Data, parentClock.Data) + require.NotSame(t, currentClock, parentClock) + + // Every absent cached sysvar and the optional alpenclock PDA are pinned as + // tombstones, preventing fallback into a newer unrooted replay generation. + for _, pubkey := range []solana.PublicKey{sealevel.SysvarRentAddr, replay.NanosecondClockAccountAddr()} { + current, err := slotCtx.GetAccount(pubkey) + require.NoError(t, err) + require.Zero(t, current.Lamports) + parent, err := slotCtx.GetParentAccount(pubkey) + require.NoError(t, err) + require.Zero(t, parent.Lamports) + } +} + +func TestNewLeaderSlotCtxInstallsPresentNanosecondClockIndependently(t *testing.T) { + const parentSlot = uint64(99) + clock := sealevel.SysvarClock{Slot: parentSlot, UnixTimestamp: 1234} + schedule := sealevel.SysvarEpochSchedule{SlotsPerEpoch: 54_000} + parentSysvars, err := sealevel.NewBankSysvars(parentSlot, &accounts.Account{ + Key: sealevel.SysvarClockAddr, + Lamports: 1, + Data: clock.MustMarshal(), + }, leaderTestEpochScheduleAccount(t, schedule)) + require.NoError(t, err) + nanoClockAddr := replay.NanosecondClockAccountAddr() + nanoClock := &accounts.Account{ + Key: nanoClockAddr, + Lamports: 42, + Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + } + + slotCtx, err := NewLeaderSlotCtx(100, parentSlot, nil, ParentContext{ + BankSysvars: parentSysvars, + NanosecondClockAccount: nanoClock, + HasNanosecondClockAccount: true, + }, &schedule) + require.NoError(t, err) + + current, err := slotCtx.GetAccount(nanoClockAddr) + require.NoError(t, err) + parent, err := slotCtx.GetParentAccount(nanoClockAddr) + require.NoError(t, err) + require.Equal(t, nanoClock.Data, current.Data) + require.Equal(t, nanoClock.Data, parent.Data) + + // Updating the child account must leave the pinned parent before-image and + // the published ChainTip copy untouched. + current.Data[0] = 9 + require.NoError(t, slotCtx.SetAccount(nanoClockAddr, current)) + parent, err = slotCtx.GetParentAccount(nanoClockAddr) + require.NoError(t, err) + require.Equal(t, byte(1), parent.Data[0]) + require.Equal(t, byte(1), nanoClock.Data[0]) +} + +func TestPrepareLeaderSlotSysvarsUsesPinnedParentSnapshot(t *testing.T) { + const parentSlot = uint64(99) + const childSlot = uint64(100) + schedule := sealevel.SysvarEpochSchedule{SlotsPerEpoch: 54_000, LeaderScheduleSlotOffset: 54_000} + parentClock := sealevel.SysvarClock{Slot: parentSlot, Epoch: 0, UnixTimestamp: 1_234} + parentSlotHashes := sealevel.SysvarSlotHashes{{Slot: 98, Hash: [32]byte{1}}} + parentSysvars, err := sealevel.NewBankSysvars(parentSlot, + &accounts.Account{Key: sealevel.SysvarClockAddr, Lamports: 1, Data: parentClock.MustMarshal()}, + &accounts.Account{Key: sealevel.SysvarSlotHashesAddr, Lamports: 1, Data: parentSlotHashes.MustMarshal()}, + leaderTestEpochScheduleAccount(t, schedule), + ) + require.NoError(t, err) + + previousClock := sealevel.SysvarCache.Clock + previousSlotHashes := sealevel.SysvarCache.SlotHashes + t.Cleanup(func() { + sealevel.SysvarCache.Clock = previousClock + sealevel.SysvarCache.SlotHashes = previousSlotHashes + }) + conflictingClock := sealevel.SysvarClock{Slot: 9_999, UnixTimestamp: 9_999} + conflictingSlotHashes := sealevel.SysvarSlotHashes{{Slot: 9_999, Hash: [32]byte{9}}} + sealevel.SysvarCache.Clock.Sysvar = &conflictingClock + sealevel.SysvarCache.SlotHashes.Sysvar = &conflictingSlotHashes + + slotCtx, err := NewLeaderSlotCtx(childSlot, parentSlot, nil, ParentContext{ + BankSysvars: parentSysvars, + UnrootedRead: panicLeaderAccountReader{}, + }, &schedule) + require.NoError(t, err) + parentBankhash := solana.Hash{7} + require.NoError(t, replay.PrepareLeaderSlotSysvars(slotCtx, &b.Block{ + Slot: childSlot, ParentSlot: parentSlot, Epoch: 0, ParentBankhash: parentBankhash, + }, true)) + + childClock, ok := slotCtx.BankSysvars().Clock() + require.True(t, ok) + require.Equal(t, childSlot, childClock.Slot) + require.Equal(t, parentClock.UnixTimestamp, childClock.UnixTimestamp) + childSlotHashes, ok := slotCtx.BankSysvars().SlotHashes() + require.True(t, ok) + require.NotEmpty(t, childSlotHashes) + require.Equal(t, parentSlot, childSlotHashes[0].Slot) + require.Equal(t, [32]byte(parentBankhash), childSlotHashes[0].Hash) + + // The immutable parent generation remains unchanged. + unchangedClock, _ := parentSysvars.Clock() + require.Equal(t, parentClock, unchangedClock) + unchangedSlotHashes, _ := parentSysvars.SlotHashes() + require.Equal(t, parentSlotHashes, unchangedSlotHashes) } diff --git a/pkg/replay/alpenglow_engine.go b/pkg/replay/alpenglow_engine.go index 1b58f5fd..274d7aca 100644 --- a/pkg/replay/alpenglow_engine.go +++ b/pkg/replay/alpenglow_engine.go @@ -180,6 +180,18 @@ func applyAlpenglowFooterClockWithCache(slotCtx *sealevel.SlotCtx, block *b.Bloc if err := slotCtx.SetAccount(sealevel.SysvarClockAddr, clockAcct); err != nil { return fmt.Errorf("unable to write Alpenglow footer clock back to slot state: %w", err) } + bankSysvars := slotCtx.BankSysvars() + if bankSysvars == nil { + bankSysvars, err = sealevel.NewBankSysvars(slotCtx.Slot, clockAcct) + } else { + bankSysvars, err = bankSysvars.WithAccounts(clockAcct) + } + if err != nil { + return fmt.Errorf("update bank-local Clock snapshot: %w", err) + } + if err := slotCtx.PublishBankSysvars(bankSysvars); err != nil { + return err + } if updateCache { sealevel.SysvarCache.Clock.Sysvar = &clock sealevel.SysvarCache.Clock.Acct = clockAcct diff --git a/pkg/replay/alpenglow_footer_clock_test.go b/pkg/replay/alpenglow_footer_clock_test.go index 7cdb0f8b..91b9e8da 100644 --- a/pkg/replay/alpenglow_footer_clock_test.go +++ b/pkg/replay/alpenglow_footer_clock_test.go @@ -22,6 +22,9 @@ func newClockSlotCtx(t *testing.T, clock sealevel.SysvarClock) *sealevel.SlotCtx } slotCtx := &sealevel.SlotCtx{Accounts: mem} require.NoError(t, slotCtx.SetAccount(sealevel.SysvarClockAddr, acct)) + bankSysvars, err := sealevel.NewBankSysvars(slotCtx.Slot, acct) + require.NoError(t, err) + require.NoError(t, slotCtx.PublishBankSysvars(bankSysvars)) return slotCtx } @@ -134,6 +137,9 @@ func TestApplyAlpenglowFooterClockLocalDoesNotPublishSpeculativeClock(t *testing require.NoError(t, candidate.UnmarshalWithDecoder(bin.NewBinDecoder(stored.Data))) require.Equal(t, blk.Slot, candidate.Slot) require.Equal(t, int64(1779999999), candidate.UnixTimestamp) + cached, err := sealevel.ReadClockSysvar(&sealevel.ExecutionCtx{SlotCtx: slotCtx}) + require.NoError(t, err) + require.Equal(t, candidate, cached) // Ordered replay still sees the genuine parent Clock until it accepts the // produced block itself. diff --git a/pkg/replay/alpenglow_nanosecond_clock.go b/pkg/replay/alpenglow_nanosecond_clock.go index b4c5ed6a..217389ec 100644 --- a/pkg/replay/alpenglow_nanosecond_clock.go +++ b/pkg/replay/alpenglow_nanosecond_clock.go @@ -24,14 +24,15 @@ func NanosecondClockBounds(parentNanos int64, elapsedSlotDurationNanos uint64) ( lower++ } - offset := elapsedSlotDurationNanos - if offset > uint64(math.MaxInt64)/2 { - offset = uint64(math.MaxInt64) / 2 + var maxOffset int64 + if elapsedSlotDurationNanos > uint64(math.MaxInt64)/2 { + maxOffset = math.MaxInt64 + } else { + maxOffset = int64(elapsedSlotDurationNanos * 2) } - maxOffset := int64(offset * 2) upper := parentNanos - if maxOffset > math.MaxInt64-parentNanos { + if parentNanos > math.MaxInt64-maxOffset { upper = math.MaxInt64 } else { upper += maxOffset @@ -76,24 +77,94 @@ func ReadNanosecondClockAt(acctsDb *accountsdb.AccountsDb, slot uint64) (int64, return clock.UnixTimestamp * 1_000_000_000, true } -// ReadNanosecondClockFromSlotCtx reads the parent-visible clock through the -// slot's AccountReader, so locally produced descendants see unrooted replay. +// ReadNanosecondClockFromSlotCtx returns the exact parent-time anchor used for +// Alpenglow footer bounds. The parent account is pinned when the bank is built; +// a transaction in the child bank must not be able to change this value. +// +// Before the nanosecond account is populated, Agave falls back to the child +// bank's pre-footer Clock timestamp. Neither path consults a process-global or +// unrooted account view. func ReadNanosecondClockFromSlotCtx(slotCtx *sealevel.SlotCtx) (int64, bool) { + nanos, err := nanosecondClockAnchor(slotCtx) + return nanos, err == nil +} + +func nanosecondClockAnchor(slotCtx *sealevel.SlotCtx) (int64, error) { if slotCtx == nil { - return 0, false + return 0, fmt.Errorf("missing slot context") } - if acct, err := slotCtx.GetAccountFromAccountsDb(NanosecondClockAccountAddr()); err == nil && acct != nil && len(acct.Data) >= nanosecondClockDataLen { - return int64(binary.LittleEndian.Uint64(acct.Data[:nanosecondClockDataLen])), true + var nanoClockAcct *accounts.Account + if slotCtx.ParentAccts != nil { + acct, err := slotCtx.GetParentAccount(NanosecondClockAccountAddr()) + if err != nil { + return 0, fmt.Errorf("parent nanosecond clock was not pinned: %w", err) + } + nanoClockAcct = acct + } else if slotCtx.Accounts != nil { + // Compatibility for isolated callers that predate parent snapshots. + nanoClockAcct, _ = slotCtx.GetAccount(NanosecondClockAccountAddr()) + } + if nanoClockAcct != nil && nanoClockAcct.Lamports > 0 && len(nanoClockAcct.Data) != 0 { + if len(nanoClockAcct.Data) < nanosecondClockDataLen { + return 0, fmt.Errorf("parent nanosecond clock has invalid data length %d", len(nanoClockAcct.Data)) + } + return int64(binary.LittleEndian.Uint64(nanoClockAcct.Data[:nanosecondClockDataLen])), nil } - clockAcct, err := slotCtx.GetAccountFromAccountsDb(sealevel.SysvarClockAddr) - if err != nil || clockAcct == nil { - return 0, false + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + if clock, ok := bankSysvars.Clock(); ok { + return secondsToNanosecondsSaturating(clock.UnixTimestamp), nil + } } - var clock sealevel.SysvarClock - if err := clock.UnmarshalWithDecoder(bin.NewBinDecoder(clockAcct.Data)); err != nil { - return 0, false + return 0, fmt.Errorf("bank-local Clock sysvar is unavailable") +} + +func secondsToNanosecondsSaturating(seconds int64) int64 { + const nanosPerSecond = int64(1_000_000_000) + if seconds > math.MaxInt64/nanosPerSecond { + return math.MaxInt64 } - return clock.UnixTimestamp * 1_000_000_000, true + if seconds < math.MinInt64/nanosPerSecond { + return math.MinInt64 + } + return seconds * nanosPerSecond +} + +// validateAlpenglowFooterNanosecondClock mirrors Agave's footer-time check. +// FooterProducerTimeNanos is a required wire value here: zero is numeric zero, +// not an invitation to fall back to the second-resolution compatibility field. +func validateAlpenglowFooterNanosecondClock(slotCtx *sealevel.SlotCtx, block *b.Block) error { + if slotCtx == nil || block == nil { + return fmt.Errorf("cannot validate Alpenglow footer nanosecond clock without bank state") + } + if !block.HasAlpenglowFooter { + return fmt.Errorf("slot %d missing block footer", block.Slot) + } + if block.FooterProducerTimeNanos > uint64(math.MaxInt64) { + return fmt.Errorf("slot %d footer nanosecond clock out of bounds: producer time %d overflows i64", block.Slot, block.FooterProducerTimeNanos) + } + parentNanos, err := nanosecondClockAnchor(slotCtx) + if err != nil { + return fmt.Errorf("slot %d footer nanosecond clock: %w", block.Slot, err) + } + + var elapsed uint64 + if slotCtx.Slot > slotCtx.ParentSlot { + slotGap := slotCtx.Slot - slotCtx.ParentSlot + if slotGap > math.MaxUint64/uint64(alpenglowNsPerSlot) { + elapsed = math.MaxUint64 + } else { + elapsed = slotGap * uint64(alpenglowNsPerSlot) + } + } + lower, upper := NanosecondClockBounds(parentNanos, elapsed) + producerNanos := int64(block.FooterProducerTimeNanos) + if producerNanos < lower || producerNanos > upper { + return fmt.Errorf( + "slot %d footer nanosecond clock out of bounds: producer=%d parent=%d bounds=[%d,%d]", + block.Slot, producerNanos, parentNanos, lower, upper, + ) + } + return nil } func alpenglowFooterProducerTimeNanos(block *b.Block) (int64, bool, error) { @@ -130,15 +201,10 @@ func updateAlpenglowNanosecondClockAccount(slotCtx *sealevel.SlotCtx, block *b.B addr := NanosecondClockAccountAddr() acct, err := slotCtx.GetAccount(addr) if err != nil { - acct, err = slotCtx.GetAccountFromAccountsDb(addr) - if err != nil { - acct = &accounts.Account{ - Key: addr, - Owner: a.SystemProgramAddr, - RentEpoch: 0, - } - } else { - acct = acct.Clone() + acct = &accounts.Account{ + Key: addr, + Owner: a.SystemProgramAddr, + RentEpoch: 0, } } diff --git a/pkg/replay/alpenglow_nanosecond_clock_test.go b/pkg/replay/alpenglow_nanosecond_clock_test.go index fac52d46..b9b46c5a 100644 --- a/pkg/replay/alpenglow_nanosecond_clock_test.go +++ b/pkg/replay/alpenglow_nanosecond_clock_test.go @@ -2,6 +2,7 @@ package replay import ( "encoding/binary" + "math" "sync" "testing" @@ -70,6 +71,10 @@ func TestNanosecondClockBounds(t *testing.T) { // Multi-slot gap scales the upper bound. _, upper5 := NanosecondClockBounds(parent, 5*slotNanos) require.Equal(t, parent+2*5*slotNanos, upper5) + + // Agave saturates the doubled elapsed duration to the full i64 maximum. + _, saturatedUpper := NanosecondClockBounds(0, math.MaxUint64) + require.Equal(t, int64(math.MaxInt64), saturatedUpper) } func TestSkewBlockProducerTimeNanosClampsBothEnds(t *testing.T) { @@ -88,3 +93,88 @@ func TestSkewBlockProducerTimeNanosClampsBothEnds(t *testing.T) { inBounds := parent + slotNanos require.Equal(t, inBounds, SkewBlockProducerTimeNanos(parent, inBounds, slotNanos)) } + +func TestValidateAlpenglowFooterNanosecondClockBounds(t *testing.T) { + const ( + parentSlot = uint64(100) + workingSlot = uint64(101) + parentNanos = int64(1_782_240_542_000_000_000) + ) + parentAccts := accounts.NewMemAccounts() + nanoClock := &accounts.Account{ + Key: NanosecondClockAccountAddr(), + Lamports: 1, + Data: encodeNanosecondClockData(parentNanos), + } + require.NoError(t, parentAccts.SetAccountWithoutLock(nanoClock.Key, nanoClock)) + clock := sealevel.SysvarClock{Slot: workingSlot, UnixTimestamp: parentNanos / 1_000_000_000} + clockAcct := &accounts.Account{Key: sealevel.SysvarClockAddr, Lamports: 1, Data: clock.MustMarshal()} + bankSysvars, err := sealevel.NewBankSysvars(workingSlot, clockAcct) + require.NoError(t, err) + slotCtx := &sealevel.SlotCtx{ + Slot: workingSlot, + ParentSlot: parentSlot, + ParentAccts: parentAccts, + } + require.NoError(t, slotCtx.PublishBankSysvars(bankSysvars)) + + lower, upper := NanosecondClockBounds(parentNanos, uint64(alpenglowNsPerSlot)) + for _, tc := range []struct { + name string + producer uint64 + wantError bool + }{ + {name: "inclusive lower", producer: uint64(lower)}, + {name: "inclusive upper", producer: uint64(upper)}, + {name: "zero", producer: 0, wantError: true}, + {name: "equal parent", producer: uint64(parentNanos), wantError: true}, + {name: "above upper", producer: uint64(upper + 1), wantError: true}, + {name: "i64 overflow", producer: uint64(math.MaxInt64) + 1, wantError: true}, + } { + t.Run(tc.name, func(t *testing.T) { + block := &b.Block{ + Slot: workingSlot, + ParentSlot: parentSlot, + HasAlpenglowFooter: true, + FooterProducerTimeNanos: tc.producer, + } + err := validateAlpenglowFooterNanosecondClock(slotCtx, block) + if tc.wantError { + require.ErrorContains(t, err, "nanosecond clock out of bounds") + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNanosecondClockAnchorUsesPinnedParentAndBankClockFallback(t *testing.T) { + const ( + parentNanos = int64(1_782_240_542_000_000_000) + childNanos = parentNanos + 123 + ) + parentAccts := accounts.NewMemAccounts() + currentAccts := accounts.NewMemAccounts() + parentNano := &accounts.Account{Key: NanosecondClockAccountAddr(), Lamports: 1, Data: encodeNanosecondClockData(parentNanos)} + childNano := &accounts.Account{Key: NanosecondClockAccountAddr(), Lamports: 1, Data: encodeNanosecondClockData(childNanos)} + require.NoError(t, parentAccts.SetAccountWithoutLock(parentNano.Key, parentNano)) + require.NoError(t, currentAccts.SetAccountWithoutLock(childNano.Key, childNano)) + clock := sealevel.SysvarClock{Slot: 101, UnixTimestamp: 1234} + clockAcct := &accounts.Account{Key: sealevel.SysvarClockAddr, Lamports: 1, Data: clock.MustMarshal()} + bankSysvars, err := sealevel.NewBankSysvars(101, clockAcct) + require.NoError(t, err) + slotCtx := &sealevel.SlotCtx{Slot: 101, ParentSlot: 100, Accounts: currentAccts, ParentAccts: parentAccts} + require.NoError(t, slotCtx.PublishBankSysvars(bankSysvars)) + + got, ok := ReadNanosecondClockFromSlotCtx(slotCtx) + require.True(t, ok) + require.Equal(t, parentNanos, got) + + // A prefunded but not-yet-populated PDA is still an account before-image, + // but it supplies no time value. Agave falls back to the bank-start Clock. + prefunded := &accounts.Account{Key: NanosecondClockAccountAddr(), Lamports: 1} + require.NoError(t, parentAccts.SetAccountWithoutLock(prefunded.Key, prefunded)) + got, ok = ReadNanosecondClockFromSlotCtx(slotCtx) + require.True(t, ok) + require.Equal(t, int64(1_234_000_000_000), got) +} diff --git a/pkg/replay/alpenglow_switch.go b/pkg/replay/alpenglow_switch.go index 61ebdf5c..0890a48f 100644 --- a/pkg/replay/alpenglow_switch.go +++ b/pkg/replay/alpenglow_switch.go @@ -165,12 +165,15 @@ const ( unwindFallbackRewardsWindow = "rewards-window" unwindFallbackVoteStakeDirty = "vote-stake-dirty" unwindFallbackMissingContext = "missing-context" + unwindFallbackMissingSysvars = "missing-bank-sysvars" + unwindFallbackSysvarSlot = "bank-sysvar-slot-mismatch" unwindFallbackContextRebuild = "context-rebuild" ) // tryInLoopUnwind attempts the in-RAM fork switch. On success it returns the -// rebuilt resume state and "". Otherwise it returns nil and the guard reason -// that forced the rooted-checkpoint fallback. +// rebuilt resume state, the exact immutable sysvar snapshot of its parent bank, +// and "". Otherwise it returns nil values and the guard reason that forced the +// rooted-checkpoint fallback. func tryInLoopUnwind( sw *CertifiedSwitch, tail *unrootedTail, @@ -178,15 +181,18 @@ func tryInLoopUnwind( epochSchedule *sealevel.SysvarEpochSchedule, currentEpoch uint64, partitionedRewardsInfo *rewards.PartitionedRewardDistributionInfo, -) (*ResumeState, string) { +) (*ResumeState, *sealevel.BankSysvars, string) { if tail == nil || sw.Slot == 0 { - return nil, unwindFallbackNilTail + return nil, nil, unwindFallbackNilTail } if epochSchedule.GetEpoch(sw.Slot-1) != currentEpoch || epochSchedule.GetEpoch(sw.Slot) != currentEpoch { - return nil, unwindFallbackCrossEpoch + return nil, nil, unwindFallbackCrossEpoch } - if partitionedRewardsInfo != nil && partitionedRewardsInfo.NumRewardPartitionsRemaining > 0 { - return nil, unwindFallbackRewardsWindow + if partitionedRewardsInfo != nil { + // Even after the last partition is distributed, the in-memory spool and + // completed-distribution bookkeeping describe the abandoned suffix. They + // cannot be reconstructed safely without re-running the epoch boundary. + return nil, nil, unwindFallbackRewardsWindow } // Vote/stake cache safety, BOTH directions. The unwind cannot roll the // global vote/stake caches back (a write in the UNWOUND suffix >= sw.Slot @@ -200,22 +206,31 @@ func tryInLoopUnwind( // exact by construction. Vote-program writes are rare in Alpenglow blocks // (vote transactions are off-chain), so the fast path still dominates. if voteStakeDirtySlot.Load() > mithrilState.LastRootedSlot { - return nil, unwindFallbackVoteStakeDirty + return nil, nil, unwindFallbackVoteStakeDirty } - ctx := tail.unwind(sw.Slot) + ctx, bankSysvars := tail.unwind(sw.Slot) if ctx == nil && sw.Slot-1 == mithrilState.LastRootedSlot && mithrilState.LastRootedContext != nil { - // Parent is exactly the durable fold boundary: its context lives in - // the state file rather than the tail. - ctx = mithrilState.LastRootedContext + // The durable boundary carries a persisted ResumeContext, but deliberately + // no in-memory BankSysvars pointer. Re-entering through the normal rooted + // recovery path rebuilds the complete snapshot; using process globals here + // could resurrect a sysvar generation from the discarded suffix. + return nil, nil, unwindFallbackMissingSysvars } if ctx == nil { - return nil, unwindFallbackMissingContext + return nil, nil, unwindFallbackMissingContext + } + if bankSysvars == nil { + return nil, nil, unwindFallbackMissingSysvars + } + if bankSysvars.Slot() != ctx.Slot { + mlog.Log.Warnf("alpenglow switch: retained bank sysvars at slot %d do not match resume context slot %d", bankSysvars.Slot(), ctx.Slot) + return nil, nil, unwindFallbackSysvarSlot } rs, err := ResumeStateFromRootedContext(ctx, nil) if err != nil { mlog.Log.Warnf("alpenglow switch: cannot rebuild resume state from retained context at slot %d: %v", ctx.Slot, err) - return nil, unwindFallbackContextRebuild + return nil, nil, unwindFallbackContextRebuild } - return rs, "" + return rs, bankSysvars, "" } diff --git a/pkg/replay/alpenglow_unwind_test.go b/pkg/replay/alpenglow_unwind_test.go index 62fa701f..dd478e0f 100644 --- a/pkg/replay/alpenglow_unwind_test.go +++ b/pkg/replay/alpenglow_unwind_test.go @@ -1,6 +1,7 @@ package replay import ( + "bytes" "encoding/base64" "reflect" "testing" @@ -9,11 +10,26 @@ import ( "github.com/Overclock-Validator/mithril/pkg/rewards" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/Overclock-Validator/mithril/pkg/state" + bin "github.com/gagliardetto/binary" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func testUnwindBankSysvars(t *testing.T, slot uint64, epochRewardsMarker uint64) *sealevel.BankSysvars { + t.Helper() + clock := &sealevel.SysvarClock{Slot: slot} + rewardsSysvar := &sealevel.SysvarEpochRewards{DistributedRewards: epochRewardsMarker} + var rewardsData bytes.Buffer + require.NoError(t, rewardsSysvar.MarshalWithEncoder(bin.NewBinEncoder(&rewardsData))) + snapshot, err := sealevel.NewBankSysvars(slot, + &accounts.Account{Key: sealevel.SysvarClockAddr, Lamports: 1, Data: clock.MustMarshal()}, + &accounts.Account{Key: sealevel.SysvarEpochRewardsAddr, Lamports: 1, Data: rewardsData.Bytes()}, + ) + require.NoError(t, err) + return snapshot +} + // Tripwire: the fork-switch unwind and checkpoint resume are only correct if // EVERY runtime side effect a slot produces is carried in ResumeContext and // restored. If you add a field to state.ResumeContext, this test forces you to @@ -124,24 +140,31 @@ func TestResumeStateFromRootedContextRoundTrip(t *testing.T) { func TestUnwindReturnsExecutedParentAcrossSkips(t *testing.T) { tail := newUnrootedTail(&fakeDurable{}, &fakeCommitter{durable: accounts.NewMemAccounts()}, 512, 1, "") // Executed slots 5 and 8 (6, 7 skipped -> no context), then 9. + bank5 := testUnwindBankSysvars(t, 5, 50) + bank8 := testUnwindBankSysvars(t, 8, 80) + bank9 := testUnwindBankSysvars(t, 9, 90) tail.Add(5, []*accounts.Account{testAccount(1, 51)}, testHashBytes(5)) - tail.SetContext(5, &state.ResumeContext{Slot: 5, Bankhash: "bh5"}) + tail.SetContext(5, &state.ResumeContext{Slot: 5, Bankhash: "bh5"}, bank5) tail.Add(8, []*accounts.Account{testAccount(2, 82)}, testHashBytes(8)) - tail.SetContext(8, &state.ResumeContext{Slot: 8, Bankhash: "bh8"}) + tail.SetContext(8, &state.ResumeContext{Slot: 8, Bankhash: "bh8"}, bank8) tail.Add(9, []*accounts.Account{testAccount(3, 93)}, testHashBytes(9)) - tail.SetContext(9, &state.ResumeContext{Slot: 9, Bankhash: "bh9"}) + tail.SetContext(9, &state.ResumeContext{Slot: 9, Bankhash: "bh9"}, bank9) // Switch at slot 9: the parent is the executed slot 8. - ctx := tail.unwind(9) + ctx, bankSysvars := tail.unwind(9) require.NotNil(t, ctx) assert.Equal(t, uint64(8), ctx.Slot) + assert.Same(t, bank8, bankSysvars) + assert.NotContains(t, tail.bankSysvars, uint64(9), "discarded suffix snapshot must be evicted") // Switch at slot 8: slots 6,7 were skipped, so the executed parent is slot 5 // — returned even though it is not numerically 8-1=7 (the old code rejected // this and forced a rooted re-replay). - ctx = tail.unwind(8) + ctx, bankSysvars = tail.unwind(8) require.NotNil(t, ctx, "parent across skipped slots must be returned") assert.Equal(t, uint64(5), ctx.Slot) + assert.Same(t, bank5, bankSysvars, "exact parent snapshot survives skipped slots") + assert.NotContains(t, tail.bankSysvars, uint64(8), "second discarded suffix snapshot must be evicted") } // The seed for the running transaction count: exact from a checkpoint that @@ -189,6 +212,14 @@ func TestVoteStakeDirtyWatermark(t *testing.T) { // slot in that suffix mutated them (P1) — the unwind can only roll back account // state, not those process-global caches. func TestTryInLoopUnwindFallsBackWhenVoteStakeDirty(t *testing.T) { + previousEpochRewards := sealevel.SysvarCache.EpochRewards + t.Cleanup(func() { + sealevel.SysvarCache.EpochRewards.Sysvar = previousEpochRewards.Sysvar + sealevel.SysvarCache.EpochRewards.Acct = previousEpochRewards.Acct + }) + staleEpochRewards := &sealevel.SysvarEpochRewards{DistributedRewards: 999} + sealevel.SysvarCache.EpochRewards.Sysvar = staleEpochRewards + // A resume context the rebuild accepts: base58 bankhash + base64 lt-hash // (1024 uint16 elements = 2048 bytes). ctxTxCount := uint64(999) @@ -201,7 +232,7 @@ func TestTryInLoopUnwindFallsBackWhenVoteStakeDirty(t *testing.T) { newTail := func() *unrootedTail { tail := newUnrootedTail(&fakeDurable{}, &fakeCommitter{durable: accounts.NewMemAccounts()}, 512, 1, "") tail.Add(7, []*accounts.Account{testAccount(1, 71)}, testHashBytes(7)) - tail.SetContext(7, validCtx) + tail.SetContext(7, validCtx, testUnwindBankSysvars(t, 7, 700)) return tail } sched := &sealevel.SysvarEpochSchedule{SlotsPerEpoch: 432000, LeaderScheduleSlotOffset: 432000} @@ -213,15 +244,21 @@ func TestTryInLoopUnwindFallsBackWhenVoteStakeDirty(t *testing.T) { // carries the parent's transaction count so the discarded fork's txs can be // dropped from the running count. resetVoteStakeDirty() - rs, _ := tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) + rs, bankSysvars, _ := tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) require.NotNil(t, rs, "clean unwind should succeed in-loop") + require.NotNil(t, bankSysvars, "clean unwind returns its exact parent sysvars") + assert.Equal(t, uint64(7), bankSysvars.Slot()) + retainedEpochRewards, ok := bankSysvars.EpochRewards() + require.True(t, ok) + assert.Equal(t, uint64(700), retainedEpochRewards.DistributedRewards, + "retained parent snapshot wins over the abandoned process-global EpochRewards generation") require.NotNil(t, rs.TransactionCount, "unwind carries the parent's tx count for restore") assert.Equal(t, uint64(999), *rs.TransactionCount) // A global cache was mutated in the UNWOUND suffix (at the switch slot): // the unwind cannot roll the caches back -> must fall back (nil). markVoteStakeDirty(8) - rs, reason := tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) + rs, _, reason := tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) assert.Nil(t, rs, "dirty cache in the unwound suffix must force the rooted re-replay fallback") assert.Equal(t, unwindFallbackVoteStakeDirty, reason) resetVoteStakeDirty() @@ -231,7 +268,7 @@ func TestTryInLoopUnwindFallsBackWhenVoteStakeDirty(t *testing.T) { // cache from durable, which cannot see the retained suffix's writes — the // reload would REGRESS the cache below live account state. markVoteStakeDirty(7) // retained slot; rooted watermark is 0 - rs, reason = tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) + rs, _, reason = tryInLoopUnwind(sw, newTail(), mithrilState, sched, epoch, nil) assert.Nil(t, rs, "dirty cache in the retained suffix must also force the fallback") assert.Equal(t, unwindFallbackVoteStakeDirty, reason) resetVoteStakeDirty() @@ -240,7 +277,7 @@ func TestTryInLoopUnwindFallsBackWhenVoteStakeDirty(t *testing.T) { // durable and reload-from-durable is exact again -> fast path allowed. markVoteStakeDirty(7) rootedPast := &state.MithrilState{LastRootedSlot: 7} - rs, _ = tryInLoopUnwind(sw, newTail(), rootedPast, sched, epoch, nil) + rs, _, _ = tryInLoopUnwind(sw, newTail(), rootedPast, sched, epoch, nil) require.NotNil(t, rs, "dirtiness at/below the rooted watermark is durably folded — fast path is safe") resetVoteStakeDirty() } @@ -258,7 +295,7 @@ func TestTryInLoopUnwindGuardMatrix(t *testing.T) { tail := newUnrootedTail(&fakeDurable{}, &fakeCommitter{durable: accounts.NewMemAccounts()}, 512, 1, "") tail.Add(7, []*accounts.Account{testAccount(1, 71)}, testHashBytes(7)) if withCtx { - tail.SetContext(7, validCtx) + tail.SetContext(7, validCtx, testUnwindBankSysvars(t, 7, 700)) } return tail } @@ -281,10 +318,30 @@ func TestTryInLoopUnwindGuardMatrix(t *testing.T) { sw = &CertifiedSwitch{Slot: 8, Executed: swHash(1), Certified: swHash(2)} rewardsActive := &rewards.PartitionedRewardDistributionInfo{NumRewardPartitionsRemaining: 3} assertUnwindFallbackReason(t, unwindFallbackRewardsWindow, sw, newTail(true), mithrilState, sched, 0, rewardsActive) + rewardsCompleted := &rewards.PartitionedRewardDistributionInfo{} + assertUnwindFallbackReason(t, unwindFallbackRewardsWindow, sw, newTail(true), mithrilState, sched, 0, rewardsCompleted) // Missing parent context: nothing retained to rebuild execution state from. assertUnwindFallbackReason(t, unwindFallbackMissingContext, sw, newTail(false), mithrilState, sched, 0, nil) + // A context without its in-memory-only bank snapshot cannot use the fast + // path: process globals may describe the discarded child generation. + missingSysvars := newTail(false) + missingSysvars.SetContext(7, validCtx) + assertUnwindFallbackReason(t, unwindFallbackMissingSysvars, sw, missingSysvars, mithrilState, sched, 0, nil) + + // The persisted durable-boundary context deliberately carries no pointer; + // it must re-enter through rooted recovery, which reconstructs all sysvars. + durableBoundary := newTail(false) + durableState := &state.MithrilState{LastRootedSlot: 7, LastRootedContext: validCtx} + assertUnwindFallbackReason(t, unwindFallbackMissingSysvars, sw, durableBoundary, durableState, sched, 0, nil) + + // Snapshot/context slot mismatches fail closed rather than deriving a child + // from the wrong bank generation. + mismatched := newTail(false) + mismatched.SetContext(7, validCtx, testUnwindBankSysvars(t, 6, 600)) + assertUnwindFallbackReason(t, unwindFallbackSysvarSlot, sw, mismatched, mithrilState, sched, 0, nil) + // Control: with every guard clear, the unwind proceeds. assertUnwindOK(t, sw, newTail(true), mithrilState, sched, 0, nil) } @@ -292,16 +349,18 @@ func TestTryInLoopUnwindGuardMatrix(t *testing.T) { // assertUnwindOK asserts the in-RAM unwind proceeds (no fallback reason). func assertUnwindOK(t *testing.T, sw *CertifiedSwitch, tail *unrootedTail, ms *state.MithrilState, sched *sealevel.SysvarEpochSchedule, epoch uint64, ri *rewards.PartitionedRewardDistributionInfo) { t.Helper() - rs, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) + rs, bankSysvars, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) require.NotNil(t, rs, "expected in-RAM unwind to proceed, got fallback %q", reason) + require.NotNil(t, bankSysvars) assert.Empty(t, reason) } // assertUnwindFallback asserts the unwind falls back (any reason). func assertUnwindFallback(t *testing.T, sw *CertifiedSwitch, tail *unrootedTail, ms *state.MithrilState, sched *sealevel.SysvarEpochSchedule, epoch uint64, ri *rewards.PartitionedRewardDistributionInfo) { t.Helper() - rs, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) + rs, bankSysvars, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) assert.Nil(t, rs) + assert.Nil(t, bankSysvars) assert.NotEmpty(t, reason, "fallback must carry a reason for the instrumentation") } @@ -309,7 +368,8 @@ func assertUnwindFallback(t *testing.T, sw *CertifiedSwitch, tail *unrootedTail, // instrumented reason operators will see. func assertUnwindFallbackReason(t *testing.T, want string, sw *CertifiedSwitch, tail *unrootedTail, ms *state.MithrilState, sched *sealevel.SysvarEpochSchedule, epoch uint64, ri *rewards.PartitionedRewardDistributionInfo) { t.Helper() - rs, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) + rs, bankSysvars, reason := tryInLoopUnwind(sw, tail, ms, sched, epoch, ri) assert.Nil(t, rs) + assert.Nil(t, bankSysvars) assert.Equal(t, want, reason) } diff --git a/pkg/replay/async_promotion_test.go b/pkg/replay/async_promotion_test.go index 43bee958..49d47a49 100644 --- a/pkg/replay/async_promotion_test.go +++ b/pkg/replay/async_promotion_test.go @@ -200,6 +200,9 @@ func asyncTestTail(committer batchCommitter, slots ...uint64) *unrootedTail { func TestAsyncFoldBuildRunApply(t *testing.T) { fc := &fakeCommitter{durable: accounts.NewMemAccounts()} tail := asyncTestTail(fc, 5, 6, 7) + for _, slot := range []uint64{5, 6, 7} { + tail.SetContext(slot, tail.contexts[slot], testUnwindBankSysvars(t, slot, slot*10)) + } job, err := tail.buildFoldJob(7, false) require.NoError(t, err) @@ -221,6 +224,9 @@ func TestAsyncFoldBuildRunApply(t *testing.T) { assert.Empty(t, tail.bankhashes[uint64(5)]) _, has5 := tail.contexts[5] assert.False(t, has5, "contexts pruned through the fold") + assert.NotContains(t, tail.bankSysvars, uint64(5), "bank sysvars pruned through the fold") + assert.NotContains(t, tail.bankSysvars, uint64(6), "chunk-top bank sysvars pruned through the fold") + assert.Contains(t, tail.bankSysvars, uint64(7), "unfolded bank sysvars remain retained") } // A partial trailing chunk builds only under force (the shutdown flush). diff --git a/pkg/replay/bank_sysvar_finalize_test.go b/pkg/replay/bank_sysvar_finalize_test.go new file mode 100644 index 00000000..7e56b129 --- /dev/null +++ b/pkg/replay/bank_sysvar_finalize_test.go @@ -0,0 +1,83 @@ +package replay + +import ( + "sync" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestFinalizeBankSysvarsWritesBackFrozenState(t *testing.T) { + const slot = uint64(77) + recent := make(sealevel.SysvarRecentBlockhashes, 150) + for i := range recent { + recent[i] = sealevel.RecentBlockHashesEntry{ + Blockhash: [32]byte{byte(i + 1)}, + FeeCalculator: sealevel.FeeCalculator{LamportsPerSignature: uint64(4_000 + i)}, + } + } + expectedEvicted := recent[len(recent)-1].Blockhash + history := sealevel.SysvarSlotHistory{ + Bits: sealevel.SlotHistoryBitvec{ + Bits: sealevel.SlotHistoryInner{BlocksLen: 2, Blocks: []uint64{0, 0}}, + Len: 128, + }, + NextSlot: slot, + } + recentAcct := &accounts.Account{Key: sealevel.SysvarRecentBlockHashesAddr, Lamports: 1, Data: recent.MustMarshal()} + historyAcct := &accounts.Account{Key: sealevel.SysvarSlotHistoryAddr, Lamports: 1, Data: history.MustMarshal()} + parentSnapshot, err := sealevel.NewBankSysvars(slot, recentAcct, historyAcct) + require.NoError(t, err) + + mem := accounts.NewMemAccounts() + slotCtx := &sealevel.SlotCtx{ + Slot: slot, + Accounts: mem, + FeeRateGovernor: &sealevel.FeeRateGovernor{LamportsPerSignature: 5_000}, + AcctMapsMu: &sync.Mutex{}, + ModifiedAccts: make(map[solana.PublicKey]bool), + WritableAccts: make(map[solana.PublicKey]bool), + } + require.NoError(t, slotCtx.SetAccount(recentAcct.Key, recentAcct)) + require.NoError(t, slotCtx.SetAccount(historyAcct.Key, historyAcct)) + require.NoError(t, slotCtx.PublishBankSysvars(parentSnapshot)) + slotCtx.Blockhash = [32]byte{0xA5} + + require.NoError(t, finalizeBankSysvars(slotCtx)) + require.Equal(t, expectedEvicted, slotCtx.LatestEvictedBlockhash) + + frozen := slotCtx.BankSysvars() + require.NotSame(t, parentSnapshot, frozen) + frozenRecent, ok := frozen.RecentBlockhashes() + require.True(t, ok) + require.Len(t, frozenRecent, 150) + require.Equal(t, slotCtx.Blockhash, frozenRecent[0].Blockhash) + require.Equal(t, uint64(5_000), frozenRecent[0].FeeCalculator.LamportsPerSignature) + frozenHistory, ok := frozen.SlotHistory() + require.True(t, ok) + require.Equal(t, slot+1, frozenHistory.NextSlot) + require.NotZero(t, frozenHistory.Bits.Bits.Blocks[(slot/64)%2]&(uint64(1)<<(slot%64))) + + storedRecent, err := slotCtx.GetAccount(sealevel.SysvarRecentBlockHashesAddr) + require.NoError(t, err) + storedHistory, err := slotCtx.GetAccount(sealevel.SysvarSlotHistoryAddr) + require.NoError(t, err) + frozenRecentRaw, ok := frozen.RawView(sealevel.SysvarRecentBlockHashesAddr) + require.True(t, ok) + frozenHistoryRaw, ok := frozen.RawView(sealevel.SysvarSlotHistoryAddr) + require.True(t, ok) + require.Equal(t, frozenRecentRaw, storedRecent.Data) + require.Equal(t, frozenHistoryRaw, storedHistory.Data) + + // Copy-on-write finalization must not mutate the selected parent generation. + parentRecent, ok := parentSnapshot.RecentBlockhashes() + require.True(t, ok) + require.Equal(t, expectedEvicted, parentRecent[len(parentRecent)-1].Blockhash) + require.NotEqual(t, slotCtx.Blockhash, parentRecent[0].Blockhash) + parentHistory, ok := parentSnapshot.SlotHistory() + require.True(t, ok) + require.Equal(t, slot, parentHistory.NextSlot) +} diff --git a/pkg/replay/bank_sysvar_parent_loader_test.go b/pkg/replay/bank_sysvar_parent_loader_test.go new file mode 100644 index 00000000..2e6a63f9 --- /dev/null +++ b/pkg/replay/bank_sysvar_parent_loader_test.go @@ -0,0 +1,166 @@ +package replay + +import ( + "bytes" + "context" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + b "github.com/Overclock-Validator/mithril/pkg/block" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// parentSnapshotOnlySource permits the normal transaction-account batch but +// fails the test if a parent-derived bank tries to reload any lifecycle sysvar +// individually from the tail/AccountsDB. +type parentSnapshotOnlySource struct { + t *testing.T +} + +func (s *parentSnapshotOnlySource) GetAccount(_ uint64, pubkey solana.PublicKey) (*accounts.Account, error) { + s.t.Fatalf("unexpected parent-derived account read for %s", pubkey) + return nil, nil +} + +func (s *parentSnapshotOnlySource) GetAccountsBatch(_ context.Context, _ uint64, pubkeys []solana.PublicKey) ([]*accounts.Account, error) { + out := make([]*accounts.Account, len(pubkeys)) + for i, pubkey := range pubkeys { + out[i] = &accounts.Account{Key: pubkey} + } + return out, nil +} + +func marshalStakeHistoryForParentLoader(t *testing.T, value *sealevel.SysvarStakeHistory) []byte { + t.Helper() + var data bytes.Buffer + require.NoError(t, value.MarshalWithEncoder(bin.NewBinEncoder(&data))) + return data.Bytes() +} + +func marshalLastRestartSlotForParentLoader(t *testing.T, value sealevel.SysvarLastRestartSlot) []byte { + t.Helper() + var data bytes.Buffer + require.NoError(t, bin.NewBinEncoder(&data).WriteUint64(value.LastRestartSlot, bin.LE)) + return data.Bytes() +} + +func marshalEpochScheduleForParentLoader(t *testing.T, value sealevel.SysvarEpochSchedule) []byte { + t.Helper() + var data bytes.Buffer + encoder := bin.NewBinEncoder(&data) + require.NoError(t, encoder.WriteUint64(value.SlotsPerEpoch, bin.LE)) + require.NoError(t, encoder.WriteUint64(value.LeaderScheduleSlotOffset, bin.LE)) + require.NoError(t, encoder.WriteBool(value.Warmup)) + require.NoError(t, encoder.WriteUint64(value.FirstNormalEpoch, bin.LE)) + require.NoError(t, encoder.WriteUint64(value.FirstNormalSlot, bin.LE)) + return data.Bytes() +} + +func TestParentDerivedBankLoadsLifecycleSysvarsFromExactSnapshot(t *testing.T) { + parentSlot := uint64(7) + clock := sealevel.SysvarClock{Slot: parentSlot, EpochStartTimestamp: 111, UnixTimestamp: 222} + slotHashes := sealevel.SysvarSlotHashes{{Slot: 6, Hash: [32]byte{0x61}}} + recent := sealevel.SysvarRecentBlockhashes{{ + Blockhash: [32]byte{0x71}, + FeeCalculator: sealevel.FeeCalculator{ + LamportsPerSignature: 5_000, + }, + }} + slotHistory := sealevel.SysvarSlotHistory{ + Bits: sealevel.SlotHistoryBitvec{ + Bits: sealevel.SlotHistoryInner{BlocksLen: 1, Blocks: []uint64{0x81}}, + Len: 64, + }, + NextSlot: 8, + } + stakeHistory := sealevel.SysvarStakeHistory{{ + Epoch: 0, + Entry: sealevel.StakeHistoryEntry{ + Effective: 91, + }, + }} + lastRestart := sealevel.SysvarLastRestartSlot{LastRestartSlot: 3} + parentEpochSchedule := sealevel.SysvarEpochSchedule{ + SlotsPerEpoch: 100, + LeaderScheduleSlotOffset: 100, + } + + parentAccounts := []*accounts.Account{ + {Key: sealevel.SysvarClockAddr, Lamports: 1, Data: clock.MustMarshal()}, + {Key: sealevel.SysvarSlotHashesAddr, Lamports: 1, Data: slotHashes.MustMarshal()}, + {Key: sealevel.SysvarRecentBlockHashesAddr, Lamports: 1, Data: recent.MustMarshal()}, + {Key: sealevel.SysvarSlotHistoryAddr, Lamports: 1, Data: slotHistory.MustMarshal()}, + {Key: sealevel.SysvarStakeHistoryAddr, Lamports: 1, Data: marshalStakeHistoryForParentLoader(t, &stakeHistory)}, + {Key: sealevel.SysvarLastRestartSlotAddr, Lamports: 1, Data: marshalLastRestartSlotForParentLoader(t, lastRestart)}, + {Key: sealevel.SysvarEpochScheduleAddr, Lamports: 1, Data: marshalEpochScheduleForParentLoader(t, parentEpochSchedule)}, + } + parentSnapshot, err := sealevel.NewBankSysvars(parentSlot, parentAccounts...) + require.NoError(t, err) + + // Model the abandoned suffix: every global value that used to drive the + // lifecycle loader disagrees with the retained parent generation. + previousClock := sealevel.SysvarCache.Clock + previousSlotHashes := sealevel.SysvarCache.SlotHashes + previousRecent := sealevel.SysvarCache.RecentBlockHashes + t.Cleanup(func() { + sealevel.SysvarCache.Clock.Sysvar, sealevel.SysvarCache.Clock.Acct = previousClock.Sysvar, previousClock.Acct + sealevel.SysvarCache.SlotHashes.Sysvar, sealevel.SysvarCache.SlotHashes.Acct = previousSlotHashes.Sysvar, previousSlotHashes.Acct + sealevel.SysvarCache.RecentBlockHashes.Sysvar, sealevel.SysvarCache.RecentBlockHashes.Acct = previousRecent.Sysvar, previousRecent.Acct + }) + staleClock := sealevel.SysvarClock{Slot: 999, EpochStartTimestamp: 999, UnixTimestamp: 999} + staleSlotHashes := sealevel.SysvarSlotHashes{{Slot: 998, Hash: [32]byte{0xEE}}} + staleRecent := sealevel.SysvarRecentBlockhashes{{Blockhash: [32]byte{0xEF}}} + sealevel.SysvarCache.Clock.Sysvar = &staleClock + sealevel.SysvarCache.Clock.Acct = &accounts.Account{Key: sealevel.SysvarClockAddr, Lamports: 1, Data: staleClock.MustMarshal()} + sealevel.SysvarCache.SlotHashes.Sysvar = &staleSlotHashes + sealevel.SysvarCache.SlotHashes.Acct = &accounts.Account{Key: sealevel.SysvarSlotHashesAddr, Lamports: 1, Data: staleSlotHashes.MustMarshal()} + sealevel.SysvarCache.RecentBlockHashes.Sysvar = &staleRecent + sealevel.SysvarCache.RecentBlockHashes.Acct = &accounts.Account{Key: sealevel.SysvarRecentBlockHashesAddr, Lamports: 1, Data: staleRecent.MustMarshal()} + + block := &b.Block{ + Slot: 8, + ParentSlot: parentSlot, + ParentBankhash: [32]byte{0x88}, + PrevFeeRateGovernor: &sealevel.FeeRateGovernor{ + TargetLamportsPerSignature: 5_000, + LamportsPerSignature: 5_000, + }, + } + epochSchedule := &sealevel.SysvarEpochSchedule{ + SlotsPerEpoch: 100, + LeaderScheduleSlotOffset: 0, // deliberately stale external schedule + } + _, parentAccts, _, childSnapshot, err := loadBlockAccountsAndUpdateSysvars( + &parentSnapshotOnlySource{t: t}, block, epochSchedule, true, parentSnapshot, + ) + require.NoError(t, err) + + childClock, ok := childSnapshot.Clock() + require.True(t, ok) + require.Equal(t, uint64(8), childClock.Slot) + require.Equal(t, int64(111), childClock.EpochStartTimestamp) + require.Equal(t, int64(222), childClock.UnixTimestamp, + "Alpenglow bank start preserves the exact parent timestamp") + require.Equal(t, uint64(1), childClock.LeaderScheduleEpoch, + "Clock derivation uses the retained bank's EpochSchedule, not the stale external pointer") + childSlotHashes, ok := childSnapshot.SlotHashes() + require.True(t, ok) + require.Len(t, childSlotHashes, 2) + require.Equal(t, parentSlot, childSlotHashes[0].Slot) + require.Equal(t, [32]byte{0x88}, childSlotHashes[0].Hash) + require.Equal(t, uint64(6), childSlotHashes[1].Slot) + childRecent, ok := childSnapshot.RecentBlockhashes() + require.True(t, ok) + require.Equal(t, [32]byte{0x71}, childRecent[0].Blockhash, + "unchanged RecentBlockhashes are shared from the exact parent") + + for _, parentAccount := range parentAccounts { + got, getErr := parentAccts.GetAccountWithoutLock(parentAccount.Key) + require.NoError(t, getErr) + require.Equal(t, parentAccount.Data, got.Data, + "parent before-image for %s must come from retained BankSysvars", parentAccount.Key) + } +} diff --git a/pkg/replay/block.go b/pkg/replay/block.go index 309e592e..5901b4df 100644 --- a/pkg/replay/block.go +++ b/pkg/replay/block.go @@ -394,6 +394,17 @@ func extractAndDedupeBlockAccts(block *b.Block) ([]solana.PublicKey, int) { return pubkeys, writableAccountCount } +func includeAlpenglowParentStateAccounts(pubkeys []solana.PublicKey, alpenglowClock bool) []solana.PublicKey { + if !alpenglowClock { + return pubkeys + } + nanoClockAddr := NanosecondClockAccountAddr() + if slices.Contains(pubkeys, nanoClockAddr) { + return pubkeys + } + return append(pubkeys, nanoClockAddr) +} + func publicationMapCapacity(block *b.Block, uniqueWritableAccounts int, alpenglow bool) int { // This is an allocation hint, not a shard-count bound. Cap speculative // transaction capacity at the observed transfer workload's touch rate so @@ -428,6 +439,11 @@ func cacheFeesSysvar(acctsDb *accountsdb.AccountsDb) { } acct, err := acctsDb.GetAccount(0, sealevel.SysvarFeesAddr) if errors.Is(err, accountsdb.ErrNoAccount) { + // Absence is authoritative. Replay can be restarted in-process after a + // fork recovery, so retaining a value from an earlier bootstrap would + // incorrectly resurrect the disabled legacy sysvar in BankSysvars. + sealevel.SysvarCache.Fees.Sysvar = nil + sealevel.SysvarCache.Fees.Acct = nil return } if err != nil { @@ -535,16 +551,22 @@ func recordSysvarAccountReadStats(dst *metrics.AccountLoader, src accountsdb.Acc } } -func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.Block, epochSchedule *sealevel.SysvarEpochSchedule, alpenglowClock bool) (accounts.Accounts, accounts.Accounts, int, error) { +func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.Block, epochSchedule *sealevel.SysvarEpochSchedule, alpenglowClock bool, parentBankSysvars *sealevel.BankSysvars) (accounts.Accounts, accounts.Accounts, int, *sealevel.BankSysvars, error) { + var bankSysvars *sealevel.BankSysvars phaseStart := time.Now() err := resolveAddrTableLookups(accountsDb, block) metrics.GlobalBlockReplay.AccountLoader.AddressTableLookups.AddTimingSince(phaseStart) if err != nil { - return nil, nil, 0, err + return nil, nil, 0, bankSysvars, err } phaseStart = time.Now() dedupedAccts, uniqueWritableAccounts := extractAndDedupeBlockAccts(block) + // The footer-owned nanosecond clock is bank state even when no transaction + // mentions it. Pin the exact parent account (or AccountsDB's tombstone for + // absence) in the same batch snapshot as all other execution accounts. This + // preserves both the footer bounds anchor and the AccountsLtHash before-image. + dedupedAccts = includeAlpenglowParentStateAccounts(dedupedAccts, alpenglowClock) publicationCapacity := publicationMapCapacity(block, uniqueWritableAccounts, alpenglowClock) metrics.GlobalBlockReplay.AccountLoader.DedupeBlockAccounts.AddTimingSince(phaseStart) ctx := context.Background() @@ -553,7 +575,7 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B metrics.GlobalBlockReplay.AccountLoader.SourceBatch.AddTimingSince(phaseStart) recordAccountLoaderBatchStats(&metrics.GlobalBlockReplay.AccountLoader, batchStats) if err != nil { - return nil, nil, 0, err + return nil, nil, 0, bankSysvars, err } phaseStart = time.Now() @@ -562,13 +584,35 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B parentAccts := accounts.NewMemAccountsWithLen(uint64(numAccts)) for _, acct := range slotAccts { if err = parentAccts.SetAccountWithoutLock(acct.Key, acct); err != nil { - return nil, nil, 0, err + return nil, nil, 0, bankSysvars, err } } // accts is a branch-local overlay over the pristine parent snapshot; execution // copy-on-writes, so parentAccts stays pristine for LtHash "before" values. accts := accounts.NewOverlayAccountsWithSizing(parentAccts, numAccts, publicationCapacity) + if parentBankSysvars != nil { + if parentBankSysvars.Slot() != block.ParentSlot { + return nil, nil, 0, nil, fmt.Errorf( + "parent bank sysvar slot %d does not match block parent %d", + parentBankSysvars.Slot(), block.ParentSlot, + ) + } + // Pin every bank-owned sysvar to the exact immutable parent generation + // before applying this bank's lifecycle updates. This covers raw account + // reads as well as typed reads and installs explicit tombstones for sysvars + // absent in the parent, so neither AccountsDB nor a process-global cache can + // leak a newer abandoned-fork generation into this child. + if err := sealevel.RangeBankSysvarAddresses(func(key solana.PublicKey) error { + acct, ok := parentBankSysvars.AccountView(key) + if !ok { + acct = &accounts.Account{Key: key, RentEpoch: math.MaxUint64} + } + return parentAccts.SetAccountWithoutLock(key, acct) + }); err != nil { + return nil, nil, 0, nil, fmt.Errorf("install parent bank sysvars: %w", err) + } + } metrics.GlobalBlockReplay.AccountLoader.ParentMapBuild.AddTimingSince(phaseStart) phaseStart = time.Now() @@ -582,32 +626,47 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B // update and cache clock sysvar { var clockAcct *accounts.Account + var clock sealevel.SysvarClock + clockEpochSchedule := epochSchedule var err error - if sealevel.SysvarCache.Clock.Acct != nil { + if parentBankSysvars != nil { + var ok bool + clockAcct, ok = parentBankSysvars.CloneAccount(sealevel.SysvarClockAddr) + if !ok { + panic("required Clock sysvar is absent from parent bank snapshot") + } + clock, ok = parentBankSysvars.Clock() + if !ok { + panic("decoded Clock sysvar is absent from parent bank snapshot") + } + parentEpochSchedule, ok := parentBankSysvars.EpochSchedule() + if !ok { + panic("decoded EpochSchedule sysvar is absent from parent bank snapshot") + } + clockEpochSchedule = &parentEpochSchedule + } else if sealevel.SysvarCache.Clock.Acct != nil { // Prefer the in-RAM Clock (mirrors SlotHashes/RecentBlockhashes): on // resume it is the restored Clock as of the last rooted slot, which durable may not match. clockAcct = sealevel.SysvarCache.Clock.Acct.Clone() } else { clockAcct, err = loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarClockAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarClockRead) - if err != nil { - panic("unable to retrieve clock sysvar when updating clock") - } } - - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarClockAddr, clockAcct.Clone()) if err != nil { - panic("unable to set clock sysvar to accts") + panic("unable to retrieve clock sysvar when updating clock") } - decoder := bin.NewBinDecoder(clockAcct.Data) - var clock sealevel.SysvarClock - - err = clock.UnmarshalWithDecoder(decoder) - if err != nil { - panic("unable to unmarshal clock sysvar") + if parentBankSysvars == nil { + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarClockAddr, clockAcct.Clone()) + if err != nil { + panic("unable to set clock sysvar to accts") + } + err = clock.UnmarshalWithDecoder(bin.NewBinDecoder(clockAcct.Data)) + if err != nil { + panic("unable to unmarshal clock sysvar") + } } - err = updateClockSysvarForMode(&clock, block, epochSchedule, alpenglowClock) + err = updateClockSysvarForMode(&clock, block, clockEpochSchedule, alpenglowClock) if err != nil { panic(fmt.Sprintf("failed to update clock sysvar: %s", err)) } @@ -616,7 +675,6 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B copy(clockAcct.Data, newClockBytes) sealevel.SysvarCache.Clock.Sysvar = &clock sealevel.SysvarCache.Clock.Acct = clockAcct - err = accts.SetAccountWithoutLock(sealevel.SysvarClockAddr, clockAcct) if err != nil { panic("unable to set clock sysvar to accts") @@ -625,14 +683,30 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B // update and cache SlotHashes sysvar { - slotHashesAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarSlotHashesAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarSlotHashesRead) - if err != nil { - panic("unable to retrieve slothashes sysvar from acctsdb") - } - + var slotHashesAcct *accounts.Account var slotHashes sealevel.SysvarSlotHashes + var err error + if parentBankSysvars != nil { + var ok bool + slotHashesAcct, ok = parentBankSysvars.CloneAccount(sealevel.SysvarSlotHashesAddr) + if !ok { + panic("required SlotHashes sysvar is absent from parent bank snapshot") + } + parentSlotHashes, ok := parentBankSysvars.SlotHashes() + if !ok { + panic("decoded SlotHashes sysvar is absent from parent bank snapshot") + } + // Update mutates the slice; detach it while sharing every other + // decoded sysvar with the immutable parent snapshot. + slotHashes = append(sealevel.SysvarSlotHashes(nil), parentSlotHashes...) + } else { + slotHashesAcct, err = loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarSlotHashesAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarSlotHashesRead) + if err != nil { + panic("unable to retrieve slothashes sysvar from acctsdb") + } + } - if sealevel.SysvarCache.SlotHashes.Sysvar == nil { + if parentBankSysvars == nil && sealevel.SysvarCache.SlotHashes.Sysvar == nil { // Fresh start (first slot): unmarshal from AccountsDB decoder := bin.NewBinDecoder(slotHashesAcct.Data) err = slotHashes.UnmarshalWithDecoder(decoder) @@ -640,7 +714,7 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B panic("unable to unmarshal slothashes sysvar") } - } else { + } else if parentBankSysvars == nil { // SysvarCache already populated (either from resume state file or from previous slot). // The account data from AccountsDB may be stale (appendvec writes are not fsynced), // so overwrite it with the authoritative data from SysvarCache. @@ -654,19 +728,20 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B copy(slotHashesAcct.Data, newData) } - // Set parentAccts BEFORE updating slotHashes to ensure LtHash delta is computed correctly - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarSlotHashesAddr, slotHashesAcct.Clone()) - if err != nil { - panic("unable to set slothashes sysvar to accountsdb") + if parentBankSysvars == nil { + // Set parentAccts BEFORE updating slotHashes to ensure LtHash delta is computed correctly. + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarSlotHashesAddr, slotHashesAcct.Clone()) + if err != nil { + panic("unable to set slothashes sysvar to accountsdb") + } } // Now update with the new slot/bankhash slotHashes.Update(block.Slot, block.ParentSlot, block.ParentBankhash) newSlotHashesBytes := slotHashes.MustMarshal() - copy(slotHashesAcct.Data, newSlotHashesBytes) + slotHashesAcct.Data = newSlotHashesBytes sealevel.SysvarCache.SlotHashes.Sysvar = &slotHashes sealevel.SysvarCache.SlotHashes.Acct = slotHashesAcct - err = accts.SetAccountWithoutLock(sealevel.SysvarSlotHashesAddr, slotHashesAcct) if err != nil { panic("unable to set slothashes sysvar to accountsdb") @@ -675,137 +750,161 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B // cache RecentBlockhashes sysvar { - recentBlockhashesAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarRecentBlockHashesAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarRecentBlockhashesRead) - if err != nil { - panic("unable to get recentblockhashes") - } - - if sealevel.SysvarCache.RecentBlockHashes.Sysvar == nil { - // Fresh start (first slot): unmarshal from AccountsDB - decoder := bin.NewBinDecoder(recentBlockhashesAcct.Data) - var recentBlockhashes sealevel.SysvarRecentBlockhashes - recentBlockhashes.MustUnmarshalWithDecoder(decoder) - sealevel.SysvarCache.RecentBlockHashes.Sysvar = &recentBlockhashes - sealevel.SysvarCache.RecentBlockHashes.Acct = recentBlockhashesAcct - - // Debug: log the blockhash range on first load - if len(recentBlockhashes) > 0 { - mlog.Log.Infof("loaded RecentBlockhashes sysvar: %d entries, newest=%x, oldest=%x", - len(recentBlockhashes), recentBlockhashes[0].Blockhash[:8], recentBlockhashes[len(recentBlockhashes)-1].Blockhash[:8]) + if parentBankSysvars != nil { + if _, ok := parentBankSysvars.RecentBlockhashes(); !ok { + panic("required RecentBlockhashes sysvar is absent from parent bank snapshot") } } else { - // SysvarCache already populated (either from resume state file or from previous slot). - // The account data from AccountsDB may be stale (appendvec writes are not fsynced), - // so overwrite it with the authoritative data from SysvarCache. - // This ensures BPF programs reading the account data directly see correct values. - recentBlockhashes := sealevel.SysvarCache.RecentBlockHashes.Sysvar - newData := recentBlockhashes.MustMarshal() - if len(newData) != len(recentBlockhashesAcct.Data) { - panic(fmt.Sprintf("RecentBlockhashes data length mismatch: marshaled=%d, account=%d", - len(newData), len(recentBlockhashesAcct.Data))) + recentBlockhashesAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarRecentBlockHashesAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarRecentBlockhashesRead) + if err != nil { + panic("unable to get recentblockhashes") } - copy(recentBlockhashesAcct.Data, newData) - sealevel.SysvarCache.RecentBlockHashes.Acct = recentBlockhashesAcct - } - // Set parentAccts AFTER potential data correction to ensure LtHash delta is computed correctly - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarRecentBlockHashesAddr, recentBlockhashesAcct.Clone()) - if err != nil { - panic("unable to set recentblockhashes sysvar to accts") - } + if sealevel.SysvarCache.RecentBlockHashes.Sysvar == nil { + // Fresh start (first slot): unmarshal from AccountsDB + decoder := bin.NewBinDecoder(recentBlockhashesAcct.Data) + var recentBlockhashes sealevel.SysvarRecentBlockhashes + recentBlockhashes.MustUnmarshalWithDecoder(decoder) + sealevel.SysvarCache.RecentBlockHashes.Sysvar = &recentBlockhashes + sealevel.SysvarCache.RecentBlockHashes.Acct = recentBlockhashesAcct + + // Debug: log the blockhash range on first load + if len(recentBlockhashes) > 0 { + mlog.Log.Infof("loaded RecentBlockhashes sysvar: %d entries, newest=%x, oldest=%x", + len(recentBlockhashes), recentBlockhashes[0].Blockhash[:8], recentBlockhashes[len(recentBlockhashes)-1].Blockhash[:8]) + } + } else { + // SysvarCache already populated (either from resume state file or from previous slot). + // The account data from AccountsDB may be stale (appendvec writes are not fsynced), + // so overwrite it with the authoritative data from SysvarCache. + // This ensures BPF programs reading the account data directly see correct values. + recentBlockhashes := sealevel.SysvarCache.RecentBlockHashes.Sysvar + newData := recentBlockhashes.MustMarshal() + if len(newData) != len(recentBlockhashesAcct.Data) { + panic(fmt.Sprintf("RecentBlockhashes data length mismatch: marshaled=%d, account=%d", + len(newData), len(recentBlockhashesAcct.Data))) + } + copy(recentBlockhashesAcct.Data, newData) + sealevel.SysvarCache.RecentBlockHashes.Acct = recentBlockhashesAcct + } - err = accts.SetAccountWithoutLock(sealevel.SysvarRecentBlockHashesAddr, recentBlockhashesAcct) - if err != nil { - panic("unable to set recentblockhashes sysvar to accts") + // Set parentAccts AFTER potential data correction to ensure LtHash delta is computed correctly + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarRecentBlockHashesAddr, recentBlockhashesAcct.Clone()) + if err != nil { + panic("unable to set recentblockhashes sysvar to accts") + } + + err = accts.SetAccountWithoutLock(sealevel.SysvarRecentBlockHashesAddr, recentBlockhashesAcct) + if err != nil { + panic("unable to set recentblockhashes sysvar to accts") + } } } // cache SlotHistory sysvar { - slotHistoryAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarSlotHistoryAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarSlotHistoryRead) - if err != nil { - panic("unable to get slothistory") - } + if parentBankSysvars != nil { + if _, ok := parentBankSysvars.SlotHistory(); !ok { + panic("required SlotHistory sysvar is absent from parent bank snapshot") + } + } else { + slotHistoryAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarSlotHistoryAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarSlotHistoryRead) + if err != nil { + panic("unable to get slothistory") + } - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarSlotHistoryAddr, slotHistoryAcct.Clone()) - if err != nil { - panic("unable to set slothistory sysvar to accts") - } + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarSlotHistoryAddr, slotHistoryAcct.Clone()) + if err != nil { + panic("unable to set slothistory sysvar to accts") + } - decoder := bin.NewBinDecoder(slotHistoryAcct.Data) - var slotHistory sealevel.SysvarSlotHistory - slotHistory.MustUnmarshalWithDecoder(decoder) - sealevel.SysvarCache.SlotHistory.Sysvar = &slotHistory - sealevel.SysvarCache.SlotHistory.Acct = slotHistoryAcct + decoder := bin.NewBinDecoder(slotHistoryAcct.Data) + var slotHistory sealevel.SysvarSlotHistory + slotHistory.MustUnmarshalWithDecoder(decoder) + sealevel.SysvarCache.SlotHistory.Sysvar = &slotHistory + sealevel.SysvarCache.SlotHistory.Acct = slotHistoryAcct - err = accts.SetAccountWithoutLock(sealevel.SysvarSlotHistoryAddr, slotHistoryAcct) - if err != nil { - panic("unable to set clock sysvar to accts") + err = accts.SetAccountWithoutLock(sealevel.SysvarSlotHistoryAddr, slotHistoryAcct) + if err != nil { + panic("unable to set clock sysvar to accts") + } } } // cache StakeHistory sysvar { - stakeHistoryAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarStakeHistoryAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarStakeHistoryRead) - if err != nil { - panic("unable to get stakehistory") - } + if parentBankSysvars != nil { + if _, ok := parentBankSysvars.StakeHistory(); !ok { + panic("required StakeHistory sysvar is absent from parent bank snapshot") + } + } else { + stakeHistoryAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarStakeHistoryAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarStakeHistoryRead) + if err != nil { + panic("unable to get stakehistory") + } - var setStakeHistoryParent bool - if len(block.EpochUpdatedAccts) != 0 { - for _, a := range block.ParentEpochUpdatedAccts { - if a != nil { - if a.Key == sealevel.SysvarStakeHistoryAddr { - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, a.Clone()) - if err != nil { - panic("unable to set stakehistory sysvar to accts") + var setStakeHistoryParent bool + if len(block.EpochUpdatedAccts) != 0 { + for _, a := range block.ParentEpochUpdatedAccts { + if a != nil { + if a.Key == sealevel.SysvarStakeHistoryAddr { + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, a.Clone()) + if err != nil { + panic("unable to set stakehistory sysvar to accts") + } + setStakeHistoryParent = true } - setStakeHistoryParent = true } } } - } - if !setStakeHistoryParent { - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, stakeHistoryAcct.Clone()) - if err != nil { - panic("unable to set stakehistory sysvar to accts") + if !setStakeHistoryParent { + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, stakeHistoryAcct.Clone()) + if err != nil { + panic("unable to set stakehistory sysvar to accts") + } } - } - decoder := bin.NewBinDecoder(stakeHistoryAcct.Data) - var stakeHistory sealevel.SysvarStakeHistory - stakeHistory.MustUnmarshalWithDecoder(decoder) - sealevel.SysvarCache.StakeHistory.Sysvar = &stakeHistory - sealevel.SysvarCache.StakeHistory.Acct = stakeHistoryAcct + decoder := bin.NewBinDecoder(stakeHistoryAcct.Data) + var stakeHistory sealevel.SysvarStakeHistory + stakeHistory.MustUnmarshalWithDecoder(decoder) + sealevel.SysvarCache.StakeHistory.Sysvar = &stakeHistory + sealevel.SysvarCache.StakeHistory.Acct = stakeHistoryAcct - err = accts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, stakeHistoryAcct) - if err != nil { - panic("unable to set stakehistory sysvar to accts") + err = accts.SetAccountWithoutLock(sealevel.SysvarStakeHistoryAddr, stakeHistoryAcct) + if err != nil { + panic("unable to set stakehistory sysvar to accts") + } } } // cache LastRestartSlot sysvar { - lastRestartSlotAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarLastRestartSlotAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarLastRestartSlotRead) - if err != nil { - panic("unable to get last restart slot sysvar acct") - } + if parentBankSysvars != nil { + if _, ok := parentBankSysvars.LastRestartSlot(); !ok { + panic("required LastRestartSlot sysvar is absent from parent bank snapshot") + } + } else { + lastRestartSlotAcct, err := loadSysvarAccount(accountsDb, block.Slot, sealevel.SysvarLastRestartSlotAddr, &metrics.GlobalBlockReplay.AccountLoader.SysvarLastRestartSlotRead) + if err != nil { + panic("unable to get last restart slot sysvar acct") + } - err = parentAccts.SetAccountWithoutLock(sealevel.SysvarLastRestartSlotAddr, lastRestartSlotAcct.Clone()) - if err != nil { - panic("unable to set last restart slot sysvar to accts") - } + err = parentAccts.SetAccountWithoutLock(sealevel.SysvarLastRestartSlotAddr, lastRestartSlotAcct.Clone()) + if err != nil { + panic("unable to set last restart slot sysvar to accts") + } - decoder := bin.NewBinDecoder(lastRestartSlotAcct.Data) - var lastRestartSlot sealevel.SysvarLastRestartSlot - lastRestartSlot.MustUnmarshalWithDecoder(decoder) - sealevel.SysvarCache.LastRestartSlot.Sysvar = &lastRestartSlot - sealevel.SysvarCache.LastRestartSlot.Acct = lastRestartSlotAcct + decoder := bin.NewBinDecoder(lastRestartSlotAcct.Data) + var lastRestartSlot sealevel.SysvarLastRestartSlot + lastRestartSlot.MustUnmarshalWithDecoder(decoder) + sealevel.SysvarCache.LastRestartSlot.Sysvar = &lastRestartSlot + sealevel.SysvarCache.LastRestartSlot.Acct = lastRestartSlotAcct - err = accts.SetAccountWithoutLock(sealevel.SysvarLastRestartSlotAddr, lastRestartSlotAcct) - if err != nil { - panic("unable to set last restart slot sysvar to accts") + err = accts.SetAccountWithoutLock(sealevel.SysvarLastRestartSlotAddr, lastRestartSlotAcct) + if err != nil { + panic("unable to set last restart slot sysvar to accts") + } } } } @@ -827,8 +926,82 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb blockAccountSource, block *b.B } } + // The process-global cache is retained only as the ordered replay bootstrap + // source. Freeze a complete immutable bank-owned snapshot after applying + // every epoch-boundary override; transaction execution never reads the + // singleton once this snapshot is published to SlotCtx. + loadCurrentSysvar := func(key solana.PublicKey) (*accounts.Account, bool, error) { + accountKey := [32]byte(key) + acct, getErr := accts.GetAccount(&accountKey) + if getErr != nil || acct == nil { + return nil, false, nil + } + return acct, true, nil + } + if parentBankSysvars == nil { + // The first bank after bootstrap/resume converts the legacy ordered-replay + // cache once. Every subsequent bank derives from its immutable parent. + bankSysvars, err = sealevel.SnapshotLegacySysvarCache(block.Slot, loadCurrentSysvar) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("snapshot replay sysvars: %w", err) + } + var currentSysvars []*accounts.Account + if err := bankSysvars.RangeAccountViews(func(key solana.PublicKey, _ *accounts.Account) error { + if acct, found, _ := loadCurrentSysvar(key); found { + currentSysvars = append(currentSysvars, acct) + } + return nil + }); err != nil { + return nil, nil, 0, nil, fmt.Errorf("enumerate replay sysvars: %w", err) + } + bankSysvars, err = bankSysvars.WithAccounts(currentSysvars...) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("apply current replay sysvars: %w", err) + } + } else { + // Clock and SlotHashes change at bank start. Epoch/reward/feature staging + // contributes any other changed sysvar accounts explicitly. Everything + // else is immutable and shared with the parent snapshot without another + // clone or decode. + updates := make([]*accounts.Account, 0, 2+len(block.EpochUpdatedAccts)) + updateIndex := make(map[solana.PublicKey]int, cap(updates)) + addCurrent := func(key solana.PublicKey) error { + acct, found, loadErr := loadCurrentSysvar(key) + if loadErr != nil { + return loadErr + } + if !found { + return fmt.Errorf("updated bank sysvar %s is missing from slot accounts", key) + } + if idx, exists := updateIndex[key]; exists { + updates[idx] = acct + return nil + } + updateIndex[key] = len(updates) + updates = append(updates, acct) + return nil + } + if err := addCurrent(sealevel.SysvarClockAddr); err != nil { + return nil, nil, 0, nil, err + } + if err := addCurrent(sealevel.SysvarSlotHashesAddr); err != nil { + return nil, nil, 0, nil, err + } + for _, acct := range block.EpochUpdatedAccts { + if acct != nil && sealevel.IsBankSysvarAccount(acct.Key) { + if err := addCurrent(acct.Key); err != nil { + return nil, nil, 0, nil, err + } + } + } + bankSysvars, err = parentBankSysvars.Derive(block.Slot, updates...) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("derive replay bank sysvars: %w", err) + } + } + metrics.GlobalBlockReplay.AccountLoader.SysvarUpdates.AddTimingSince(phaseStart) - return accts, parentAccts, publicationCapacity, nil + return accts, parentAccts, publicationCapacity, bankSysvars, nil } func recordAccountLoaderBatchStats(dst *metrics.AccountLoader, src accountsdb.BatchReadStats) { @@ -1539,6 +1712,11 @@ func ReplayBlocks( startEpoch := epochSchedule.GetEpoch(startSlot) currentEpoch := initialReplayEpoch(epochSchedule, startSlot, mithrilState.ManifestParentSlot, resumeState) var lastSlotCtx *sealevel.SlotCtx + // Set only by a successful in-loop fork unwind. The next executable bank + // must derive from this exact surviving parent snapshot, not the legacy + // process-global cache left by the discarded suffix. Skipped slots leave it + // untouched until that bank arrives. + var unwoundParentBankSysvars *sealevel.BankSysvars var partitionedEpochRewardsEnabled bool var partitionedRewardsInfo *rewards.PartitionedRewardDistributionInfo var featuresActivatedInFirstSlot []*accounts.Account @@ -2224,7 +2402,7 @@ func ReplayBlocks( mlog.Log.Warnf("%v — block source rejected the fork rewind", sw) return false } - rs, fallbackReason := tryInLoopUnwind(sw, unrootedTailState, mithrilState, epochSchedule, currentEpoch, partitionedRewardsInfo) + rs, parentBankSysvars, fallbackReason := tryInLoopUnwind(sw, unrootedTailState, mithrilState, epochSchedule, currentEpoch, partitionedRewardsInfo) if rs == nil { windowSwitchFallback++ switchFallbackReasons[fallbackReason]++ @@ -2248,6 +2426,7 @@ func ReplayBlocks( global.DeleteAlpenglowBlockIDsFrom(sw.Slot) global.DeleteAlpenglowChainedRootsFrom(sw.Slot) resumeState = rs + unwoundParentBankSysvars = parentBankSysvars lastSlotCtx = nil // next block configures from the rebuilt resume context replayCtx.Capitalization = rs.Capitalization global.SetBlockHeight(rs.ParentBlockHeight) @@ -2733,7 +2912,11 @@ func ReplayBlocks( processBlockStart := time.Now() metrics.GlobalBlockReplay.PreprocessBlock.AddTiming(processBlockStart.Sub(start)) alpenglowClock := alpenglowMode - lastSlotCtx, err = ProcessBlock(acctsDb, block, epochSchedule, txParallelism, dbgOpts, persistedHashes, unrootedTailState, transactionStatuses, alpenglowClock) + parentBankSysvars := unwoundParentBankSysvars + if lastSlotCtx != nil { + parentBankSysvars = lastSlotCtx.BankSysvars() + } + lastSlotCtx, err = ProcessBlock(acctsDb, block, epochSchedule, txParallelism, dbgOpts, persistedHashes, unrootedTailState, transactionStatuses, alpenglowClock, parentBankSysvars) processBlockEnd := time.Now() metrics.GlobalBlockReplay.ProcessBlock.AddTiming(processBlockEnd.Sub(processBlockStart)) if err != nil { @@ -2743,6 +2926,9 @@ func ReplayBlocks( global.ClearPendingStakePubkeys() break } + // The successful child now owns its derived snapshot. Any later bank uses + // lastSlotCtx; the one-shot retained unwind bridge is no longer needed. + unwoundParentBankSysvars = nil postProcessBlockStart := processBlockEnd statusViewStart := time.Now() statuses := transactionStatuses.View() @@ -2758,9 +2944,9 @@ func ReplayBlocks( } } if unrootedTailState != nil { - UpdateChainTipFromSlotCtx(lastSlotCtx, block.Features, statuses, identity, unrootedTailState) + UpdateChainTipFromSlotCtxWithBankMetadata(lastSlotCtx, block.Features, statuses, identity, ChainTipBankMetadata{BlockHeight: block.BlockHeight}, unrootedTailState) } else { - UpdateChainTipFromSlotCtx(lastSlotCtx, block.Features, statuses, identity) + UpdateChainTipFromSlotCtxWithBankMetadata(lastSlotCtx, block.Features, statuses, identity, ChainTipBankMetadata{BlockHeight: block.BlockHeight}) } if alpenglowMode && block.HasAlpenglowBlockID { global.SetAlpenglowBlockID(block.Slot, solana.Hash(block.AlpenglowBlockID)) @@ -2811,6 +2997,16 @@ func ReplayBlocks( if unrootedTailState != nil && lastSlotCtx != nil { resumeContextStart := time.Now() txCountAtSlot := global.TransactionCount() // ProcessBlock already added this block's txs + var recentBlockhashes *sealevel.SysvarRecentBlockhashes + var slotHashes *sealevel.SysvarSlotHashes + if bankSysvars := lastSlotCtx.BankSysvars(); bankSysvars != nil { + if recent, ok := bankSysvars.RecentBlockhashes(); ok { + recentBlockhashes = &recent + } + if hashes, ok := bankSysvars.SlotHashes(); ok { + slotHashes = &hashes + } + } resumeCtx := &state.ResumeContext{ Slot: block.Slot, Bankhash: base58.Encode(lastSlotCtx.FinalBankhash), @@ -2819,8 +3015,8 @@ func ReplayBlocks( NumSignatures: lastSlotCtx.NumSignatures, EvictedBlockhash: base58.Encode(lastSlotCtx.LatestEvictedBlockhash[:]), Blockhash: base58.Encode(lastSlotCtx.Blockhash[:]), - RecentBlockhashes: EncodeRecentBlockhashes(sealevel.SysvarCache.RecentBlockHashes.Sysvar), - SlotHashes: EncodeSlotHashes(sealevel.SysvarCache.SlotHashes.Sysvar), + RecentBlockhashes: EncodeRecentBlockhashes(recentBlockhashes), + SlotHashes: EncodeSlotHashes(slotHashes), Capitalization: replayCtx.Capitalization, SlotsPerYear: replayCtx.SlotsPerYear, InflationInitial: replayCtx.Inflation.Initial, @@ -2839,16 +3035,18 @@ func ReplayBlocks( if lastSlotCtx.AcctsLtHash != nil { resumeCtx.AcctsLtHash = base64.StdEncoding.EncodeToString(lastSlotCtx.AcctsLtHash.Hash()) } - // Capture the Clock sysvar as of the last rooted slot: read from durable (not SysvarCache) - // during load, so resume must restore it or the first slot's LtHash diverges. - if sealevel.SysvarCache.Clock.Acct != nil { - resumeCtx.Clock = base64.StdEncoding.EncodeToString(sealevel.SysvarCache.Clock.Acct.Data) + // Persist the Clock from the completed bank snapshot, never from a + // process-global cache that may already be constructing another bank. + if bankSysvars := lastSlotCtx.BankSysvars(); bankSysvars != nil { + if raw, ok := bankSysvars.RawView(sealevel.SysvarClockAddr); ok { + resumeCtx.Clock = base64.StdEncoding.EncodeToString(raw) + } } if lastSlotCtx.FeeRateGovernor != nil { resumeCtx.LamportsPerSignature = lastSlotCtx.FeeRateGovernor.LamportsPerSignature resumeCtx.PrevLamportsPerSig = lastSlotCtx.FeeRateGovernor.PrevLamportsPerSignature } - unrootedTailState.SetContext(block.Slot, resumeCtx) + unrootedTailState.SetContext(block.Slot, resumeCtx, lastSlotCtx.BankSysvars()) metrics.GlobalBlockReplay.ResumeContext.AddTimingSince(resumeContextStart) } @@ -2880,10 +3078,18 @@ func ReplayBlocks( result.LastPrevLamportsPerSig = lastSlotCtx.FeeRateGovernor.PrevLamportsPerSignature } result.LastNumSignatures = lastSlotCtx.NumSignatures - result.LastRecentBlockhashes = sealevel.SysvarCache.RecentBlockHashes.Sysvar + if bankSysvars := lastSlotCtx.BankSysvars(); bankSysvars != nil { + if recent, ok := bankSysvars.RecentBlockhashes(); ok { + copyRecent := append(sealevel.SysvarRecentBlockhashes(nil), recent...) + result.LastRecentBlockhashes = ©Recent + } + if slotHashes, ok := bankSysvars.SlotHashes(); ok { + copySlotHashes := append(sealevel.SysvarSlotHashes(nil), slotHashes...) + result.LastSlotHashes = ©SlotHashes + } + } result.LastEvictedBlockhash = lastSlotCtx.LatestEvictedBlockhash result.LastBlockhash = lastSlotCtx.Blockhash - result.LastSlotHashes = sealevel.SysvarCache.SlotHashes.Sysvar } // Capture ReplayCtx fields for resume independence from stale manifest @@ -3230,13 +3436,20 @@ func ReplayBlocks( } result.LastNumSignatures = lastSlotCtx.NumSignatures - // Capture blockhash context from SysvarCache (required because appendvec writes are not fsynced) - result.LastRecentBlockhashes = sealevel.SysvarCache.RecentBlockHashes.Sysvar + // Capture blockhash context from the completed bank snapshot because + // appendvec writes are not necessarily fsynced yet. + if bankSysvars := lastSlotCtx.BankSysvars(); bankSysvars != nil { + if recent, ok := bankSysvars.RecentBlockhashes(); ok { + copyRecent := append(sealevel.SysvarRecentBlockhashes(nil), recent...) + result.LastRecentBlockhashes = ©Recent + } + if slotHashes, ok := bankSysvars.SlotHashes(); ok { + copySlotHashes := append(sealevel.SysvarSlotHashes(nil), slotHashes...) + result.LastSlotHashes = ©SlotHashes + } + } result.LastEvictedBlockhash = lastSlotCtx.LatestEvictedBlockhash result.LastBlockhash = lastSlotCtx.Blockhash - - // Capture SlotHashes context (same issue, vote program needs accurate slot→hash mappings) - result.LastSlotHashes = sealevel.SysvarCache.SlotHashes.Sysvar } // Capture ReplayCtx fields for resume independence from stale manifest @@ -3262,7 +3475,7 @@ func runIncinerator(slotCtx *sealevel.SlotCtx) { func compileWritableAndModifiedAccts(slotCtx *sealevel.SlotCtx, block *b.Block, rentAccts []*accounts.Account) ([]*accounts.Account, []*accounts.Account) { adhRemoved := accountsDeltaHashRemoved(slotCtx) - sysvarAccts := collectAndUpdateSysvarAcctsForAdh(slotCtx) + sysvarAccts := collectSysvarAcctsForAdh(slotCtx) var writableAccts []*accounts.Account var alreadyAdded map[solana.PublicKey]bool if !adhRemoved { @@ -3741,13 +3954,16 @@ func parallelTxLoop(slotCtx *sealevel.SlotCtx, sigverifyWg *sync.WaitGroup, bloc if txFeeInfo == nil { // This happens when IsTransactionAgeValid returns false (blockhash not found) tx := block.Transactions[idx] - recentBlockhashes := sealevel.SysvarCache.RecentBlockHashes.Sysvar + var recentBlockhashes sealevel.SysvarRecentBlockhashes + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + recentBlockhashes, _ = bankSysvars.RecentBlockhashes() + } mlog.Log.Errorf("txFeeInfo is nil for tx %s in slot %d", tx.Signatures[0], block.Slot) mlog.Log.Errorf(" tx blockhash: %s", tx.Message.RecentBlockhash) mlog.Log.Errorf(" LatestEvictedBlockhash: %x", slotCtx.LatestEvictedBlockhash[:8]) - if recentBlockhashes != nil && len(*recentBlockhashes) > 0 { + if len(recentBlockhashes) > 0 { mlog.Log.Errorf(" RecentBlockhashes: %d entries, newest=%x, oldest=%x", - len(*recentBlockhashes), (*recentBlockhashes)[0].Blockhash[:8], (*recentBlockhashes)[len(*recentBlockhashes)-1].Blockhash[:8]) + len(recentBlockhashes), recentBlockhashes[0].Blockhash[:8], recentBlockhashes[len(recentBlockhashes)-1].Blockhash[:8]) } else { mlog.Log.Errorf(" RecentBlockhashes: nil or empty!") } @@ -3807,6 +4023,7 @@ func ProcessBlock( tail unrootedState, transactionStatuses *TransactionStatusCache, alpenglowClock bool, + parentBankSysvars *sealevel.BankSysvars, ) (*sealevel.SlotCtx, error) { if block == nil { return nil, errors.New("validate transaction messages: nil block") @@ -3899,15 +4116,31 @@ func ProcessBlock( if tail != nil { blockSrc = tail } - accts, parentAccts, accountMapCapacity, err := loadBlockAccountsAndUpdateSysvars(blockSrc, block, epochSchedule, alpenglowClock) + accts, parentAccts, accountMapCapacity, bankSysvars, err := loadBlockAccountsAndUpdateSysvars(blockSrc, block, epochSchedule, alpenglowClock, parentBankSysvars) loadAcctsRegion.End() if err != nil { panic(fmt.Sprintf("unable to load slot accounts and update sysvars: %s", err)) } + if err := bankSysvars.ValidateForExecution(); err != nil { + return nil, fmt.Errorf("invalid bank sysvar snapshot at slot %d: %w", block.Slot, err) + } metrics.GlobalBlockReplay.LoadBlockAccounts.AddTimingSince(start) slotCtxSetupStart := time.Now() slotCtx := newSlotCtx(block, accts, parentAccts, acctsDb, tail, accountMapCapacity) + if err := slotCtx.PublishBankSysvars(bankSysvars); err != nil { + return nil, fmt.Errorf("publish bank sysvars at slot %d: %w", block.Slot, err) + } + bankEpochScheduleValue, ok := bankSysvars.EpochSchedule() + if !ok { + return nil, fmt.Errorf("bank-local EpochSchedule sysvar unavailable at slot %d", block.Slot) + } + bankEpochSchedule := &bankEpochScheduleValue + if requireAlpenglowBlockFooter(block, slotCtx, alpenglowClock) { + if err := validateAlpenglowFooterNanosecondClock(slotCtx, block); err != nil { + return nil, err + } + } slotCtx.TraceCtx = ctx slotCtx.NumSignatures = executionPlan.processedSignatures metrics.GlobalBlockReplay.SlotCtxSetup.AddTimingSince(slotCtxSetupStart) @@ -3943,8 +4176,11 @@ func ProcessBlock( start = time.Now() setReplayStage("collect_rent") - rentSysvar := sealevel.SysvarCache.Rent.Sysvar - rentAccts := rent.CollectRentEagerly(slotCtx, rentSysvar, epochSchedule) + bankRent, ok := slotCtx.BankSysvars().Rent() + if !ok { + return nil, fmt.Errorf("bank-local Rent sysvar unavailable at slot %d", block.Slot) + } + rentAccts := rent.CollectRentEagerly(slotCtx, &bankRent, bankEpochSchedule) metrics.GlobalBlockReplay.Rent.AddTimingSince(start) start = time.Now() @@ -3955,7 +4191,7 @@ func ProcessBlock( // Alpenglow banks set the Clock timestamp from the block footer after execution. if alpenglowClock { footerClockStart := time.Now() - if err := applyAlpenglowFooterClock(slotCtx, block, epochSchedule); err != nil { + if err := applyAlpenglowFooterClock(slotCtx, block, bankEpochSchedule); err != nil { metrics.GlobalBlockReplay.AlpenglowFooterClock.AddTimingSince(footerClockStart) return nil, fmt.Errorf("apply alpenglow footer clock at slot %d: %w", block.Slot, err) } @@ -3965,12 +4201,15 @@ func ProcessBlock( } metrics.GlobalBlockReplay.AlpenglowFooterClock.AddTimingSince(footerClockStart) voteRewardsStart := time.Now() - voteRewardsErr := ApplyAlpenglowVoteRewards(slotCtx, block, epochSchedule, block.SkipRewardCert, block.NotarRewardCert, block.BlockFinalCert, block.AlpenglowShredVersion) + voteRewardsErr := ApplyAlpenglowVoteRewards(slotCtx, block, bankEpochSchedule, block.SkipRewardCert, block.NotarRewardCert, block.BlockFinalCert, block.AlpenglowShredVersion) metrics.GlobalBlockReplay.AlpenglowVoteRewards.AddTimingSince(voteRewardsStart) if voteRewardsErr != nil { return nil, voteRewardsErr } } + if err := finalizeBankSysvars(slotCtx); err != nil { + return nil, fmt.Errorf("finalize bank sysvars at slot %d: %w", block.Slot, err) + } setReplayStage("compile_accounts") start = time.Now() diff --git a/pkg/replay/chain_state.go b/pkg/replay/chain_state.go index 7021814c..b876ef14 100644 --- a/pkg/replay/chain_state.go +++ b/pkg/replay/chain_state.go @@ -3,6 +3,7 @@ package replay import ( "sync" + "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/features" "github.com/Overclock-Validator/mithril/pkg/lthash" "github.com/Overclock-Validator/mithril/pkg/sealevel" @@ -24,10 +25,24 @@ type ChainTipSnapshot struct { PrevNumSigs uint64 PrevFeeGovernor *sealevel.FeeRateGovernor LastEntryHash solana.Hash + LastBlockhash solana.Hash + BlockHeight uint64 + LatestEvictedBlockhash [32]byte EpochRewardsActive bool + BankSysvars *sealevel.BankSysvars + EpochStakes map[solana.PublicKey]uint64 // immutable for the published epoch + TotalEpochStake uint64 + NanosecondClockAccount *accounts.Account + HasNanosecondClockAccount bool UnrootedRead sealevel.AccountReader } +// ChainTipBankMetadata contains replay-bank scalars that are not carried by +// SlotCtx but must be published in the same generation as its account state. +type ChainTipBankMetadata struct { + BlockHeight uint64 +} + // ChainTipIdentity is the Alpenglow identity committed by the same replay // transition as the parent bank, accounts view, and transaction-status view. // A producer must never recover these fields from process-wide maps after it @@ -54,7 +69,15 @@ var ( chainTipTransactionStatuses *TransactionStatusView chainTipPrevNumSigs uint64 chainTipLastEntryHash solana.Hash + chainTipLastBlockhash solana.Hash + chainTipBlockHeight uint64 + chainTipLatestEvictedBlockhash [32]byte chainTipEpochRewardsActive bool + chainTipBankSysvars *sealevel.BankSysvars + chainTipEpochStakes map[solana.PublicKey]uint64 + chainTipTotalEpochStake uint64 + chainTipNanosecondClockAccount *accounts.Account + chainTipHasNanosecondClockAccount bool chainTipPrevFeeGovernor *sealevel.FeeRateGovernor chainTipUnrootedRead sealevel.AccountReader ) @@ -75,7 +98,15 @@ func InitChainTip(acctsLtHash *lthash.LtHash, f *features.Features, prevNumSigs chainTipTransactionStatuses = nil chainTipPrevNumSigs = 0 chainTipLastEntryHash = solana.Hash{} + chainTipLastBlockhash = solana.Hash{} + chainTipBlockHeight = 0 + chainTipLatestEvictedBlockhash = [32]byte{} chainTipEpochRewardsActive = false + chainTipBankSysvars = nil + chainTipEpochStakes = nil + chainTipTotalEpochStake = 0 + chainTipNanosecondClockAccount = nil + chainTipHasNanosecondClockAccount = false chainTipPrevFeeGovernor = nil chainTipUnrootedRead = nil if acctsLtHash != nil { @@ -87,6 +118,7 @@ func InitChainTip(acctsLtHash *lthash.LtHash, f *features.Features, prevNumSigs chainTipPrevNumSigs = prevNumSigs if lastEntryHash != (solana.Hash{}) { chainTipLastEntryHash = lastEntryHash + chainTipLastBlockhash = lastEntryHash } if len(statusViews) > 0 { chainTipTransactionStatuses = statusViews[0] @@ -101,6 +133,12 @@ func ResetChainTip() { // UpdateChainTipFromSlotCtx refreshes blockprod parent context from replay progress. func UpdateChainTipFromSlotCtx(slotCtx *sealevel.SlotCtx, f *features.Features, statuses *TransactionStatusView, identity ChainTipIdentity, readers ...sealevel.AccountReader) { + UpdateChainTipFromSlotCtxWithBankMetadata(slotCtx, f, statuses, identity, ChainTipBankMetadata{}, readers...) +} + +// UpdateChainTipFromSlotCtxWithBankMetadata atomically publishes a complete +// replay-parent generation for block production. +func UpdateChainTipFromSlotCtxWithBankMetadata(slotCtx *sealevel.SlotCtx, f *features.Features, statuses *TransactionStatusView, identity ChainTipIdentity, metadata ChainTipBankMetadata, readers ...sealevel.AccountReader) { if slotCtx == nil { return } @@ -120,6 +158,17 @@ func UpdateChainTipFromSlotCtx(slotCtx *sealevel.SlotCtx, f *features.Features, chainTipAcctsLtHash = nil chainTipFeatures = nil chainTipLastEntryHash = solana.Hash{} + chainTipLastBlockhash = solana.Hash{} + chainTipBlockHeight = metadata.BlockHeight + chainTipLatestEvictedBlockhash = slotCtx.LatestEvictedBlockhash + chainTipBankSysvars = slotCtx.BankSysvars() + // VoteAccts is bank-owned epoch stake state. Replay replaces the map when + // entering a new epoch and execution never mutates it, so sharing the + // immutable map avoids copying the full validator set on every slot. + chainTipEpochStakes = slotCtx.VoteAccts + chainTipTotalEpochStake = slotCtx.TotalEpochStake + chainTipNanosecondClockAccount = nil + chainTipHasNanosecondClockAccount = false chainTipPrevFeeGovernor = nil if len(readers) > 0 { chainTipUnrootedRead = readers[0] @@ -139,9 +188,20 @@ func UpdateChainTipFromSlotCtx(slotCtx *sealevel.SlotCtx, f *features.Features, chainTipPrevNumSigs = slotCtx.NumSignatures if slotCtx.Blockhash != ([32]byte{}) { chainTipLastEntryHash = solana.Hash(slotCtx.Blockhash) + chainTipLastBlockhash = solana.Hash(slotCtx.Blockhash) + } + if slotCtx.Accounts != nil { + if nanoClock, err := slotCtx.GetAccount(NanosecondClockAccountAddr()); err == nil && nanoClock != nil && nanoClock.Lamports > 0 { + chainTipNanosecondClockAccount = nanoClock.Clone() + chainTipHasNanosecondClockAccount = true + } } - if rewards := sealevel.SysvarCache.EpochRewards.Sysvar; rewards != nil { - chainTipEpochRewardsActive = rewards.Active + if chainTipBankSysvars != nil { + if rewards, ok := chainTipBankSysvars.EpochRewards(); ok { + chainTipEpochRewardsActive = rewards.Active + } else { + chainTipEpochRewardsActive = false + } } else { chainTipEpochRewardsActive = false } @@ -169,7 +229,14 @@ func ChainTipParentContext() ChainTipSnapshot { HasAlpenglowChainedMerkleRoot: chainTipHasAlpenglowChainedMerkleRoot, PrevNumSigs: chainTipPrevNumSigs, LastEntryHash: chainTipLastEntryHash, + LastBlockhash: chainTipLastBlockhash, + BlockHeight: chainTipBlockHeight, + LatestEvictedBlockhash: chainTipLatestEvictedBlockhash, EpochRewardsActive: chainTipEpochRewardsActive, + BankSysvars: chainTipBankSysvars, + EpochStakes: chainTipEpochStakes, + TotalEpochStake: chainTipTotalEpochStake, + HasNanosecondClockAccount: chainTipHasNanosecondClockAccount, UnrootedRead: chainTipUnrootedRead, TransactionStatuses: chainTipTransactionStatuses, } @@ -183,5 +250,8 @@ func ChainTipParentContext() ChainTipSnapshot { gov := *chainTipPrevFeeGovernor ctx.PrevFeeGovernor = &gov } + if chainTipNanosecondClockAccount != nil { + ctx.NanosecondClockAccount = chainTipNanosecondClockAccount.Clone() + } return ctx } diff --git a/pkg/replay/chain_state_test.go b/pkg/replay/chain_state_test.go index 7db30604..cbc9263e 100644 --- a/pkg/replay/chain_state_test.go +++ b/pkg/replay/chain_state_test.go @@ -1,8 +1,10 @@ package replay import ( + "encoding/binary" "testing" + "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/features" "github.com/Overclock-Validator/mithril/pkg/lthash" "github.com/Overclock-Validator/mithril/pkg/sealevel" @@ -20,14 +22,30 @@ func TestChainTipTracksReplayedSlot(t *testing.T) { updated := new(lthash.LtHash).InitWithHash(make([]byte, 2048)) updated.Add(parentLtHash) + epochStakeKey := solana.PublicKey{8} + nanoClock := &accounts.Account{Key: NanosecondClockAccountAddr(), Lamports: 1, Data: make([]byte, 8)} + binary.LittleEndian.PutUint64(nanoClock.Data, 1234) + clock := sealevel.SysvarClock{Slot: 200, UnixTimestamp: 5678} + bankSysvars, err := sealevel.NewBankSysvars(200, &accounts.Account{ + Key: sealevel.SysvarClockAddr, + Lamports: 1, + Data: clock.MustMarshal(), + }) + require.NoError(t, err) slotCtx := &sealevel.SlotCtx{ - Slot: 200, - NumSignatures: 11, - AcctsLtHash: updated, - Features: feats, - FinalBankhash: append([]byte{7}, make([]byte, 31)...), - Blockhash: solana.Hash{9}, + Slot: 200, + Accounts: accounts.NewMemAccounts(), + NumSignatures: 11, + AcctsLtHash: updated, + Features: feats, + FinalBankhash: append([]byte{7}, make([]byte, 31)...), + Blockhash: solana.Hash{9}, + LatestEvictedBlockhash: [32]byte{6}, + VoteAccts: map[solana.PublicKey]uint64{epochStakeKey: 55}, + TotalEpochStake: 99, } + require.NoError(t, slotCtx.PublishBankSysvars(bankSysvars)) + require.NoError(t, slotCtx.SetAccount(nanoClock.Key, nanoClock)) statuses := NewTransactionStatusCache().View() identity := ChainTipIdentity{ AlpenglowBlockID: solana.Hash{3}, @@ -35,7 +53,8 @@ func TestChainTipTracksReplayedSlot(t *testing.T) { AlpenglowChainedMerkleRoot: solana.Hash{4}, HasAlpenglowChainedMerkleRoot: true, } - UpdateChainTipFromSlotCtx(slotCtx, feats, statuses, identity) + UpdateChainTipFromSlotCtxWithBankMetadata(slotCtx, feats, statuses, identity, ChainTipBankMetadata{BlockHeight: 190}) + binary.LittleEndian.PutUint64(nanoClock.Data, 9999) tip := ChainTipParentContext() require.Greater(t, tip.Generation, initialGeneration) @@ -46,6 +65,18 @@ func TestChainTipTracksReplayedSlot(t *testing.T) { require.Equal(t, identity.AlpenglowChainedMerkleRoot, tip.AlpenglowChainedMerkleRoot) require.True(t, tip.HasAlpenglowChainedMerkleRoot) require.Equal(t, solana.Hash{9}, tip.LastEntryHash) + require.Equal(t, solana.Hash{9}, tip.LastBlockhash) + require.Equal(t, uint64(190), tip.BlockHeight) + require.Equal(t, [32]byte{6}, tip.LatestEvictedBlockhash) + require.Equal(t, uint64(55), tip.EpochStakes[epochStakeKey]) + require.Equal(t, uint64(99), tip.TotalEpochStake) + require.True(t, tip.HasNanosecondClockAccount) + require.NotNil(t, tip.NanosecondClockAccount) + require.Equal(t, uint64(1234), binary.LittleEndian.Uint64(tip.NanosecondClockAccount.Data)) + require.Same(t, bankSysvars, tip.BankSysvars) + gotClock, ok := tip.BankSysvars.Clock() + require.True(t, ok) + require.Equal(t, clock, gotClock) require.Equal(t, uint64(11), tip.PrevNumSigs) require.NotNil(t, tip.AcctsLtHash) require.True(t, tip.AcctsLtHash.Equals(updated)) @@ -70,6 +101,35 @@ func TestResetChainTipClearsTransactionStatuses(t *testing.T) { require.Nil(t, after.TransactionStatuses) require.False(t, after.HasAlpenglowBlockID) require.False(t, after.HasAlpenglowChainedMerkleRoot) + require.Nil(t, after.BankSysvars) + require.Nil(t, after.EpochStakes) + require.Zero(t, after.TotalEpochStake) + require.Zero(t, after.BlockHeight) + require.Zero(t, after.LastBlockhash) + require.Zero(t, after.LatestEvictedBlockhash) + require.False(t, after.HasNanosecondClockAccount) + require.Nil(t, after.NanosecondClockAccount) +} + +func TestChainTipPreservesPrefundedNanosecondClockAccount(t *testing.T) { + t.Cleanup(ResetChainTip) + nanoClock := &accounts.Account{ + Key: NanosecondClockAccountAddr(), + Lamports: 42, + Owner: [32]byte{7}, + // An empty data payload is valid before the first footer populates the + // known PDA and is still part of the AccountsLtHash before-image. + } + slotCtx := &sealevel.SlotCtx{Slot: 12, Accounts: accounts.NewMemAccounts()} + require.NoError(t, slotCtx.SetAccount(nanoClock.Key, nanoClock)) + UpdateChainTipFromSlotCtx(slotCtx, nil, nil, ChainTipIdentity{}) + + tip := ChainTipParentContext() + require.True(t, tip.HasNanosecondClockAccount) + require.NotNil(t, tip.NanosecondClockAccount) + require.Equal(t, nanoClock.Lamports, tip.NanosecondClockAccount.Lamports) + require.Equal(t, nanoClock.Owner, tip.NanosecondClockAccount.Owner) + require.Empty(t, tip.NanosecondClockAccount.Data) } func TestInitChainTipFailsClosedWithoutCompleteReplayParent(t *testing.T) { diff --git a/pkg/replay/epoch.go b/pkg/replay/epoch.go index a35ab25b..6289169c 100644 --- a/pkg/replay/epoch.go +++ b/pkg/replay/epoch.go @@ -431,6 +431,9 @@ func updateEpochStakesAndRefreshVoteCache(leaderScheduleEpoch uint64, b *block.B } global.PutEpochStakes(leaderScheduleEpoch, epochStakes, epochVoteAccounts, totalEffectiveStake) - maps.Copy(b.EpochStakesPerVoteAcct, epochStakes) + // Epoch-stake maps are immutable bank snapshot state and are shared by every + // slot in an epoch (including a concurrently forged leader bank). Allocate a + // new map at the epoch boundary instead of mutating the parent's generation. + b.EpochStakesPerVoteAcct = maps.Clone(epochStakes) b.TotalEpochStake = totalEffectiveStake } diff --git a/pkg/replay/leader_finalize.go b/pkg/replay/leader_finalize.go index c8aca5dd..34b1b9b8 100644 --- a/pkg/replay/leader_finalize.go +++ b/pkg/replay/leader_finalize.go @@ -11,7 +11,6 @@ import ( "github.com/Overclock-Validator/mithril/pkg/fees" "github.com/Overclock-Validator/mithril/pkg/rent" "github.com/Overclock-Validator/mithril/pkg/sealevel" - bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" ) @@ -23,7 +22,6 @@ type CommitLeaderInput struct { AcctsDb *accountsdb.AccountsDb SlotCtx *sealevel.SlotCtx Block *b.Block - EpochSchedule *sealevel.SysvarEpochSchedule TxFeeAccumulator fees.TxFeeInfoAccumulator AlpenglowClock bool AlpenglowShredVersion uint16 @@ -31,70 +29,61 @@ type CommitLeaderInput struct { FooterProducerTimeNanos uint64 } -// PrepareLeaderSlotSysvars creates slot-local sysvar copies before TPU -// transactions execute. It never mutates the process-global sysvar cache. -func PrepareLeaderSlotSysvars(slotCtx *sealevel.SlotCtx, block *b.Block, epochSchedule *sealevel.SysvarEpochSchedule, alpenglowClock bool) error { - if slotCtx == nil || block == nil || epochSchedule == nil { +// PrepareLeaderSlotSysvars derives the child bank's dynamic sysvars from the +// immutable parent-bank snapshot installed by NewLeaderSlotCtx. It never +// consults the mutable process-global cache or unrooted account tail. +func PrepareLeaderSlotSysvars(slotCtx *sealevel.SlotCtx, block *b.Block, alpenglowClock bool) error { + if slotCtx == nil || block == nil { return fmt.Errorf("missing leader slot preparation input") } if slotCtx.ParentAccts == nil { slotCtx.ParentAccts = accounts.NewMemAccounts() } - clockAcct, err := leaderParentAccount(slotCtx, sealevel.SysvarClockAddr) - if err != nil { - return fmt.Errorf("load clock sysvar: %w", err) + bankSysvars := slotCtx.BankSysvars() + if bankSysvars == nil { + return fmt.Errorf("leader slot %d has no pinned bank sysvar snapshot", slotCtx.Slot) + } + bankEpochSchedule, ok := bankSysvars.EpochSchedule() + if !ok { + return fmt.Errorf("leader parent snapshot has no EpochSchedule sysvar") + } + epochSchedule := &bankEpochSchedule + clock, ok := bankSysvars.Clock() + if !ok { + return fmt.Errorf("leader parent snapshot has no Clock sysvar") } - var clock sealevel.SysvarClock - if err := clock.UnmarshalWithDecoder(bin.NewBinDecoder(clockAcct.Data)); err != nil { - return fmt.Errorf("decode clock sysvar: %w", err) + clockAcct, ok := bankSysvars.CloneAccount(sealevel.SysvarClockAddr) + if !ok { + return fmt.Errorf("leader parent snapshot has no Clock account") } if err := updateClockSysvarForMode(&clock, block, epochSchedule, alpenglowClock); err != nil { return err } - if err := installLeaderSysvar(slotCtx, clockAcct, clock.MustMarshal()); err != nil { + clockAcct.Data = clock.MustMarshal() + if err := slotCtx.SetAccount(clockAcct.Key, clockAcct); err != nil { return err } - slotHashesAcct, err := leaderParentAccount(slotCtx, sealevel.SysvarSlotHashesAddr) - if err != nil { - return fmt.Errorf("load slot hashes sysvar: %w", err) + slotHashes, ok := bankSysvars.SlotHashes() + if !ok { + return fmt.Errorf("leader parent snapshot has no SlotHashes sysvar") } - var slotHashes sealevel.SysvarSlotHashes - if sealevel.SysvarCache.SlotHashes.Sysvar != nil { - slotHashes = append(slotHashes, (*sealevel.SysvarCache.SlotHashes.Sysvar)...) - copy(slotHashesAcct.Data, slotHashes.MustMarshal()) - if err := replaceLeaderParent(slotCtx, slotHashesAcct); err != nil { - return err - } - } else if err := slotHashes.UnmarshalWithDecoder(bin.NewBinDecoder(slotHashesAcct.Data)); err != nil { - return fmt.Errorf("decode slot hashes sysvar: %w", err) + slotHashes = append(sealevel.SysvarSlotHashes(nil), slotHashes...) + slotHashesAcct, ok := bankSysvars.CloneAccount(sealevel.SysvarSlotHashesAddr) + if !ok { + return fmt.Errorf("leader parent snapshot has no SlotHashes account") } slotHashes.Update(block.Slot, block.ParentSlot, block.ParentBankhash) - if err := setLeaderCurrent(slotCtx, slotHashesAcct, slotHashes.MustMarshal()); err != nil { - return err - } - - recentAcct, err := leaderParentAccount(slotCtx, sealevel.SysvarRecentBlockHashesAddr) - if err != nil { - return fmt.Errorf("load recent blockhashes sysvar: %w", err) - } - if sealevel.SysvarCache.RecentBlockHashes.Sysvar != nil { - recent := append(sealevel.SysvarRecentBlockhashes(nil), (*sealevel.SysvarCache.RecentBlockHashes.Sysvar)...) - copy(recentAcct.Data, recent.MustMarshal()) - if err := replaceLeaderParent(slotCtx, recentAcct); err != nil { - return err - } - } - if err := setLeaderCurrent(slotCtx, recentAcct, recentAcct.Data); err != nil { + slotHashesAcct.Data = slotHashes.MustMarshal() + if err := slotCtx.SetAccount(slotHashesAcct.Key, slotHashesAcct); err != nil { return err } - - slotHistoryAcct, err := leaderParentAccount(slotCtx, sealevel.SysvarSlotHistoryAddr) + bankSysvars, err := bankSysvars.WithAccounts(clockAcct, slotHashesAcct) if err != nil { - return fmt.Errorf("load slot history sysvar: %w", err) + return fmt.Errorf("derive leader bank sysvars: %w", err) } - if err := setLeaderCurrent(slotCtx, slotHistoryAcct, slotHistoryAcct.Data); err != nil { + if err := slotCtx.PublishBankSysvars(bankSysvars); err != nil { return err } return nil @@ -103,10 +92,19 @@ func PrepareLeaderSlotSysvars(slotCtx *sealevel.SlotCtx, block *b.Block, epochSc // CommitLeaderSlot freezes a forged bank and computes the footer bank hash. It // does not write AccountsDB, update global replay progress, or bypass forkchoice. func CommitLeaderSlot(in CommitLeaderInput) (*sealevel.SlotCtx, error) { - if in.AcctsDb == nil || in.SlotCtx == nil || in.Block == nil || in.EpochSchedule == nil { + if in.AcctsDb == nil || in.SlotCtx == nil || in.Block == nil { return nil, fmt.Errorf("missing leader finalization input") } slotCtx, block := in.SlotCtx, in.Block + bankSysvars := slotCtx.BankSysvars() + if bankSysvars == nil { + return nil, fmt.Errorf("leader slot %d has no bank sysvar snapshot", slotCtx.Slot) + } + bankEpochSchedule, ok := bankSysvars.EpochSchedule() + if !ok { + return nil, fmt.Errorf("leader slot %d has no bank-local EpochSchedule sysvar", slotCtx.Slot) + } + epochSchedule := &bankEpochSchedule block.FooterProducerTimeNanos = in.FooterProducerTimeNanos block.UnixTimestamp = in.FooterTimestamp slotCtx.Blockhash = block.Blockhash @@ -114,23 +112,27 @@ func CommitLeaderSlot(in CommitLeaderInput) (*sealevel.SlotCtx, error) { slotCtx.FeeRateGovernor = block.FeeRateGovernor slotCtx.NumSignatures = block.NumSignatures - if _, err := slotCtx.GetAccount(sealevel.SysvarClockAddr); err != nil { - if err := PrepareLeaderSlotSysvars(slotCtx, block, in.EpochSchedule, in.AlpenglowClock); err != nil { + preparedClock, hasPreparedClock := slotCtx.BankSysvars().Clock() + if !hasPreparedClock || preparedClock.Slot != block.Slot { + if err := PrepareLeaderSlotSysvars(slotCtx, block, in.AlpenglowClock); err != nil { return nil, err } } if in.AlpenglowClock { + if err := validateAlpenglowFooterNanosecondClock(slotCtx, block); err != nil { + return nil, err + } // This is a speculative producer bank until the forged block passes // through ordered replay. Keep its Clock slot-local: publishing it to the // global replay cache here would replace the true parent Clock before // ProcessBlock loads the block and would produce a different LtHash. - if err := applyAlpenglowFooterClockLocal(slotCtx, block, in.EpochSchedule); err != nil { + if err := applyAlpenglowFooterClockLocal(slotCtx, block, epochSchedule); err != nil { return nil, err } if err := updateAlpenglowNanosecondClockAccount(slotCtx, block); err != nil { return nil, err } - if err := ApplyAlpenglowVoteRewards(slotCtx, block, in.EpochSchedule, block.SkipRewardCert, block.NotarRewardCert, block.BlockFinalCert, in.AlpenglowShredVersion); err != nil { + if err := ApplyAlpenglowVoteRewards(slotCtx, block, epochSchedule, block.SkipRewardCert, block.NotarRewardCert, block.BlockFinalCert, in.AlpenglowShredVersion); err != nil { return nil, err } } @@ -139,7 +141,16 @@ func CommitLeaderSlot(in CommitLeaderInput) (*sealevel.SlotCtx, error) { slotCtx.LamportsBurnt = fees.DistributeTxFeesToSlotLeader(in.AcctsDb, slotCtx, block.Leader, &in.TxFeeAccumulator) slotCtx.RecordModifiedAcct(block.Leader) } - rentAccts := rent.CollectRentEagerly(slotCtx, sealevel.SysvarCache.Rent.Sysvar, in.EpochSchedule) + var rentSysvar *sealevel.SysvarRent + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + if bankRent, ok := bankSysvars.Rent(); ok { + rentSysvar = &bankRent + } + } + if rentSysvar == nil { + return nil, fmt.Errorf("leader slot %d has no bank-local Rent sysvar", slotCtx.Slot) + } + rentAccts := rent.CollectRentEagerly(slotCtx, rentSysvar, epochSchedule) runIncinerator(slotCtx) if err := finishLeaderSysvars(slotCtx, block); err != nil { return nil, err @@ -155,68 +166,16 @@ func CommitLeaderSlot(in CommitLeaderInput) (*sealevel.SlotCtx, error) { } func finishLeaderSysvars(slotCtx *sealevel.SlotCtx, block *b.Block) error { - recentAcct, err := slotCtx.GetAccount(sealevel.SysvarRecentBlockHashesAddr) - if err != nil { - return err - } - var recent sealevel.SysvarRecentBlockhashes - recent.MustUnmarshalWithDecoder(bin.NewBinDecoder(recentAcct.Data)) - slotCtx.LatestEvictedBlockhash = recent.PushLatest(block.Blockhash, slotCtx.FeeRateGovernor.LamportsPerSignature) - if err := slotCtx.SetAccount(sealevel.SysvarRecentBlockHashesAddr, withData(recentAcct, recent.MustMarshal())); err != nil { + if err := finalizeBankSysvars(slotCtx); err != nil { return err } slotCtx.RecordModifiedAcct(sealevel.SysvarRecentBlockHashesAddr) - - historyAcct, err := slotCtx.GetAccount(sealevel.SysvarSlotHistoryAddr) - if err != nil { - return err - } - var history sealevel.SysvarSlotHistory - history.MustUnmarshalWithDecoder(bin.NewBinDecoder(historyAcct.Data)) - history.Add(block.Slot) - history.SetNextSlot(block.Slot + 1) - if err := slotCtx.SetAccount(sealevel.SysvarSlotHistoryAddr, withData(historyAcct, history.MustMarshal())); err != nil { - return err - } slotCtx.RecordModifiedAcct(sealevel.SysvarSlotHistoryAddr) slotCtx.RecordModifiedAcct(sealevel.SysvarClockAddr) slotCtx.RecordModifiedAcct(sealevel.SysvarSlotHashesAddr) return nil } -func leaderParentAccount(slotCtx *sealevel.SlotCtx, key solana.PublicKey) (*accounts.Account, error) { - acct, err := slotCtx.GetAccountFromAccountsDb(key) - if err != nil { - return nil, err - } - acct = acct.Clone() - if err := replaceLeaderParent(slotCtx, acct); err != nil { - return nil, err - } - return acct.Clone(), nil -} - -func replaceLeaderParent(slotCtx *sealevel.SlotCtx, acct *accounts.Account) error { - return slotCtx.ParentAccts.SetAccountWithoutLock(acct.Key, acct.Clone()) -} - -func installLeaderSysvar(slotCtx *sealevel.SlotCtx, acct *accounts.Account, data []byte) error { - if err := replaceLeaderParent(slotCtx, acct); err != nil { - return err - } - return setLeaderCurrent(slotCtx, acct, data) -} - -func setLeaderCurrent(slotCtx *sealevel.SlotCtx, acct *accounts.Account, data []byte) error { - return slotCtx.SetAccount(acct.Key, withData(acct, data)) -} - -func withData(acct *accounts.Account, data []byte) *accounts.Account { - out := acct.Clone() - out.Data = append(out.Data[:0], data...) - return out -} - func ensureParentAccountsForModified(slotCtx *sealevel.SlotCtx, modified []*accounts.Account) error { if slotCtx.Features == nil || !slotCtx.Features.IsActive(features.AccountsLtHash) { return nil diff --git a/pkg/replay/lean_writable_fastpath_test.go b/pkg/replay/lean_writable_fastpath_test.go index 6a582476..c6c95b72 100644 --- a/pkg/replay/lean_writable_fastpath_test.go +++ b/pkg/replay/lean_writable_fastpath_test.go @@ -435,7 +435,6 @@ func TestCompileWritableAndModifiedAcctsGatesWritableListOnADH(t *testing.T) { require.NoError(t, slotCtx.SetAccount(epochAcct.Key, epochAcct)) rentAcct := &accounts.Account{Key: solana.PublicKey{0xF2}, Lamports: 1} - previousSlotHistory := sealevel.SysvarCache.SlotHistory.Sysvar slotHistory := sealevel.SysvarSlotHistory{ Bits: sealevel.SlotHistoryBitvec{ Bits: sealevel.SlotHistoryInner{BlocksLen: 1, Blocks: []uint64{0}}, @@ -443,8 +442,6 @@ func TestCompileWritableAndModifiedAcctsGatesWritableListOnADH(t *testing.T) { }, NextSlot: slotCtx.Slot, } - sealevel.SysvarCache.SlotHistory.Sysvar = &slotHistory - defer func() { sealevel.SysvarCache.SlotHistory.Sysvar = previousSlotHistory }() clock := sealevel.SysvarClock{} slotHashes := sealevel.SysvarSlotHashes{} @@ -454,12 +451,11 @@ func TestCompileWritableAndModifiedAcctsGatesWritableListOnADH(t *testing.T) { {Key: sealevel.SysvarSlotHashesAddr, Lamports: 1, Data: slotHashes.MustMarshal()}, {Key: sealevel.SysvarSlotHistoryAddr, Lamports: 1, Data: slotHistory.MustMarshal()}, } - originalRecentData := append([]byte(nil), sysvarAccts[1].Data...) - originalSlotHistoryData := append([]byte(nil), sysvarAccts[3].Data...) for _, acct := range sysvarAccts { require.NoError(t, slotCtx.SetAccount(acct.Key, acct)) } slotCtx.Blockhash = [32]byte{0xA5} + require.NoError(t, finalizeBankSysvars(slotCtx)) block := &b.Block{EpochUpdatedAccts: []*accounts.Account{epochAcct}} writable, modified := compileWritableAndModifiedAccts(slotCtx, block, []*accounts.Account{rentAcct}) @@ -488,24 +484,28 @@ func TestCompileWritableAndModifiedAcctsGatesWritableListOnADH(t *testing.T) { compiledSlotHistory := modifiedByKey[sealevel.SysvarSlotHistoryAddr] require.NotNil(t, compiledRecent) require.NotNil(t, compiledSlotHistory) - expectedRecentData := sealevel.SysvarCache.RecentBlockHashes.Sysvar.MustMarshal() + bankRecent, ok := slotCtx.BankSysvars().RecentBlockhashes() + require.True(t, ok) + expectedRecentData := bankRecent.MustMarshal() assert.Equal(t, expectedRecentData, compiledRecent.Data[:len(expectedRecentData)], - "bank-hash input must retain the cloned RecentBlockhashes update") - assert.Equal(t, slotHistory.MustMarshal(), compiledSlotHistory.Data, - "bank-hash input must retain the cloned SlotHistory update") + "bank-hash input must retain the finalized RecentBlockhashes update") + bankHistory, ok := slotCtx.BankSysvars().SlotHistory() + require.True(t, ok) + assert.Equal(t, bankHistory.MustMarshal(), compiledSlotHistory.Data, + "bank-hash input must retain the finalized SlotHistory update") storedRecent, err := slotCtx.GetAccount(sealevel.SysvarRecentBlockHashesAddr) require.NoError(t, err) storedSlotHistory, err := slotCtx.GetAccount(sealevel.SysvarSlotHistoryAddr) require.NoError(t, err) - assert.Equal(t, originalRecentData, storedRecent.Data, - "the test must exercise a clone-only sysvar update, not an overlay write") - assert.Equal(t, originalSlotHistoryData, storedSlotHistory.Data, - "the test must exercise a clone-only sysvar update, not an overlay write") - - assert.Equal(t, slotCtx.Slot+1, slotHistory.NextSlot, "required sysvar updates must still run when ADH is removed") - assert.NotZero(t, slotHistory.Bits.Bits.Blocks[0]&(uint64(1)<<(slotCtx.Slot%64))) - assert.Equal(t, slotCtx.Blockhash, (*sealevel.SysvarCache.RecentBlockHashes.Sysvar)[0].Blockhash) + assert.Equal(t, compiledRecent.Data, storedRecent.Data, + "frozen SlotCtx and bank-hash input must use identical RecentBlockhashes bytes") + assert.Equal(t, compiledSlotHistory.Data, storedSlotHistory.Data, + "frozen SlotCtx and bank-hash input must use identical SlotHistory bytes") + + assert.Equal(t, slotCtx.Slot+1, bankHistory.NextSlot, "required sysvar updates must still run when ADH is removed") + assert.NotZero(t, bankHistory.Bits.Bits.Blocks[0]&(uint64(1)<<(slotCtx.Slot%64))) + assert.Equal(t, slotCtx.Blockhash, bankRecent[0].Blockhash) }) } } diff --git a/pkg/replay/promotion.go b/pkg/replay/promotion.go index d41802d9..c750009e 100644 --- a/pkg/replay/promotion.go +++ b/pkg/replay/promotion.go @@ -11,6 +11,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/accountsdb" "github.com/Overclock-Validator/mithril/pkg/global" "github.com/Overclock-Validator/mithril/pkg/mlog" + "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/Overclock-Validator/mithril/pkg/state" "github.com/gagliardetto/solana-go" ) @@ -108,7 +109,7 @@ func getAccountsBatchSharedWithStats(ctx context.Context, source blockAccountSou type unrootedState interface { blockAccountSource Add(slot uint64, delta []*accounts.Account, bankhash []byte) - SetContext(slot uint64, ctx *state.ResumeContext) + SetContext(slot uint64, ctx *state.ResumeContext, bankSysvars ...*sealevel.BankSysvars) promote(through uint64) (uint64, *state.ResumeContext, error) // flush force-folds the trailing partial chunk <= through. Epoch-boundary // scans use it to settle the durable AccountsDB view; graceful shutdown uses @@ -129,7 +130,14 @@ type unrootedTail struct { // contexts holds the deep-copied end-of-slot resume context per held slot, // retained until promotion so the context as of the last rooted slot survives for resume. contexts map[uint64]*state.ResumeContext - haltCap int // halt replay if held slots exceed this (rooting stalled) + // bankSysvars is the in-memory-only immutable sysvar snapshot paired with + // each retained context. It is deliberately not part of ResumeContext (and + // therefore never enters persisted JSON): the durable resume path rebuilds + // its first snapshot from AccountsDB, while an in-loop fork unwind needs the + // exact surviving unrooted parent rather than the process-global cache left + // by the abandoned suffix. + bankSysvars map[uint64]*sealevel.BankSysvars + haltCap int // halt replay if held slots exceed this (rooting stalled) transactionStatusCheckpointHooks TransactionStatusCheckpointHooks } @@ -146,6 +154,7 @@ func newUnrootedTail(durable blockAccountSource, committer batchCommitter, haltC batchSlots: batchSlots, stakeIdxDir: stakeIdxDir, contexts: make(map[uint64]*state.ResumeContext), + bankSysvars: make(map[uint64]*sealevel.BankSysvars), haltCap: haltCap, } } @@ -284,11 +293,19 @@ func (t *unrootedTail) Add(slot uint64, delta []*accounts.Account, bankhash []by t.bankhashes[slot] = slotBankhash } -// SetContext attaches a held slot's end-of-slot resume context. ctx MUST be -// deep-copied (no pointers into the global SysvarCache); retained until promotion. -func (t *unrootedTail) SetContext(slot uint64, ctx *state.ResumeContext) { +// SetContext attaches a held slot's end-of-slot resume context and, when +// supplied, its immutable bank-local sysvar snapshot. ctx MUST be deep-copied +// (no pointers into the global SysvarCache); both values are retained until +// promotion. The variadic snapshot preserves compatibility for test/bootstrap +// callers that only exercise durable context persistence; such entries are +// intentionally ineligible for an in-loop fork unwind. +func (t *unrootedTail) SetContext(slot uint64, ctx *state.ResumeContext, bankSysvars ...*sealevel.BankSysvars) { if ctx != nil { t.contexts[slot] = ctx + delete(t.bankSysvars, slot) + if len(bankSysvars) > 0 && bankSysvars[0] != nil { + t.bankSysvars[slot] = bankSysvars[0] + } } } @@ -316,6 +333,11 @@ func (t *unrootedTail) promoteChunked(through uint64, force bool) (uint64, *stat delete(t.contexts, s) } } + for s := range t.bankSysvars { + if s <= promotedThrough { + delete(t.bankSysvars, s) + } + } return promotedThrough, ctx, err } @@ -482,6 +504,11 @@ func (t *unrootedTail) applyFoldJob(job *foldJob) *state.ResumeContext { delete(t.contexts, s) } } + for s := range t.bankSysvars { + if s <= job.through { + delete(t.bankSysvars, s) + } + } return job.ctx } @@ -561,11 +588,11 @@ func (p *asyncPromoter) stop() { } // unwind drops all held slots >= fromSlot (the execute-on-receipt fork -// switch) and returns the retained resume context of the last surviving slot -// so the replay loop can rebuild execution state and re-run the certified -// version. Returns nil when no context for fromSlot-1 is retained (caller -// falls back to the rooted-checkpoint re-replay). -func (t *unrootedTail) unwind(fromSlot uint64) *state.ResumeContext { +// switch) and returns the retained resume context and immutable bank-sysvar +// snapshot of the last surviving executed slot so the replay loop can rebuild +// execution state and re-run the certified version. Either result may be nil; +// the caller validates the pair and falls back to rooted-checkpoint re-replay. +func (t *unrootedTail) unwind(fromSlot uint64) (*state.ResumeContext, *sealevel.BankSysvars) { t.overlay.EvictFrom(fromSlot) // Branch-scoped side effect: stake pubkeys enqueued by the evicted slots // must never reach the durable index — drop them with the state. @@ -578,13 +605,20 @@ func (t *unrootedTail) unwind(fromSlot uint64) *state.ResumeContext { } } var ctx *state.ResumeContext + var ctxSlot uint64 for s, c := range t.contexts { if s >= fromSlot { delete(t.contexts, s) continue } - if ctx == nil || s > ctx.Slot { + if ctx == nil || s > ctxSlot { ctx = c + ctxSlot = s + } + } + for s := range t.bankSysvars { + if s >= fromSlot { + delete(t.bankSysvars, s) } } // ctx is the highest retained context with slot < fromSlot: the ACTUAL @@ -593,7 +627,10 @@ func (t *unrootedTail) unwind(fromSlot uint64) *state.ResumeContext { // slots call SetContext), so the last executed slot IS the parent bank of the // certified block at fromSlot. Returning nil (parent already durably folded, // or nothing retained) routes the caller to the rooted-checkpoint fallback. - return ctx + if ctx == nil { + return nil, nil + } + return ctx, t.bankSysvars[ctxSlot] } // OverCap reports whether the unrooted tail has grown past the halt cap, i.e. diff --git a/pkg/replay/promotion_test.go b/pkg/replay/promotion_test.go index b743c006..ed430eef 100644 --- a/pkg/replay/promotion_test.go +++ b/pkg/replay/promotion_test.go @@ -400,11 +400,11 @@ func TestUnrootedTailOverCap(t *testing.T) { func TestUnrootedTailContextCaptureAndPromote(t *testing.T) { tail := newUnrootedTail(&fakeDurable{}, &fakeCommitter{durable: accounts.NewMemAccounts()}, 512, 1, "") tail.Add(5, []*accounts.Account{testAccount(1, 51)}, testHashBytes(5)) - tail.SetContext(5, &state.ResumeContext{Slot: 5, Bankhash: "bh5"}) + tail.SetContext(5, &state.ResumeContext{Slot: 5, Bankhash: "bh5"}, testUnwindBankSysvars(t, 5, 50)) tail.Add(7, []*accounts.Account{testAccount(2, 72)}, testHashBytes(7)) - tail.SetContext(7, &state.ResumeContext{Slot: 7, Bankhash: "bh7"}) + tail.SetContext(7, &state.ResumeContext{Slot: 7, Bankhash: "bh7"}, testUnwindBankSysvars(t, 7, 70)) tail.Add(9, []*accounts.Account{testAccount(3, 93)}, testHashBytes(9)) - tail.SetContext(9, &state.ResumeContext{Slot: 9, Bankhash: "bh9"}) + tail.SetContext(9, &state.ResumeContext{Slot: 9, Bankhash: "bh9"}, testUnwindBankSysvars(t, 9, 90)) promotedThrough, ctx, err := tail.promote(7) require.NoError(t, err) @@ -418,6 +418,9 @@ func TestUnrootedTailContextCaptureAndPromote(t *testing.T) { assert.False(t, has5, "promoted context pruned") assert.False(t, has7, "promoted context pruned") assert.True(t, has9, "still-held context retained") + assert.NotContains(t, tail.bankSysvars, uint64(5), "promoted sysvar snapshot pruned") + assert.NotContains(t, tail.bankSysvars, uint64(7), "promoted sysvar snapshot pruned") + assert.Contains(t, tail.bankSysvars, uint64(9), "still-held sysvar snapshot retained") } // Nothing to promote (through below all held slots) is a no-op, not an error. diff --git a/pkg/replay/publication_capacity_test.go b/pkg/replay/publication_capacity_test.go index a171b7fa..df5cbe5d 100644 --- a/pkg/replay/publication_capacity_test.go +++ b/pkg/replay/publication_capacity_test.go @@ -73,3 +73,12 @@ func TestExtractAndDedupeBlockAcctsPublicationCapacity(t *testing.T) { assert.Equal(t, 5+nonTransactionCapacity+len(block.EpochStakesPerVoteAcct), publicationMapCapacity(block, uniqueWritableAccounts, true)) assert.Equal(t, len(block.Transactions)*expectedTouchedAccountsPerTransaction+nonTransactionCapacity, publicationMapCapacity(block, 20, false)) } + +func TestIncludeAlpenglowParentStateAccountsPinsNanosecondClockOnce(t *testing.T) { + other := solana.PublicKey{1} + nanoClock := NanosecondClockAccountAddr() + + require.Equal(t, []solana.PublicKey{other}, includeAlpenglowParentStateAccounts([]solana.PublicKey{other}, false)) + require.Equal(t, []solana.PublicKey{other, nanoClock}, includeAlpenglowParentStateAccounts([]solana.PublicKey{other}, true)) + require.Equal(t, []solana.PublicKey{other, nanoClock}, includeAlpenglowParentStateAccounts([]solana.PublicKey{other, nanoClock}, true)) +} diff --git a/pkg/replay/sysvar.go b/pkg/replay/sysvar.go index bb17284f..7f168b7d 100644 --- a/pkg/replay/sysvar.go +++ b/pkg/replay/sysvar.go @@ -10,6 +10,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/safemath" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/Overclock-Validator/wide" + bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/tidwall/btree" ) @@ -206,7 +207,86 @@ func calculateStakeWeightedTimestamp( return estimate, nil } -func collectAndUpdateSysvarAcctsForAdh(slotCtx *sealevel.SlotCtx) []*accounts.Account { +// finalizeBankSysvars applies the bank-end updates that are shared by replay +// and local production. The account overlay and the immutable bank sysvar +// snapshot are replaced together before either bank hashing or chain-tip +// publication, so no consumer can observe the pre-finalization bytes. +func finalizeBankSysvars(slotCtx *sealevel.SlotCtx) error { + if slotCtx == nil { + return fmt.Errorf("missing slot context while finalizing bank sysvars") + } + + recentAcct, err := slotCtx.GetAccount(sealevel.SysvarRecentBlockHashesAddr) + if err != nil { + return fmt.Errorf("get RecentBlockhashes sysvar: %w", err) + } + var recent sealevel.SysvarRecentBlockhashes + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + if cached, ok := bankSysvars.RecentBlockhashes(); ok { + recent = append(sealevel.SysvarRecentBlockhashes(nil), cached...) + } + } + if recent == nil { + recent.MustUnmarshalWithDecoder(bin.NewBinDecoder(recentAcct.Data)) + } + slotCtx.LatestEvictedBlockhash = recent.PushLatest(slotCtx.Blockhash, slotCtx.FeeRateGovernor.LamportsPerSignature) + recentAcct.Data = recent.MustMarshal() + + historyAcct, err := slotCtx.GetAccount(sealevel.SysvarSlotHistoryAddr) + if err != nil { + return fmt.Errorf("get SlotHistory sysvar: %w", err) + } + var history sealevel.SysvarSlotHistory + hasCachedHistory := false + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + if cached, ok := bankSysvars.SlotHistory(); ok { + history = cached + history.Bits.Bits.Blocks = append([]uint64(nil), cached.Bits.Bits.Blocks...) + hasCachedHistory = true + } + } + if !hasCachedHistory { + history.MustUnmarshalWithDecoder(bin.NewBinDecoder(historyAcct.Data)) + } + history.Add(slotCtx.Slot) + history.SetNextSlot(slotCtx.Slot + 1) + historyAcct.Data = history.MustMarshal() + + if err := slotCtx.SetAccount(recentAcct.Key, recentAcct); err != nil { + return fmt.Errorf("write RecentBlockhashes sysvar: %w", err) + } + if err := slotCtx.SetAccount(historyAcct.Key, historyAcct); err != nil { + return fmt.Errorf("write SlotHistory sysvar: %w", err) + } + + bankSysvars := slotCtx.BankSysvars() + if bankSysvars == nil { + bankSysvars, err = sealevel.NewBankSysvars(slotCtx.Slot, recentAcct, historyAcct) + } else { + bankSysvars, err = bankSysvars.WithAccounts(recentAcct, historyAcct) + } + if err != nil { + return fmt.Errorf("update finalized bank sysvar snapshot: %w", err) + } + if err := slotCtx.PublishBankSysvars(bankSysvars); err != nil { + return err + } + + // The legacy singleton remains an ordered-replay bootstrap/checkpoint aid. + // Never publish speculative producer state into it. + if slotCtx.Replay { + recentForLegacy := append(sealevel.SysvarRecentBlockhashes(nil), recent...) + historyForLegacy := history + historyForLegacy.Bits.Bits.Blocks = append([]uint64(nil), history.Bits.Bits.Blocks...) + sealevel.SysvarCache.RecentBlockHashes.Sysvar = &recentForLegacy + sealevel.SysvarCache.RecentBlockHashes.Acct = recentAcct.Clone() + sealevel.SysvarCache.SlotHistory.Sysvar = &historyForLegacy + sealevel.SysvarCache.SlotHistory.Acct = historyAcct.Clone() + } + return nil +} + +func collectSysvarAcctsForAdh(slotCtx *sealevel.SlotCtx) []*accounts.Account { sysvarPubkeys := []solana.PublicKey{sealevel.SysvarClockAddr, sealevel.SysvarRecentBlockHashesAddr, sealevel.SysvarSlotHashesAddr, sealevel.SysvarSlotHistoryAddr} var sysvarAccts []*accounts.Account @@ -216,21 +296,6 @@ func collectAndUpdateSysvarAcctsForAdh(slotCtx *sealevel.SlotCtx) []*accounts.Ac panic(fmt.Sprintf("unable to get sysvar account for ADH: %s", pk)) } - if acct.Key == sealevel.SysvarSlotHistoryAddr { - slotHistory := sealevel.SysvarCache.SlotHistory.Sysvar - slotHistory.Add(slotCtx.Slot) - slotHistory.SetNextSlot(slotCtx.Slot + 1) - newSlotHistoryBytes := slotHistory.MustMarshal() - copy(acct.Data, newSlotHistoryBytes) - } - - if acct.Key == sealevel.SysvarRecentBlockHashesAddr { - recentBlockhashes := sealevel.SysvarCache.RecentBlockHashes.Sysvar - slotCtx.LatestEvictedBlockhash = recentBlockhashes.PushLatest(slotCtx.Blockhash, slotCtx.FeeRateGovernor.LamportsPerSignature) - newRecentBlockhashesBytes := recentBlockhashes.MustMarshal() - copy(acct.Data, newRecentBlockhashesBytes) - } - sysvarAccts = append(sysvarAccts, acct) } return sysvarAccts diff --git a/pkg/replay/sysvar_cache_test.go b/pkg/replay/sysvar_cache_test.go index 54537a7a..516516e0 100644 --- a/pkg/replay/sysvar_cache_test.go +++ b/pkg/replay/sysvar_cache_test.go @@ -3,6 +3,8 @@ package replay import ( "testing" + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/accountsdb" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/stretchr/testify/require" ) @@ -17,3 +19,20 @@ func TestCacheFeesSysvar_MissingAccountNoPanic(t *testing.T) { require.Nil(t, sealevel.SysvarCache.Fees.Sysvar) require.Nil(t, sealevel.SysvarCache.Fees.Acct) } + +func TestCacheFeesSysvar_MissingAccountClearsStaleValue(t *testing.T) { + prev := sealevel.SysvarCache.Fees + t.Cleanup(func() { sealevel.SysvarCache.Fees = prev }) + stale := sealevel.SysvarFees{FeeCalculator: sealevel.FeeCalculator{LamportsPerSignature: 5_000}} + sealevel.SysvarCache.Fees.Sysvar = &stale + sealevel.SysvarCache.Fees.Acct = &accounts.Account{ + Key: sealevel.SysvarFeesAddr, Lamports: 1, Data: []byte{1}, + } + + emptyDb := &accountsdb.AccountsDb{} + emptyDb.InitCaches() + cacheFeesSysvar(emptyDb) + + require.Nil(t, sealevel.SysvarCache.Fees.Sysvar) + require.Nil(t, sealevel.SysvarCache.Fees.Acct) +} diff --git a/pkg/replay/sysvar_recent_blockhashes.go b/pkg/replay/sysvar_recent_blockhashes.go index 37bf2965..d77b9266 100644 --- a/pkg/replay/sysvar_recent_blockhashes.go +++ b/pkg/replay/sysvar_recent_blockhashes.go @@ -37,9 +37,9 @@ func SeedRecentBlockhashesCache(recent sealevel.SysvarRecentBlockhashes) { sealevel.SysvarCache.RecentBlockHashes.Sysvar = &recent } -// cloneRecentBlockhashesFromCache returns a copy of the in-memory RecentBlockhashes deque. -// SysvarCache is the authoritative source during replay and leader production; AccountsDB -// may hold a snapshot-era sysvar account but it is not updated reliably enough to reload from. +// cloneRecentBlockhashesFromCache returns a copy of the legacy ordered-replay +// bootstrap deque. Executing replay and leader banks use SlotCtx.BankSysvars; +// this singleton must not be treated as transaction-visible bank state. func cloneRecentBlockhashesFromCache() (sealevel.SysvarRecentBlockhashes, error) { if sealevel.SysvarCache.RecentBlockHashes.Sysvar == nil { return nil, fmt.Errorf("RecentBlockhashes sysvar cache is nil") diff --git a/pkg/replay/transaction.go b/pkg/replay/transaction.go index d67a53bf..0ca65875 100644 --- a/pkg/replay/transaction.go +++ b/pkg/replay/transaction.go @@ -60,6 +60,20 @@ func (discardLogger) Log(string) {} func newExecCtx(slotCtx *sealevel.SlotCtx, transactionAccts *sealevel.TransactionAccounts, computeBudgetLimits *sealevel.ComputeBudgetLimits, log sealevel.Logger) *sealevel.ExecutionCtx { txCtx := sealevel.NewTransactionCtx(*transactionAccts, maxStackCapacity, maxInstrTraceCapacity) + if bankSysvars := slotCtx.BankSysvars(); bankSysvars != nil { + bankRent, ok := bankSysvars.Rent() + if !ok { + // Production bank construction validates the complete snapshot once + // before entering the transaction loop. Do not silently execute with a + // zero Rent value if an isolated caller violates that invariant. + panic(fmt.Sprintf("bank sysvar snapshot for slot %d is missing Rent", slotCtx.Slot)) + } + txCtx.Rent = bankRent + } else if sealevel.SysvarCache.Rent.Sysvar != nil { + // Compatibility for isolated legacy test/simulation contexts. Production + // replay and leader banks always publish a complete bank snapshot. + txCtx.Rent = *sealevel.SysvarCache.Rent.Sysvar + } execCtx := &sealevel.ExecutionCtx{Log: log, TransactionContext: txCtx, ComputeMeter: cu.NewComputeMeter(uint64(computeBudgetLimits.ComputeUnitLimit)), PrevLamportsPerSignature: slotCtx.FeeRateGovernor.PrevLamportsPerSignature} execCtx.Features = *slotCtx.Features diff --git a/pkg/replay/transaction_bank_sysvar_test.go b/pkg/replay/transaction_bank_sysvar_test.go new file mode 100644 index 00000000..c5d91106 --- /dev/null +++ b/pkg/replay/transaction_bank_sysvar_test.go @@ -0,0 +1,53 @@ +package replay + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/stretchr/testify/require" +) + +func TestNewExecCtxUsesBankLocalRent(t *testing.T) { + bankRent := sealevel.SysvarRent{ + LamportsPerUint8Year: 12_345, + ExemptionThreshold: 3.5, + BurnPercent: 17, + } + snapshot, err := sealevel.NewBankSysvars(42, &accounts.Account{ + Key: sealevel.SysvarRentAddr, Lamports: 1, Data: bankRent.MustMarshal(), + }) + require.NoError(t, err) + slotCtx := &sealevel.SlotCtx{ + Slot: 42, + Features: features.NewFeaturesDefault(), + FeeRateGovernor: &sealevel.FeeRateGovernor{}, + } + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + + previousRent := sealevel.SysvarCache.Rent.Sysvar + conflictingRent := sealevel.NewDefaultRentSysvar() + sealevel.SysvarCache.Rent.Sysvar = &conflictingRent + t.Cleanup(func() { sealevel.SysvarCache.Rent.Sysvar = previousRent }) + + execCtx := newExecCtx(slotCtx, &sealevel.TransactionAccounts{}, &sealevel.ComputeBudgetLimits{}, nil) + require.Equal(t, bankRent, execCtx.TransactionContext.Rent) +} + +func TestNewExecCtxRejectsBankSnapshotWithoutRent(t *testing.T) { + snapshot, err := sealevel.NewBankSysvars(42, &accounts.Account{ + Key: sealevel.SysvarClockAddr, Lamports: 1, Data: (&sealevel.SysvarClock{Slot: 42}).MustMarshal(), + }) + require.NoError(t, err) + slotCtx := &sealevel.SlotCtx{ + Slot: 42, + Features: features.NewFeaturesDefault(), + FeeRateGovernor: &sealevel.FeeRateGovernor{}, + } + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + + require.PanicsWithValue(t, "bank sysvar snapshot for slot 42 is missing Rent", func() { + newExecCtx(slotCtx, &sealevel.TransactionAccounts{}, &sealevel.ComputeBudgetLimits{}, nil) + }) +} diff --git a/pkg/replay/transaction_status_test.go b/pkg/replay/transaction_status_test.go index bb915855..7264049d 100644 --- a/pkg/replay/transaction_status_test.go +++ b/pkg/replay/transaction_status_test.go @@ -97,7 +97,7 @@ func TestProcessBlockRejectsDuplicateMessagesBeforeAccountAccess(t *testing.T) { require.NoError(t, err) block := &b.Block{Slot: 77, Transactions: []*solana.Transaction{tx, tx}} - _, err = ProcessBlock(nil, block, nil, 0, nil, nil, nil, NewTransactionStatusCache(), false) + _, err = ProcessBlock(nil, block, nil, 0, nil, nil, nil, NewTransactionStatusCache(), false, nil) var duplicateErr *DuplicateTransactionMessagesError require.Error(t, err) require.True(t, errors.As(err, &duplicateErr)) diff --git a/pkg/sealevel/bank_sysvars.go b/pkg/sealevel/bank_sysvars.go new file mode 100644 index 00000000..67f8acf3 --- /dev/null +++ b/pkg/sealevel/bank_sysvars.go @@ -0,0 +1,572 @@ +package sealevel + +import ( + "bytes" + "fmt" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +// bankSysvarID is the stable index of a bank-owned sysvar. Instructions is not +// included: that sysvar is synthesized separately for each transaction. +type bankSysvarID uint8 + +const ( + bankSysvarClock bankSysvarID = iota + bankSysvarRent + bankSysvarEpochSchedule + bankSysvarEpochRewards + bankSysvarSlotHashes + bankSysvarStakeHistory + bankSysvarLastRestartSlot + bankSysvarRecentBlockhashes + bankSysvarSlotHistory + bankSysvarFees + bankSysvarCount +) + +type bankSysvarMask uint16 + +// BankSysvars is an immutable, bank-owned view of all cached sysvars. It owns +// the account pointers in accounts and the decoded values below. Slice-bearing +// decoded values are immutable views; callers that update a sysvar must first +// copy it and publish a derived snapshot. +// +// A derived snapshot shallowly shares all unchanged accounts and decoded slice +// backing arrays with its parent. This is safe because there is no mutating API +// on BankSysvars and keeps per-slot work proportional to the sysvars that +// actually changed. +type BankSysvars struct { + slot uint64 + accounts [bankSysvarCount]*accounts.Account + decoded bankSysvarMask + + clock SysvarClock + rent SysvarRent + epochSchedule SysvarEpochSchedule + epochRewards SysvarEpochRewards + slotHashes SysvarSlotHashes + stakeHistory SysvarStakeHistory + lastRestartSlot SysvarLastRestartSlot + recentBlockhashes SysvarRecentBlockhashes + slotHistory SysvarSlotHistory + fees SysvarFees +} + +// SysvarAccountLoader supplies an account template while converting the +// process-global legacy cache at bootstrap. found=false represents a sysvar +// that does not exist on this cluster (most commonly the deprecated Fees +// sysvar). +type SysvarAccountLoader func(solana.PublicKey) (acct *accounts.Account, found bool, err error) + +func bankSysvarIDForAddress(pubkey solana.PublicKey) (bankSysvarID, bool) { + switch [32]byte(pubkey) { + case SysvarClockAddr: + return bankSysvarClock, true + case SysvarRentAddr: + return bankSysvarRent, true + case SysvarEpochScheduleAddr: + return bankSysvarEpochSchedule, true + case SysvarEpochRewardsAddr: + return bankSysvarEpochRewards, true + case SysvarSlotHashesAddr: + return bankSysvarSlotHashes, true + case SysvarStakeHistoryAddr: + return bankSysvarStakeHistory, true + case SysvarLastRestartSlotAddr: + return bankSysvarLastRestartSlot, true + case SysvarRecentBlockHashesAddr: + return bankSysvarRecentBlockhashes, true + case SysvarSlotHistoryAddr: + return bankSysvarSlotHistory, true + case SysvarFeesAddr: + return bankSysvarFees, true + default: + return 0, false + } +} + +func bankSysvarAddress(id bankSysvarID) solana.PublicKey { + switch id { + case bankSysvarClock: + return solana.PublicKey(SysvarClockAddr) + case bankSysvarRent: + return solana.PublicKey(SysvarRentAddr) + case bankSysvarEpochSchedule: + return solana.PublicKey(SysvarEpochScheduleAddr) + case bankSysvarEpochRewards: + return solana.PublicKey(SysvarEpochRewardsAddr) + case bankSysvarSlotHashes: + return solana.PublicKey(SysvarSlotHashesAddr) + case bankSysvarStakeHistory: + return solana.PublicKey(SysvarStakeHistoryAddr) + case bankSysvarLastRestartSlot: + return solana.PublicKey(SysvarLastRestartSlotAddr) + case bankSysvarRecentBlockhashes: + return solana.PublicKey(SysvarRecentBlockHashesAddr) + case bankSysvarSlotHistory: + return solana.PublicKey(SysvarSlotHistoryAddr) + case bankSysvarFees: + return solana.PublicKey(SysvarFeesAddr) + default: + return solana.PublicKey{} + } +} + +// IsBankSysvarAccount reports whether pubkey has a bank-owned cache entry. +func IsBankSysvarAccount(pubkey solana.PublicKey) bool { + _, ok := bankSysvarIDForAddress(pubkey) + return ok +} + +// RangeBankSysvarAddresses visits the complete registry in stable order, +// including entries that are absent from a particular bank snapshot. +func RangeBankSysvarAddresses(fn func(solana.PublicKey) error) error { + if fn == nil { + return nil + } + for id := bankSysvarID(0); id < bankSysvarCount; id++ { + if err := fn(bankSysvarAddress(id)); err != nil { + return err + } + } + return nil +} + +// NewBankSysvars constructs a snapshot and defensively clones every input. +func NewBankSysvars(slot uint64, accts ...*accounts.Account) (*BankSysvars, error) { + owned := make([]*accounts.Account, len(accts)) + for i, acct := range accts { + if acct != nil { + owned[i] = acct.Clone() + } + } + return NewBankSysvarsOwned(slot, owned...) +} + +// NewBankSysvarsOwned constructs a snapshot by adopting the supplied account +// pointers. The caller must pass fresh accounts and must not mutate them after +// this call. +func NewBankSysvarsOwned(slot uint64, accts ...*accounts.Account) (*BankSysvars, error) { + snapshot := &BankSysvars{slot: slot} + if err := snapshot.applyOwnedAccounts(accts...); err != nil { + return nil, err + } + return snapshot, nil +} + +// Derive creates a child-bank snapshot, cloning all updated accounts. +func (s *BankSysvars) Derive(slot uint64, updates ...*accounts.Account) (*BankSysvars, error) { + owned := make([]*accounts.Account, len(updates)) + for i, acct := range updates { + if acct != nil { + owned[i] = acct.Clone() + } + } + return s.DeriveOwned(slot, owned...) +} + +// DeriveOwned creates a child-bank snapshot and adopts fresh updated account +// pointers. Unchanged entries remain shared with the parent snapshot. +func (s *BankSysvars) DeriveOwned(slot uint64, updates ...*accounts.Account) (*BankSysvars, error) { + var next BankSysvars + if s != nil { + next = *s + } + next.slot = slot + if err := next.applyOwnedAccounts(updates...); err != nil { + return nil, err + } + return &next, nil +} + +// WithAccounts returns a same-bank snapshot with cloned account updates. +func (s *BankSysvars) WithAccounts(updates ...*accounts.Account) (*BankSysvars, error) { + return s.Derive(s.Slot(), updates...) +} + +// WithOwnedAccounts returns a same-bank snapshot and adopts fresh account +// updates. +func (s *BankSysvars) WithOwnedAccounts(updates ...*accounts.Account) (*BankSysvars, error) { + return s.DeriveOwned(s.Slot(), updates...) +} + +// Without returns a snapshot in which the listed optional sysvars are absent. +func (s *BankSysvars) Without(pubkeys ...solana.PublicKey) *BankSysvars { + var next BankSysvars + if s != nil { + next = *s + } + for _, pubkey := range pubkeys { + if id, ok := bankSysvarIDForAddress(pubkey); ok { + next.accounts[id] = nil + next.decoded &^= 1 << id + next.clearValue(id) + } + } + return &next +} + +func (s *BankSysvars) applyOwnedAccounts(accts ...*accounts.Account) error { + for _, acct := range accts { + if acct == nil { + return fmt.Errorf("nil bank sysvar account") + } + id, ok := bankSysvarIDForAddress(acct.Key) + if !ok { + return fmt.Errorf("account %s is not a cached bank sysvar", acct.Key) + } + + s.accounts[id] = nil + s.decoded &^= 1 << id + s.clearValue(id) + // A zero-lamport account is a tombstone and therefore absent from the + // runtime sysvar cache. + if acct.Lamports == 0 { + continue + } + if err := s.decodeValue(id, acct.Data); err != nil { + return fmt.Errorf("decode bank sysvar %s: %w", acct.Key, err) + } + s.accounts[id] = acct + s.decoded |= 1 << id + } + return nil +} + +func (s *BankSysvars) clearValue(id bankSysvarID) { + switch id { + case bankSysvarClock: + s.clock = SysvarClock{} + case bankSysvarRent: + s.rent = SysvarRent{} + case bankSysvarEpochSchedule: + s.epochSchedule = SysvarEpochSchedule{} + case bankSysvarEpochRewards: + s.epochRewards = SysvarEpochRewards{} + case bankSysvarSlotHashes: + s.slotHashes = nil + case bankSysvarStakeHistory: + s.stakeHistory = nil + case bankSysvarLastRestartSlot: + s.lastRestartSlot = SysvarLastRestartSlot{} + case bankSysvarRecentBlockhashes: + s.recentBlockhashes = nil + case bankSysvarSlotHistory: + s.slotHistory = SysvarSlotHistory{} + case bankSysvarFees: + s.fees = SysvarFees{} + } +} + +func (s *BankSysvars) decodeValue(id bankSysvarID, data []byte) error { + decoder := bin.NewBinDecoder(data) + switch id { + case bankSysvarClock: + return s.clock.UnmarshalWithDecoder(decoder) + case bankSysvarRent: + return s.rent.UnmarshalWithDecoder(decoder) + case bankSysvarEpochSchedule: + return s.epochSchedule.UnmarshalWithDecoder(decoder) + case bankSysvarEpochRewards: + return s.epochRewards.UnmarshalWithDecoder(decoder) + case bankSysvarSlotHashes: + return s.slotHashes.UnmarshalWithDecoder(decoder) + case bankSysvarStakeHistory: + return s.stakeHistory.UnmarshalWithDecoder(decoder) + case bankSysvarLastRestartSlot: + return s.lastRestartSlot.UnmarshalWithDecoder(decoder) + case bankSysvarRecentBlockhashes: + return s.recentBlockhashes.UnmarshalWithDecoder(decoder) + case bankSysvarSlotHistory: + return s.slotHistory.UnmarshalWithDecoder(decoder) + case bankSysvarFees: + return s.fees.UnmarshalWithDecoder(decoder) + default: + return fmt.Errorf("unknown bank sysvar id %d", id) + } +} + +// Slot is the bank slot represented by this snapshot. +func (s *BankSysvars) Slot() uint64 { + if s == nil { + return 0 + } + return s.slot +} + +func (s *BankSysvars) hasDecoded(id bankSysvarID) bool { + return s != nil && s.decoded&(1< currentSlot { @@ -654,7 +660,10 @@ func LoaderV4ProcessRetract(execCtx *ExecutionCtx) error { return err } - clock := SysvarCache.Clock.Sysvar + clock, err := ReadClockSysvar(execCtx) + if err != nil { + return err + } currentSlot := clock.Slot if (state.Slot + deploymentCooldownInSlots) > currentSlot { diff --git a/pkg/sealevel/syscalls_sysvar.go b/pkg/sealevel/syscalls_sysvar.go index f68b649e..acd8929b 100644 --- a/pkg/sealevel/syscalls_sysvar.go +++ b/pkg/sealevel/syscalls_sysvar.go @@ -234,6 +234,14 @@ func fetchSysvarBytesForPubkey(execCtx *ExecutionCtx, pubkey solana.PublicKey) ( if !slices.Contains(permittedSysvarAddrs, pubkey) { return nil, fmt.Errorf("unrecognised sysvar") } + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + if data, ok := bankSysvars.RawView(pubkey); ok { + return data, nil + } + return nil, fmt.Errorf("sysvar account not found in bank snapshot") + } + } accts := addrObjectForLookup(execCtx) if accts != nil && *accts != nil { diff --git a/pkg/sealevel/sysvar_bank_scope_test.go b/pkg/sealevel/sysvar_bank_scope_test.go new file mode 100644 index 00000000..61a9767d --- /dev/null +++ b/pkg/sealevel/sysvar_bank_scope_test.go @@ -0,0 +1,183 @@ +package sealevel + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/stretchr/testify/require" +) + +type countingSysvarAccounts struct { + accounts.Accounts + getCalls int +} + +func (accts *countingSysvarAccounts) GetAccount(pubkey *[32]byte) (*accounts.Account, error) { + accts.getCalls++ + return accts.Accounts.GetAccount(pubkey) +} + +func TestReadClockSysvarPrefersBankLocalAccount(t *testing.T) { + previous := SysvarCache.Clock + t.Cleanup(func() { SysvarCache.Clock = previous }) + + parentClock := SysvarClock{Slot: 41, Epoch: 2, UnixTimestamp: 1_700_000_000} + parentAccount := clockSysvarTestAccount(parentClock) + SysvarCache.Clock.Sysvar = &parentClock + SysvarCache.Clock.Acct = parentAccount + + bankClock := SysvarClock{Slot: 42, Epoch: 2, UnixTimestamp: 1_700_000_000} + snapshot, err := NewBankSysvars(42, clockSysvarTestAccount(bankClock)) + require.NoError(t, err) + slotCtx := &SlotCtx{Slot: 42, Accounts: accounts.NewMemAccounts(), Replay: false} + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + execCtx := &ExecutionCtx{Accounts: accounts.NewMemAccounts(), SlotCtx: slotCtx} + + got, err := ReadClockSysvar(execCtx) + require.NoError(t, err) + require.Equal(t, bankClock, got) +} + +func TestGetSysvarBytesPrefersBankLocalAccount(t *testing.T) { + previous := SysvarCache.Clock + t.Cleanup(func() { SysvarCache.Clock = previous }) + + parentClock := SysvarClock{Slot: 41, Epoch: 2, UnixTimestamp: 1_700_000_000} + SysvarCache.Clock.Sysvar = &parentClock + SysvarCache.Clock.Acct = clockSysvarTestAccount(parentClock) + + bankClock := SysvarClock{Slot: 42, Epoch: 2, UnixTimestamp: 1_700_000_000} + snapshot, err := NewBankSysvars(42, clockSysvarTestAccount(bankClock)) + require.NoError(t, err) + slotCtx := &SlotCtx{Slot: 42, Accounts: accounts.NewMemAccounts(), Replay: false} + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + execCtx := &ExecutionCtx{Accounts: accounts.NewMemAccounts(), SlotCtx: slotCtx} + + got, err := fetchSysvarBytesForPubkey(execCtx, SysvarClockAddr) + require.NoError(t, err) + require.Equal(t, bankClock.MustMarshal(), got) +} + +func TestReadClockSysvarFallsBackWhenBankHasNoClock(t *testing.T) { + previous := SysvarCache.Clock + t.Cleanup(func() { SysvarCache.Clock = previous }) + + cachedClock := SysvarClock{Slot: 41, Epoch: 2, UnixTimestamp: 1_700_000_000} + SysvarCache.Clock.Sysvar = &cachedClock + SysvarCache.Clock.Acct = clockSysvarTestAccount(cachedClock) + + got, err := ReadClockSysvar(&ExecutionCtx{ + SlotCtx: &SlotCtx{Accounts: accounts.NewMemAccounts(), Replay: false}, + }) + require.NoError(t, err) + require.Equal(t, cachedClock, got) + + got, err = ReadClockSysvar(nil) + require.NoError(t, err) + require.Equal(t, cachedClock, got) +} + +func TestReadSlotHashesSysvarPrefersBankLocalAccount(t *testing.T) { + previous := SysvarCache.SlotHashes + t.Cleanup(func() { SysvarCache.SlotHashes = previous }) + + parentSlotHashes := SysvarSlotHashes{{Slot: 41, Hash: [32]byte{41}}} + SysvarCache.SlotHashes.Sysvar = &parentSlotHashes + SysvarCache.SlotHashes.Acct = slotHashesSysvarTestAccount(parentSlotHashes) + + bankSlotHashes := SysvarSlotHashes{{Slot: 42, Hash: [32]byte{42}}} + snapshot, err := NewBankSysvars(42, slotHashesSysvarTestAccount(bankSlotHashes)) + require.NoError(t, err) + slotCtx := &SlotCtx{Slot: 42, Accounts: accounts.NewMemAccounts(), Replay: false} + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + + got, err := ReadSlotHashesSysvar(&ExecutionCtx{Accounts: accounts.NewMemAccounts(), SlotCtx: slotCtx}) + require.NoError(t, err) + require.Equal(t, bankSlotHashes, got) +} + +func TestBankSysvarCacheAvoidsRepeatedAccountReads(t *testing.T) { + previousClock := SysvarCache.Clock + previousSlotHashes := SysvarCache.SlotHashes + t.Cleanup(func() { + SysvarCache.Clock = previousClock + SysvarCache.SlotHashes = previousSlotHashes + }) + + parentClock := SysvarClock{Slot: 41} + parentSlotHashes := SysvarSlotHashes{{Slot: 41, Hash: [32]byte{41}}} + SysvarCache.Clock.Sysvar = &parentClock + SysvarCache.Clock.Acct = clockSysvarTestAccount(parentClock) + SysvarCache.SlotHashes.Sysvar = &parentSlotHashes + SysvarCache.SlotHashes.Acct = slotHashesSysvarTestAccount(parentSlotHashes) + + bankClock := SysvarClock{Slot: 42, Epoch: 2, UnixTimestamp: 1_700_000_000} + bankSlotHashes := SysvarSlotHashes{{Slot: 42, Hash: [32]byte{42}}} + expectedSlotHashes := append(SysvarSlotHashes(nil), bankSlotHashes...) + expectedSlotHashesRaw := expectedSlotHashes.MustMarshal() + counting := &countingSysvarAccounts{Accounts: accounts.NewMemAccounts()} + clockAccount := clockSysvarTestAccount(bankClock) + slotHashesAccount := slotHashesSysvarTestAccount(bankSlotHashes) + snapshot, err := NewBankSysvars(42, clockAccount, slotHashesAccount) + require.NoError(t, err) + slotCtx := &SlotCtx{Slot: 42, Accounts: counting} + require.NoError(t, slotCtx.PublishBankSysvars(snapshot)) + execCtx := &ExecutionCtx{SlotCtx: slotCtx} + + // Snapshot construction owns defensive copies; later caller mutation cannot change the + // bank's immutable view. + clockAccount.Data[0]++ + slotHashesAccount.Data[0]++ + bankSlotHashes[0].Slot++ + + for range 10 { + gotClock, err := ReadClockSysvar(execCtx) + require.NoError(t, err) + require.Equal(t, bankClock, gotClock) + + gotSlotHashes, err := ReadSlotHashesSysvar(execCtx) + require.NoError(t, err) + require.Equal(t, expectedSlotHashes, gotSlotHashes) + + gotClockRaw, err := fetchSysvarBytesForPubkey(execCtx, SysvarClockAddr) + require.NoError(t, err) + require.Equal(t, bankClock.MustMarshal(), gotClockRaw) + + gotSlotHashesRaw, err := fetchSysvarBytesForPubkey(execCtx, SysvarSlotHashesAddr) + require.NoError(t, err) + require.Equal(t, expectedSlotHashesRaw, gotSlotHashesRaw) + } + require.Zero(t, counting.getCalls) + + allocs := testing.AllocsPerRun(1_000, func() { + if _, err := ReadClockSysvar(execCtx); err != nil { + panic(err) + } + if _, err := ReadSlotHashesSysvar(execCtx); err != nil { + panic(err) + } + if _, err := fetchSysvarBytesForPubkey(execCtx, SysvarClockAddr); err != nil { + panic(err) + } + if _, err := fetchSysvarBytesForPubkey(execCtx, SysvarSlotHashesAddr); err != nil { + panic(err) + } + }) + require.Zero(t, allocs) +} + +func clockSysvarTestAccount(clock SysvarClock) *accounts.Account { + return &accounts.Account{ + Key: SysvarClockAddr, + Lamports: 1, + Data: clock.MustMarshal(), + } +} + +func slotHashesSysvarTestAccount(slotHashes SysvarSlotHashes) *accounts.Account { + return &accounts.Account{ + Key: SysvarSlotHashesAddr, + Lamports: 1, + Data: slotHashes.MustMarshal(), + } +} diff --git a/pkg/sealevel/sysvar_cache.go b/pkg/sealevel/sysvar_cache.go index 6b476c50..960c04f9 100644 --- a/pkg/sealevel/sysvar_cache.go +++ b/pkg/sealevel/sysvar_cache.go @@ -1,8 +1,6 @@ package sealevel -import ( - "github.com/Overclock-Validator/mithril/pkg/accounts" -) +import "github.com/Overclock-Validator/mithril/pkg/accounts" type sysvarCache struct { RecentBlockHashes recentBlockhashesCache diff --git a/pkg/sealevel/sysvar_clock.go b/pkg/sealevel/sysvar_clock.go index 6b175497..db7bd605 100644 --- a/pkg/sealevel/sysvar_clock.go +++ b/pkg/sealevel/sysvar_clock.go @@ -6,7 +6,6 @@ import ( "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/base58" - "github.com/Overclock-Validator/mithril/pkg/mlog" bin "github.com/gagliardetto/binary" ) @@ -97,31 +96,35 @@ func (sc *SysvarClock) MustMarshal() []byte { } func ReadClockSysvar(execCtx *ExecutionCtx) (SysvarClock, error) { + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + clock, ok := bankSysvars.Clock() + if !ok { + return SysvarClock{}, InstrErrUnsupportedSysvar + } + return clock, nil + } + } + + if clockAccount, ok := localSysvarAccount(execCtx, SysvarClockAddr); ok { + if clockAccount.Lamports == 0 { + return SysvarClock{}, InstrErrUnsupportedSysvar + } + var clock SysvarClock + if err := clock.UnmarshalWithDecoder(bin.NewBinDecoder(clockAccount.Data)); err != nil { + return SysvarClock{}, InstrErrUnsupportedSysvar + } + return clock, nil + } + + // The global cache remains the fallback for execution contexts that do not + // carry a bank-local Clock (primarily isolated native-program tests). A + // present bank account is authoritative even when the bank is a speculative + // leader bank rather than an ordered-replay bank. if SysvarCache.Clock.Sysvar != nil { return *SysvarCache.Clock.Sysvar, nil } - - accts := addrObjectForLookup(execCtx) - clockAccount, err := (*accts).GetAccount(&SysvarClockAddr) - if err != nil { - mlog.Log.Infof("returning at [1] for clock: %+v\n", clockAccount) - return SysvarClock{}, InstrErrUnsupportedSysvar - } - - if clockAccount.Lamports == 0 { - mlog.Log.Infof("returning at [2] for clock: %+v\n", clockAccount) - return SysvarClock{}, InstrErrUnsupportedSysvar - } - - dec := bin.NewBinDecoder(clockAccount.Data) - var clock SysvarClock - err = clock.UnmarshalWithDecoder(dec) - if err != nil { - mlog.Log.Infof("returning at [3] for clock: %+v\n", clockAccount) - return SysvarClock{}, InstrErrUnsupportedSysvar - } - - return clock, nil + return SysvarClock{}, InstrErrUnsupportedSysvar } func WriteClockSysvar(accts *accounts.Accounts, clock SysvarClock) { diff --git a/pkg/sealevel/sysvar_common.go b/pkg/sealevel/sysvar_common.go index cc7f6abd..6d826c2c 100644 --- a/pkg/sealevel/sysvar_common.go +++ b/pkg/sealevel/sysvar_common.go @@ -1,11 +1,38 @@ package sealevel -import "github.com/Overclock-Validator/mithril/pkg/accounts" +import ( + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/gagliardetto/solana-go" +) func addrObjectForLookup(execCtx *ExecutionCtx) *accounts.Accounts { - if execCtx.SlotCtx != nil && execCtx.SlotCtx.Replay { + if execCtx == nil { + return nil + } + // A transaction always observes the accounts of the bank that executes it. + // Replay describes the execution mode; it does not determine account + // ownership. In particular, speculative leader banks have Replay=false but + // still own a complete bank-local sysvar view. + if execCtx.SlotCtx != nil && execCtx.SlotCtx.Accounts != nil { return &execCtx.SlotCtx.Accounts - } else { - return &execCtx.Accounts } + return &execCtx.Accounts +} + +// localSysvarAccount returns a sysvar account from the execution bank when it +// is explicitly available. Bank snapshots are handled by the typed readers +// before this helper; this is the compatibility path for older/isolated SlotCtx +// fixtures. A present local account is authoritative over the process-global +// bootstrap cache. +func localSysvarAccount(execCtx *ExecutionCtx, pubkey solana.PublicKey) (*accounts.Account, bool) { + accts := addrObjectForLookup(execCtx) + if accts == nil || *accts == nil { + return nil, false + } + key := [32]byte(pubkey) + acct, err := (*accts).GetAccount(&key) + if err != nil || acct == nil { + return nil, false + } + return acct, true } diff --git a/pkg/sealevel/sysvar_epoch_rewards.go b/pkg/sealevel/sysvar_epoch_rewards.go index 80cf0e84..6e42b152 100644 --- a/pkg/sealevel/sysvar_epoch_rewards.go +++ b/pkg/sealevel/sysvar_epoch_rewards.go @@ -136,30 +136,31 @@ func (sr *SysvarEpochRewards) Distribute(amount uint64) { } func ReadEpochRewardsSysvar(execCtx *ExecutionCtx) (SysvarEpochRewards, error) { - if SysvarCache.EpochRewards.Sysvar != nil { - return *SysvarCache.EpochRewards.Sysvar, nil - } - - accts := addrObjectForLookup(execCtx) - - epochRewardsSysvarAcct, err := (*accts).GetAccount(&SysvarEpochRewardsAddr) - if err != nil { - return SysvarEpochRewards{}, InstrErrUnsupportedSysvar + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + epochRewards, ok := bankSysvars.EpochRewards() + if !ok { + return SysvarEpochRewards{}, InstrErrUnsupportedSysvar + } + return epochRewards, nil + } + } + + if epochRewardsAcct, ok := localSysvarAccount(execCtx, SysvarEpochRewardsAddr); ok { + if epochRewardsAcct.Lamports == 0 { + return SysvarEpochRewards{}, InstrErrUnsupportedSysvar + } + var epochRewards SysvarEpochRewards + if err := epochRewards.UnmarshalWithDecoder(bin.NewBinDecoder(epochRewardsAcct.Data)); err != nil { + return SysvarEpochRewards{}, InstrErrUnsupportedSysvar + } + return epochRewards, nil } - if epochRewardsSysvarAcct.Lamports == 0 { - return SysvarEpochRewards{}, InstrErrUnsupportedSysvar - } - - dec := bin.NewBinDecoder(epochRewardsSysvarAcct.Data) - - var epochRewards SysvarEpochRewards - err = epochRewards.UnmarshalWithDecoder(dec) - if err != nil { - return SysvarEpochRewards{}, InstrErrUnsupportedSysvar + if SysvarCache.EpochRewards.Sysvar != nil { + return *SysvarCache.EpochRewards.Sysvar, nil } - - return epochRewards, nil + return SysvarEpochRewards{}, InstrErrUnsupportedSysvar } func WriteEpochRewardsSysvar(accts *accounts.Accounts, epochRewards SysvarEpochRewards) { diff --git a/pkg/sealevel/sysvar_epoch_schedule.go b/pkg/sealevel/sysvar_epoch_schedule.go index 3c08a26d..9e2add24 100644 --- a/pkg/sealevel/sysvar_epoch_schedule.go +++ b/pkg/sealevel/sysvar_epoch_schedule.go @@ -157,23 +157,31 @@ func (sr *SysvarEpochSchedule) LeaderScheduleEpoch(slot uint64) uint64 { } func ReadEpochScheduleSysvar(execCtx *ExecutionCtx) (SysvarEpochSchedule, error) { - if SysvarCache.EpochSchedule.Sysvar != nil { - return *SysvarCache.EpochSchedule.Sysvar, nil + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + epochSchedule, ok := bankSysvars.EpochSchedule() + if !ok { + return SysvarEpochSchedule{}, InstrErrUnsupportedSysvar + } + return epochSchedule, nil + } } - accts := addrObjectForLookup(execCtx) - - epochScheduleSysvarAcct, err := (*accts).GetAccount(&SysvarEpochScheduleAddr) - if err != nil { - return SysvarEpochSchedule{}, InstrErrUnsupportedSysvar + if epochScheduleAcct, ok := localSysvarAccount(execCtx, SysvarEpochScheduleAddr); ok { + if epochScheduleAcct.Lamports == 0 { + return SysvarEpochSchedule{}, InstrErrUnsupportedSysvar + } + var epochSchedule SysvarEpochSchedule + if err := epochSchedule.UnmarshalWithDecoder(bin.NewBinDecoder(epochScheduleAcct.Data)); err != nil { + return SysvarEpochSchedule{}, InstrErrUnsupportedSysvar + } + return epochSchedule, nil } - dec := bin.NewBinDecoder(epochScheduleSysvarAcct.Data) - - var epochSchedule SysvarEpochSchedule - err = epochSchedule.UnmarshalWithDecoder(dec) - - return epochSchedule, err + if SysvarCache.EpochSchedule.Sysvar != nil { + return *SysvarCache.EpochSchedule.Sysvar, nil + } + return SysvarEpochSchedule{}, InstrErrUnsupportedSysvar } func WriteEpochScheduleSysvar(accts *accounts.Accounts, epochSchedule SysvarEpochSchedule) { diff --git a/pkg/sealevel/sysvar_fees.go b/pkg/sealevel/sysvar_fees.go index de841534..9d8e9659 100644 --- a/pkg/sealevel/sysvar_fees.go +++ b/pkg/sealevel/sysvar_fees.go @@ -42,22 +42,30 @@ func (sf *SysvarFees) Update(lamportsPerSignature uint64) { sf.FeeCalculator.LamportsPerSignature = lamportsPerSignature } -func ReadFeesSysvar(accts *accounts.Accounts) SysvarFees { - if SysvarCache.Fees.Sysvar != nil { - return *SysvarCache.Fees.Sysvar +func ReadFeesSysvar(execCtx *ExecutionCtx) SysvarFees { + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + fees, ok := bankSysvars.Fees() + if !ok { + panic("Fees sysvar is absent from bank snapshot") + } + return fees + } } - feesSysvarAcct, err := (*accts).GetAccount(&SysvarFeesAddr) - if err != nil { - panic("failed to read fees sysvar account") + if feesAcct, ok := localSysvarAccount(execCtx, SysvarFeesAddr); ok { + if feesAcct.Lamports == 0 { + panic("Fees sysvar account is absent") + } + var fees SysvarFees + fees.MustUnmarshalWithDecoder(bin.NewBinDecoder(feesAcct.Data)) + return fees } - dec := bin.NewBinDecoder(feesSysvarAcct.Data) - - var fees SysvarFees - fees.MustUnmarshalWithDecoder(dec) - - return fees + if SysvarCache.Fees.Sysvar != nil { + return *SysvarCache.Fees.Sysvar + } + panic("failed to read fees sysvar account") } func WriteFeesSysvar(accts *accounts.Accounts, fees SysvarFees) { diff --git a/pkg/sealevel/sysvar_last_restart_slot.go b/pkg/sealevel/sysvar_last_restart_slot.go index 95fd9fca..f11f38f0 100644 --- a/pkg/sealevel/sysvar_last_restart_slot.go +++ b/pkg/sealevel/sysvar_last_restart_slot.go @@ -36,26 +36,31 @@ func (sr *SysvarLastRestartSlot) MustUnmarshalWithDecoder(decoder *bin.Decoder) } func ReadLastRestartSlotSysvar(execCtx *ExecutionCtx) (SysvarLastRestartSlot, error) { - if SysvarCache.LastRestartSlot.Sysvar != nil { - return *SysvarCache.LastRestartSlot.Sysvar, nil + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + lastRestartSlot, ok := bankSysvars.LastRestartSlot() + if !ok { + return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar + } + return lastRestartSlot, nil + } } - accts := addrObjectForLookup(execCtx) - - lrsAcct, err := (*accts).GetAccount(&SysvarLastRestartSlotAddr) - if err != nil { - return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar + if lrsAcct, ok := localSysvarAccount(execCtx, SysvarLastRestartSlotAddr); ok { + if lrsAcct.Lamports == 0 { + return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar + } + var lrs SysvarLastRestartSlot + if err := lrs.UnmarshalWithDecoder(bin.NewBinDecoder(lrsAcct.Data)); err != nil { + return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar + } + return lrs, nil } - dec := bin.NewBinDecoder(lrsAcct.Data) - - var lrs SysvarLastRestartSlot - err = lrs.UnmarshalWithDecoder(dec) - if err != nil { - return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar + if SysvarCache.LastRestartSlot.Sysvar != nil { + return *SysvarCache.LastRestartSlot.Sysvar, nil } - - return lrs, nil + return SysvarLastRestartSlot{}, InstrErrUnsupportedSysvar } func WriteLastRestartSlotSysvar(accts *accounts.Accounts, lastRestartSlot SysvarLastRestartSlot) { diff --git a/pkg/sealevel/sysvar_recent_blockhashes.go b/pkg/sealevel/sysvar_recent_blockhashes.go index 6017cf98..98b15c08 100644 --- a/pkg/sealevel/sysvar_recent_blockhashes.go +++ b/pkg/sealevel/sysvar_recent_blockhashes.go @@ -143,27 +143,29 @@ func (recentBlockhashes *SysvarRecentBlockhashes) IsBlockhashAgeValid(hash [32]b } func ReadRecentBlockHashesSysvar(execCtx *ExecutionCtx) (SysvarRecentBlockhashes, error) { - if SysvarCache.RecentBlockHashes.Sysvar != nil { - return *SysvarCache.RecentBlockHashes.Sysvar, nil - } - - accts := addrObjectForLookup(execCtx) - recentBlockhashesAcct, err := (*accts).GetAccount(&SysvarRecentBlockHashesAddr) - if err != nil { - return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + recentBlockhashes, ok := bankSysvars.RecentBlockhashes() + if !ok { + return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + } + return recentBlockhashes, nil + } } - if recentBlockhashesAcct.Lamports == 0 || len(recentBlockhashesAcct.Data) == 0 { - return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + if recentAcct, ok := localSysvarAccount(execCtx, SysvarRecentBlockHashesAddr); ok { + if recentAcct.Lamports == 0 || len(recentAcct.Data) == 0 { + return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + } + var recentBlockhashes SysvarRecentBlockhashes + if err := recentBlockhashes.UnmarshalWithDecoder(bin.NewBinDecoder(recentAcct.Data)); err != nil { + return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + } + return recentBlockhashes, nil } - dec := bin.NewBinDecoder(recentBlockhashesAcct.Data) - - var recentBlockhashes SysvarRecentBlockhashes - err = recentBlockhashes.UnmarshalWithDecoder(dec) - if err != nil { - return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar + if SysvarCache.RecentBlockHashes.Sysvar != nil { + return *SysvarCache.RecentBlockHashes.Sysvar, nil } - - return recentBlockhashes, nil + return SysvarRecentBlockhashes{}, InstrErrUnsupportedSysvar } diff --git a/pkg/sealevel/sysvar_rent.go b/pkg/sealevel/sysvar_rent.go index e2c3c275..f12f532c 100644 --- a/pkg/sealevel/sysvar_rent.go +++ b/pkg/sealevel/sysvar_rent.go @@ -97,25 +97,31 @@ func (sr *SysvarRent) InitializeDefault() { } func ReadRentSysvar(execCtx *ExecutionCtx) (SysvarRent, error) { - if SysvarCache.Rent.Sysvar != nil { - return *SysvarCache.Rent.Sysvar, nil - } - - accts := addrObjectForLookup(execCtx) - rentAcct, err := (*accts).GetAccount(&SysvarRentAddr) - if err != nil { - return SysvarRent{}, err + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + rent, ok := bankSysvars.Rent() + if !ok { + return SysvarRent{}, InstrErrUnsupportedSysvar + } + return rent, nil + } + } + + if rentAcct, ok := localSysvarAccount(execCtx, SysvarRentAddr); ok { + if rentAcct.Lamports == 0 { + return SysvarRent{}, InstrErrUnsupportedSysvar + } + var rent SysvarRent + if err := rent.UnmarshalWithDecoder(bin.NewBinDecoder(rentAcct.Data)); err != nil { + return SysvarRent{}, InstrErrUnsupportedSysvar + } + return rent, nil } - dec := bin.NewBinDecoder(rentAcct.Data) - - var rent SysvarRent - err = rent.UnmarshalWithDecoder(dec) - if err != nil { - return SysvarRent{}, InstrErrUnsupportedSysvar + if SysvarCache.Rent.Sysvar != nil { + return *SysvarCache.Rent.Sysvar, nil } - - return rent, nil + return SysvarRent{}, InstrErrUnsupportedSysvar } func WriteRentSysvar(accts *accounts.Accounts, rent SysvarRent) { diff --git a/pkg/sealevel/sysvar_slot_hashes.go b/pkg/sealevel/sysvar_slot_hashes.go index b3cf188f..35828613 100644 --- a/pkg/sealevel/sysvar_slot_hashes.go +++ b/pkg/sealevel/sysvar_slot_hashes.go @@ -142,29 +142,31 @@ func (sh *SysvarSlotHashes) Update(slot uint64, parentSlot uint64, hash [32]byte } func ReadSlotHashesSysvar(execCtx *ExecutionCtx) (SysvarSlotHashes, error) { - if SysvarCache.SlotHashes.Sysvar != nil { - return *SysvarCache.SlotHashes.Sysvar, nil - } - - accts := addrObjectForLookup(execCtx) - slotHashesSysvarAcct, err := (*accts).GetAccount(&SysvarSlotHashesAddr) - if err != nil { - return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + slotHashes, ok := bankSysvars.SlotHashes() + if !ok { + return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + } + return slotHashes, nil + } } - if slotHashesSysvarAcct.Lamports == 0 { - return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + if slotHashesAcct, ok := localSysvarAccount(execCtx, SysvarSlotHashesAddr); ok { + if slotHashesAcct.Lamports == 0 { + return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + } + var slotHashes SysvarSlotHashes + if err := slotHashes.UnmarshalWithDecoder(bin.NewBinDecoder(slotHashesAcct.Data)); err != nil { + return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + } + return slotHashes, nil } - dec := bin.NewBinDecoder(slotHashesSysvarAcct.Data) - - var slotHashes SysvarSlotHashes - err = slotHashes.UnmarshalWithDecoder(dec) - if err != nil { - return SysvarSlotHashes{}, InstrErrUnsupportedSysvar + if SysvarCache.SlotHashes.Sysvar != nil { + return *SysvarCache.SlotHashes.Sysvar, nil } - - return slotHashes, nil + return SysvarSlotHashes{}, InstrErrUnsupportedSysvar } func WriteSlotHashesSysvar(accts *accounts.Accounts, slotHashes SysvarSlotHashes) { diff --git a/pkg/sealevel/sysvar_slot_history.go b/pkg/sealevel/sysvar_slot_history.go index 9278f344..08a2e2dd 100644 --- a/pkg/sealevel/sysvar_slot_history.go +++ b/pkg/sealevel/sysvar_slot_history.go @@ -144,22 +144,29 @@ func (sr *SysvarSlotHistory) SetNextSlot(nextSlot uint64) { } func ReadSlotHistorySysvar(execCtx *ExecutionCtx) SysvarSlotHistory { - if SysvarCache.SlotHistory.Sysvar != nil { - return *SysvarCache.SlotHistory.Sysvar + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + slotHistory, ok := bankSysvars.SlotHistory() + if !ok { + panic("SlotHistory sysvar is absent from bank snapshot") + } + return slotHistory + } } - accts := addrObjectForLookup(execCtx) - slotHistorySysvarAcct, err := (*accts).GetAccount(&SysvarSlotHistoryAddr) - if err != nil { - panic("failed to read SlotHistory sysvar account") + if slotHistoryAcct, ok := localSysvarAccount(execCtx, SysvarSlotHistoryAddr); ok { + if slotHistoryAcct.Lamports == 0 { + panic("SlotHistory sysvar account is absent") + } + var slotHistory SysvarSlotHistory + slotHistory.MustUnmarshalWithDecoder(bin.NewBinDecoder(slotHistoryAcct.Data)) + return slotHistory } - dec := bin.NewBinDecoder(slotHistorySysvarAcct.Data) - - var slotHistory SysvarSlotHistory - slotHistory.MustUnmarshalWithDecoder(dec) - - return slotHistory + if SysvarCache.SlotHistory.Sysvar != nil { + return *SysvarCache.SlotHistory.Sysvar + } + panic("failed to read SlotHistory sysvar account") } func WriteSlotHistorySysvar(accts *accounts.Accounts, slotHistory SysvarSlotHistory) { diff --git a/pkg/sealevel/sysvar_stake_history.go b/pkg/sealevel/sysvar_stake_history.go index 26808c13..204f878e 100644 --- a/pkg/sealevel/sysvar_stake_history.go +++ b/pkg/sealevel/sysvar_stake_history.go @@ -153,26 +153,31 @@ func (sh *SysvarStakeHistory) String() string { } func ReadStakeHistorySysvar(execCtx *ExecutionCtx) (SysvarStakeHistory, error) { - if SysvarCache.StakeHistory.Sysvar != nil { - return *SysvarCache.StakeHistory.Sysvar, nil + if execCtx != nil && execCtx.SlotCtx != nil { + if bankSysvars := execCtx.SlotCtx.BankSysvars(); bankSysvars != nil { + stakeHistory, ok := bankSysvars.StakeHistory() + if !ok { + return SysvarStakeHistory{}, InstrErrUnsupportedSysvar + } + return stakeHistory, nil + } } - accts := addrObjectForLookup(execCtx) - stakeHistorySysvarAcct, err := (*accts).GetAccount(&SysvarStakeHistoryAddr) - if err != nil { - return SysvarStakeHistory{}, InstrErrUnsupportedSysvar + if stakeHistoryAcct, ok := localSysvarAccount(execCtx, SysvarStakeHistoryAddr); ok { + if stakeHistoryAcct.Lamports == 0 { + return SysvarStakeHistory{}, InstrErrUnsupportedSysvar + } + var stakeHistory SysvarStakeHistory + if err := stakeHistory.UnmarshalWithDecoder(bin.NewBinDecoder(stakeHistoryAcct.Data)); err != nil { + return SysvarStakeHistory{}, InstrErrUnsupportedSysvar + } + return stakeHistory, nil } - if stakeHistorySysvarAcct.Lamports == 0 { - return SysvarStakeHistory{}, InstrErrUnsupportedSysvar + if SysvarCache.StakeHistory.Sysvar != nil { + return *SysvarCache.StakeHistory.Sysvar, nil } - - dec := bin.NewBinDecoder(stakeHistorySysvarAcct.Data) - - var stakeHistory SysvarStakeHistory - stakeHistory.MustUnmarshalWithDecoder(dec) - - return stakeHistory, nil + return SysvarStakeHistory{}, InstrErrUnsupportedSysvar } func WriteStakeHistorySysvar(accts *accounts.Accounts, stakeHistory SysvarStakeHistory) {