diff --git a/.cargo-husky/hooks/prepare-commit-msg b/.cargo-husky/hooks/prepare-commit-msg deleted file mode 100755 index 1661f5de6..000000000 --- a/.cargo-husky/hooks/prepare-commit-msg +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# -# git prepare-commit-msg hook for automatically prepending an issue key -# from the start of the current branch name to commit messages. - -# check if commit is merge commit or a commit ammend -if [ $2 = "merge" ] || [ $2 = "commit" ]; then - exit -fi -ISSUE_KEY=`git branch | grep -o "\* \(.*/\)*[A-Z]\{2,\}-[0-9]\+" | grep -o "[A-Z]\{2,\}-[0-9]\+"` -if [ $? -ne 0 ]; then - # no issue key in branch, use the default message - exit -fi -# issue key matched from branch prefix, prepend to commit message -TEMP=`mktemp /tmp/commitmsg-XXXXX` -(echo "$ISSUE_KEY: $(cat $1)") > $TEMP -cat $TEMP > $1 \ No newline at end of file diff --git a/CITATION.cft b/CITATION.cft deleted file mode 100644 index e69de29bb..000000000 diff --git a/Cargo.lock b/Cargo.lock index f64718f4c..868e85b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4746,6 +4746,7 @@ dependencies = [ "sp-version", "substrate-wasm-builder", "subtensor-custom-rpc-runtime-api", + "subtensor-macros", ] [[package]] @@ -4954,6 +4955,7 @@ dependencies = [ "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.10.0)", "sp-weights", "substrate-fixed", + "subtensor-macros", ] [[package]] @@ -5017,6 +5019,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.10.0)", + "subtensor-macros", ] [[package]] @@ -5034,6 +5037,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.10.0)", + "subtensor-macros", ] [[package]] @@ -5152,6 +5156,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.10.0)", + "subtensor-macros", ] [[package]] @@ -5245,6 +5250,7 @@ dependencies = [ "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.10.0)", "sp-version", "substrate-fixed", + "subtensor-macros", ] [[package]] @@ -9131,6 +9137,16 @@ dependencies = [ "sp-api", ] +[[package]] +name = "subtensor-macros" +version = "0.1.0" +dependencies = [ + "ahash 0.8.11", + "proc-macro2", + "quote", + "syn 2.0.67", +] + [[package]] name = "subtle" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 148f22b15..8d9eff122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "pallets/commitments", "pallets/subtensor", "runtime", + "support/macros", ] resolver = "2" @@ -36,6 +37,8 @@ serde_with = { version = "=2.0.0", default-features = false } smallvec = "1.13.2" litep2p = { git = "https://github.com/paritytech/litep2p", branch = "master" } +subtensor-macros = { path = "support/macros" } + frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.10.0", default-features = false } frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.10.0" } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.10.0", default-features = false } diff --git a/Dockerfile b/Dockerfile index 451b80a42..2fc6cbcc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,9 @@ COPY ./raw_spec_finney.json /subtensor/raw_spec_finney.json COPY ./node /subtensor/node COPY ./pallets /subtensor/pallets COPY ./runtime /subtensor/runtime +COPY ./support /subtensor/support -# Update to nightly toolchain +# Copy our toolchain COPY rust-toolchain.toml /subtensor/ RUN /subtensor/scripts/init.sh diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index 00e070ce3..859972fce 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -16,6 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +subtensor-macros.workspace = true codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive", ] } diff --git a/pallets/collective/Cargo.toml b/pallets/collective/Cargo.toml index 95120c235..cf311f404 100644 --- a/pallets/collective/Cargo.toml +++ b/pallets/collective/Cargo.toml @@ -16,6 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +subtensor-macros.workspace = true codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ "derive", ] } diff --git a/pallets/collective/src/lib.rs b/pallets/collective/src/lib.rs index 3be6529e0..96040f99c 100644 --- a/pallets/collective/src/lib.rs +++ b/pallets/collective/src/lib.rs @@ -65,6 +65,7 @@ mod benchmarking; pub mod weights; pub use pallet::*; +use subtensor_macros::freeze_struct; pub use weights::WeightInfo; const LOG_TARGET: &str = "runtime::collective"; @@ -150,6 +151,7 @@ impl GetBacking for RawOrigin { } /// Info for keeping track of a motion being voted on. +#[freeze_struct("a8e7b0b34ad52b17")] #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct Votes { /// The proposal's unique index. diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index 13a06c51e..1cff429d5 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -16,6 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +subtensor-macros.workspace = true codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive", "max-encoded-len", diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 81802b64a..d5d132034 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -8,6 +8,7 @@ pub mod types; pub mod weights; pub use pallet::*; +use subtensor_macros::freeze_struct; pub use types::*; pub use weights::WeightInfo; @@ -208,6 +209,7 @@ use { }, }; +#[freeze_struct("6a00398e14a8a984")] #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] pub struct CommitmentsSignedExtension(pub PhantomData); diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 5a1d0bd64..912a474c0 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -29,6 +29,7 @@ use sp_runtime::{ RuntimeDebug, }; use sp_std::{fmt::Debug, iter::once, prelude::*}; +use subtensor_macros::freeze_struct; /// Either underlying data blob if it is at most 32 bytes, or a hash of it. If the data is greater /// than 32-bytes then it will be truncated when encoding. @@ -283,11 +284,12 @@ impl Default for Data { } } +#[freeze_struct("25c84048dcc90813")] #[derive( CloneNoBound, Encode, Decode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, )] #[codec(mel_bound())] -#[cfg_attr(test, derive(frame_support::DefaultNoBound))] +#[derive(frame_support::DefaultNoBound)] #[scale_info(skip_type_params(FieldLimit))] pub struct CommitmentInfo> { pub fields: BoundedVec, diff --git a/pallets/registry/Cargo.toml b/pallets/registry/Cargo.toml index 7c495a42f..e6aca55f6 100644 --- a/pallets/registry/Cargo.toml +++ b/pallets/registry/Cargo.toml @@ -16,6 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +subtensor-macros.workspace = true codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive", "max-encoded-len", diff --git a/pallets/registry/src/types.rs b/pallets/registry/src/types.rs index 12ee857d2..0badd5669 100644 --- a/pallets/registry/src/types.rs +++ b/pallets/registry/src/types.rs @@ -30,6 +30,7 @@ use sp_runtime::{ RuntimeDebug, }; use sp_std::{fmt::Debug, iter::once, ops::Add, prelude::*}; +use subtensor_macros::freeze_struct; /// Either underlying data blob if it is at most 32 bytes, or a hash of it. If the data is greater /// than 32-bytes then it will be truncated when encoding. @@ -278,11 +279,12 @@ impl TypeInfo for IdentityFields { /// /// NOTE: This should be stored at the end of the storage item to facilitate the addition of extra /// fields in a backwards compatible way through a specialized `Decode` impl. +#[freeze_struct("98e2d7fc7536226b")] #[derive( CloneNoBound, Encode, Decode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, )] #[codec(mel_bound())] -#[cfg_attr(test, derive(frame_support::DefaultNoBound))] +#[derive(frame_support::DefaultNoBound)] #[scale_info(skip_type_params(FieldLimit))] pub struct IdentityInfo> { /// Additional fields of the identity that are not catered for with the struct's explicit diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index f93d8c426..a0835008f 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -16,6 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +subtensor-macros.workspace = true codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive", ] } diff --git a/pallets/subtensor/src/delegate_info.rs b/pallets/subtensor/src/delegate_info.rs index 1f8b06b69..ea9cd656a 100644 --- a/pallets/subtensor/src/delegate_info.rs +++ b/pallets/subtensor/src/delegate_info.rs @@ -7,6 +7,7 @@ extern crate alloc; use codec::Compact; use sp_core::hexdisplay::AsBytesRef; +#[freeze_struct("5752e4c650a83e0d")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct DelegateInfo { delegate_ss58: T::AccountId, diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index c4b0c74cd..2b966b361 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -74,6 +74,8 @@ pub mod pallet { use sp_std::vec; use sp_std::vec::Vec; + use subtensor_macros::freeze_struct; + #[cfg(not(feature = "std"))] use alloc::boxed::Box; #[cfg(feature = "std")] @@ -691,6 +693,7 @@ pub mod pallet { pub type AxonInfoOf = AxonInfo; /// Data structure for Axon information. + #[freeze_struct("3545cfb0cac4c1f5")] #[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] pub struct AxonInfo { /// Axon serving block. @@ -714,6 +717,7 @@ pub mod pallet { /// Struct for Prometheus. pub type PrometheusInfoOf = PrometheusInfo; /// Data structure for Prometheus information. + #[freeze_struct("5dde687e63baf0cd")] #[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] pub struct PrometheusInfo { /// Prometheus serving block. @@ -2235,6 +2239,7 @@ pub enum CallType { Other, } +#[freeze_struct("61e2b893d5ce6701")] #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] pub struct SubtensorSignedExtension(pub PhantomData); @@ -2468,6 +2473,7 @@ use sp_std::vec; // used not 25 lines below #[allow(unused)] use sp_std::vec::Vec; +use subtensor_macros::freeze_struct; /// Trait for managing a membership pallet instance in the runtime pub trait MemberManagement { diff --git a/pallets/subtensor/src/neuron_info.rs b/pallets/subtensor/src/neuron_info.rs index e3b84ab20..a4b58d666 100644 --- a/pallets/subtensor/src/neuron_info.rs +++ b/pallets/subtensor/src/neuron_info.rs @@ -4,6 +4,7 @@ use frame_support::storage::IterableStorageDoubleMap; extern crate alloc; use codec::Compact; +#[freeze_struct("45e69321f5c74b4b")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct NeuronInfo { hotkey: T::AccountId, @@ -28,6 +29,7 @@ pub struct NeuronInfo { pruning_score: Compact, } +#[freeze_struct("c21f0f4f22bcb2a1")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct NeuronInfoLite { hotkey: T::AccountId, diff --git a/pallets/subtensor/src/stake_info.rs b/pallets/subtensor/src/stake_info.rs index d66235657..1b39e9936 100644 --- a/pallets/subtensor/src/stake_info.rs +++ b/pallets/subtensor/src/stake_info.rs @@ -4,6 +4,7 @@ extern crate alloc; use codec::Compact; use sp_core::hexdisplay::AsBytesRef; +#[freeze_struct("86d64c14d71d44b9")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct StakeInfo { hotkey: T::AccountId, diff --git a/pallets/subtensor/src/subnet_info.rs b/pallets/subtensor/src/subnet_info.rs index f0dc9fbfa..4e9e756a0 100644 --- a/pallets/subtensor/src/subnet_info.rs +++ b/pallets/subtensor/src/subnet_info.rs @@ -4,6 +4,7 @@ use frame_support::storage::IterableStorageMap; extern crate alloc; use codec::Compact; +#[freeze_struct("fe79d58173da662a")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct SubnetInfo { netuid: Compact, @@ -26,6 +27,7 @@ pub struct SubnetInfo { owner: T::AccountId, } +#[freeze_struct("55b472510f10e76a")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct SubnetHyperparams { rho: Compact, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b572aecee..fcb02a24c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -20,6 +20,7 @@ name = "spec_version" path = "src/spec_version.rs" [dependencies] +subtensor-macros.workspace = true subtensor-custom-rpc-runtime-api = { version = "0.0.2", path = "../pallets/subtensor/runtime-api", default-features = false } smallvec = { workspace = true } log = { workspace = true } diff --git a/runtime/src/check_nonce.rs b/runtime/src/check_nonce.rs index b257d6a49..fd2a3a0db 100644 --- a/runtime/src/check_nonce.rs +++ b/runtime/src/check_nonce.rs @@ -11,6 +11,7 @@ use sp_runtime::{ Saturating, }; use sp_std::vec; +use subtensor_macros::freeze_struct; /// Nonce check and increment to give replay protection for transactions. /// @@ -19,6 +20,7 @@ use sp_std::vec; /// This extension affects `requires` and `provides` tags of validity, but DOES NOT /// set the `priority` field. Make sure that AT LEAST one of the signed extension sets /// some kind of priority upon validating transactions. +#[freeze_struct("610b76f62cdb521e")] #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct CheckNonce(#[codec(compact)] pub T::Nonce); diff --git a/support/macros/Cargo.toml b/support/macros/Cargo.toml new file mode 100644 index 000000000..10a15ba0d --- /dev/null +++ b/support/macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "subtensor-macros" +version = "0.1.0" +edition = "2021" +license = "MIT" + +description = "support macros for Subtensor" +repository = "https://github.com/opentensor/subtensor" +homepage = "https://bittensor.com/" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full", "visit-mut", "extra-traits"] } +proc-macro2 = "1" +quote = "1" +ahash = "0.8" + +[lints] +workspace = true diff --git a/support/macros/src/lib.rs b/support/macros/src/lib.rs new file mode 100644 index 000000000..97dd76082 --- /dev/null +++ b/support/macros/src/lib.rs @@ -0,0 +1,68 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::ToTokens; +use syn::{parse2, visit_mut::visit_item_struct_mut, Error, ItemStruct, LitStr, Result}; + +mod visitor; +use visitor::*; + +/// Freezes the layout of a struct to the current hash of its fields, ensuring that future +/// changes require updating the hash. +/// +/// ``` +/// use subtensor_macros::freeze_struct; +/// +/// #[freeze_struct("13f75e4ea46b4e80")] +/// #[derive(Copy, Clone, PartialEq, Eq)] +/// pub struct MyStruct { +/// pub a: u32, +/// pub b: u64, +/// } +/// ``` +#[proc_macro_attribute] +pub fn freeze_struct(attr: TokenStream, tokens: TokenStream) -> TokenStream { + match freeze_struct_impl(attr, tokens) { + Ok(item_struct) => item_struct.to_token_stream().into(), + Err(err) => err.to_compile_error().into(), + } +} + +fn freeze_struct_impl( + attr: impl Into, + tokens: impl Into, +) -> Result { + let attr = attr.into(); + let tokens = tokens.into(); + + let item = parse2::(tokens)?; + let mut item_clone = item.clone(); + + let mut visitor = CleanDocComments::new(); + visit_item_struct_mut(&mut visitor, &mut item_clone); + + let calculated_hash = generate_hash(&item_clone); + let calculated_hash_hex = format!("{:x}", calculated_hash); + + if attr.is_empty() { + return Err(Error::new_spanned(item, + format!("You must provide a hashcode in the `freeze_struct` attribute to freeze this struct.\n\n\ + expected hashcode: `#[freeze_struct(\"{calculated_hash_hex}\")]`"), + )); + } + + let parsed_attr = parse2::(attr)?; + let provided_hash_hex = parsed_attr.value().to_lowercase(); + + if provided_hash_hex != calculated_hash_hex { + return Err(Error::new_spanned(item, + format!( + "You have made a non-trivial change to this struct and the provided hashcode no longer matches:\n{} != {}\n\n\ + If this was intentional, please update the hashcode in the `freeze_struct` attribute to:\n\ + {}\n\nNote that if you are changing a storage struct in any way, including simply re-ordering fields, \ + you will need a migration to prevent data corruption.", + provided_hash_hex, calculated_hash_hex, calculated_hash_hex + ), + )); + } + Ok(item) +} diff --git a/support/macros/src/visitor.rs b/support/macros/src/visitor.rs new file mode 100644 index 000000000..a5fc15dc7 --- /dev/null +++ b/support/macros/src/visitor.rs @@ -0,0 +1,113 @@ +use ahash::RandomState; +use syn::{parse_quote, visit_mut::VisitMut}; + +pub struct CleanDocComments; + +impl CleanDocComments { + pub fn new() -> Self { + Self + } +} + +impl VisitMut for CleanDocComments { + fn visit_attribute_mut(&mut self, attr: &mut syn::Attribute) { + if attr.path().is_ident("doc") { + *attr = parse_quote!(#[doc = ""]); + } + if attr.path().is_ident("freeze_struct") { + *attr = parse_quote!(#[freeze_struct]); + } + syn::visit_mut::visit_attribute_mut(self, attr); + } +} + +pub fn generate_hash + Clone>(item: &T) -> u64 { + let item = item.clone(); + + // Define a fixed seed + const SEED1: u64 = 0x12345678; + const SEED2: u64 = 0x87654321; + + // use a fixed seed for predictable hashes + let fixed_state = RandomState::with_seeds(SEED1, SEED2, SEED1, SEED2); + + // hash item + let item = Into::::into(item); + fixed_state.hash_one(&item) +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::Item; + + #[test] + fn test_clean_doc_comments() { + // Example code with doc comments + let item: Item = parse_quote! { + /// This is a doc comment + #[cfg(feature = "example")] + fn example() { + println!("Hello, world!"); + } + }; + + let hash_before = generate_hash(&item); + + let mut item_clone = item.clone(); + let mut cleaner = CleanDocComments; + cleaner.visit_item_mut(&mut item_clone); + + // Calculate the hash of the cleaned item + let hash_after = generate_hash(&item_clone); + + assert_ne!(hash_before, hash_after); + + let item2: Item = parse_quote! { + #[doc = ""] + #[cfg(feature = "example")] + fn example() { + println!("Hello, world!"); + } + }; + + assert_eq!(hash_after, generate_hash(&item2)); + } + + #[test] + fn test_clean_doc_comments_struct() { + // Example code with doc comments in a struct + let item: Item = parse_quote! { + /// Another doc comment + struct MyStruct { + #[cfg(feature = "field")] + field1: i32, + /// Field doc comment + field2: String, + } + }; + + let hash_before = generate_hash(&item); + + let mut item_clone = item.clone(); + let mut cleaner = CleanDocComments; + cleaner.visit_item_mut(&mut item_clone); + + // Calculate the hash of the cleaned item + let hash_after = generate_hash(&item_clone); + + assert_ne!(hash_before, hash_after); + + let item2: Item = parse_quote! { + #[doc = ""] + struct MyStruct { + #[cfg(feature = "field")] + field1: i32, + #[doc = ""] + field2: String, + } + }; + + assert_eq!(hash_after, generate_hash(&item2)); + } +} diff --git a/support/macros/tests/tests.rs b/support/macros/tests/tests.rs new file mode 100644 index 000000000..9c8159e44 --- /dev/null +++ b/support/macros/tests/tests.rs @@ -0,0 +1,7 @@ +use subtensor_macros::freeze_struct; + +#[freeze_struct("ecdcaac0f6da589a")] +pub struct MyStruct { + pub ip: u32, + pub port: u32, +}