From 31b78cebfe0af752d0024cd2ad2ebb1a855e5e24 Mon Sep 17 00:00:00 2001 From: smcio Date: Wed, 8 Apr 2026 09:32:39 +0200 Subject: [PATCH 1/2] implement remaining agave 3.1 features --- pkg/accountsdb/accountsdb.go | 43 -------- pkg/features/gates.go | 3 +- pkg/replay/block.go | 207 +++++++++++++++++++---------------- 3 files changed, 116 insertions(+), 137 deletions(-) diff --git a/pkg/accountsdb/accountsdb.go b/pkg/accountsdb/accountsdb.go index 166668fa..11693e38 100644 --- a/pkg/accountsdb/accountsdb.go +++ b/pkg/accountsdb/accountsdb.go @@ -196,24 +196,6 @@ type ProgramCacheEntry struct { DeploymentSlot uint64 } -type PebbleMetricsSnapshot struct { - BlockCacheHits int64 - BlockCacheMisses int64 - BlockCacheSize int64 - TableCacheHits int64 - TableCacheMisses int64 - TableCacheSize int64 - ReadAmp int - CompactionDebt uint64 - L0NumFiles int64 - L0Sublevels int32 - MemTableSize uint64 - MemTableCount int64 - WALFiles int64 - WALSize uint64 - WALBytesWritten uint64 -} - func (accountsDb *AccountsDb) MaybeGetProgramFromCache(pubkey solana.PublicKey) (*ProgramCacheEntry, bool) { return accountsDb.ProgramCache.Get(pubkey) } @@ -226,31 +208,6 @@ func (accountsDb *AccountsDb) RemoveProgramFromCache(pubkey solana.PublicKey) { accountsDb.ProgramCache.Delete(pubkey) } -func (accountsDb *AccountsDb) IndexMetricsSnapshot() PebbleMetricsSnapshot { - if accountsDb == nil || accountsDb.Index == nil { - return PebbleMetricsSnapshot{} - } - - metrics := accountsDb.Index.Metrics() - return PebbleMetricsSnapshot{ - BlockCacheHits: metrics.BlockCache.Hits, - BlockCacheMisses: metrics.BlockCache.Misses, - BlockCacheSize: metrics.BlockCache.Size, - TableCacheHits: metrics.TableCache.Hits, - TableCacheMisses: metrics.TableCache.Misses, - TableCacheSize: metrics.TableCache.Size, - ReadAmp: metrics.ReadAmp(), - CompactionDebt: metrics.Compact.EstimatedDebt, - L0NumFiles: metrics.Levels[0].NumFiles, - L0Sublevels: metrics.Levels[0].Sublevels, - MemTableSize: metrics.MemTable.Size, - MemTableCount: metrics.MemTable.Count, - WALFiles: metrics.WAL.Files, - WALSize: metrics.WAL.Size, - WALBytesWritten: metrics.WAL.BytesWritten, - } -} - func (accountsDb *AccountsDb) GetAccount(slot uint64, pubkey solana.PublicKey) (*accounts.Account, error) { accts := accountsDb.getStoreInProgressAccounts([]solana.PublicKey{pubkey}) if accts[0] != nil { diff --git a/pkg/features/gates.go b/pkg/features/gates.go index 837d1162..650ef518 100644 --- a/pkg/features/gates.go +++ b/pkg/features/gates.go @@ -48,6 +48,7 @@ var PicoInflation = FeatureGate{Name: "PicoInflation", Address: base58.MustDecod var DisableAccountLoaderSpecialCase = FeatureGate{Name: "DisableAccountLoaderSpecialCase", Address: base58.MustDecodeFromString("EQUMpNFr7Nacb1sva56xn1aLfBxppEoSBH8RRVdkcD1x")} var EnableGetEpochStakeSyscall = FeatureGate{Name: "EnableGetEpochStakeSyscall", Address: base58.MustDecodeFromString("FKe75t4LXxGaQnVHdUKM6DSFifVVraGZ8LyNo7oPwy1Z")} var ReserveMinimalCUsForBuiltinInstructions = FeatureGate{Name: "ReserveMinimalCUsForBuiltinInstructions", Address: base58.MustDecodeFromString("C9oAhLxDBm3ssWtJx1yBGzPY55r2rArHmN1pbQn6HogH")} +var RelaxIntraBatchAccountLocks = FeatureGate{Name: "RelaxIntraBatchAccountLocks", Address: base58.MustDecodeFromString("4WeHX6QoXCCwqbSFgi6dxnB6QsPo6YApaNTH7P4MLQ99")} var MaskOutRentEpochInVmSerialization = FeatureGate{Name: "MaskOutRentEpochInVmSerialization", Address: base58.MustDecodeFromString("RENtePQcDLrAbxAsP3k8dwVcnNYQ466hi2uKvALjnXx")} var RemoveAccountsExecutableFlagChecks = FeatureGate{Name: "RemoveAccountsExecutableFlagchecks", Address: base58.MustDecodeFromString("FXs1zh47QbNnhXcnB6YiAQoJ4sGB91tKF3UFHLcKT7PM")} var AccountsLtHash = FeatureGate{Name: "AccountsLtHash", Address: base58.MustDecodeFromString("LTHasHQX6661DaDD4S6A2TFi6QBuiwXKv66fB1obfHq")} @@ -78,7 +79,7 @@ var AllFeatureGates = []FeatureGate{StopTruncatingStringsInSyscalls, EnableParti RewardFullPriorityFee, StakeMinimumDelegationForRewards, MoveStakeAndMoveLamportsIxs, GetSysvarSyscallEnabled, AddNewReservedAccountKeys, EnableSecp256r1Precompile, FixAltBn128MultiplicationInputLength, EnableTowerSyncIx, SkipRentRewrites, FullInflationVote, FullInflationEnable, FullInflationDevnetAndTestnet, PicoInflation, DisableAccountLoaderSpecialCase, EnableGetEpochStakeSyscall, - ReserveMinimalCUsForBuiltinInstructions, MaskOutRentEpochInVmSerialization, RemoveAccountsExecutableFlagChecks, + ReserveMinimalCUsForBuiltinInstructions, RelaxIntraBatchAccountLocks, MaskOutRentEpochInVmSerialization, RemoveAccountsExecutableFlagChecks, AccountsLtHash, RemoveAccountsDeltaHash, EnableLoaderV4, EnableSbpfV1DeploymentAndExecution, EnableSbpfV2DeploymentAndExecution, EnableSbpfV3DeploymentAndExecution, DisableSbpfV0Execution, ReenableSbpfV0Execution, FormalizeLoadedTransactionDataSize, IncreaseCpiAccountInfoLimit, StaticInstructionLimit, PoseidonEnforcePadding, FixAltBn128PairingLengthCheck, DeprecateRentExemptionThreshold, diff --git a/pkg/replay/block.go b/pkg/replay/block.go index 75da4d9e..1aab7c07 100644 --- a/pkg/replay/block.go +++ b/pkg/replay/block.go @@ -113,53 +113,6 @@ func formatBlockSourceStatus(fetchStats blockstream.FetchStatsSnapshot) string { return fetchStats.SourceStatus } -func counterDeltaInt64(after, before int64) int64 { - if after <= before { - return 0 - } - return after - before -} - -func counterDeltaUint64(after, before uint64) uint64 { - if after <= before { - return 0 - } - return after - before -} - -func nonNegativeUint64(v int64) uint64 { - if v <= 0 { - return 0 - } - return uint64(v) -} - -func formatSummaryBytes(bytes uint64) string { - const ( - kib = 1024 - mib = 1024 * kib - gib = 1024 * mib - ) - switch { - case bytes >= gib: - return fmt.Sprintf("%.1fGiB", float64(bytes)/gib) - case bytes >= mib: - return fmt.Sprintf("%.1fMiB", float64(bytes)/mib) - case bytes >= kib: - return fmt.Sprintf("%.1fKiB", float64(bytes)/kib) - default: - return fmt.Sprintf("%dB", bytes) - } -} - -func formatWindowCacheStats(hits, misses int64) string { - requests := hits + misses - if requests <= 0 { - return "no lookups" - } - return fmt.Sprintf("%.1f%% (%d/%d)", float64(hits)/float64(requests)*100, hits, requests) -} - // ReplayResult contains the result of a replay operation, including shutdown state type ReplayResult struct { // LastPersistedSlot is the last slot whose state was successfully persisted to AccountsDB @@ -1443,7 +1396,6 @@ func ReplayBlocks( var voteTxCounts []uint64 // vote txns per block var nonVoteTxCounts []uint64 // non-vote txns per block var justCrossedEpochBoundary bool - indexMetricsSummaryStart := acctsDb.IndexMetricsSnapshot() // Preallocate slices for 100 blocks const summaryInterval = 100 @@ -2217,13 +2169,6 @@ func ReplayBlocks( medVoteTx := medianUint(voteTxCounts) medNonVoteTx := medianUint(nonVoteTxCounts) - indexMetricsSnapshot := acctsDb.IndexMetricsSnapshot() - blockCacheHits := counterDeltaInt64(indexMetricsSnapshot.BlockCacheHits, indexMetricsSummaryStart.BlockCacheHits) - blockCacheMisses := counterDeltaInt64(indexMetricsSnapshot.BlockCacheMisses, indexMetricsSummaryStart.BlockCacheMisses) - tableCacheHits := counterDeltaInt64(indexMetricsSnapshot.TableCacheHits, indexMetricsSummaryStart.TableCacheHits) - tableCacheMisses := counterDeltaInt64(indexMetricsSnapshot.TableCacheMisses, indexMetricsSummaryStart.TableCacheMisses) - walBytesWritten := counterDeltaUint64(indexMetricsSnapshot.WALBytesWritten, indexMetricsSummaryStart.WALBytesWritten) - // Blocks per second based on median total time var blocksPerSec float64 if medTotal > 0 { @@ -2293,22 +2238,6 @@ func ReplayBlocks( loadedMB, clonedMB, touchedMB, avgLoadedPerTx, avgClonedPerTx, avgTouchedPerTx) } - mlog.Log.InfofPrecise(" pebble cache: block %s, %s | table %s, %s", - formatWindowCacheStats(blockCacheHits, blockCacheMisses), - formatSummaryBytes(nonNegativeUint64(indexMetricsSnapshot.BlockCacheSize)), - formatWindowCacheStats(tableCacheHits, tableCacheMisses), - formatSummaryBytes(nonNegativeUint64(indexMetricsSnapshot.TableCacheSize))) - mlog.Log.InfofPrecise(" pebble idx: read amp %d | debt %s | L0 %d files/%d sublevels | mem %d (%s) | WAL +%s (%d files, live %s)", - indexMetricsSnapshot.ReadAmp, - formatSummaryBytes(indexMetricsSnapshot.CompactionDebt), - indexMetricsSnapshot.L0NumFiles, - indexMetricsSnapshot.L0Sublevels, - indexMetricsSnapshot.MemTableCount, - formatSummaryBytes(indexMetricsSnapshot.MemTableSize), - formatSummaryBytes(walBytesWritten), - indexMetricsSnapshot.WALFiles, - formatSummaryBytes(indexMetricsSnapshot.WALSize)) - var mem runtime.MemStats runtime.ReadMemStats(&mem) const gib = 1024 * 1024 * 1024 @@ -2341,7 +2270,6 @@ func ReplayBlocks( mlog.Log.InfofPrecise("") // Reset slices (reuse capacity) - indexMetricsSummaryStart = indexMetricsSnapshot execTimes = execTimes[:0] waitTimes = waitTimes[:0] cuValues = cuValues[:0] @@ -2546,6 +2474,79 @@ func sequentialTxLoop(slotCtx *sealevel.SlotCtx, sigverifyWg *sync.WaitGroup, bl return txFeeAccumulator } +func lightbringerEntryExecutionBatches(transactions []*solana.Transaction, entry *b.TxEntry, relaxIntraBatchAccountLocks bool) [][]uint64 { + if len(entry.Indices) == 0 { + return nil + } + if !relaxIntraBatchAccountLocks { + return [][]uint64{entry.Indices} + } + + flushDerivableSegment := func(batches *[][]uint64, segment []uint64) { + if len(segment) == 0 { + return + } + + // Build batches directly instead of allocating a full dependency graph per + // entry segment. The per-entry fallback is intentionally cheap because the + // common Lightbringer path should stay on the whole-block planner. + segmentBatches := make([][]uint64, 0, 1) + lastReadBatch := make(map[solana.PublicKey]int, len(segment)*4) + lastWriteBatch := make(map[solana.PublicKey]int, len(segment)*2) + for _, txIdx := range segment { + tx := transactions[txIdx] + readonlyAccounts := messageReadonlyAccounts(&tx.Message) + writableAccounts := messageWritableAccounts(&tx.Message) + + batchIdx := 0 + for _, roAcct := range readonlyAccounts { + if writeBatch, exists := lastWriteBatch[roAcct]; exists && writeBatch >= batchIdx { + batchIdx = writeBatch + 1 + } + } + for _, writeAcct := range writableAccounts { + if readBatch, exists := lastReadBatch[writeAcct]; exists && readBatch >= batchIdx { + batchIdx = readBatch + 1 + } + if writeBatch, exists := lastWriteBatch[writeAcct]; exists && writeBatch >= batchIdx { + batchIdx = writeBatch + 1 + } + } + + for len(segmentBatches) <= batchIdx { + segmentBatches = append(segmentBatches, nil) + } + segmentBatches[batchIdx] = append(segmentBatches[batchIdx], txIdx) + for _, roAcct := range readonlyAccounts { + lastReadBatch[roAcct] = batchIdx + } + for _, writeAcct := range writableAccounts { + lastWriteBatch[writeAcct] = batchIdx + } + } + *batches = append(*batches, segmentBatches...) + } + + // Under SIMD-0083, Lightbringer entries may legally contain intra-entry + // conflicts. Recover safe parallelism for contiguous derivable segments and + // treat unresolved transactions as ordering barriers. + batches := make([][]uint64, 0, len(entry.Indices)) + derivableSegment := make([]uint64, 0, len(entry.Indices)) + for _, txIdx := range entry.Indices { + if canDeriveAccountsFromMessage(transactions[txIdx]) { + derivableSegment = append(derivableSegment, txIdx) + continue + } + + flushDerivableSegment(&batches, derivableSegment) + derivableSegment = derivableSegment[:0] + batches = append(batches, []uint64{txIdx}) + } + flushDerivableSegment(&batches, derivableSegment) + + return batches +} + func parallelTxLoop(slotCtx *sealevel.SlotCtx, sigverifyWg *sync.WaitGroup, block *b.Block, rblock *b.Block, txParallelism int, dbgOpts *DebugOptions) fees.TxFeeInfoAccumulator { var txFeeAccumulator fees.TxFeeInfoAccumulator txFeeInfos := make([]*fees.TxFeeInfo, len(block.Transactions)) @@ -2596,20 +2597,51 @@ func parallelTxLoop(slotCtx *sealevel.SlotCtx, sigverifyWg *sync.WaitGroup, bloc wg.Wait() close(done) } else if rblock.FromLightbringer { - wg := &sync.WaitGroup{} - workerPool, _ := ants.NewPoolWithFunc(txParallelism, func(i interface{}) { - defer wg.Done() - idx := i.(uint64) - txFeeInfos[idx], errs[idx] = ProcessTransaction(slotCtx, sigverifyWg, rblock.Transactions[idx], nil, dbgOpts, nil) - }) + batchWg := &sync.WaitGroup{} + workersWg := &sync.WaitGroup{} + do := make(chan uint64, txParallelism) + workersWg.Add(txParallelism) + for i := range txParallelism { + go func(workerIdx int) { + defer workersWg.Done() + for idx := range do { + txStart := time.Now() + tx := block.Transactions[idx] + var txMeta *rpc.TransactionMeta + if int(idx) < len(rblock.TxMetas) { + txMeta = rblock.TxMetas[idx] + } + txFeeInfos[idx], errs[idx] = ProcessTransaction(slotCtx, sigverifyWg, rblock.Transactions[idx], txMeta, dbgOpts, sealevel.BorrowedAccountArenas[workerIdx]) + txErr := errs[idx] + if txMeta != nil && txErr == nil && txMeta.Err != nil { + mlog.Log.Errorf("[run:%s] DIVERGENCE in slot %d: tx %s succeeded locally but failed onchain: %+v", + CurrentRunID, block.Slot, tx.Signatures[0], txMeta.Err) + panic(fmt.Sprintf("tx %s return value divergence: txErr was nil, but onchain err was %+v", tx.Signatures[0], txMeta.Err)) + } else if txMeta != nil && txErr != nil && txMeta.Err == nil { + mlog.Log.Errorf("[run:%s] DIVERGENCE in slot %d: tx %s failed locally (%v) but succeeded onchain", + CurrentRunID, block.Slot, tx.Signatures[0], txErr) + panic(fmt.Sprintf("tx %s return value divergence: txErr was %+v (%s), but onchain err was nil", tx.Signatures[0], txErr, txErr)) + } + txDurations[workerIdx] += time.Since(txStart) + batchWg.Done() + } + }(i) + } + relaxIntraBatchAccountLocks := rblock.Features != nil && + rblock.Features.IsActive(features.RelaxIntraBatchAccountLocks) for _, entry := range rblock.Entries { - for _, txIdx := range entry.Indices { - wg.Add(1) - workerPool.Invoke(txIdx) + batches := lightbringerEntryExecutionBatches(rblock.Transactions, entry, relaxIntraBatchAccountLocks) + for _, batch := range batches { + batchWg.Add(len(batch)) + for _, txIdx := range batch { + do <- txIdx + } + batchWg.Wait() } - wg.Wait() } + close(do) + workersWg.Wait() } else { panic("dependency planner unavailable for non-Lightbringer block") } @@ -2815,17 +2847,6 @@ func ProcessBlock( err = acctsDb.StoreAccounts(modifiedAccts, slotCtx.Slot, afterStoreAccounts) } - /* - // EAH workaround - see comment at top of replay loop for details - // Commented since it seems to be disabled but preserved for the curious? - if slotCtx.HasEahWorkaround { - slotCtx.FinalBankhash = slotCtx.EahWorkaroundBankhash - commitInProgress.Store(false) - commitSlot.Store(0) - return slotCtx, err - } - */ - global.IncrTransactionCount(uint64(len(block.Transactions))) setReplayStage("done") return slotCtx, err From 9efd45144148a8683ca4c7d48a107c0ad9ad70f5 Mon Sep 17 00:00:00 2001 From: smcio Date: Thu, 9 Apr 2026 09:30:46 +0200 Subject: [PATCH 2/2] implement "SIMD-0444: Relax program data account check in migration" and "SIMD-0266: Efficient Token program" --- pkg/features/gates.go | 4 +- pkg/replay/block.go | 77 +------ pkg/replay/feature_activation.go | 366 +++++++++++++++++++++++++++++++ pkg/sealevel/bpf_loader.go | 15 +- 4 files changed, 383 insertions(+), 79 deletions(-) create mode 100644 pkg/replay/feature_activation.go diff --git a/pkg/features/gates.go b/pkg/features/gates.go index 650ef518..653f4084 100644 --- a/pkg/features/gates.go +++ b/pkg/features/gates.go @@ -67,6 +67,8 @@ var FixAltBn128PairingLengthCheck = FeatureGate{Name: "FixAltBn128PairingLengthC var DeprecateRentExemptionThreshold = FeatureGate{Name: "DeprecateRentExemptionThreshold", Address: base58.MustDecodeFromString("rent6iVy6PDoViPBeJ6k5EJQrkj62h7DPyLbWGHwjrC")} var ProvideInstructionDataOffsetInVmR2 = FeatureGate{Name: "ProvideInstructionDataOffsetInVmR2", Address: base58.MustDecodeFromString("5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL")} var VoteStateV4 = FeatureGate{Name: "VoteStateV4", Address: base58.MustDecodeFromString("Gx4XFcrVMt4HUvPzTpTSVkdDVgcDSjKhDN1RqRS6KDuZ")} +var RelaxProgramdataAccountCheckMigration = FeatureGate{Name: "RelaxProgramdataAccountCheckMigration", Address: base58.MustDecodeFromString("rexav5eNTUSNT1K2N7cfRjnthwhcP5BC25v2tA4rW4h")} +var ReplaceSplTokenWithPToken = FeatureGate{Name: "ReplaceSplTokenWithPToken", Address: base58.MustDecodeFromString("ptokFjwyJtrwCa9Kgo9xoDS59V4QccBGEaRFnRPnSdP")} var AllFeatureGates = []FeatureGate{StopTruncatingStringsInSyscalls, EnablePartitionedEpochReward, EnablePartitionedEpochRewardsSuperfeature, LastRestartSlotSysvar, Libsecp256k1FailOnBadCount, Libsecp256k1FailOnBadCount2, EnableBpfLoaderSetAuthorityCheckedIx, @@ -84,4 +86,4 @@ var AllFeatureGates = []FeatureGate{StopTruncatingStringsInSyscalls, EnableParti EnableSbpfV3DeploymentAndExecution, DisableSbpfV0Execution, ReenableSbpfV0Execution, FormalizeLoadedTransactionDataSize, IncreaseCpiAccountInfoLimit, StaticInstructionLimit, PoseidonEnforcePadding, FixAltBn128PairingLengthCheck, DeprecateRentExemptionThreshold, ProvideInstructionDataOffsetInVmR2, - VoteStateV4} + VoteStateV4, RelaxProgramdataAccountCheckMigration, ReplaceSplTokenWithPToken} diff --git a/pkg/replay/block.go b/pkg/replay/block.go index 1aab7c07..c1aa95e9 100644 --- a/pkg/replay/block.go +++ b/pkg/replay/block.go @@ -657,79 +657,6 @@ func loadBlockAccountsAndUpdateSysvars(accountsDb *accountsdb.AccountsDb, block return accts, parentAccts, nil } -func scanAndEnableFeatures(acctsDb *accountsdb.AccountsDb, slot uint64, startOfEpoch bool) (*features.Features, []*accounts.Account, []*accounts.Account) { - parentAccts := make([]*accounts.Account, 0) - modifiedAccts := make([]*accounts.Account, 0) - - f := features.NewFeaturesDefault() - - for _, featureGate := range features.AllFeatureGates { - acct, err := acctsDb.GetAccount(slot, featureGate.Address) - if err == nil { - if acct.Owner != a.FeatureAddr { - continue - } - parentAccts = append(parentAccts, acct.Clone()) - - featureAcct := features.UnmarshalFeatureAcct(acct.Data) - - // already activated - if featureAcct.ActivatedAt != nil && slot >= *featureAcct.ActivatedAt { - f.EnableFeature(featureGate, *featureAcct.ActivatedAt) - } - - if featureAcct.ActivatedAt == nil && startOfEpoch { - newFeatureAcct := &features.FeatureAcct{ActivatedAt: &slot} - newFeatureAcctBytes, err := features.MarshalFeatureAcct(newFeatureAcct) - if err != nil { - panic(err) - } - - acct.Data = newFeatureAcctBytes - modifiedAccts = append(modifiedAccts, acct) - - f.EnableFeature(featureGate, slot) - } - } - } - - if len(modifiedAccts) != 0 { - err := acctsDb.StoreAccounts(modifiedAccts, slot, nil) - if err != nil { - panic(err) - } - } - - for _, featureAcct := range modifiedAccts { - // Handle *SIMD-0194: Deprecate rent exemption threshold* feature activation by updating the Rent sysvar. - if f.IsActive(features.DeprecateRentExemptionThreshold) && featureAcct.Key == features.DeprecateRentExemptionThreshold.Address { - var rentSysvar sealevel.SysvarRent - rentSysvar.LamportsPerUint8Year = uint64(float64(sealevel.SysvarCache.Rent.Sysvar.LamportsPerUint8Year) * sealevel.SysvarCache.Rent.Sysvar.ExemptionThreshold) - rentSysvar.ExemptionThreshold = 1.0 - rentSysvar.BurnPercent = sealevel.SysvarCache.Rent.Sysvar.BurnPercent - - rentAcct, err := acctsDb.GetAccount(slot, sealevel.SysvarRentAddr) - if err != nil { - panic(err) - } - parentAccts = append(parentAccts, rentAcct.Clone()) - - newRentSysvarBytes := rentSysvar.MustMarshal() - copy(rentAcct.Data, newRentSysvarBytes) - err = acctsDb.StoreAccounts([]*accounts.Account{rentAcct}, slot, nil) - if err != nil { - panic(err) - } - modifiedAccts = append(modifiedAccts, rentAcct) - - sealevel.SysvarCache.Rent.Sysvar = &rentSysvar - sealevel.SysvarCache.Rent.Acct = rentAcct - } - } - - return f, modifiedAccts, parentAccts -} - // setupInitialVoteAcctsAndStakeAccts populates the vote and stake caches at startup. // // For stake accounts, we read ALL delegation fields from AccountsDB rather than trusting @@ -1276,7 +1203,7 @@ func ReplayBlocks( // Use state file for transaction count (required) global.IncrTransactionCount(mithrilState.ManifestTransactionCount) isFirstSlotInEpoch := epochSchedule.FirstSlotInEpoch(currentEpoch) == startSlot - replayCtx.CurrentFeatures, featuresActivatedInFirstSlot, parentFeaturesActivatedInFirstSlot = scanAndEnableFeatures(acctsDb, startSlot, isFirstSlotInEpoch) + replayCtx.CurrentFeatures, featuresActivatedInFirstSlot, parentFeaturesActivatedInFirstSlot = scanAndEnableFeatures(acctsDb, replayCtx, startSlot, isFirstSlotInEpoch) partitionedEpochRewardsEnabled = replayCtx.CurrentFeatures.IsActive(features.EnablePartitionedEpochReward) || replayCtx.CurrentFeatures.IsActive(features.EnablePartitionedEpochRewardsSuperfeature) // Load epoch stakes - persisted stakes on resume, state file on fresh start @@ -1816,7 +1743,7 @@ func ReplayBlocks( mlog.Log.Infof("%d -> %d", currentEpoch, currentEpoch+1) var newlyActivatedFeatures, parentNewlyActivatedFeatures []*accounts.Account - replayCtx.CurrentFeatures, newlyActivatedFeatures, parentNewlyActivatedFeatures = scanAndEnableFeatures(acctsDb, currentSlot, true) + replayCtx.CurrentFeatures, newlyActivatedFeatures, parentNewlyActivatedFeatures = scanAndEnableFeatures(acctsDb, replayCtx, currentSlot, true) partitionedEpochRewardsEnabled = replayCtx.CurrentFeatures.IsActive(features.EnablePartitionedEpochReward) || replayCtx.CurrentFeatures.IsActive(features.EnablePartitionedEpochRewardsSuperfeature) partitionedRewardsInfo = handleEpochTransition(acctsDb, partitionedEpochRewardsEnabled, lastSlotCtx, replayCtx, epochSchedule, replayCtx.CurrentFeatures, block, currentEpoch) currentEpoch = block.Epoch diff --git a/pkg/replay/feature_activation.go b/pkg/replay/feature_activation.go new file mode 100644 index 00000000..712f588b --- /dev/null +++ b/pkg/replay/feature_activation.go @@ -0,0 +1,366 @@ +package replay + +import ( + "bytes" + "errors" + "fmt" + "math" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/accountsdb" + a "github.com/Overclock-Validator/mithril/pkg/addresses" + "github.com/Overclock-Validator/mithril/pkg/base58" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/mlog" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +const ( + upgradeableLoaderProgramStateSize = 36 + upgradeableLoaderBufferMetadataSize = 37 + upgradeableLoaderProgramDataMetadataSize = 45 +) + +var splTokenProgramID = base58.MustDecodeFromString("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +var pTokenProgramBuffer = base58.MustDecodeFromString("ptok6rngomXrDbWf5v5Mkmu5CEbB51hzSCPDoj9DrvF") + +type loaderV2ProgramMigration struct { + modifiedAccts []*accounts.Account + parentAccts []*accounts.Account + lamportsToBurn uint64 + lamportsToFund uint64 +} + +func scanAndEnableFeatures(acctsDb *accountsdb.AccountsDb, replayCtx *ReplayCtx, slot uint64, startOfEpoch bool) (*features.Features, []*accounts.Account, []*accounts.Account) { + parentAccts := make([]*accounts.Account, 0) + modifiedAccts := make([]*accounts.Account, 0) + + f := features.NewFeaturesDefault() + + for _, featureGate := range features.AllFeatureGates { + acct, err := acctsDb.GetAccount(slot, featureGate.Address) + if err != nil { + continue + } + if acct.Owner != a.FeatureAddr { + continue + } + + parentAccts = append(parentAccts, acct.Clone()) + featureAcct := features.UnmarshalFeatureAcct(acct.Data) + + if featureAcct.ActivatedAt != nil && slot >= *featureAcct.ActivatedAt { + f.EnableFeature(featureGate, *featureAcct.ActivatedAt) + } + + if featureAcct.ActivatedAt == nil && startOfEpoch { + newFeatureAcct := &features.FeatureAcct{ActivatedAt: &slot} + newFeatureAcctBytes, err := features.MarshalFeatureAcct(newFeatureAcct) + if err != nil { + panic(err) + } + + acct.Data = newFeatureAcctBytes + modifiedAccts = append(modifiedAccts, acct) + f.EnableFeature(featureGate, slot) + } + } + + if len(modifiedAccts) != 0 { + if err := acctsDb.StoreAccounts(modifiedAccts, slot, nil); err != nil { + panic(err) + } + } + + for _, featureAcct := range modifiedAccts { + if f.IsActive(features.DeprecateRentExemptionThreshold) && featureAcct.Key == features.DeprecateRentExemptionThreshold.Address { + modifiedRentAcct, parentRentAcct, err := applyDeprecateRentExemptionThresholdActivation(acctsDb, slot) + if err != nil { + panic(err) + } + modifiedAccts = append(modifiedAccts, modifiedRentAcct) + parentAccts = append(parentAccts, parentRentAcct) + } + + if f.IsActive(features.ReplaceSplTokenWithPToken) && featureAcct.Key == features.ReplaceSplTokenWithPToken.Address { + migratedAccts, parentMigratedAccts, err := applyReplaceSplTokenWithPTokenActivation(acctsDb, replayCtx, slot, f) + if err != nil { + mlog.Log.Warnf("Failed to replace SPL Token with p-token buffer '%s': %v", pTokenProgramBuffer, err) + continue + } + + modifiedAccts = append(modifiedAccts, migratedAccts...) + parentAccts = append(parentAccts, parentMigratedAccts...) + } + } + + return f, modifiedAccts, parentAccts +} + +func applyDeprecateRentExemptionThresholdActivation(acctsDb *accountsdb.AccountsDb, slot uint64) (*accounts.Account, *accounts.Account, error) { + rentSysvar := sealevel.SysvarRent{ + LamportsPerUint8Year: uint64(float64(sealevel.SysvarCache.Rent.Sysvar.LamportsPerUint8Year) * sealevel.SysvarCache.Rent.Sysvar.ExemptionThreshold), + ExemptionThreshold: 1.0, + BurnPercent: sealevel.SysvarCache.Rent.Sysvar.BurnPercent, + } + + rentAcct, err := acctsDb.GetAccount(slot, sealevel.SysvarRentAddr) + if err != nil { + return nil, nil, err + } + parentRentAcct := rentAcct.Clone() + + newRentSysvarBytes := rentSysvar.MustMarshal() + copy(rentAcct.Data, newRentSysvarBytes) + if err := acctsDb.StoreAccounts([]*accounts.Account{rentAcct}, slot, nil); err != nil { + return nil, nil, err + } + + sealevel.SysvarCache.Rent.Sysvar = &rentSysvar + sealevel.SysvarCache.Rent.Acct = rentAcct + + return rentAcct, parentRentAcct, nil +} + +func marshalUpgradeableLoaderStateSized(state *sealevel.UpgradeableLoaderState, size int) ([]byte, error) { + var buf bytes.Buffer + encoder := bin.NewBinEncoder(&buf) + if err := state.MarshalWithEncoder(encoder); err != nil { + return nil, err + } + + serialized := buf.Bytes() + if len(serialized) > size { + return nil, fmt.Errorf("serialized upgradeable loader state length %d exceeds fixed size %d", len(serialized), size) + } + + stateBytes := make([]byte, size) + copy(stateBytes, serialized) + return stateBytes, nil +} + +func deriveUpgradeableLoaderProgramDataAddress(programAddress solana.PublicKey) (solana.PublicKey, error) { + programDataAddress, _, err := solana.FindProgramAddress([][]byte{programAddress.Bytes()}, a.BpfLoaderUpgradeableAddr) + return programDataAddress, err +} + +func migrationAccountExists(acct *accounts.Account) bool { + if acct == nil { + return false + } + + var zeroOwner [32]byte + return acct.Lamports != 0 || len(acct.Data) != 0 || acct.Owner != zeroOwner || acct.Executable +} + +func buildLoaderV2ProgramUpgradeToLoaderV3( + slot uint64, + rentSysvar *sealevel.SysvarRent, + targetProgram *accounts.Account, + sourceBuffer *accounts.Account, + existingProgramData *accounts.Account, + allowPrefunded bool, +) (*loaderV2ProgramMigration, error) { + if rentSysvar == nil { + return nil, errors.New("rent sysvar unavailable for p-token migration") + } + if targetProgram == nil { + return nil, errors.New("missing SPL Token program account") + } + if targetProgram.Owner != a.BpfLoader2Addr { + return nil, fmt.Errorf("SPL Token program owner %s is not loader v2", solana.PublicKeyFromBytes(targetProgram.Owner[:])) + } + if !targetProgram.Executable { + return nil, errors.New("SPL Token program account is not executable") + } + if sourceBuffer == nil { + return nil, errors.New("missing p-token source buffer account") + } + if sourceBuffer.Owner != a.BpfLoaderUpgradeableAddr { + return nil, fmt.Errorf("p-token source buffer owner %s is not loader v3", solana.PublicKeyFromBytes(sourceBuffer.Owner[:])) + } + if len(sourceBuffer.Data) < upgradeableLoaderBufferMetadataSize { + return nil, errors.New("p-token source buffer data too short") + } + + sourceBufferState, err := sealevel.UnmarshalUpgradeableLoaderState(sourceBuffer.Data) + if err != nil { + return nil, fmt.Errorf("failed to decode p-token source buffer state: %w", err) + } + if sourceBufferState.Type != sealevel.UpgradeableLoaderStateTypeBuffer { + return nil, fmt.Errorf("p-token source buffer has invalid state type %d", sourceBufferState.Type) + } + + programDataAddress, err := deriveUpgradeableLoaderProgramDataAddress(targetProgram.Key) + if err != nil { + return nil, fmt.Errorf("failed to derive programdata address for %s: %w", targetProgram.Key, err) + } + + programDataParentAcct := &accounts.Account{Key: programDataAddress, RentEpoch: math.MaxUint64} + if migrationAccountExists(existingProgramData) { + if !allowPrefunded { + return nil, fmt.Errorf("programdata account %s already exists", programDataAddress) + } + if existingProgramData.Owner != a.SystemProgramAddr { + return nil, fmt.Errorf("prefunded programdata account %s has unexpected owner %s", programDataAddress, solana.PublicKeyFromBytes(existingProgramData.Owner[:])) + } + programDataParentAcct = existingProgramData.Clone() + } + + elfBytes := sourceBuffer.Data[upgradeableLoaderBufferMetadataSize:] + + programStateBytes, err := marshalUpgradeableLoaderStateSized(&sealevel.UpgradeableLoaderState{ + Type: sealevel.UpgradeableLoaderStateTypeProgram, + Program: sealevel.UpgradeableLoaderStateProgram{ + ProgramDataAddress: programDataAddress, + }, + }, upgradeableLoaderProgramStateSize) + if err != nil { + return nil, fmt.Errorf("failed to encode migrated program account state: %w", err) + } + + programDataStateBytes, err := marshalUpgradeableLoaderStateSized(&sealevel.UpgradeableLoaderState{ + Type: sealevel.UpgradeableLoaderStateTypeProgramData, + ProgramData: sealevel.UpgradeableLoaderStateProgramData{ + Slot: slot, + UpgradeAuthorityAddress: nil, + }, + }, upgradeableLoaderProgramDataMetadataSize) + if err != nil { + return nil, fmt.Errorf("failed to encode migrated programdata account state: %w", err) + } + + programDataBytes := make([]byte, upgradeableLoaderProgramDataMetadataSize+len(elfBytes)) + copy(programDataBytes, programDataStateBytes) + copy(programDataBytes[upgradeableLoaderProgramDataMetadataSize:], elfBytes) + + newProgramAcct := &accounts.Account{ + Slot: slot, + Key: targetProgram.Key, + Lamports: rentSysvar.MinimumBalance(upgradeableLoaderProgramStateSize), + Data: programStateBytes, + Owner: a.BpfLoaderUpgradeableAddr, + Executable: true, + RentEpoch: math.MaxUint64, + } + + newProgramDataAcct := &accounts.Account{ + Slot: slot, + Key: programDataAddress, + Lamports: rentSysvar.MinimumBalance(uint64(len(programDataBytes))), + Data: programDataBytes, + Owner: a.BpfLoaderUpgradeableAddr, + Executable: false, + RentEpoch: math.MaxUint64, + } + + clearedSourceBuffer := &accounts.Account{ + Slot: slot, + Key: sourceBuffer.Key, + RentEpoch: math.MaxUint64, + } + + lamportsToBurn := targetProgram.Lamports + sourceBuffer.Lamports + if migrationAccountExists(existingProgramData) { + lamportsToBurn += existingProgramData.Lamports + } + + return &loaderV2ProgramMigration{ + modifiedAccts: []*accounts.Account{ + newProgramAcct, + newProgramDataAcct, + clearedSourceBuffer, + }, + parentAccts: []*accounts.Account{ + targetProgram.Clone(), + programDataParentAcct, + sourceBuffer.Clone(), + }, + lamportsToBurn: lamportsToBurn, + lamportsToFund: newProgramAcct.Lamports + newProgramDataAcct.Lamports, + }, nil +} + +func applyCapitalizationChange(replayCtx *ReplayCtx, lamportsToBurn uint64, lamportsToFund uint64) error { + if replayCtx == nil { + return errors.New("replay context unavailable for capitalization update") + } + + switch { + case lamportsToBurn > lamportsToFund: + diff := lamportsToBurn - lamportsToFund + if replayCtx.Capitalization < diff { + return fmt.Errorf("capitalization underflow applying migration delta %d", diff) + } + replayCtx.Capitalization -= diff + case lamportsToFund > lamportsToBurn: + diff := lamportsToFund - lamportsToBurn + if math.MaxUint64-replayCtx.Capitalization < diff { + return fmt.Errorf("capitalization overflow applying migration delta %d", diff) + } + replayCtx.Capitalization += diff + } + + return nil +} + +func applyReplaceSplTokenWithPTokenActivation( + acctsDb *accountsdb.AccountsDb, + replayCtx *ReplayCtx, + slot uint64, + f *features.Features, +) ([]*accounts.Account, []*accounts.Account, error) { + targetProgram, err := acctsDb.GetAccount(slot, splTokenProgramID) + if err != nil { + return nil, nil, err + } + + sourceBuffer, err := acctsDb.GetAccount(slot, pTokenProgramBuffer) + if err != nil { + return nil, nil, err + } + + programDataAddress, err := deriveUpgradeableLoaderProgramDataAddress(splTokenProgramID) + if err != nil { + return nil, nil, err + } + + var existingProgramData *accounts.Account + existingProgramData, err = acctsDb.GetAccount(slot, programDataAddress) + if err != nil && !errors.Is(err, accountsdb.ErrNoAccount) { + return nil, nil, err + } + if errors.Is(err, accountsdb.ErrNoAccount) { + existingProgramData = nil + } + + if err := sealevel.ValidateUpgradeableLoaderProgram(sourceBuffer.Data[upgradeableLoaderBufferMetadataSize:], f); err != nil { + return nil, nil, fmt.Errorf("failed to validate p-token program bytes: %w", err) + } + + migration, err := buildLoaderV2ProgramUpgradeToLoaderV3( + slot, + sealevel.SysvarCache.Rent.Sysvar, + targetProgram, + sourceBuffer, + existingProgramData, + f.IsActive(features.RelaxProgramdataAccountCheckMigration), + ) + if err != nil { + return nil, nil, err + } + + if err := applyCapitalizationChange(replayCtx, migration.lamportsToBurn, migration.lamportsToFund); err != nil { + return nil, nil, err + } + + acctsDb.RemoveProgramFromCache(splTokenProgramID) + + if err := acctsDb.StoreAccounts(migration.modifiedAccts, slot, nil); err != nil { + return nil, nil, err + } + + return migration.modifiedAccts, migration.parentAccts, nil +} diff --git a/pkg/sealevel/bpf_loader.go b/pkg/sealevel/bpf_loader.go index a1c8c382..3a643c36 100644 --- a/pkg/sealevel/bpf_loader.go +++ b/pkg/sealevel/bpf_loader.go @@ -396,12 +396,12 @@ func writeProgramData(execCtx *ExecutionCtx, programDataOffset uint64, bytes []b return nil } -func deployProgram(execCtx *ExecutionCtx, programData []byte) (*sbpf.Program, error) { +func validateUpgradeableLoaderProgram(programData []byte, f *features.Features) (*sbpf.Program, error) { syscallRegistry := sbpf.SyscallRegistry(func(u uint32) (sbpf.Syscall, bool) { - return Syscalls(&execCtx.Features, true, u) + return Syscalls(f, true, u) }) - loader, err := loader.NewLoaderWithSyscalls(programData, syscallRegistry, true, &execCtx.Features) + loader, err := loader.NewLoaderWithSyscalls(programData, syscallRegistry, true, f) if err != nil { //mlog.Log.Debugf("failed to create loader: %s", err) return nil, err @@ -422,6 +422,15 @@ func deployProgram(execCtx *ExecutionCtx, programData []byte) (*sbpf.Program, er return program, nil } +func ValidateUpgradeableLoaderProgram(programData []byte, f *features.Features) error { + _, err := validateUpgradeableLoaderProgram(programData, f) + return err +} + +func deployProgram(execCtx *ExecutionCtx, programData []byte) (*sbpf.Program, error) { + return validateUpgradeableLoaderProgram(programData, &execCtx.Features) +} + const ( kibibyte uint64 = 1024 pageSizeKB uint64 = 32