diff --git a/Cargo.lock b/Cargo.lock index 9b0f2c9e6..d10098fdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4566,6 +4566,7 @@ dependencies = [ "pbkdf2", "rand 0.7.3", "rand_chacha 0.2.2", + "rustc_version", "serde", "serde_bytes", "serde_derive", @@ -4574,6 +4575,7 @@ dependencies = [ "solana-crate-features", "solana-logger", "solana-sdk-macro", + "solana-sdk-macro-frozen-abi", "thiserror", "tiny-bip39", ] @@ -4588,6 +4590,17 @@ dependencies = [ "syn 1.0.27", ] +[[package]] +name = "solana-sdk-macro-frozen-abi" +version = "1.3.0" +dependencies = [ + "lazy_static", + "proc-macro2 1.0.17", + "quote 1.0.1", + "rustc_version", + "syn 1.0.27", +] + [[package]] name = "solana-stake-accounts" version = "1.3.0" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index b54983ba8..05aa7a3f8 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -101,6 +101,7 @@ * [Snapshot Verification](implemented-proposals/snapshot-verification.md) * [Cross-Program Invocation](implemented-proposals/cross-program-invocation.md) * [Program Derived Addresses](implemented-proposals/program-derived-addresses.md) + * [ABI Management](implemented-proposals/abi-management.md) * [Accepted Design Proposals](proposals/README.md) * [Ledger Replication](proposals/ledger-replication-to-implement.md) * [Optimistic Confirmation and Slashing](proposals/optimistic-confirmation-and-slashing.md) @@ -114,6 +115,5 @@ * [Slashing](proposals/slashing.md) * [Tick Verification](proposals/tick-verification.md) * [Block Confirmation](proposals/block-confirmation.md) - * [ABI Management](proposals/abi-management.md) * [Rust Clients](proposals/rust-clients.md) * [Optimistic Confirmation](proposals/optimistic_confirmation.md) diff --git a/docs/src/proposals/abi-management.md b/docs/src/implemented-proposals/abi-management.md similarity index 50% rename from docs/src/proposals/abi-management.md rename to docs/src/implemented-proposals/abi-management.md index 61f04934b..6cbd1fd1b 100644 --- a/docs/src/proposals/abi-management.md +++ b/docs/src/implemented-proposals/abi-management.md @@ -55,7 +55,7 @@ fields. # Example ```patch -+#[frozen_abi(digest="1c6a53e9")] ++#[frozen_abi(digest="eXSMM7b89VY72V...")] #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Vote { /// A stack of votes starting with the oldest vote @@ -73,16 +73,85 @@ digest from the assertion test error message. In general, once we add `frozen_abi` and its change is published in the stable release channel, its digest should never change. If such a change is needed, we -should opt for defining a new struct like `FooV1`. And special release flow like -hard forks should be approached. +should opt for defining a new `struct` like `FooV1`. And special release flow +like hard forks should be approached. # Implementation remarks We use some degree of macro machinery to automatically generate unit tests and calculate a digest from ABI items. This is doable by clever use of -`serde::Serialize` (`[1]`) and `any::typename` (`[2]`). For a precedent for similar +`serde::Serialize` (`[1]`) and `any::type_name` (`[2]`). For a precedent for similar implementation, `ink` from the Parity Technologies `[3]` could be informational. +# Implementation details + +The implementation's goal is to detect unintended ABI changes automatically as +much as possible. To that end, the digest of structural ABI information is +calculated with best-effort accuracy and stability. + +When the ABI digest check is run, it dynamically computes an ABI digest by +recursively digesting the ABI of fields of the ABI item, by re-using the +`serde`'s serialization functionality, proc macro and generic specialization. +And then, the check `assert!`s that its finalized digest value is identical as +what is specified in the `frozen_abi` attribute. + +To realize that, it creates an example instance of the type and a custom +`Serializer` instance for `serde` to recursively traverse its fields as if +serializing the example for real. This traversing must be done via `serde` to +really capture what kinds of data actually would be serialized by `serde`, even +considering custom non-`derive`d `Serialize` trait implementations. + +# The ABI digesting process + +This part is a bit complex. There is three inter-depending parts: `AbiExample`, +`AbiDigester` and `AbiEnumVisitor`. + +First, the generated test creates an example instance of the digested type with +a trait called `AbiExample`, which should be implemented for all of digested +types like the `Serialize` and return `Self` like the `Default` trait. Usually, +it's provided via generic trait specialization for most of common types. Also +it is possible to `derive` for `struct` and `enum` and can be hand-written if +needed. + +The custom `Serializer` is called `AbiDigester`. And when it's called by `serde` +to serialize some data, it recursively collects ABI information as much as +possible. `AbiDigester`'s internal state for the ABI digest is updated +differentially depending on the type of data. This logic is specifically +redirected via with a trait called `AbiEnumVisitor` for each `enum` type. As the +name suggests, there is no need to implement `AbiEnumVisitor` for other types. + +To summarize this interplay, `serde` handles the recursive serialization control +flow in tandem with `AbiDigester`. The initial entry point in tests and child +`AbiDigester`s use `AbiExample` recursively to create an example object +hierarchal graph. And `AbiDigester` uses `AbiEnumVisitor` to inquiry the actual +ABI information using the constructed sample. + +`Default` isn't enough for `AbiExample`. Various collection's `::default()` is +empty, yet, we want to digest them with actual items. And, ABI digesting can't +be realized only with `AbiEnumVisitor`. `AbiExample` is required because an +actual instance of type is needed to actually traverse the data via `serde`. + +On the other hand, ABI digesting can't be done only with `AbiExample`, either. +`AbiEnumVisitor` is required because all variants of an `enum` cannot be +traversed just with a single variant of it as a ABI example. + +Digestable information: + +- rust's type name +- `serde`'s data type name +- all fields in `struct` +- all variants in `enum` +- `struct`: normal(`struct {...}`) and tuple-style (`struct(...)`) +- `enum`: normal variants and `struct`- and `tuple`- styles. +- attributes: `serde(serialize_with=...)` and `serde(skip)` + +Not digestable information: + +- Any custom serialize code path not touched by the sample provided by + `AbiExample`. (technically not possible) +- generics (must be a concrete type; use `frozen_abi` on concrete type + aliases) + # References 1. [(De)Serialization with type info · Issue #1095 · serde-rs/serde](https://github.com/serde-rs/serde/issues/1095#issuecomment-345483479) diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 092e1014d..4f66c215b 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -1647,6 +1647,7 @@ dependencies = [ "pbkdf2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", "serde_bytes 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1655,6 +1656,7 @@ dependencies = [ "solana-crate-features 1.3.0", "solana-logger 1.3.0", "solana-sdk-macro 1.3.0", + "solana-sdk-macro-frozen-abi 1.3.0", "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1672,6 +1674,17 @@ dependencies = [ "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-sdk-macro-frozen-abi" +version = "1.3.0" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "solana-stake-program" version = "1.3.0" diff --git a/programs/librapay/Cargo.lock b/programs/librapay/Cargo.lock index 8dc46fb02..7799be5c2 100644 --- a/programs/librapay/Cargo.lock +++ b/programs/librapay/Cargo.lock @@ -2450,6 +2450,7 @@ dependencies = [ "pbkdf2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", "serde_bytes 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2458,6 +2459,7 @@ dependencies = [ "solana-crate-features 1.3.0", "solana-logger 1.3.0", "solana-sdk-macro 1.3.0", + "solana-sdk-macro-frozen-abi 1.3.0", "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2471,6 +2473,17 @@ dependencies = [ "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-sdk-macro-frozen-abi" +version = "1.3.0" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "solana-stake-program" version = "1.3.0" diff --git a/programs/move_loader/Cargo.lock b/programs/move_loader/Cargo.lock index 0f4899876..4ec28acb0 100644 --- a/programs/move_loader/Cargo.lock +++ b/programs/move_loader/Cargo.lock @@ -2540,6 +2540,7 @@ dependencies = [ "pbkdf2", "rand 0.7.3", "rand_chacha 0.2.2", + "rustc_version", "serde", "serde_bytes", "serde_derive", @@ -2548,6 +2549,7 @@ dependencies = [ "solana-crate-features", "solana-logger", "solana-sdk-macro", + "solana-sdk-macro-frozen-abi", "thiserror", ] @@ -2561,6 +2563,17 @@ dependencies = [ "syn 1.0.27", ] +[[package]] +name = "solana-sdk-macro-frozen-abi" +version = "1.3.0" +dependencies = [ + "lazy_static", + "proc-macro2 1.0.17", + "quote 1.0.6", + "rustc_version", + "syn 1.0.27", +] + [[package]] name = "solana_libra_bytecode_verifier" version = "0.0.1-sol5" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index b965b5f99..186c7eea2 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -54,9 +54,13 @@ ed25519-dalek = { version = "=1.0.0-pre.3", optional = true } solana-crate-features = { path = "../crate-features", version = "1.3.0", optional = true } solana-logger = { path = "../logger", version = "1.3.0", optional = true } solana-sdk-macro = { path = "macro", version = "1.3.0" } +solana-sdk-macro-frozen-abi = { path = "macro-frozen-abi", version = "1.3.0" } [dev-dependencies] tiny-bip39 = "0.7.0" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] + +[build-dependencies] +rustc_version = "0.2" diff --git a/sdk/build.rs b/sdk/build.rs new file mode 100644 index 000000000..e17ca70cb --- /dev/null +++ b/sdk/build.rs @@ -0,0 +1,22 @@ +extern crate rustc_version; +use rustc_version::{version_meta, Channel}; + +fn main() { + // Copied and adapted from + // https://github.com/Kimundi/rustc-version-rs/blob/1d692a965f4e48a8cb72e82cda953107c0d22f47/README.md#example + // Licensed under Apache-2.0 + MIT + match version_meta().unwrap().channel { + Channel::Stable => { + println!("cargo:rustc-cfg=RUSTC_WITHOUT_SPECIALIZATION"); + } + Channel::Beta => { + println!("cargo:rustc-cfg=RUSTC_WITHOUT_SPECIALIZATION"); + } + Channel::Nightly => { + println!("cargo:rustc-cfg=RUSTC_WITH_SPECIALIZATION"); + } + Channel::Dev => { + println!("cargo:rustc-cfg=RUSTC_WITH_SPECIALIZATION"); + } + } +} diff --git a/sdk/macro-frozen-abi/Cargo.toml b/sdk/macro-frozen-abi/Cargo.toml new file mode 100644 index 000000000..30985d12e --- /dev/null +++ b/sdk/macro-frozen-abi/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "solana-sdk-macro-frozen-abi" +version = "1.3.0" +description = "Solana SDK Macro frozen abi" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +license = "Apache-2.0" +edition = "2018" +build = "../build.rs" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full", "extra-traits"] } +lazy_static = "1.4.0" + +[build-dependencies] +rustc_version = "0.2" diff --git a/sdk/macro-frozen-abi/src/lib.rs b/sdk/macro-frozen-abi/src/lib.rs new file mode 100644 index 000000000..a83e612eb --- /dev/null +++ b/sdk/macro-frozen-abi/src/lib.rs @@ -0,0 +1,453 @@ +extern crate proc_macro; + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +#[macro_use] +extern crate lazy_static; + +// This file littered with these essential cfgs so ensure them. +#[cfg(not(any(RUSTC_WITH_SPECIALIZATION, RUSTC_WITHOUT_SPECIALIZATION)))] +compile_error!("rustc_version is missing in build dependency and build.rs is not specified"); + +#[cfg(any(RUSTC_WITH_SPECIALIZATION, RUSTC_WITHOUT_SPECIALIZATION))] +use proc_macro::TokenStream; + +// Define dummy macro_attribute and macro_derive for stable rustc + +#[cfg(RUSTC_WITHOUT_SPECIALIZATION)] +#[proc_macro_attribute] +pub fn frozen_abi(_attrs: TokenStream, item: TokenStream) -> TokenStream { + item +} + +#[cfg(RUSTC_WITHOUT_SPECIALIZATION)] +#[proc_macro_derive(AbiExample)] +pub fn derive_abi_sample(_item: TokenStream) -> TokenStream { + "".parse().unwrap() +} + +#[cfg(RUSTC_WITHOUT_SPECIALIZATION)] +#[proc_macro_derive(AbiEnumVisitor)] +pub fn derive_abi_enum_visitor(_item: TokenStream) -> TokenStream { + "".parse().unwrap() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree::Group}; +#[cfg(RUSTC_WITH_SPECIALIZATION)] +use quote::quote; +#[cfg(RUSTC_WITH_SPECIALIZATION)] +use syn::{ + parse_macro_input, Attribute, AttributeArgs, Error, Fields, Ident, Item, ItemEnum, ItemStruct, + ItemType, Lit, Meta, NestedMeta, Variant, +}; + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn filter_serde_attrs(attrs: &[Attribute]) -> bool { + let mut skip = false; + + for attr in attrs { + let ss = &attr.path.segments.first().unwrap().ident.to_string(); + if ss.starts_with("serde") { + for token in attr.tokens.clone() { + if let Group(token) = token { + for ident in token.stream() { + if ident.to_string() == "skip" { + skip = true; + } + } + } + } + } + } + + skip +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn filter_allow_attrs(attrs: &mut Vec) { + attrs.retain(|attr| { + let ss = &attr.path.segments.first().unwrap().ident.to_string(); + ss.starts_with("allow") + }); +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn quote_for_specialization_detection() -> TokenStream2 { + lazy_static! { + static ref SPECIALIZATION_DETECTOR_INJECTED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + } + + if !SPECIALIZATION_DETECTOR_INJECTED.load(std::sync::atomic::Ordering::Relaxed) { + SPECIALIZATION_DETECTOR_INJECTED.store(true, std::sync::atomic::Ordering::Relaxed); + quote! { + mod specialization_detector { + trait SpecializedTrait { + fn specialized_fn() {} + } + impl SpecializedTrait for T { + default fn specialized_fn() {} + } + impl SpecializedTrait for T { + fn specialized_fn() {} + } + } + } + } else { + quote! {} + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn derive_abi_sample_enum_type(input: ItemEnum) -> TokenStream { + let type_name = &input.ident; + + let mut sample_variant = quote! {}; + let mut sample_variant_found = false; + + for variant in &input.variants { + let variant_name = &variant.ident; + let variant = &variant.fields; + if *variant == Fields::Unit { + sample_variant.extend(quote! { + #type_name::#variant_name + }); + } else if let Fields::Unnamed(variant_fields) = variant { + let mut fields = quote! {}; + for field in &variant_fields.unnamed { + if !(field.ident.is_none() && field.colon_token.is_none()) { + unimplemented!("tuple enum: {:?}", field); + } + let field_type = &field.ty; + fields.extend(quote! { + <#field_type>::example(), + }); + } + sample_variant.extend(quote! { + #type_name::#variant_name(#fields) + }); + } else if let Fields::Named(variant_fields) = variant { + let mut fields = quote! {}; + for field in &variant_fields.named { + if field.ident.is_none() || field.colon_token.is_none() { + unimplemented!("tuple enum: {:?}", field); + } + let field_type = &field.ty; + let field_name = &field.ident; + fields.extend(quote! { + #field_name: <#field_type>::example(), + }); + } + sample_variant.extend(quote! { + #type_name::#variant_name{#fields} + }); + } else { + unimplemented!("{:?}", variant); + } + + if !sample_variant_found { + sample_variant_found = true; + break; + } + } + + if !sample_variant_found { + unimplemented!("empty enum"); + } + + let mut attrs = input.attrs.clone(); + filter_allow_attrs(&mut attrs); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let injection = quote_for_specialization_detection(); + + let result = quote! { + #injection + #[automatically_derived] + #( #attrs )* + impl #impl_generics ::solana_sdk::abi_example::AbiExample for #type_name #ty_generics #where_clause { + fn example() -> Self { + ::log::info!( + "AbiExample for enum: {}", + std::any::type_name::<#type_name #ty_generics>() + ); + #sample_variant + } + } + }; + result.into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream { + let type_name = &input.ident; + let mut sample_fields = quote! {}; + let fields = &input.fields; + + match fields { + Fields::Named(_) => { + for field in fields { + let field_name = &field.ident; + sample_fields.extend(quote! { + #field_name: AbiExample::example(), + }); + } + sample_fields = quote! { + { #sample_fields } + } + } + Fields::Unnamed(_) => { + for _ in fields { + sample_fields.extend(quote! { + AbiExample::example(), + }); + } + sample_fields = quote! { + ( #sample_fields ) + } + } + _ => unimplemented!("fields: {:?}", fields), + } + + let mut attrs = input.attrs.clone(); + filter_allow_attrs(&mut attrs); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let turbofish = ty_generics.as_turbofish(); + let injection = quote_for_specialization_detection(); + + let result = quote! { + #injection + #[automatically_derived] + #( #attrs )* + impl #impl_generics ::solana_sdk::abi_example::AbiExample for #type_name #ty_generics #where_clause { + fn example() -> Self { + ::log::info!( + "AbiExample for struct: {}", + std::any::type_name::<#type_name #ty_generics>() + ); + use ::solana_sdk::abi_example::AbiExample; + + #type_name #turbofish #sample_fields + } + } + }; + + result.into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +#[proc_macro_derive(AbiExample)] +pub fn derive_abi_sample(item: TokenStream) -> TokenStream { + let item = parse_macro_input!(item as Item); + + match item { + Item::Struct(input) => derive_abi_sample_struct_type(input), + Item::Enum(input) => derive_abi_sample_enum_type(input), + _ => Error::new_spanned(item, "AbiSample isn't applicable; only for struct and enum") + .to_compile_error() + .into(), + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn do_derive_abi_enum_visitor(input: ItemEnum) -> TokenStream { + let type_name = &input.ident; + let mut serialized_variants = quote! {}; + let mut variant_count = 0; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + for variant in &input.variants { + // Don't digest a variant with serde(skip) + if filter_serde_attrs(&variant.attrs) { + continue; + }; + let sample_variant = quote_sample_variant(&type_name, &ty_generics, &variant); + variant_count += 1; + serialized_variants.extend(quote! { + #sample_variant; + Serialize::serialize(&sample_variant, digester.create_enum_child())?; + }); + } + + let type_str = format!("{}", type_name); + (quote! { + impl #impl_generics ::solana_sdk::abi_example::AbiEnumVisitor for #type_name #ty_generics #where_clause { + fn visit_for_abi(&self, digester: &mut ::solana_sdk::abi_digester::AbiDigester) -> ::solana_sdk::abi_digester::DigestResult { + let enum_name = #type_str; + use ::serde::ser::Serialize; + use ::solana_sdk::abi_example::AbiExample; + digester.update_with_string(format!("enum {} (variants = {})", enum_name, #variant_count)); + #serialized_variants + Ok(digester.create_child()) + } + } + }).into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +#[proc_macro_derive(AbiEnumVisitor)] +pub fn derive_abi_enum_visitor(item: TokenStream) -> TokenStream { + let item = parse_macro_input!(item as Item); + + match item { + Item::Enum(input) => do_derive_abi_enum_visitor(input), + _ => Error::new_spanned(item, "AbiEnumVisitor not applicable; only for enum") + .to_compile_error() + .into(), + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn quote_for_test( + test_mod_ident: &Ident, + type_name: &Ident, + expected_digest: &str, +) -> TokenStream2 { + // escape from nits.sh... + let p = Ident::new(&("ep".to_owned() + "rintln"), Span::call_site()); + quote! { + #[cfg(test)] + mod #test_mod_ident { + use super::*; + use ::solana_sdk::{abi_digester::AbiEnumVisitor, abi_example::AbiExample}; + + #[test] + fn test_abi_digest() { + ::solana_logger::setup(); + let mut digester = ::solana_sdk::abi_digester::AbiDigester::create(); + let example = <#type_name>::example(); + let result = <_>::visit_for_abi(&&example, &mut digester); + let mut hash = digester.finalize(); + // pretty-print error + if result.is_err() { + ::log::error!("digest error: {:#?}", result); + } + result.unwrap(); + if ::std::env::var("SOLANA_ABI_BULK_UPDATE").is_ok() { + if #expected_digest != format!("{}", hash) { + #p!("sed -i -e 's/{}/{}/g' $(git grep --files-with-matches frozen_abi)", #expected_digest, hash); + } + ::log::warn!("Not testing the abi digest under SOLANA_ABI_BULK_UPDATE!"); + } else { + assert_eq!(#expected_digest, format!("{}", hash), "Possibly ABI changed? Confirm the diff by rerunning before and after this test failed with SOLANA_ABI_DUMP_DIR"); + } + } + } + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn test_mod_name(type_name: &Ident) -> Ident { + Ident::new( + &format!("{}_frozen_abi", type_name.to_string()), + Span::call_site(), + ) +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream { + let type_name = &input.ident; + let test = quote_for_test(&test_mod_name(type_name), type_name, &expected_digest); + let result = quote! { + #input + #test + }; + result.into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn frozen_abi_struct_type(input: ItemStruct, expected_digest: &str) -> TokenStream { + let type_name = &input.ident; + let test = quote_for_test(&test_mod_name(type_name), type_name, &expected_digest); + let result = quote! { + #input + #test + }; + result.into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn quote_sample_variant( + type_name: &Ident, + ty_generics: &syn::TypeGenerics, + variant: &Variant, +) -> TokenStream2 { + let variant_name = &variant.ident; + let variant = &variant.fields; + if *variant == Fields::Unit { + quote! { + let sample_variant: #type_name #ty_generics = #type_name::#variant_name; + } + } else if let Fields::Unnamed(variant_fields) = variant { + let mut fields = quote! {}; + for field in &variant_fields.unnamed { + if !(field.ident.is_none() && field.colon_token.is_none()) { + unimplemented!(); + } + let ty = &field.ty; + fields.extend(quote! { + <#ty>::example(), + }); + } + quote! { + let sample_variant: #type_name #ty_generics = #type_name::#variant_name(#fields); + } + } else if let Fields::Named(variant_fields) = variant { + let mut fields = quote! {}; + for field in &variant_fields.named { + if field.ident.is_none() || field.colon_token.is_none() { + unimplemented!(); + } + let field_type_name = &field.ty; + let field_name = &field.ident; + fields.extend(quote! { + #field_name: <#field_type_name>::example(), + }); + } + quote! { + let sample_variant: #type_name #ty_generics = #type_name::#variant_name{#fields}; + } + } else { + unimplemented!("variant: {:?}", variant) + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream { + let type_name = &input.ident; + let test = quote_for_test(&test_mod_name(type_name), type_name, &expected_digest); + let result = quote! { + #input + #test + }; + result.into() +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +#[proc_macro_attribute] +pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attrs as AttributeArgs); + let mut expected_digest: Option = None; + for arg in args { + match arg { + NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("digest") => { + if let Lit::Str(lit) = nv.lit { + expected_digest = Some(lit.value()); + } + } + _ => {} + } + } + let expected_digest = expected_digest.expect("the required \"digest\" = ... is missing."); + + let item = parse_macro_input!(item as Item); + match item { + Item::Struct(input) => frozen_abi_struct_type(input, &expected_digest), + Item::Enum(input) => frozen_abi_enum_type(input, &expected_digest), + Item::Type(input) => frozen_abi_type_alias(input, &expected_digest), + _ => Error::new_spanned( + item, + "frozen_abi isn't applicable; only for struct, enum and type", + ) + .to_compile_error() + .into(), + } +} diff --git a/sdk/src/abi_digester.rs b/sdk/src/abi_digester.rs new file mode 100644 index 000000000..1fb5ab534 --- /dev/null +++ b/sdk/src/abi_digester.rs @@ -0,0 +1,635 @@ +use crate::abi_example::{normalize_type_name, AbiEnumVisitor}; +use crate::hash::{Hash, Hasher}; + +use log::*; + +use serde::ser::Error as SerdeError; +use serde::ser::*; +use serde::{Serialize, Serializer}; + +use std::any::type_name; +use std::io::Write; + +use thiserror::Error; + +#[derive(Debug)] +pub struct AbiDigester { + data_types: std::rc::Rc>>, + depth: usize, + for_enum: bool, + opaque_scope: Option, +} + +pub type DigestResult = Result; +type NoResult = Result<(), DigestError>; +type Sstr = &'static str; + +#[derive(Debug, Error)] +pub enum DigestError { + #[error("Option::None is serialized; no ABI digest for Option::Some")] + NoneIsSerialized, + #[error("nested error")] + Node(Sstr, Box), + #[error("leaf error")] + Leaf(Sstr, Sstr, Box), +} + +impl SerdeError for DigestError { + fn custom(_msg: T) -> DigestError { + unreachable!("This error should never be used"); + } +} + +impl DigestError { + pub(crate) fn wrap_by_type(e: DigestError) -> DigestError { + DigestError::Node(type_name::(), Box::new(e)) + } + + pub(crate) fn wrap_by_str(e: DigestError, s: Sstr) -> DigestError { + DigestError::Node(s, Box::new(e)) + } +} + +const INDENT_WIDTH: usize = 4; + +impl AbiDigester { + pub fn create() -> Self { + AbiDigester { + data_types: std::rc::Rc::new(std::cell::RefCell::new(vec![])), + for_enum: false, + depth: 0, + opaque_scope: None, + } + } + + // must create separate instances because we can't pass the single instnace to + // `.serialize()` multiple times + pub fn create_new(&self) -> Self { + Self { + data_types: self.data_types.clone(), + depth: self.depth, + for_enum: false, + opaque_scope: self.opaque_scope.clone(), + } + } + + pub fn create_new_opaque(&self, top_scope: &str) -> Self { + Self { + data_types: self.data_types.clone(), + depth: self.depth, + for_enum: false, + opaque_scope: Some(top_scope.to_owned()), + } + } + + pub fn create_child(&self) -> Self { + Self { + data_types: self.data_types.clone(), + depth: self.depth + 1, + for_enum: false, + opaque_scope: self.opaque_scope.clone(), + } + } + + pub fn create_enum_child(&self) -> Self { + Self { + data_types: self.data_types.clone(), + depth: self.depth + 1, + for_enum: true, + opaque_scope: self.opaque_scope.clone(), + } + } + + pub fn digest_data(&mut self, value: &T) -> DigestResult { + let type_name = normalize_type_name(type_name::()); + if type_name.ends_with("__SerializeWith") + || (self.opaque_scope.is_some() + && type_name.starts_with(self.opaque_scope.as_ref().unwrap())) + { + // we can't use the AbiEnumVisitor trait for these cases. + value.serialize(self.create_new()) + } else { + // Don't call value.visit_for_abi(...) to prefer autoref specialization + // resolution for IgnoreAsHelper + <&T>::visit_for_abi(&value, &mut self.create_new()) + } + } + + pub fn update(&mut self, strs: &[&str]) { + let mut buf = strs + .iter() + .map(|s| { + // this is a bit crude, but just normalize all strings as if they're + // `type_name`s! + normalize_type_name(s) + }) + .collect::>() + .join(" "); + buf = format!("{:0width$}{}\n", "", buf, width = self.depth * INDENT_WIDTH); + info!("updating with: {}", buf.trim_end()); + (*self.data_types.borrow_mut()).push(buf); + } + + pub fn update_with_type(&mut self, label: &str) { + self.update(&[label, type_name::()]); + } + + pub fn update_with_string(&mut self, label: String) { + self.update(&[&label]); + } + + fn digest_primitive(mut self) -> Result { + self.update_with_type::("primitive"); + Ok(self) + } + + fn digest_element(&mut self, v: &T) -> NoResult { + self.update_with_type::("element"); + self.create_child().digest_data(v).map(|_| ()) + } + + fn digest_named_field(&mut self, key: Sstr, v: &T) -> NoResult { + self.update_with_string(format!("field {}: {}", key, type_name::())); + self.create_child() + .digest_data(v) + .map(|_| ()) + .map_err(|e| DigestError::wrap_by_str(e, key)) + } + + fn digest_unnamed_field(&mut self, v: &T) -> NoResult { + self.update_with_type::("field"); + self.create_child().digest_data(v).map(|_| ()) + } + + fn check_for_enum(&mut self, label: &'static str, variant: &'static str) -> NoResult { + if !self.for_enum { + panic!("derive AbiEnumVisitor or implement it for the enum, which contains a variant ({}) named {}", label, variant); + } + Ok(()) + } + + pub fn finalize(self) -> Hash { + let mut hasher = Hasher::default(); + + for buf in (*self.data_types.borrow()).iter() { + hasher.hash(buf.as_bytes()); + } + + let hash = hasher.result(); + + if let Ok(dir) = std::env::var("SOLANA_ABI_DUMP_DIR") { + let thread_name = std::thread::current() + .name() + .unwrap_or("unknown-test-thread") + .replace(':', "_"); + if thread_name == "main" { + error!("Bad thread name detected for dumping; Maybe, --test-threads=1? Sorry, SOLANA_ABI_DUMP_DIR doesn't work under 1; increase it"); + } + + let path = format!("{}/{}_{}", dir, thread_name, hash,); + let mut file = std::fs::File::create(path).unwrap(); + for buf in (*self.data_types.borrow()).iter() { + file.write_all(buf.as_bytes()).unwrap(); + } + file.sync_data().unwrap(); + } + + hash + } +} + +impl Serializer for AbiDigester { + type Ok = Self; + type Error = DigestError; + type SerializeSeq = Self; + type SerializeTuple = Self; + type SerializeTupleStruct = Self; + type SerializeTupleVariant = Self; + type SerializeMap = Self; + type SerializeStruct = Self; + type SerializeStructVariant = Self; + + fn serialize_bool(self, _data: bool) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_i8(self, _data: i8) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_i16(self, _data: i16) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_i32(self, _data: i32) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_i64(self, _data: i64) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_i128(self, _data: i128) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_u8(self, _data: u8) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_u16(self, _data: u16) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_u32(self, _data: u32) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_u64(self, _data: u64) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_u128(self, _data: u128) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_f32(self, _data: f32) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_f64(self, _data: f64) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_char(self, _data: char) -> DigestResult { + self.digest_primitive::() + } + + fn serialize_str(self, _data: &str) -> DigestResult { + self.digest_primitive::<&str>() + } + + fn serialize_unit(self) -> DigestResult { + self.digest_primitive::<()>() + } + + fn serialize_bytes(mut self, v: &[u8]) -> DigestResult { + self.update_with_string(format!("bytes [u8] (len = {})", v.len())); + Ok(self) + } + + fn serialize_none(self) -> DigestResult { + Err(DigestError::NoneIsSerialized) + } + + fn serialize_some(mut self, v: &T) -> DigestResult + where + T: ?Sized + Serialize, + { + // emulate the ABI digest for the Option enum; see TestMyOption + self.update(&["enum Option (variants = 2)"]); + let mut variant_digester = self.create_child(); + + variant_digester.update_with_string("variant(0) None (unit)".to_owned()); + variant_digester + .update_with_string(format!("variant(1) Some({}) (newtype)", type_name::())); + variant_digester.create_child().digest_data(v) + } + + fn serialize_unit_struct(mut self, name: Sstr) -> DigestResult { + self.update(&["struct", name, "(unit)"]); + Ok(self) + } + + fn serialize_unit_variant(mut self, _name: Sstr, index: u32, variant: Sstr) -> DigestResult { + self.check_for_enum("unit_variant", variant)?; + self.update_with_string(format!("variant({}) {} (unit)", index, variant)); + Ok(self) + } + + fn serialize_newtype_struct(mut self, name: Sstr, v: &T) -> DigestResult + where + T: ?Sized + Serialize, + { + self.update_with_string(format!("struct {}({}) (newtype)", name, type_name::())); + self.create_child() + .digest_data(v) + .map_err(|e| DigestError::wrap_by_str(e, "newtype_struct")) + } + + fn serialize_newtype_variant( + mut self, + _name: Sstr, + i: u32, + variant: Sstr, + v: &T, + ) -> DigestResult + where + T: ?Sized + Serialize, + { + self.check_for_enum("newtype_variant", variant)?; + self.update_with_string(format!( + "variant({}) {}({}) (newtype)", + i, + variant, + type_name::() + )); + self.create_child() + .digest_data(v) + .map_err(|e| DigestError::wrap_by_str(e, "newtype_variant")) + } + + fn serialize_seq(mut self, len: Option) -> DigestResult { + self.update_with_string(format!("seq (elements = {})", len.unwrap())); + Ok(self.create_child()) + } + + fn serialize_tuple(mut self, len: usize) -> DigestResult { + self.update_with_string(format!("tuple (elements = {})", len)); + Ok(self.create_child()) + } + + fn serialize_tuple_struct(mut self, name: Sstr, len: usize) -> DigestResult { + self.update_with_string(format!("struct {} (fields = {}) (tuple)", name, len)); + Ok(self.create_child()) + } + + fn serialize_tuple_variant( + mut self, + _name: Sstr, + i: u32, + variant: Sstr, + len: usize, + ) -> DigestResult { + self.check_for_enum("tuple_variant", variant)?; + self.update_with_string(format!("variant({}) {} (fields = {})", i, variant, len)); + Ok(self.create_child()) + } + + fn serialize_map(mut self, len: Option) -> DigestResult { + self.update_with_string(format!("map (entries = {})", len.unwrap())); + Ok(self.create_child()) + } + + fn serialize_struct(mut self, name: Sstr, len: usize) -> DigestResult { + self.update_with_string(format!("struct {} (fields = {})", name, len)); + Ok(self.create_child()) + } + + fn serialize_struct_variant( + mut self, + _name: Sstr, + i: u32, + variant: Sstr, + len: usize, + ) -> DigestResult { + self.check_for_enum("struct_variant", variant)?; + self.update_with_string(format!( + "variant({}) struct {} (fields = {})", + i, variant, len + )); + Ok(self.create_child()) + } +} + +impl SerializeSeq for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_element(&mut self, data: &T) -> NoResult { + self.digest_element(data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +impl SerializeTuple for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_element(&mut self, data: &T) -> NoResult { + self.digest_element(data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} +impl SerializeTupleStruct for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_field(&mut self, data: &T) -> NoResult { + self.digest_unnamed_field(data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +impl SerializeTupleVariant for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_field(&mut self, data: &T) -> NoResult { + self.digest_unnamed_field(data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +impl SerializeMap for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_key(&mut self, key: &T) -> NoResult { + self.update_with_type::("key"); + self.create_child().digest_data(key).map(|_| ()) + } + + fn serialize_value(&mut self, value: &T) -> NoResult { + self.update_with_type::("value"); + self.create_child().digest_data(value).map(|_| ()) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +impl SerializeStruct for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_field(&mut self, key: Sstr, data: &T) -> NoResult { + self.digest_named_field(key, data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +impl SerializeStructVariant for AbiDigester { + type Ok = Self; + type Error = DigestError; + + fn serialize_field(&mut self, key: Sstr, data: &T) -> NoResult { + self.digest_named_field(key, data) + } + + fn end(self) -> DigestResult { + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::atomic::AtomicIsize; + + #[frozen_abi(digest = "CQiGCzsGquChkwffHjZKFqa3tCYtS3GWYRRYX7iDR38Q")] + type TestTypeAlias = i32; + + #[frozen_abi(digest = "Apwkp9Ah9zKirzwuSzVoU9QRc43EghpkD1nGVakJLfUY")] + #[derive(Serialize, AbiExample)] + struct TestStruct { + test_field: i8, + test_field2: i8, + } + + #[frozen_abi(digest = "4LbuvQLX78XPbm4hqqZcHFHpseDJcw4qZL9EUZXSi2Ss")] + #[derive(Serialize, AbiExample)] + struct TestTupleStruct(i8, i8); + + #[frozen_abi(digest = "FNHa6mNYJZa59Fwbipep5dXRXcFreaDHn9jEUZEH1YLv")] + #[derive(Serialize, AbiExample)] + struct TestNewtypeStruct(i8); + + #[frozen_abi(digest = "5qio5qYurHDv6fq5kcwP2ue2RBEazSZF8CPk2kUuwC2j")] + #[derive(Serialize, AbiExample)] + struct TestStructReversed { + test_field2: i8, + test_field: i8, + } + + #[frozen_abi(digest = "DLLrTWprsMjdJGR447A4mui9HpqxbKdsFXBfaWPcwhny")] + #[derive(Serialize, AbiExample)] + struct TestStructAnotherType { + test_field: i16, + test_field2: i8, + } + + #[frozen_abi(digest = "Hv597t4PieHYvgiXnwRSpKBRTWqteUS4nHZHY6ZxX69v")] + #[derive(Serialize, AbiExample)] + struct TestNest { + nested_field: [TestStruct; 5], + } + + #[frozen_abi(digest = "GttWH8FAY3teUjTaSds9mL3YbiDQ7qWw7WAvDXKd4ZzX")] + type TestUnitStruct = std::marker::PhantomData; + + #[frozen_abi(digest = "2zvXde11f8sNnFbc9E6ZZeFxV7D2BTVLKEZmNTsCDBpS")] + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestEnum { + VARIANT1, + VARIANT2, + } + + #[frozen_abi(digest = "6keb3v7GXLahhL6zoinzCWwSvB3KhmvZMB3tN2mamAm3")] + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestTupleVariant { + VARIANT1(u8, u16), + VARIANT2(u8, u16), + } + + #[frozen_abi(digest = "CKxzv7VjyUrNR9fGJpTpKyMBWJM4gepKshCS8oV14T1Q")] + #[derive(Serialize, AbiExample)] + struct TestVecEnum { + enums: Vec, + } + + #[derive(Serialize, AbiExample)] + struct TestGenericStruct { + test_field: T, + } + + #[frozen_abi(digest = "2Dr5k3Z513mV4KrGeUfcMwjsVHLmVyLiZarmfnXawEbf")] + type TestConcreteStruct = TestGenericStruct; + + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestGenericEnum { + TestVariant(T), + } + + #[frozen_abi(digest = "2B2HqxHaziSfW3kdxJqV9vEMpCpRaEipXL6Bskv1GV7J")] + type TestConcreteEnum = TestGenericEnum; + + #[frozen_abi(digest = "GyExD8nkYb9e6tijFL5S1gFtdN9GfY6L2sUDjTLhVGn4")] + type TestMap = HashMap; + + #[frozen_abi(digest = "AFLTVyVBkjc1SAPnzyuwTvmie994LMhJGN7PrP7hCVwL")] + type TestVec = Vec; + + #[frozen_abi(digest = "F5RniBQtNMBiDnyLEf72aQKHskV1TuBrD4jrEH5odPAW")] + type TestArray = [f64; 10]; + + #[frozen_abi(digest = "8cgZGpckC4dFovh3QuZpgvcvK2125ig7P4HsK9KCw39N")] + type TestUnit = (); + + #[frozen_abi(digest = "FgnBPy2T5iNNbykMteq1M4FRpNeSkzRoi9oXeCjEW6uq")] + type TestResult = Result; + + #[frozen_abi(digest = "F5s6YyJkfz7LM56q5j9RzTLa7QX4Utx1ecNkHX5UU9Fp")] + type TestAtomic = AtomicIsize; + + #[frozen_abi(digest = "7rH7gnEhJ8YouzqPT6VPyUDELvL51DGednSPcoLXG2rg")] + type TestOptionWithIsize = Option; + + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestMyOption { + None, + Some(T), + } + #[frozen_abi(digest = "BzXkoRacijFTCPW4PyyvhkqMVgcuhmvPXjZfMsHJCeet")] + type TestMyOptionWithIsize = TestMyOption; + + #[frozen_abi(digest = "9PMdHRb49BpkywrmPoJyZWMsEmf5E1xgmsFGkGmea5RW")] + type TestBitVec = bv::BitVec; + + mod skip_should_be_same { + #[frozen_abi(digest = "4LbuvQLX78XPbm4hqqZcHFHpseDJcw4qZL9EUZXSi2Ss")] + #[derive(Serialize, AbiExample)] + struct TestTupleStruct(i8, i8, #[serde(skip)] i8); + + #[frozen_abi(digest = "Hk7BYjZ71upWQJAx2PqoNcapggobPmFbMJd34xVdvRso")] + #[derive(Serialize, AbiExample)] + struct TestStruct { + test_field: i8, + #[serde(skip)] + _skipped_test_field: i8, + } + + #[frozen_abi(digest = "2zvXde11f8sNnFbc9E6ZZeFxV7D2BTVLKEZmNTsCDBpS")] + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestEnum { + VARIANT1, + VARIANT2, + #[serde(skip)] + #[allow(dead_code)] + VARIANT3, + } + + #[frozen_abi(digest = "6keb3v7GXLahhL6zoinzCWwSvB3KhmvZMB3tN2mamAm3")] + #[derive(Serialize, AbiExample, AbiEnumVisitor)] + enum TestTupleVariant { + VARIANT1(u8, u16), + VARIANT2(u8, u16, #[serde(skip)] u32), + } + } +} diff --git a/sdk/src/abi_example.rs b/sdk/src/abi_example.rs new file mode 100644 index 000000000..043d0b992 --- /dev/null +++ b/sdk/src/abi_example.rs @@ -0,0 +1,507 @@ +use crate::abi_digester::{AbiDigester, DigestError, DigestResult}; + +use log::*; + +use serde::Serialize; +use std::any::type_name; + +pub trait AbiExample: Sized { + fn example() -> Self; +} + +// Following code snippets are copied and adapted from the official rustc implementation to +// implement AbiExample trait for most of basic types. +// These are licensed under Apache-2.0 + MIT (compatible because we're Apache-2.0) + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/tuple.rs#L7 +macro_rules! tuple_example_impls { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T:AbiExample),+> AbiExample for ($($T,)+) { + fn example() -> Self { + ($({ let x: $T = AbiExample::example(); x},)+) + } + } + )+ + } +} + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/tuple.rs#L110 +tuple_example_impls! { + Tuple1 { + (0) -> A + } + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/array/mod.rs#L417 +macro_rules! array_example_impls { + {$n:expr, $t:ident $($ts:ident)*} => { + impl AbiExample for [T; $n] where T: AbiExample { + fn example() -> Self { + [$t::example(), $($ts::example()),*] + } + } + array_example_impls!{($n - 1), $($ts)*} + }; + {$n:expr,} => { + impl AbiExample for [T; $n] { + fn example() -> Self { [] } + } + }; +} + +array_example_impls! {32, T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T} + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/default.rs#L137 +macro_rules! example_impls { + ($t:ty, $v:expr) => { + impl AbiExample for $t { + fn example() -> Self { + $v + } + } + }; +} + +example_impls! { (), () } +example_impls! { bool, false } +example_impls! { char, '\x00' } + +example_impls! { usize, 0 } +example_impls! { u8, 0 } +example_impls! { u16, 0 } +example_impls! { u32, 0 } +example_impls! { u64, 0 } +example_impls! { u128, 0 } + +example_impls! { isize, 0 } +example_impls! { i8, 0 } +example_impls! { i16, 0 } +example_impls! { i32, 0 } +example_impls! { i64, 0 } +example_impls! { i128, 0 } + +example_impls! { f32, 0.0f32 } +example_impls! { f64, 0.0f64 } +example_impls! { String, String::new() } +example_impls! { std::time::Duration, std::time::Duration::from_secs(0) } + +use std::sync::atomic::*; + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/sync/atomic.rs#L1199 +macro_rules! atomic_example_impls { + ($atomic_type: ident) => { + impl AbiExample for $atomic_type { + fn example() -> Self { + Self::new(AbiExample::example()) + } + } + }; +} +atomic_example_impls! { AtomicU8 } +atomic_example_impls! { AtomicU16 } +atomic_example_impls! { AtomicU32 } +atomic_example_impls! { AtomicU64 } +atomic_example_impls! { AtomicUsize } +atomic_example_impls! { AtomicI8 } +atomic_example_impls! { AtomicI16 } +atomic_example_impls! { AtomicI32 } +atomic_example_impls! { AtomicI64 } +atomic_example_impls! { AtomicIsize } +atomic_example_impls! { AtomicBool } + +#[cfg(not(feature = "program"))] +use generic_array::{ArrayLength, GenericArray}; +#[cfg(not(feature = "program"))] +impl> AbiExample for GenericArray { + fn example() -> Self { + Self::default() + } +} + +use bv::{BitVec, BlockType}; +impl AbiExample for BitVec { + fn example() -> Self { + Self::default() + } +} + +impl IgnoreAsHelper for BitVec {} +impl EvenAsOpaque for BitVec {} + +pub(crate) fn normalize_type_name(type_name: &str) -> String { + type_name.chars().filter(|c| *c != '&').collect() +} + +type Placeholder = (); + +impl AbiExample for T { + default fn example() -> Self { + ::type_erased_example() + } +} + +// this works like a type erasure and a hatch to escape type error to runtime error +trait TypeErasedExample { + fn type_erased_example() -> T; +} + +impl TypeErasedExample for Placeholder { + default fn type_erased_example() -> T { + panic!( + "derive or implement AbiExample/AbiEnumVisitor for {}", + type_name::() + ); + } +} + +impl TypeErasedExample for Placeholder { + default fn type_erased_example() -> T { + let original_type_name = type_name::(); + let normalized_type_name = normalize_type_name(original_type_name); + + if normalized_type_name.starts_with("solana") { + panic!( + "derive or implement AbiExample/AbiEnumVisitor for {}", + original_type_name + ); + } else { + panic!( + "new unrecognized type for ABI digest!: {}", + original_type_name + ) + } + } +} + +impl AbiExample for Option { + fn example() -> Self { + info!("AbiExample for (Option): {}", type_name::()); + Some(T::example()) + } +} + +impl AbiExample for Result { + fn example() -> Self { + info!("AbiExample for (Result): {}", type_name::()); + Ok(O::example()) + } +} + +impl AbiExample for Box { + fn example() -> Self { + info!("AbiExample for (Box): {}", type_name::()); + Box::new(T::example()) + } +} + +impl AbiExample for Box () + Sync + Send> { + fn example() -> Self { + info!("AbiExample for (Box): {}", type_name::()); + Box::new(move |_t: &mut T| {}) + } +} + +impl AbiExample for Box<[T]> { + fn example() -> Self { + info!("AbiExample for (Box<[T]>): {}", type_name::()); + Box::new([T::example()]) + } +} + +impl AbiExample for std::marker::PhantomData { + fn example() -> Self { + info!("AbiExample for (PhantomData): {}", type_name::()); + >::default() + } +} + +impl AbiExample for std::sync::Arc { + fn example() -> Self { + info!("AbiExample for (Arc): {}", type_name::()); + std::sync::Arc::new(T::example()) + } +} + +impl AbiExample for std::rc::Rc { + fn example() -> Self { + info!("AbiExample for (Rc): {}", type_name::()); + std::rc::Rc::new(T::example()) + } +} + +impl AbiExample for std::sync::Mutex { + fn example() -> Self { + info!("AbiExample for (Mutex): {}", type_name::()); + std::sync::Mutex::new(T::example()) + } +} + +impl AbiExample for std::sync::RwLock { + fn example() -> Self { + info!("AbiExample for (RwLock): {}", type_name::()); + std::sync::RwLock::new(T::example()) + } +} + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + +impl< + T: std::cmp::Eq + std::hash::Hash + AbiExample, + S: AbiExample, + H: std::hash::BuildHasher + Default, + > AbiExample for HashMap +{ + fn example() -> Self { + info!("AbiExample for (HashMap): {}", type_name::()); + let mut map = HashMap::default(); + map.insert(T::example(), S::example()); + map + } +} + +impl AbiExample for BTreeMap { + fn example() -> Self { + info!("AbiExample for (BTreeMap): {}", type_name::()); + let mut map = BTreeMap::default(); + map.insert(T::example(), S::example()); + map + } +} + +impl AbiExample for Vec { + fn example() -> Self { + info!("AbiExample for (Vec): {}", type_name::()); + vec![T::example()] + } +} + +impl AbiExample + for HashSet +{ + fn example() -> Self { + info!("AbiExample for (HashSet): {}", type_name::()); + let mut set: HashSet = HashSet::default(); + set.insert(T::example()); + set + } +} + +impl AbiExample for BTreeSet { + fn example() -> Self { + info!("AbiExample for (BTreeSet): {}", type_name::()); + let mut set: BTreeSet = BTreeSet::default(); + set.insert(T::example()); + set + } +} + +#[cfg(not(feature = "program"))] +impl AbiExample for memmap::MmapMut { + fn example() -> Self { + memmap::MmapMut::map_anon(1).expect("failed to map the data file") + } +} + +#[cfg(not(feature = "program"))] +impl AbiExample for std::path::PathBuf { + fn example() -> Self { + std::path::PathBuf::from(String::example()) + } +} + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +impl AbiExample for SocketAddr { + fn example() -> Self { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + } +} + +// This is a control flow indirection needed for digesting all variants of an enum +pub trait AbiEnumVisitor: Serialize { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult; +} + +pub trait IgnoreAsHelper {} +pub trait EvenAsOpaque {} + +impl AbiEnumVisitor for T { + default fn visit_for_abi(&self, _digester: &mut AbiDigester) -> DigestResult { + unreachable!( + "AbiEnumVisitor must be implemented for {}", + type_name::() + ); + } +} + +impl AbiEnumVisitor for T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (default): {}", type_name::()); + T::example() + .serialize(digester.create_new()) + .map_err(DigestError::wrap_by_type::) + } +} + +// even (experimental) rust specialization isn't enough for us, resort to +// the autoref hack: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md +// relevant test: TestVecEnum +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (&default): {}", type_name::()); + // Don't call self.visit_for_abi(...) to avoid the infinite recursion! + T::visit_for_abi(&self, digester) + } +} + +// force to call self.serialize instead of T::visit_for_abi() for serialization +// helper structs like ad-hoc iterator `struct`s +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (IgnoreAsHelper): {}", type_name::()); + self.serialize(digester.create_new()) + .map_err(DigestError::wrap_by_type::) + } +} + +// force to call self.serialize instead of T::visit_for_abi() to work around the +// inability of implementing AbiExample for private structs from other crates +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (IgnoreAsOpaque): {}", type_name::()); + let top_scope = type_name::().split("::").next().unwrap(); + self.serialize(digester.create_new_opaque(top_scope)) + .map_err(DigestError::wrap_by_type::) + } +} + +// Because Option and Result enums are so common enums, provide generic trait implementations +// The digesting pattern must match with what is derived from #[derive(AbiEnumVisitor)] +impl AbiEnumVisitor for Option { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (Option): {}", type_name::()); + + let variant: Self = Option::Some(T::example()); + // serde calls serialize_some(); not serialize_variant(); + // so create_new is correct, not create_enum_child or create_enum_new + variant.serialize(digester.create_new()) + } +} + +impl AbiEnumVisitor for Result { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (Result): {}", type_name::()); + + digester.update(&["enum Result (variants = 2)"]); + let variant: Self = Result::Ok(O::example()); + variant.serialize(digester.create_enum_child())?; + + let variant: Self = Result::Err(E::example()); + variant.serialize(digester.create_enum_child())?; + + Ok(digester.create_child()) + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 7a8d4e46b..00e4de11e 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,6 +1,13 @@ +#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(specialization))] + // Allows macro expansion of `use ::solana_sdk::*` to work within this crate extern crate self as solana_sdk; +#[cfg(RUSTC_WITH_SPECIALIZATION)] +pub mod abi_digester; +#[cfg(RUSTC_WITH_SPECIALIZATION)] +pub mod abi_example; + pub mod account; pub mod account_utils; pub mod bpf_loader; @@ -91,3 +98,8 @@ pub mod transport; extern crate serde_derive; pub extern crate bs58; extern crate log as logger; + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +#[cfg(test)] +#[macro_use] +extern crate solana_sdk_macro_frozen_abi;