Metadata difference command (#1015)

* diffing pallets and runtime apis

* print diff

* clippy fix and format

* change formatting

* fmt

* diff working with storage details

* fix diff

* cargo fmt

* remove printing of node

* change strings

* handle parsing differently

* clippy fix

* cargo fmt

* more abstraction

* clippy fix and fmt

* add unit test and ordering

* fix small issue
This commit is contained in:
Tadeo Hepperle
2023-06-21 14:33:21 +02:00
committed by GitHub
parent b4eb406ee5
commit 2a990edaca
9 changed files with 541 additions and 23 deletions
+464
View File
@@ -0,0 +1,464 @@
use clap::Args;
use codec::Decode;
use frame_metadata::RuntimeMetadataPrefixed;
use std::collections::HashMap;
use std::hash::Hash;
use crate::utils::FileOrUrl;
use color_eyre::owo_colors::OwoColorize;
use scale_info::form::PortableForm;
use scale_info::Variant;
use subxt_metadata::{
ConstantMetadata, Metadata, PalletMetadata, RuntimeApiMetadata, StorageEntryMetadata,
StorageEntryType,
};
/// Explore the differences between two nodes
///
/// # Example
/// ```
/// subxt diff ./artifacts/polkadot_metadata_small.scale ./artifacts/polkadot_metadata_tiny.scale
/// subxt diff ./artifacts/polkadot_metadata_small.scale wss://rpc.polkadot.io:443
/// ```
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct Opts {
/// metadata file or node URL
metadata_or_url_1: FileOrUrl,
/// metadata file or node URL
metadata_or_url_2: FileOrUrl,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
let (entry_1_metadata, entry_2_metadata) = get_metadata(&opts).await?;
let metadata_diff = MetadataDiff::construct(&entry_1_metadata, &entry_2_metadata);
if metadata_diff.is_empty() {
writeln!(output, "No difference in metadata found.")?;
return Ok(());
}
if !metadata_diff.pallets.is_empty() {
writeln!(output, "Pallets:")?;
for diff in metadata_diff.pallets {
match diff {
Diff::Added(new) => {
writeln!(output, "{}", format!(" + {}", new.name()).green())?
}
Diff::Removed(old) => {
writeln!(output, "{}", format!(" - {}", old.name()).red())?
}
Diff::Changed { from, to } => {
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?;
let pallet_diff = PalletDiff::construct(&from, &to);
if !pallet_diff.calls.is_empty() {
writeln!(output, " Calls:")?;
for diff in pallet_diff.calls {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", &new.name).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", &old.name).red()
)?,
Diff::Changed { from, to: _ } => {
writeln!(
output,
"{}",
format!(" ~ {}", &from.name).yellow()
)?;
}
}
}
}
if !pallet_diff.constants.is_empty() {
writeln!(output, " Constants:")?;
for diff in pallet_diff.constants {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", new.name()).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", old.name()).red()
)?,
Diff::Changed { from, to: _ } => writeln!(
output,
"{}",
format!(" ~ {}", from.name()).yellow()
)?,
}
}
}
if !pallet_diff.storage_entries.is_empty() {
writeln!(output, " Storage Entries:")?;
for diff in pallet_diff.storage_entries {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", new.name()).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", old.name()).red()
)?,
Diff::Changed { from, to } => {
let storage_diff = StorageEntryDiff::construct(
from,
to,
&entry_1_metadata,
&entry_2_metadata,
);
writeln!(
output,
"{}",
format!(
" ~ {} (Changed: {})",
from.name(),
storage_diff.to_strings().join(", ")
)
.yellow()
)?;
}
}
}
}
}
}
}
}
if !metadata_diff.runtime_apis.is_empty() {
writeln!(output, "Runtime APIs:")?;
for diff in metadata_diff.runtime_apis {
match diff {
Diff::Added(new) => {
writeln!(output, "{}", format!(" + {}", new.name()).green())?
}
Diff::Removed(old) => {
writeln!(output, "{}", format!(" - {}", old.name()).red())?
}
Diff::Changed { from, to: _ } => {
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?
}
}
}
}
Ok(())
}
struct MetadataDiff<'a> {
pallets: Vec<Diff<PalletMetadata<'a>>>,
runtime_apis: Vec<Diff<RuntimeApiMetadata<'a>>>,
}
impl<'a> MetadataDiff<'a> {
fn construct(metadata_1: &'a Metadata, metadata_2: &'a Metadata) -> MetadataDiff<'a> {
let pallets = pallet_differences(metadata_1, metadata_2);
let runtime_apis = runtime_api_differences(metadata_1, metadata_2);
MetadataDiff {
pallets,
runtime_apis,
}
}
fn is_empty(&self) -> bool {
self.pallets.is_empty() && self.runtime_apis.is_empty()
}
}
#[derive(Default)]
struct PalletDiff<'a> {
calls: Vec<Diff<&'a Variant<PortableForm>>>,
constants: Vec<Diff<&'a ConstantMetadata>>,
storage_entries: Vec<Diff<&'a StorageEntryMetadata>>,
}
impl<'a> PalletDiff<'a> {
fn construct(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> PalletDiff<'a> {
let calls = calls_differences(pallet_metadata_1, pallet_metadata_2);
let constants = constants_differences(pallet_metadata_1, pallet_metadata_2);
let storage_entries = storage_differences(pallet_metadata_1, pallet_metadata_2);
PalletDiff {
calls,
constants,
storage_entries,
}
}
}
struct StorageEntryDiff {
key_different: bool,
value_different: bool,
default_different: bool,
modifier_different: bool,
}
impl StorageEntryDiff {
fn construct(
storage_entry_1: &StorageEntryMetadata,
storage_entry_2: &StorageEntryMetadata,
metadata_1: &Metadata,
metadata_2: &Metadata,
) -> Self {
let value_1_ty_id = match storage_entry_1.entry_type() {
StorageEntryType::Plain(value_ty) | StorageEntryType::Map { value_ty, .. } => value_ty,
};
let value_1_hash = metadata_1
.type_hash(*value_1_ty_id)
.expect("type should be present");
let value_2_ty_id = match storage_entry_2.entry_type() {
StorageEntryType::Plain(value_ty) | StorageEntryType::Map { value_ty, .. } => value_ty,
};
let value_2_hash = metadata_1
.type_hash(*value_2_ty_id)
.expect("type should be present");
let value_different = value_1_hash != value_2_hash;
let key_1_hash = match storage_entry_1.entry_type() {
StorageEntryType::Plain(_) => None,
StorageEntryType::Map { key_ty, .. } => Some(*key_ty),
}
.map(|key_ty| {
metadata_1
.type_hash(key_ty)
.expect("type should be present")
})
.unwrap_or_default();
let key_2_hash = match storage_entry_2.entry_type() {
StorageEntryType::Plain(_) => None,
StorageEntryType::Map { key_ty, .. } => Some(*key_ty),
}
.map(|key_ty| {
metadata_2
.type_hash(key_ty)
.expect("type should be present")
})
.unwrap_or_default();
let key_different = key_1_hash != key_2_hash;
StorageEntryDiff {
key_different,
value_different,
default_different: storage_entry_1.default_bytes() != storage_entry_2.default_bytes(),
modifier_different: storage_entry_1.modifier() != storage_entry_2.modifier(),
}
}
fn to_strings(&self) -> Vec<&str> {
let mut strings = Vec::<&str>::new();
if self.key_different {
strings.push("key type");
}
if self.value_different {
strings.push("value type");
}
if self.modifier_different {
strings.push("modifier");
}
if self.default_different {
strings.push("default value");
}
strings
}
}
async fn get_metadata(opts: &Opts) -> color_eyre::Result<(Metadata, Metadata)> {
let bytes = opts.metadata_or_url_1.fetch().await?;
let entry_1_metadata: Metadata =
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
let bytes = opts.metadata_or_url_2.fetch().await?;
let entry_2_metadata: Metadata =
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
Ok((entry_1_metadata, entry_2_metadata))
}
fn storage_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a StorageEntryMetadata>> {
diff(
pallet_metadata_1
.storage()
.map(|s| s.entries())
.unwrap_or_default(),
pallet_metadata_2
.storage()
.map(|s| s.entries())
.unwrap_or_default(),
|e| {
pallet_metadata_1
.storage_hash(e.name())
.expect("storage entry should be present")
},
|e| {
pallet_metadata_2
.storage_hash(e.name())
.expect("storage entry should be present")
},
|e| e.name(),
)
}
fn calls_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a Variant<PortableForm>>> {
return diff(
pallet_metadata_1.call_variants().unwrap_or_default(),
pallet_metadata_2.call_variants().unwrap_or_default(),
|e| {
pallet_metadata_1
.call_hash(&e.name)
.expect("call should be present")
},
|e| {
pallet_metadata_2
.call_hash(&e.name)
.expect("call should be present")
},
|e| &e.name,
);
}
fn constants_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a ConstantMetadata>> {
diff(
pallet_metadata_1.constants(),
pallet_metadata_2.constants(),
|e| {
pallet_metadata_1
.constant_hash(e.name())
.expect("constant should be present")
},
|e| {
pallet_metadata_2
.constant_hash(e.name())
.expect("constant should be present")
},
|e| e.name(),
)
}
fn runtime_api_differences<'a>(
metadata_1: &'a Metadata,
metadata_2: &'a Metadata,
) -> Vec<Diff<RuntimeApiMetadata<'a>>> {
diff(
metadata_1.runtime_api_traits(),
metadata_2.runtime_api_traits(),
RuntimeApiMetadata::hash,
RuntimeApiMetadata::hash,
RuntimeApiMetadata::name,
)
}
fn pallet_differences<'a>(
metadata_1: &'a Metadata,
metadata_2: &'a Metadata,
) -> Vec<Diff<PalletMetadata<'a>>> {
diff(
metadata_1.pallets(),
metadata_2.pallets(),
PalletMetadata::hash,
PalletMetadata::hash,
PalletMetadata::name,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Diff<T> {
Added(T),
Changed { from: T, to: T },
Removed(T),
}
fn diff<T, C: PartialEq, I: Hash + PartialEq + Eq + Ord>(
items_a: impl IntoIterator<Item = T>,
items_b: impl IntoIterator<Item = T>,
hash_fn_a: impl Fn(&T) -> C,
hash_fn_b: impl Fn(&T) -> C,
key_fn: impl Fn(&T) -> I,
) -> Vec<Diff<T>> {
let mut entries: HashMap<I, (Option<T>, Option<T>)> = HashMap::new();
for t1 in items_a {
let key = key_fn(&t1);
let (e1, _) = entries.entry(key).or_default();
*e1 = Some(t1);
}
for t2 in items_b {
let key = key_fn(&t2);
let (e1, e2) = entries.entry(key).or_default();
// skip all entries with the same hash:
if let Some(e1_inner) = e1 {
let e1_hash = hash_fn_a(e1_inner);
let e2_hash = hash_fn_b(&t2);
if e1_hash == e2_hash {
entries.remove(&key_fn(&t2));
continue;
}
}
*e2 = Some(t2);
}
// sort the values by key before returning
let mut diff_vec_with_keys: Vec<_> = entries.into_iter().collect();
diff_vec_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
diff_vec_with_keys
.into_iter()
.map(|(_, tuple)| match tuple {
(None, None) => panic!("At least one value is inserted when the key exists; qed"),
(Some(old), None) => Diff::Removed(old),
(None, Some(new)) => Diff::Added(new),
(Some(old), Some(new)) => Diff::Changed { from: old, to: new },
})
.collect()
}
#[cfg(test)]
mod test {
use crate::commands::diff::{diff, Diff};
#[test]
fn test_diff_fn() {
let old_pallets = [("Babe", 7), ("Claims", 9), ("Balances", 23)];
let new_pallets = [("Claims", 9), ("Balances", 22), ("System", 3), ("NFTs", 5)];
let hash_fn = |e: &(&str, i32)| e.0.len() as i32 * e.1;
let differences = diff(old_pallets, new_pallets, hash_fn, hash_fn, |e| e.0);
let expected_differences = vec![
Diff::Removed(("Babe", 7)),
Diff::Changed {
from: ("Balances", 23),
to: ("Balances", 22),
},
Diff::Added(("NFTs", 5)),
Diff::Added(("System", 3)),
];
assert_eq!(differences, expected_differences);
}
}
-1
View File
@@ -80,7 +80,6 @@ pub enum PalletSubcommand {
Storage(StorageSubcommand),
}
/// cargo run -- explore --file=../artifacts/polkadot_metadata.scale
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
// get the metadata
let bytes = opts.file_or_url.fetch().await?;
+7 -3
View File
@@ -47,7 +47,7 @@ pub async fn explore_storage(
};
// if specified call storage entry wrong, show user the storage entries to choose from (but this time as an error):
let Some(storage) = storage_metadata.entries().find(|entry| entry.name().to_lowercase() == entry_name.to_lowercase()) else {
let Some(storage) = storage_metadata.entries().iter().find(|entry| entry.name().to_lowercase() == entry_name.to_lowercase()) else {
let storage_entries = print_available_storage_entries(storage_metadata, pallet_name);
let description = format!("Usage:\n subxt explore {pallet_name} storage <STORAGE_ENTRY>\n view details for a specific storage entry\n\n{storage_entries}");
return Err(eyre!("Storage entry \"{entry_name}\" not found in \"{pallet_name}\" pallet!\n\n{description}"));
@@ -164,14 +164,18 @@ fn print_available_storage_entries(
storage_metadata: &StorageMetadata,
pallet_name: &str,
) -> String {
if storage_metadata.entries().len() == 0 {
if storage_metadata.entries().is_empty() {
format!("No <STORAGE_ENTRY>'s available in the \"{pallet_name}\" pallet.")
} else {
let mut output = format!(
"Available <STORAGE_ENTRY>'s in the \"{}\" pallet:",
pallet_name
);
let mut strings: Vec<_> = storage_metadata.entries().map(|s| s.name()).collect();
let mut strings: Vec<_> = storage_metadata
.entries()
.iter()
.map(|s| s.name())
.collect();
strings.sort();
for entry in strings {
write!(output, "\n {}", entry).unwrap();
+1
View File
@@ -4,6 +4,7 @@
pub mod codegen;
pub mod compatibility;
pub mod diff;
pub mod explore;
pub mod metadata;
pub mod version;
+2
View File
@@ -15,6 +15,7 @@ enum Command {
Metadata(commands::metadata::Opts),
Codegen(commands::codegen::Opts),
Compatibility(commands::compatibility::Opts),
Diff(commands::diff::Opts),
Version(commands::version::Opts),
Explore(commands::explore::Opts),
}
@@ -28,6 +29,7 @@ async fn main() -> color_eyre::Result<()> {
Command::Metadata(opts) => commands::metadata::run(opts, &mut output).await,
Command::Codegen(opts) => commands::codegen::run(opts, &mut output).await,
Command::Compatibility(opts) => commands::compatibility::run(opts, &mut output).await,
Command::Diff(opts) => commands::diff::run(opts, &mut output).await,
Command::Version(opts) => commands::version::run(opts, &mut output),
Command::Explore(opts) => commands::explore::run(opts, &mut output).await,
}
+26 -1
View File
@@ -5,14 +5,16 @@
use clap::Args;
use color_eyre::eyre;
use std::str::FromStr;
use std::{fs, io::Read, path::PathBuf};
use subxt_codegen::utils::{MetadataVersion, Uri};
pub mod type_description;
pub mod type_example;
/// The source of the metadata.
#[derive(Debug, Args)]
#[derive(Debug, Args, Clone)]
pub struct FileOrUrl {
/// The url of the substrate node to query for metadata for codegen.
#[clap(long, value_parser)]
@@ -94,3 +96,26 @@ pub fn with_indent(s: String, indent: usize) -> String {
.collect::<Vec<_>>()
.join("\n")
}
impl FromStr for FileOrUrl {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let path = std::path::Path::new(s);
if path.exists() {
Ok(FileOrUrl {
url: None,
file: Some(PathBuf::from(s)),
version: None,
})
} else {
Uri::from_str(s)
.map_err(|_| "no path or uri could be crated")
.map(|uri| FileOrUrl {
url: Some(uri),
file: None,
version: None,
})
}
}
}