diff --git a/sdk/src/deserialize_utils.rs b/sdk/src/deserialize_utils.rs new file mode 100644 index 0000000000..e6293e293d --- /dev/null +++ b/sdk/src/deserialize_utils.rs @@ -0,0 +1,131 @@ +use serde::{Deserialize, Deserializer}; + +/// This helper function enables successful deserialization of versioned structs; new structs may +/// include additional fields if they impl Default and are added to the end of the struct. Right +/// now, this function is targeted at `bincode` deserialization; the error match may need to be +/// updated if another package needs to be used in the future. +pub fn default_on_eof<'de, T, D>(d: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de> + Default, +{ + let result = T::deserialize(d); + match result { + Err(err) if err.to_string() == "io error: unexpected end of file" => Ok(T::default()), + result => result, + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use bincode::deserialize; + + #[test] + fn test_default_on_eof() { + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct Foo { + bar: u16, + #[serde(deserialize_with = "default_on_eof")] + baz: Option, + #[serde(deserialize_with = "default_on_eof")] + quz: String, + } + + let data = vec![1, 0]; + assert_eq!( + Foo { + bar: 1, + baz: None, + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + + let data = vec![1, 0, 0]; + assert_eq!( + Foo { + bar: 1, + baz: None, + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + + let data = vec![1, 0, 1]; + assert_eq!( + Foo { + bar: 1, + baz: None, + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + + let data = vec![1, 0, 1, 0]; + assert_eq!( + Foo { + bar: 1, + baz: None, + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + + let data = vec![1, 0, 1, 0, 0, 1]; + assert_eq!( + Foo { + bar: 1, + baz: Some(0), + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + + let data = vec![1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 116]; + assert_eq!( + Foo { + bar: 1, + baz: Some(0), + quz: "t".to_string(), + }, + deserialize(&data).unwrap() + ); + } + + #[test] + #[should_panic] + fn test_default_on_eof_additional_untagged_fields() { + // If later fields are not tagged `deserialize_with = "default_on_eof"`, deserialization + // will panic on any missing fields/data + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct Foo { + bar: u16, + #[serde(deserialize_with = "default_on_eof")] + baz: Option, + quz: String, + } + + // Fully populated struct will deserialize + let data = vec![1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 116]; + assert_eq!( + Foo { + bar: 1, + baz: Some(0), + quz: "t".to_string(), + }, + deserialize(&data).unwrap() + ); + + // Will panic because `quz` is missing, even though `baz` is tagged + let data = vec![1, 0, 1, 0]; + assert_eq!( + Foo { + bar: 1, + baz: None, + quz: "".to_string(), + }, + deserialize(&data).unwrap() + ); + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 1752185d52..6dc21378f3 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -17,6 +17,7 @@ pub mod builtins; pub mod clock; pub mod commitment_config; pub mod decode_error; +pub mod deserialize_utils; pub mod entrypoint_native; pub mod epoch_info; pub mod epoch_schedule; diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index e7ee0c10bb..9ec6a59e55 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -14,6 +14,7 @@ use crate::{ use solana_sdk::{ clock::{Slot, UnixTimestamp}, commitment_config::CommitmentConfig, + deserialize_utils::default_on_eof, instruction::CompiledInstruction, message::{Message, MessageHeader}, pubkey::Pubkey, @@ -141,6 +142,7 @@ pub struct TransactionStatusMeta { pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, + #[serde(deserialize_with = "default_on_eof")] pub inner_instructions: Option>, }