Repository: solana-labs/solana-program-library
Pull request #6062 (2024-01-05 00:20:40)

[spl-record] Remove `Data` type from `RecordData` type
#### Problem
The record program uses the struct `RecordData` to represent record data in accounts.
```
pub struct RecordData {
    pub version: u8,
    pub authority: Pubkey,
    pub data: Data,
}
```
The `Data` type is an array of fixed size 8. It seems like this struct was added just for initial testing. The logic to support arbitrary length record data already exists in the processor logic.

By removing `Data` from the `RecordData` type and using it just as a header for data, we should be able to support arbitrary length record data.

#### Summary of changes
Removed the `Data` struct from state and updated the record program to support arbitrary length data.

This is a follow-up to https://github.com/solana-labs/solana-program-library/pull/6054#discussion_r1441021286.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b0ef54fb5..c9153d59d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7146,6 +7146,7 @@ dependencies = [
  "solana-program",
  "solana-program-test",
  "solana-sdk",
+ "solana-security-txt",
  "solana-vote-program",
  "spl-associated-token-account 2.3.0",
  "spl-token 4.0.0",
diff --git a/record/program/src/instruction.rs b/record/program/src/instruction.rs
index 6df52d351..0c2df24cd 100644
--- a/record/program/src/instruction.rs
+++ b/record/program/src/instruction.rs
@@ -162,10 +162,7 @@ pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey
 
 #[cfg(test)]
 mod tests {
-    use {
-        super::*, crate::state::tests::TEST_DATA, solana_program::program_error::ProgramError,
-        spl_pod::bytemuck::pod_bytes_of,
-    };
+    use {super::*, crate::state::tests::TEST_BYTES, solana_program::program_error::ProgramError};
 
     #[test]
     fn serialize_initialize() {
@@ -177,7 +174,7 @@ mod tests {
 
     #[test]
     fn serialize_write() {
-        let data = pod_bytes_of(&TEST_DATA);
+        let data = &TEST_BYTES;
         let offset = 0u64;
         let instruction = RecordInstruction::Write { offset: 0, data };
         let mut expected = vec![1];
@@ -207,7 +204,7 @@ mod tests {
     #[test]
     fn deserialize_invalid_instruction() {
         let mut expected = vec![12];
-        expected.append(&mut pod_bytes_of(&TEST_DATA).to_vec());
+        expected.extend_from_slice(&TEST_BYTES);
         let err: ProgramError = RecordInstruction::unpack(&expected).unwrap_err();
         assert_eq!(err, ProgramError::InvalidInstructionData);
     }
diff --git a/record/program/src/processor.rs b/record/program/src/processor.rs
index 472740cb2..8c6c55939 100644
--- a/record/program/src/processor.rs
+++ b/record/program/src/processor.rs
@@ -1,11 +1,7 @@
 //! Program state processor
 
 use {
-    crate::{
-        error::RecordError,
-        instruction::RecordInstruction,
-        state::{Data, RecordData},
-    },
+    crate::{error::RecordError, instruction::RecordInstruction, state::RecordData},
     solana_program::{
         account_info::{next_account_info, AccountInfo},
         entrypoint::ProgramResult,
@@ -46,7 +42,13 @@ pub fn process_instruction(
             let authority_info = next_account_info(account_info_iter)?;
 
             let raw_data = &mut data_info.data.borrow_mut();
-            let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
+            if raw_data.len() < RecordData::WRITABLE_START_INDEX {
+                return Err(ProgramError::InvalidAccountData);
+            }
+
+            let account_data = pod_from_bytes_mut::<RecordData>(
+                &mut raw_data[..RecordData::WRITABLE_START_INDEX],
+            )?;
             if account_data.is_initialized() {
                 msg!("Record account already initialized");
                 return Err(ProgramError::AccountAlreadyInitialized);
@@ -63,7 +65,11 @@ pub fn process_instruction(
             let authority_info = next_account_info(account_info_iter)?;
             {
                 let raw_data = &data_info.data.borrow();
-                let account_data = pod_from_bytes::<RecordData>(raw_data)?;
+                if raw_data.len() < RecordData::WRITABLE_START_INDEX {
+                    return Err(ProgramError::InvalidAccountData);
+                }
+                let account_data =
+                    pod_from_bytes::<RecordData>(&raw_data[..RecordData::WRITABLE_START_INDEX])?;
                 if !account_data.is_initialized() {
                     msg!("Record account not initialized");
                     return Err(ProgramError::UninitializedAccount);
@@ -86,7 +92,12 @@ pub fn process_instruction(
             let authority_info = next_account_info(account_info_iter)?;
             let new_authority_info = next_account_info(account_info_iter)?;
             let raw_data = &mut data_info.data.borrow_mut();
-            let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
+            if raw_data.len() < RecordData::WRITABLE_START_INDEX {
+                return Err(ProgramError::InvalidAccountData);
+            }
+            let account_data = pod_from_bytes_mut::<RecordData>(
+                &mut raw_data[..RecordData::WRITABLE_START_INDEX],
+            )?;
             if !account_data.is_initialized() {
                 msg!("Record account not initialized");
                 return Err(ProgramError::UninitializedAccount);
@@ -102,7 +113,12 @@ pub fn process_instruction(
             let authority_info = next_account_info(account_info_iter)?;
             let destination_info = next_account_info(account_info_iter)?;
             let raw_data = &mut data_info.data.borrow_mut();
-            let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
+            if raw_data.len() < RecordData::WRITABLE_START_INDEX {
+                return Err(ProgramError::InvalidAccountData);
+            }
+            let account_data = pod_from_bytes_mut::<RecordData>(
+                &mut raw_data[..RecordData::WRITABLE_START_INDEX],
+            )?;
             if !account_data.is_initialized() {
                 msg!("Record not initialized");
                 return Err(ProgramError::UninitializedAccount);
@@ -114,7 +130,6 @@ pub fn process_instruction(
             **destination_info.lamports.borrow_mut() = destination_starting_lamports
                 .checked_add(data_lamports)
                 .ok_or(RecordError::Overflow)?;
-            account_data.data = Data::default();
             Ok(())
         }
     }
diff --git a/record/program/src/state.rs b/record/program/src/state.rs
index 2dfec2e0b..c809ec0a9 100644
--- a/record/program/src/state.rs
+++ b/record/program/src/state.rs
@@ -4,7 +4,7 @@ use {
     solana_program::{program_pack::IsInitialized, pubkey::Pubkey},
 };
 
-/// Struct wrapping data and providing metadata
+/// Header type for recorded account data
 #[repr(C)]
 #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
 pub struct RecordData {
@@ -13,25 +13,6 @@ pub struct RecordData {
 
     /// The account allowed to update the data
     pub authority: Pubkey,
-
-    /// The data contained by the account, could be anything serializable
-    pub data: Data,
-}
-
-/// The length of the data contained in the account for testing.
-const DATA_SIZE: usize = 8;
-
-/// Struct just for data
-#[repr(C)]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
-pub struct Data {
-    /// The data contained by the account, could be anything or serializable
-    pub bytes: [u8; DATA_SIZE],
-}
-
-impl Data {
-    /// very small data for easy testing
-    pub const DATA_SIZE: usize = 8;
 }
 
 impl RecordData {
@@ -62,21 +43,17 @@ pub mod tests {
     /// Pubkey for tests
     pub const TEST_PUBKEY: Pubkey = Pubkey::new_from_array([100; 32]);
     /// Bytes for tests
-    pub const TEST_BYTES: [u8; Data::DATA_SIZE] = [42; Data::DATA_SIZE];
-    /// Data for tests
-    pub const TEST_DATA: Data = Data { bytes: TEST_BYTES };
+    pub const TEST_BYTES: [u8; 8] = [42; 8];
     /// RecordData for tests
     pub const TEST_RECORD_DATA: RecordData = RecordData {
         version: TEST_VERSION,
         authority: TEST_PUBKEY,
-        data: TEST_DATA,
     };
 
     #[test]
     fn serialize_data() {
         let mut expected = vec![TEST_VERSION];
         expected.extend_from_slice(&TEST_PUBKEY.to_bytes());
-        expected.extend_from_slice(&TEST_DATA.bytes);
         assert_eq!(pod_bytes_of(&TEST_RECORD_DATA), expected);
         assert_eq!(
             *pod_from_bytes::<RecordData>(&expected).unwrap(),
@@ -86,10 +63,9 @@ pub mod tests {
 
     #[test]
     fn deserialize_invalid_slice() {
-        let data = [200; Data::DATA_SIZE - 1];
         let mut expected = vec![TEST_VERSION];
         expected.extend_from_slice(&TEST_PUBKEY.to_bytes());
-        expected.extend_from_slice(&data);
+        expected.extend_from_slice(&TEST_BYTES);
         let err: ProgramError = pod_from_bytes::<RecordData>(&expected).unwrap_err();
         assert_eq!(err, ProgramError::InvalidArgument);
     }
diff --git a/single-pool/program/Cargo.toml b/single-pool/program/Cargo.toml
index 7779bc8f1..616bf3e13 100644
--- a/single-pool/program/Cargo.toml
+++ b/single-pool/program/Cargo.toml
@@ -18,6 +18,7 @@ num-derive = "0.4"
 num-traits = "0.2"
 num_enum = "0.7.1"
 solana-program = "1.17.6"
+solana-security-txt = "1.1.1"
 spl-token = { version = "4.0", path = "../../token/program", features = [ "no-entrypoint" ] }
 spl-associated-token-account = { version = "2.0", path = "../../associated-token-account/program", features = [ "no-entrypoint" ] }
 thiserror = "1.0"
diff --git a/single-pool/program/src/entrypoint.rs b/single-pool/program/src/entrypoint.rs
index 68d598e5d..07d7ae60f 100644
--- a/single-pool/program/src/entrypoint.rs
+++ b/single-pool/program/src/entrypoint.rs
@@ -8,6 +8,7 @@ use {
         account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError,
         pubkey::Pubkey,
     },
+    solana_security_txt::security_txt,
 };
 
 solana_program::entrypoint!(process_instruction);
@@ -24,3 +25,18 @@ fn process_instruction(
         Ok(())
     }
 }
+
+security_txt! {
+    // Required fields
+    name: "SPL Single-Validator Stake Pool",
+    project_url: "https://spl.solana.com/single-pool",
+    contacts: "link:https://github.com/solana-labs/solana-program-library/security/advisories/new,mailto:security@solana.com,discord:https://discord.gg/solana",
+    policy: "https://github.com/solana-labs/solana-program-library/blob/master/SECURITY.md",
+
+    // Optional Fields
+    preferred_languages: "en",
+    source_code: "https://github.com/solana-labs/solana-program-library/tree/master/single-pool/program",
+    source_revision: "ef44df985e76a697ee9a8aabb3a223610e4cf1dc",
+    source_release: "single-pool-v1.0.0",
+    auditors: "https://github.com/solana-labs/security-audits#single-stake-pool"
+}


Test patch:
diff --git a/record/program/tests/functional.rs b/record/program/tests/functional.rs
index 64e068f66..6204c5073 100644
--- a/record/program/tests/functional.rs
+++ b/record/program/tests/functional.rs
@@ -12,12 +12,9 @@ use {
         signature::{Keypair, Signer},
         transaction::{Transaction, TransactionError},
     },
-    spl_pod::bytemuck::{pod_bytes_of, pod_from_bytes, pod_get_packed_len},
+    spl_pod::bytemuck::{pod_from_bytes, pod_get_packed_len},
     spl_record::{
-        error::RecordError,
-        id, instruction,
-        processor::process_instruction,
-        state::{Data, RecordData},
+        error::RecordError, id, instruction, processor::process_instruction, state::RecordData,
     },
 };
 
@@ -29,24 +26,22 @@ async fn initialize_storage_account(
     context: &mut ProgramTestContext,
     authority: &Keypair,
     account: &Keypair,
-    data: Data,
+    data: &[u8],
 ) {
+    let account_length = pod_get_packed_len::<RecordData>()
+        .checked_add(data.len())
+        .unwrap();
     let transaction = Transaction::new_signed_with_payer(
         &[
             system_instruction::create_account(
                 &context.payer.pubkey(),
                 &account.pubkey(),
-                1.max(Rent::default().minimum_balance(pod_get_packed_len::<RecordData>())),
-                pod_get_packed_len::<RecordData>() as u64,
+                1.max(Rent::default().minimum_balance(account_length)),
+                account_length as u64,
                 &id(),
             ),
             instruction::initialize(&account.pubkey(), &authority.pubkey()),
-            instruction::write(
-                &account.pubkey(),
-                &authority.pubkey(),
-                0,
-                pod_bytes_of(&data),
-            ),
+            instruction::write(&account.pubkey(), &authority.pubkey(), 0, data),
         ],
         Some(&context.payer.pubkey()),
         &[&context.payer, account, authority],
@@ -65,9 +60,7 @@ async fn initialize_success() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [111u8; Data::DATA_SIZE],
-    };
+    let data = &[111u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
     let account = context
@@ -76,10 +69,11 @@ async fn initialize_success() {
         .await
         .unwrap()
         .unwrap();
-    let account_data = pod_from_bytes::<RecordData>(&account.data).unwrap();
-    assert_eq!(account_data.data, data);
+    let account_data =
+        pod_from_bytes::<RecordData>(&account.data[..RecordData::WRITABLE_START_INDEX]).unwrap();
     assert_eq!(account_data.authority, authority.pubkey());
     assert_eq!(account_data.version, RecordData::CURRENT_VERSION);
+    assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data);
 }
 
 #[tokio::test]
@@ -89,9 +83,10 @@ async fn initialize_with_seed_success() {
     let authority = Keypair::new();
     let seed = "storage";
     let account = Pubkey::create_with_seed(&authority.pubkey(), seed, &id()).unwrap();
-    let data = Data {
-        bytes: [111u8; Data::DATA_SIZE],
-    };
+    let data = &[111u8; 8];
+    let account_length = pod_get_packed_len::<RecordData>()
+        .checked_add(data.len())
+        .unwrap();
     let transaction = Transaction::new_signed_with_payer(
         &[
             system_instruction::create_account_with_seed(
@@ -99,12 +94,12 @@ async fn initialize_with_seed_success() {
                 &account,
                 &authority.pubkey(),
                 seed,
-                1.max(Rent::default().minimum_balance(pod_get_packed_len::<RecordData>())),
-                pod_get_packed_len::<RecordData>() as u64,
+                1.max(Rent::default().minimum_balance(account_length)),
+                account_length as u64,
                 &id(),
             ),
             instruction::initialize(&account, &authority.pubkey()),
-            instruction::write(&account, &authority.pubkey(), 0, pod_bytes_of(&data)),
+            instruction::write(&account, &authority.pubkey(), 0, data),
         ],
         Some(&context.payer.pubkey()),
         &[&context.payer, &authority],
@@ -121,10 +116,11 @@ async fn initialize_with_seed_success() {
         .await
         .unwrap()
         .unwrap();
-    let account_data = pod_from_bytes::<RecordData>(&account.data).unwrap();
-    assert_eq!(account_data.data, data);
+    let account_data =
+        pod_from_bytes::<RecordData>(&account.data[..RecordData::WRITABLE_START_INDEX]).unwrap();
     assert_eq!(account_data.authority, authority.pubkey());
     assert_eq!(account_data.version, RecordData::CURRENT_VERSION);
+    assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data);
 }
 
 #[tokio::test]
@@ -133,9 +129,7 @@ async fn initialize_twice_fail() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [111u8; Data::DATA_SIZE],
-    };
+    let data = &[111u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
     let transaction = Transaction::new_signed_with_payer(
         &[instruction::initialize(
@@ -163,20 +157,16 @@ async fn write_success() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
-    let new_data = Data {
-        bytes: [200u8; Data::DATA_SIZE],
-    };
+    let new_data = &[200u8; 8];
     let transaction = Transaction::new_signed_with_payer(
         &[instruction::write(
             &account.pubkey(),
             &authority.pubkey(),
             0,
-            pod_bytes_of(&new_data),
+            new_data,
         )],
         Some(&context.payer.pubkey()),
         &[&context.payer, &authority],
@@ -194,10 +184,11 @@ async fn write_success() {
         .await
         .unwrap()
         .unwrap();
-    let account_data = pod_from_bytes::<RecordData>(&account.data).unwrap();
-    assert_eq!(account_data.data, new_data);
+    let account_data =
+        pod_from_bytes::<RecordData>(&account.data[..RecordData::WRITABLE_START_INDEX]).unwrap();
     assert_eq!(account_data.authority, authority.pubkey());
     assert_eq!(account_data.version, RecordData::CURRENT_VERSION);
+    assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], new_data);
 }
 
 #[tokio::test]
@@ -206,21 +197,17 @@ async fn write_fail_wrong_authority() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
-    let new_data = Data {
-        bytes: [200u8; Data::DATA_SIZE],
-    };
+    let new_data = &[200u8; 8];
     let wrong_authority = Keypair::new();
     let transaction = Transaction::new_signed_with_payer(
         &[instruction::write(
             &account.pubkey(),
             &wrong_authority.pubkey(),
             0,
-            pod_bytes_of(&new_data),
+            new_data,
         )],
         Some(&context.payer.pubkey()),
         &[&context.payer, &wrong_authority],
@@ -246,14 +233,10 @@ async fn write_fail_unsigned() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
-    let data = pod_bytes_of(&Data {
-        bytes: [200u8; Data::DATA_SIZE],
-    });
+    let data = &[200u8; 8];
 
     let transaction = Transaction::new_signed_with_payer(
         &[Instruction {
@@ -285,9 +268,10 @@ async fn close_account_success() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
+    let account_length = pod_get_packed_len::<RecordData>()
+        .checked_add(data.len())
+        .unwrap();
     initialize_storage_account(&mut context, &authority, &account, data).await;
     let recipient = Pubkey::new_unique();
 
@@ -315,7 +299,7 @@ async fn close_account_success() {
         .unwrap();
     assert_eq!(
         account.lamports,
-        1.max(Rent::default().minimum_balance(pod_get_packed_len::<RecordData>()))
+        1.max(Rent::default().minimum_balance(account_length))
     );
 }
 
@@ -325,9 +309,7 @@ async fn close_account_fail_wrong_authority() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
     let wrong_authority = Keypair::new();
@@ -365,9 +347,7 @@ async fn close_account_fail_unsigned() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8, 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
     let transaction = Transaction::new_signed_with_payer(
@@ -401,9 +381,7 @@ async fn set_authority_success() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
     let new_authority = Keypair::new();
 
@@ -429,18 +407,18 @@ async fn set_authority_success() {
         .await
         .unwrap()
         .unwrap();
-    let account_data = pod_from_bytes::<RecordData>(&account_handle.data).unwrap();
+    let account_data =
+        pod_from_bytes::<RecordData>(&account_handle.data[..RecordData::WRITABLE_START_INDEX])
+            .unwrap();
     assert_eq!(account_data.authority, new_authority.pubkey());
 
-    let new_data = Data {
-        bytes: [200u8; Data::DATA_SIZE],
-    };
+    let new_data = &[200u8; 8];
     let transaction = Transaction::new_signed_with_payer(
         &[instruction::write(
             &account.pubkey(),
             &new_authority.pubkey(),
             0,
-            pod_bytes_of(&new_data),
+            new_data,
         )],
         Some(&context.payer.pubkey()),
         &[&context.payer, &new_authority],
@@ -458,10 +436,15 @@ async fn set_authority_success() {
         .await
         .unwrap()
         .unwrap();
-    let account_data = pod_from_bytes::<RecordData>(&account_handle.data).unwrap();
-    assert_eq!(account_data.data, new_data);
+    let account_data =
+        pod_from_bytes::<RecordData>(&account_handle.data[..RecordData::WRITABLE_START_INDEX])
+            .unwrap();
     assert_eq!(account_data.authority, new_authority.pubkey());
     assert_eq!(account_data.version, RecordData::CURRENT_VERSION);
+    assert_eq!(
+        &account_handle.data[RecordData::WRITABLE_START_INDEX..],
+        new_data,
+    );
 }
 
 #[tokio::test]
@@ -470,9 +453,7 @@ async fn set_authority_fail_wrong_authority() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
     let wrong_authority = Keypair::new();
@@ -510,9 +491,7 @@ async fn set_authority_fail_unsigned() {
 
     let authority = Keypair::new();
     let account = Keypair::new();
-    let data = Data {
-        bytes: [222u8; Data::DATA_SIZE],
-    };
+    let data = &[222u8; 8];
     initialize_storage_account(&mut context, &authority, &account, data).await;
 
     let transaction = Transaction::new_signed_with_payer(