Skip to content

StakeStakeV2 zero copy api [wincode]#222

Open
grod220 wants to merge 23 commits intomainfrom
wincode-state
Open

StakeStakeV2 zero copy api [wincode]#222
grod220 wants to merge 23 commits intomainfrom
wincode-state

Conversation

@grod220
Copy link
Member

@grod220 grod220 commented Dec 19, 2025

Adds a new zero‑copy state API backed by wincode.

Key Changes

  • Adds p-stake-interface crate (#![no_std] compatible)
  • Integrates alignment-1 POD types for safe zero-copy from unaligned slices
  • New API via StakeStateV2:
    • StakeStateV2::from_bytes(&[u8]) -> Result<&StakeStateV2, StakeStateError> for read-only access
    • StakeStateV2::from_bytes_mut(&mut [u8]) -> Result<&mut StakeStateV2, StakeStateError> for
      mutations
    • Tag-checked accessors: meta(), stake(), meta_mut(), stake_mut()
    • State transitions: initialize(), delegate()
  • Maintains full ABI compatibility with legacy bincode-encoded stake accounts

Motivation

Deserialization via bincode/borsh is computationally expensive for programs that only need to access specific fields or verify the state. This zero-copy approach allows for significantly lower compute unit (CU) usage when interacting with stake accounts.

@grod220 grod220 changed the title Wincode spike StakeStakeV2 zero copy api [wincode] Jan 12, 2026
@grod220 grod220 force-pushed the wincode-state branch 2 times, most recently from e0471c2 to 3b7d1b1 Compare January 12, 2026 18:11
@grod220 grod220 marked this pull request as ready for review January 12, 2026 18:27
@grod220 grod220 requested review from febo and joncinque January 12, 2026 18:27
@grod220 grod220 requested a review from febo January 16, 2026 16:06
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great overall! Mostly small things on my side

Copy link
Contributor

@febo febo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a few more small comments.

Comment on lines +74 to +77
#[test]
fn tag_len_is_4_bytes() {
assert_eq!(StakeStateV2Tag::TAG_LEN, 4);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one feels that it could be a static assert instead of a test.

const _: () = assert!(StakeStateV2Tag::TAG_LEN == size_of::<u32>());

Comment on lines +147 to +160
/// Parse stake account data into a read-only reference.
pub fn from_bytes(data: &[u8]) -> Result<&Self, StakeStateError> {
let state = <Self as ZeroCopy>::from_bytes(data).map_err(|_| StakeStateError::Decode)?;
StakeStateV2Tag::assert_valid_tag(u32::from(state.tag))?;
Ok(state)
}

/// Parse stake account data into a mutable reference.
pub fn from_bytes_mut(data: &mut [u8]) -> Result<&mut Self, StakeStateError> {
let state =
<Self as ZeroCopy>::from_bytes_mut(data).map_err(|_| StakeStateError::Decode)?;
StakeStateV2Tag::assert_valid_tag(u32::from(state.tag))?;
Ok(state)
}
Copy link
Contributor

@febo febo Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we can rely on the assert_valid_tag to always being used. There is a scenario where I can add a stake: StakeStateV2 field to my zero-copy type. When a load my type, these helpers won't be used (from_bytes or from_bytes_mut), so no validation will happen. Then if I use the stake.tag() method I can be in UB.

#[derive(Clone, Debug, PartialEq, SchemaWrite, SchemaRead)]
#[wincode(assert_zero_copy)]
pub struct StakeStateV2 {
tag: PodU32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joncinque Do you think we can split this to be:

tag: u8,
_unused: [u8; 3],

This way the conversion won't require going from [u8; 4] to u32 everytime. The other alternative is adding a TryFrom<PodU32> that looks at the first byte only.

Comment on lines +82 to +94
#[inline]
pub(crate) unsafe fn from_u32_unchecked(v: u32) -> Self {
debug_assert!(v <= Self::RewardsPool as u32);
core::mem::transmute::<u32, StakeStateV2Tag>(v)
}

#[inline]
pub(crate) fn assert_valid_tag(v: u32) -> Result<(), StakeStateError> {
match v {
0..=3 => Ok(()),
other => Err(StakeStateError::InvalidTag(other)),
}
}
Copy link
Contributor

@febo febo Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if these helpers can be used (see comment here). We might need to move this logic to the TryFrom impl.

Copy link
Contributor

@febo febo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments, mainly related to StakeStateV2Tag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants