From 9049752d8fcf93d9fe30f8cfbfef08a82b45da32 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:48:01 -0600 Subject: [PATCH] feat: implement SIMD-0185 vote state v4 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add VoteState4 struct with all V4 fields (inflation/block revenue collectors, commission as u16 bps, pending delegator rewards, BLS pubkey). Handle discriminant 3 in all VoteStateVersions switch sites (unmarshal, marshal, IsInitialized, ConvertToCurrent, LastTimestamp, NodePubkey). V4 write path serializes as discriminant 3 with no V1_14_11 fallback on resize failure (returns AccountNotRentExempt per SIMD-0185). Preserve V4-specific fields through the VoteState working struct round-trip; first-time V3→V4 conversion defaults match Agave (inflation_rewards_collector=vote_pubkey, block_revenue_collector= node_pubkey). Behavioral changes: V4 purges authorized voters at currentEpoch-1 (retains one extra epoch) and skips PriorVoters tracking. Update all external consumers (rewards, transaction, manifest_decoder). Co-Authored-By: Claude Opus 4.6 --- pkg/features/gates.go | 4 +- pkg/replay/transaction.go | 3 + pkg/rewards/rewards.go | 4 + pkg/sealevel/vote_program.go | 15 +- pkg/sealevel/vote_state.go | 383 +++++++++++++++++++++++++++++-- pkg/snapshot/manifest_decoder.go | 6 + 6 files changed, 395 insertions(+), 20 deletions(-) diff --git a/pkg/features/gates.go b/pkg/features/gates.go index 1cac3539..837d1162 100644 --- a/pkg/features/gates.go +++ b/pkg/features/gates.go @@ -65,6 +65,7 @@ var PoseidonEnforcePadding = FeatureGate{Name: "PoseidonEnforcePadding", Address var FixAltBn128PairingLengthCheck = FeatureGate{Name: "FixAltBn128PairingLengthCheck", Address: base58.MustDecodeFromString("bnYzodLwmybj7e1HAe98yZrdJTd7we69eMMLgCXqKZm")} 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 AllFeatureGates = []FeatureGate{StopTruncatingStringsInSyscalls, EnablePartitionedEpochReward, EnablePartitionedEpochRewardsSuperfeature, LastRestartSlotSysvar, Libsecp256k1FailOnBadCount, Libsecp256k1FailOnBadCount2, EnableBpfLoaderSetAuthorityCheckedIx, @@ -81,4 +82,5 @@ var AllFeatureGates = []FeatureGate{StopTruncatingStringsInSyscalls, EnableParti AccountsLtHash, RemoveAccountsDeltaHash, EnableLoaderV4, EnableSbpfV1DeploymentAndExecution, EnableSbpfV2DeploymentAndExecution, EnableSbpfV3DeploymentAndExecution, DisableSbpfV0Execution, ReenableSbpfV0Execution, FormalizeLoadedTransactionDataSize, IncreaseCpiAccountInfoLimit, StaticInstructionLimit, PoseidonEnforcePadding, FixAltBn128PairingLengthCheck, DeprecateRentExemptionThreshold, - ProvideInstructionDataOffsetInVmR2} + ProvideInstructionDataOffsetInVmR2, + VoteStateV4} diff --git a/pkg/replay/transaction.go b/pkg/replay/transaction.go index 3e2506d5..5658d53c 100644 --- a/pkg/replay/transaction.go +++ b/pkg/replay/transaction.go @@ -225,6 +225,9 @@ func recordVoteTimestampAndSlot(slotCtx *sealevel.SlotCtx, acct *accounts.Accoun case sealevel.VoteStateVersionV1_14_11: timestamp = voteStateVersioned.V1_14_11.LastTimestamp + + case sealevel.VoteStateVersionV4: + timestamp = voteStateVersioned.V4.LastTimestamp } slotCtx.VoteTimestampMu.Lock() diff --git a/pkg/rewards/rewards.go b/pkg/rewards/rewards.go index dd5588ec..de95bc32 100644 --- a/pkg/rewards/rewards.go +++ b/pkg/rewards/rewards.go @@ -388,6 +388,8 @@ func voteCommissionSplit(voteState *sealevel.VoteStateVersions, rewards uint64) commission = voteState.V0_23_5.Commission case sealevel.VoteStateVersionV1_14_11: commission = voteState.V1_14_11.Commission + case sealevel.VoteStateVersionV4: + commission = byte(voteState.V4.InflationRewardsCommissionBps / 100) } commissionRate := uint64(min(commission, 100)) @@ -429,6 +431,8 @@ func calculateStakePointsAndCredits( epochCredits = voteState.V0_23_5.EpochCredits case sealevel.VoteStateVersionV1_14_11: epochCredits = voteState.V1_14_11.EpochCredits + case sealevel.VoteStateVersionV4: + epochCredits = voteState.V4.EpochCredits default: panic("invalid vote state - should be impossible") } diff --git a/pkg/sealevel/vote_program.go b/pkg/sealevel/vote_program.go index 490100fb..318f5512 100644 --- a/pkg/sealevel/vote_program.go +++ b/pkg/sealevel/vote_program.go @@ -1108,7 +1108,7 @@ func VoteProgramAuthorize(execCtx *ExecutionCtx, voteAcct *BorrowedAccount, auth } else { return verifySigner(epochAuthorizedVoter, signers) } - }) + }, f) if err != nil { return err } @@ -1246,12 +1246,15 @@ func VoteProgramUpdateCommission(execCtx *ExecutionCtx, voteAcct *BorrowedAccoun } voteState.Commission = commission + if voteState.wasV4 { + voteState.v4InflationRewardsCommBps = uint16(commission) * 100 + } err = setVoteAccountState(execCtx, voteAcct, voteState, f) return err } -func verifyAndGetVoteState(voteAcct *BorrowedAccount, clock SysvarClock, signers []solana.PublicKey) (*VoteState, error) { +func verifyAndGetVoteState(voteAcct *BorrowedAccount, clock SysvarClock, signers []solana.PublicKey, f features.Features) (*VoteState, error) { versioned, err := UnmarshalVersionedVoteState(voteAcct.Data()) if err != nil { return nil, err @@ -1262,7 +1265,7 @@ func verifyAndGetVoteState(voteAcct *BorrowedAccount, clock SysvarClock, signers } voteState := versioned.ConvertToCurrent() - authVoter, err := voteState.GetAndUpdateAuthorizedVoter(clock.Epoch) + authVoter, err := voteState.GetAndUpdateAuthorizedVoter(clock.Epoch, f) if err != nil { return nil, err } @@ -1378,7 +1381,7 @@ func processVote(voteState *VoteState, vote *VoteInstrVote, slotHashes SysvarSlo func VoteProgramProcessVote(execCtx *ExecutionCtx, voteAcct *BorrowedAccount, slotHashes SysvarSlotHashes, clock SysvarClock, vote *VoteInstrVote, signers []solana.PublicKey, f features.Features) error { //mlog.Log.Debugf("Vote / VoteSwitch") - voteState, err := verifyAndGetVoteState(voteAcct, clock, signers) + voteState, err := verifyAndGetVoteState(voteAcct, clock, signers, f) if err != nil { return err } @@ -1769,7 +1772,7 @@ func processNewVoteState(voteState *VoteState, newState *deque.Deque[LandedVote] func VoteProgramProcessVoteStateUpdate(execCtx *ExecutionCtx, voteAcct *BorrowedAccount, slotHashes SysvarSlotHashes, clock SysvarClock, voteStateUpdate *VoteInstrUpdateVoteState, signers []solana.PublicKey, f features.Features) error { //mlog.Log.Debugf("VoteStateUpdate") - voteState, err := verifyAndGetVoteState(voteAcct, clock, signers) + voteState, err := verifyAndGetVoteState(voteAcct, clock, signers, f) if err != nil { return err } @@ -1877,7 +1880,7 @@ var ( ) func VoteProgramProcessTowerSync(execCtx *ExecutionCtx, voteAcct *BorrowedAccount, slotHashes SysvarSlotHashes, clock SysvarClock, towerSync *VoteInstrTowerSync, signers []solana.PublicKey, f features.Features) error { - voteState, err := verifyAndGetVoteState(voteAcct, clock, signers) + voteState, err := verifyAndGetVoteState(voteAcct, clock, signers, f) if err != nil { return err } diff --git a/pkg/sealevel/vote_state.go b/pkg/sealevel/vote_state.go index fe3610f4..1d48215b 100644 --- a/pkg/sealevel/vote_state.go +++ b/pkg/sealevel/vote_state.go @@ -20,6 +20,7 @@ const ( VoteStateVersionV0_23_5 = iota VoteStateVersionV1_14_11 VoteStateVersionCurrent + VoteStateVersionV4 ) const ( @@ -118,6 +119,32 @@ type VoteState struct { PriorVoters PriorVoters EpochCredits []EpochCredits LastTimestamp BlockTimestamp + + // V4-specific fields preserved through the processing loop. + // Populated by ConvertToCurrent when source is V4; used by newVoteState4FromCurrent. + wasV4 bool + v4InflationRewardsCollector solana.PublicKey + v4BlockRevenueCollector solana.PublicKey + v4InflationRewardsCommBps uint16 + v4BlockRevenueCommBps uint16 + v4PendingDelegatorRewards uint64 + v4BlsPubkeyCompressed *[48]byte +} + +type VoteState4 struct { + NodePubkey solana.PublicKey + AuthorizedWithdrawer solana.PublicKey + InflationRewardsCollector solana.PublicKey + BlockRevenueCollector solana.PublicKey + InflationRewardsCommissionBps uint16 + BlockRevenueCommissionBps uint16 + PendingDelegatorRewards uint64 + BlsPubkeyCompressed *[48]byte // Option<[u8;48]>, nil = None + Votes deque.Deque[LandedVote] + RootSlot *uint64 + AuthorizedVoters AuthorizedVoters + EpochCredits []EpochCredits + LastTimestamp BlockTimestamp } type VoteStateVersions struct { @@ -125,6 +152,7 @@ type VoteStateVersions struct { V0_23_5 VoteState0_23_5 V1_14_11 VoteState1_14_11 Current VoteState + V4 VoteState4 } func (priorVoter *PriorVoter) UnmarshalWithDecoder(decoder *bin.Decoder, isVersion0_23_5 bool) error { @@ -990,13 +1018,231 @@ func (voteState *VoteState) MarshalWithEncoder(encoder *bin.Encoder) error { return err } -func (voteState *VoteState) GetAndUpdateAuthorizedVoter(currentEpoch uint64) (solana.PublicKey, error) { +func (voteState *VoteState4) UnmarshalWithDecoder(decoder *bin.Decoder) error { + nodePk, err := decoder.ReadBytes(solana.PublicKeyLength) + if err != nil { + return err + } + copy(voteState.NodePubkey[:], nodePk) + + authWithdrawer, err := decoder.ReadBytes(solana.PublicKeyLength) + if err != nil { + return err + } + copy(voteState.AuthorizedWithdrawer[:], authWithdrawer) + + inflationCollector, err := decoder.ReadBytes(solana.PublicKeyLength) + if err != nil { + return err + } + copy(voteState.InflationRewardsCollector[:], inflationCollector) + + blockCollector, err := decoder.ReadBytes(solana.PublicKeyLength) + if err != nil { + return err + } + copy(voteState.BlockRevenueCollector[:], blockCollector) + + voteState.InflationRewardsCommissionBps, err = decoder.ReadUint16(bin.LE) + if err != nil { + return err + } + + voteState.BlockRevenueCommissionBps, err = decoder.ReadUint16(bin.LE) + if err != nil { + return err + } + + voteState.PendingDelegatorRewards, err = decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + + // Option<[u8; 48]> + hasBls, err := ReadBool(decoder) + if err != nil { + return err + } + if hasBls { + blsBytes, err := decoder.ReadBytes(48) + if err != nil { + return err + } + var bls [48]byte + copy(bls[:], blsBytes) + voteState.BlsPubkeyCompressed = &bls + } else { + voteState.BlsPubkeyCompressed = nil + } + + numLockouts, err := decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + + voteState.Votes.Clear() + voteState.Votes.SetBaseCap(int(numLockouts)) + for count := uint64(0); count < numLockouts; count++ { + var landedVote LandedVote + err = landedVote.UnmarshalWithDecoder(decoder) + if err != nil { + return err + } + voteState.Votes.PushBack(landedVote) + } + + hasRootSlot, err := ReadBool(decoder) + if err != nil { + return err + } + + if hasRootSlot { + rootSlot, err := decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + voteState.RootSlot = &rootSlot + } + + err = voteState.AuthorizedVoters.UnmarshalWithDecoder(decoder) + if err != nil { + return err + } + + numEpochCredits, err := decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + + voteState.EpochCredits = slices.Grow(voteState.EpochCredits, int(numEpochCredits)) + for count := uint64(0); count < numEpochCredits; count++ { + var epochCredits EpochCredits + err = epochCredits.UnmarshalWithDecoder(decoder) + if err != nil { + return err + } + voteState.EpochCredits = append(voteState.EpochCredits, epochCredits) + } + + err = voteState.LastTimestamp.UnmarshalWithDecoder(decoder) + return err +} + +func (voteState *VoteState4) MarshalWithEncoder(encoder *bin.Encoder) error { + err := encoder.WriteBytes(voteState.NodePubkey[:], false) + if err != nil { + return err + } + + err = encoder.WriteBytes(voteState.AuthorizedWithdrawer[:], false) + if err != nil { + return err + } + + err = encoder.WriteBytes(voteState.InflationRewardsCollector[:], false) + if err != nil { + return err + } + + err = encoder.WriteBytes(voteState.BlockRevenueCollector[:], false) + if err != nil { + return err + } + + err = encoder.WriteUint16(voteState.InflationRewardsCommissionBps, bin.LE) + if err != nil { + return err + } + + err = encoder.WriteUint16(voteState.BlockRevenueCommissionBps, bin.LE) + if err != nil { + return err + } + + err = encoder.WriteUint64(voteState.PendingDelegatorRewards, bin.LE) + if err != nil { + return err + } + + // Option<[u8; 48]> + if voteState.BlsPubkeyCompressed != nil { + err = encoder.WriteBool(true) + if err != nil { + return err + } + err = encoder.WriteBytes(voteState.BlsPubkeyCompressed[:], false) + if err != nil { + return err + } + } else { + err = encoder.WriteBool(false) + if err != nil { + return err + } + } + + err = encoder.WriteUint64(uint64(voteState.Votes.Len()), bin.LE) + if err != nil { + return err + } + + for i := 0; i < voteState.Votes.Len(); i++ { + landedVote := voteState.Votes.At(i) + err = landedVote.MarshalWithEncoder(encoder) + if err != nil { + break + } + } + + if voteState.RootSlot != nil { + err = encoder.WriteBool(true) + if err != nil { + return err + } + + err = encoder.WriteUint64(*voteState.RootSlot, bin.LE) + if err != nil { + return err + } + } else { + err = encoder.WriteBool(false) + if err != nil { + return err + } + } + + err = voteState.AuthorizedVoters.MarshalWithEncoder(encoder) + if err != nil { + return err + } + + err = encoder.WriteUint64(uint64(len(voteState.EpochCredits)), bin.LE) + if err != nil { + return err + } + + for _, epochCredits := range voteState.EpochCredits { + err = epochCredits.MarshalWithEncoder(encoder) + if err != nil { + return err + } + } + + err = voteState.LastTimestamp.MarshalWithEncoder(encoder) + return err +} + +func (voteState *VoteState) GetAndUpdateAuthorizedVoter(currentEpoch uint64, f features.Features) (solana.PublicKey, error) { pubkey, err := voteState.AuthorizedVoters.GetAndCacheAuthorizedVoterForEpoch(currentEpoch) if err != nil { return pubkey, InstrErrInvalidAccountData } - voteState.AuthorizedVoters.PurgeAuthorizedVoters(currentEpoch) + purgeEpoch := currentEpoch + if f.IsActive(features.VoteStateV4) { + purgeEpoch = safemath.SaturatingSubU64(currentEpoch, 1) + } + voteState.AuthorizedVoters.PurgeAuthorizedVoters(purgeEpoch) return pubkey, nil } @@ -1008,8 +1254,8 @@ func (voteState *VoteState) Credits() uint64 { } } -func (voteState *VoteState) SetNewAuthorizedVoter(authorized solana.PublicKey, currentEpoch uint64, targetEpoch uint64, verify func(epochAuthorizedVoter solana.PublicKey) error) error { - epochAuthorizedVoter, err := voteState.GetAndUpdateAuthorizedVoter(currentEpoch) +func (voteState *VoteState) SetNewAuthorizedVoter(authorized solana.PublicKey, currentEpoch uint64, targetEpoch uint64, verify func(epochAuthorizedVoter solana.PublicKey) error, f features.Features) error { + epochAuthorizedVoter, err := voteState.GetAndUpdateAuthorizedVoter(currentEpoch, f) if err != nil { return err } @@ -1034,19 +1280,22 @@ func (voteState *VoteState) SetNewAuthorizedVoter(authorized solana.PublicKey, c latestAuthPubkey := iter.Value() if latestAuthPubkey != authorized { - var epochOfLastAuthorizedSwitch uint64 - last := voteState.PriorVoters.Last() - if last != nil { - epochOfLastAuthorizedSwitch = last.EpochEnd - } else { - epochOfLastAuthorizedSwitch = 0 + // V4: skip PriorVoters tracking entirely (V4 has no PriorVoters field) + if !f.IsActive(features.VoteStateV4) { + var epochOfLastAuthorizedSwitch uint64 + last := voteState.PriorVoters.Last() + if last != nil { + epochOfLastAuthorizedSwitch = last.EpochEnd + } else { + epochOfLastAuthorizedSwitch = 0 + } + + voteState.PriorVoters.Append(PriorVoter{Pubkey: latestAuthPubkey, EpochStart: epochOfLastAuthorizedSwitch, EpochEnd: targetEpoch}) } if targetEpoch <= latestEpoch { return InstrErrInvalidAccountData } - - voteState.PriorVoters.Append(PriorVoter{Pubkey: latestAuthPubkey, EpochStart: epochOfLastAuthorizedSwitch, EpochEnd: targetEpoch}) } voteState.AuthorizedVoters.AuthorizedVoters.Set(targetEpoch, authorized) @@ -1235,6 +1484,10 @@ func (voteStateVersions *VoteStateVersions) UnmarshalWithDecoder(decoder *bin.De { err = voteStateVersions.Current.UnmarshalWithDecoder(decoder) } + case VoteStateVersionV4: + { + err = voteStateVersions.V4.UnmarshalWithDecoder(decoder) + } default: { //mlog.Log.Debugf("invalid vote state type: %d", voteStateVersions.Type) @@ -1263,6 +1516,10 @@ func (voteStateVersions *VoteStateVersions) MarshalWithEncoder(encoder *bin.Enco { err = voteStateVersions.Current.MarshalWithEncoder(encoder) } + case VoteStateVersionV4: + { + err = voteStateVersions.V4.MarshalWithEncoder(encoder) + } } return err @@ -1282,6 +1539,10 @@ func (voteStateVersions *VoteStateVersions) IsInitialized() bool { { return voteStateVersions.Current.AuthorizedVoters.AuthorizedVoters.Len() != 0 } + case VoteStateVersionV4: + { + return voteStateVersions.V4.AuthorizedVoters.AuthorizedVoters.Len() != 0 + } default: { panic("VoteStateVersions in invalid state - programming error") @@ -1346,6 +1607,34 @@ func (voteStateVersions *VoteStateVersions) ConvertToCurrent() *VoteState { return &voteStateVersions.Current } + case VoteStateVersionV4: + { + state := &voteStateVersions.V4 + + newVoteState := &VoteState{ + NodePubkey: state.NodePubkey, + AuthorizedWithdrawer: state.AuthorizedWithdrawer, + Commission: byte(state.InflationRewardsCommissionBps / 100), + RootSlot: state.RootSlot, + AuthorizedVoters: state.AuthorizedVoters, + PriorVoters: PriorVoters{Index: 31, IsEmpty: true}, + EpochCredits: state.EpochCredits, + LastTimestamp: state.LastTimestamp, + Votes: state.Votes, + + // Preserve V4-specific fields for the write path + wasV4: true, + v4InflationRewardsCollector: state.InflationRewardsCollector, + v4BlockRevenueCollector: state.BlockRevenueCollector, + v4InflationRewardsCommBps: state.InflationRewardsCommissionBps, + v4BlockRevenueCommBps: state.BlockRevenueCommissionBps, + v4PendingDelegatorRewards: state.PendingDelegatorRewards, + v4BlsPubkeyCompressed: state.BlsPubkeyCompressed, + } + + return newVoteState + } + default: { panic("vote account in invalid state - potential programming error") @@ -1370,6 +1659,11 @@ func (voteStateVersions *VoteStateVersions) LastTimestamp() *BlockTimestamp { return &voteStateVersions.Current.LastTimestamp } + case VoteStateVersionV4: + { + return &voteStateVersions.V4.LastTimestamp + } + default: { panic("vote account in invalid state - potential programming error") @@ -1390,6 +1684,8 @@ func (voteStateVersions *VoteStateVersions) NodePubkey() solana.PublicKey { return voteStateVersions.V1_14_11.NodePubkey case VoteStateVersionCurrent: return voteStateVersions.Current.NodePubkey + case VoteStateVersionV4: + return voteStateVersions.V4.NodePubkey default: // Log unknown version once to avoid flooding hot paths unknownVoteStateVersionOnce.Do(func() { @@ -1470,9 +1766,70 @@ func newVoteStateFromVoteInit(voteInit VoteInstrVoteInit, clock SysvarClock) *Vo return voteState } +func newVoteState4FromCurrent(vs *VoteState, votePubkey solana.PublicKey) *VoteState4 { + vs4 := &VoteState4{ + NodePubkey: vs.NodePubkey, + AuthorizedWithdrawer: vs.AuthorizedWithdrawer, + Votes: vs.Votes, + RootSlot: vs.RootSlot, + AuthorizedVoters: vs.AuthorizedVoters, + EpochCredits: vs.EpochCredits, + LastTimestamp: vs.LastTimestamp, + } + + if vs.wasV4 { + // Preserve V4-specific fields from the original V4 account + vs4.InflationRewardsCollector = vs.v4InflationRewardsCollector + vs4.BlockRevenueCollector = vs.v4BlockRevenueCollector + vs4.InflationRewardsCommissionBps = vs.v4InflationRewardsCommBps + vs4.BlockRevenueCommissionBps = vs.v4BlockRevenueCommBps + vs4.PendingDelegatorRewards = vs.v4PendingDelegatorRewards + vs4.BlsPubkeyCompressed = vs.v4BlsPubkeyCompressed + } else { + // First-time V3/V1_14_11 → V4 conversion: use Agave defaults + vs4.InflationRewardsCollector = votePubkey + vs4.BlockRevenueCollector = vs.NodePubkey + vs4.InflationRewardsCommissionBps = uint16(vs.Commission) * 100 + vs4.BlockRevenueCommissionBps = 10000 + vs4.PendingDelegatorRewards = 0 + vs4.BlsPubkeyCompressed = nil + } + + return vs4 +} + func setVoteAccountState(execCtx *ExecutionCtx, acct *BorrowedAccount, voteState *VoteState, f features.Features) error { var err error - if f.IsActive(features.VoteStateAddVoteLatency) { + if f.IsActive(features.VoteStateV4) { + vsz := VoteStateV3Size + resizeNeeded := len(acct.Data()) < vsz + + resizeRentExempt := acct.IsRentExemptAtDataLength(uint64(vsz)) + resizeFailed := false + + if resizeNeeded && resizeRentExempt { + err = acct.SetDataLength(VoteStateV3Size, f) + if err != nil { + resizeFailed = true + } + } + + // V4: NO V1_14_11 fallback — error on resize failure (per SIMD-0185) + if resizeNeeded && (!resizeRentExempt || resizeFailed) { + return InstrErrAccountNotRentExempt + } + + vs4 := newVoteState4FromCurrent(voteState, acct.Key()) + newVersioned := &VoteStateVersions{Type: VoteStateVersionV4} + newVersioned.V4 = *vs4 + execCtx.AddModifiedVoteState(acct.Key(), newVersioned) + voteStateBytes, err := marshalVersionedVoteState(newVersioned) + defer voteStateBufPool.Put(voteStateBytes) + if err != nil { + return err + } + return acct.SetState(f, voteStateBytes) + } else if f.IsActive(features.VoteStateAddVoteLatency) { vsz := VoteStateV3Size resizeNeeded := len(acct.Data()) < vsz diff --git a/pkg/snapshot/manifest_decoder.go b/pkg/snapshot/manifest_decoder.go index 0c660314..fc13849f 100644 --- a/pkg/snapshot/manifest_decoder.go +++ b/pkg/snapshot/manifest_decoder.go @@ -410,6 +410,12 @@ func (voteAcct *VoteAccount) UnmarshalWithDecoder(decoder *bin.Decoder) error { voteAcct.NodePubkey = voteState.V1_14_11.NodePubkey } + case sealevel.VoteStateVersionV4: + { + voteTimestamp = voteState.V4.LastTimestamp + voteAcct.NodePubkey = voteState.V4.NodePubkey + } + default: { panic("shouldn't be possible - programming error")