Skip to content

Commit abb7d7b

Browse files
sebtombaYour Name
authored andcommitted
Post-migration integrity validation tool (#186)
* feat: Add initial structure of the movement migration validation tool. * feat: Add global storage includes check to validation tool * feat: Add matching feature flags api check to validation tool * fix: validation tool default tracing log level * fix: AptosDB pruner config for readonly access * feature: remove api global feature flag check * feature: add integration test to validation tool * feature: compare deserialized account resources and balances
1 parent db2aa6c commit abb7d7b

File tree

16 files changed

+826
-0
lines changed

16 files changed

+826
-0
lines changed

Cargo.lock

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ members = [
153153
"keyless/pepper/example-client-rust",
154154
"keyless/pepper/service",
155155
"mempool",
156+
"movement-migration/validation-tool",
156157
"network/benchmark",
157158
"network/builder",
158159
"network/discovery",
@@ -828,6 +829,7 @@ which = "4.2.5"
828829
whoami = "1.5.0"
829830
# This allows for zeroize 1.6 to be used. Version 1.2.0 of x25519-dalek locks zeroize to 1.3.
830831
x25519-dalek = { git = "https://github.com/aptos-labs/x25519-dalek", rev = "b9cdbaf36bf2a83438d9f660e5a708c82ed60d8e" }
832+
xz2 = "0.1"
831833
z3tracer = "0.8.0"
832834

833835
# MOVE DEPENDENCIES
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "validation-tool"
3+
version = "0.1.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
homepage.workspace = true
7+
license.workspace = true
8+
publish.workspace = true
9+
repository.workspace = true
10+
rust-version.workspace = true
11+
12+
[dependencies]
13+
anyhow = { workspace = true }
14+
aptos-config = { workspace = true }
15+
aptos-db = { workspace = true }
16+
aptos-rest-client = { workspace = true }
17+
aptos-storage-interface = { workspace = true }
18+
aptos-types = { workspace = true }
19+
bcs = { workspace = true }
20+
bytes = { workspace = true }
21+
clap = { workspace = true }
22+
either = { workspace = true }
23+
serde_json = { workspace = true }
24+
move-core-types = { workspace = true }
25+
thiserror = { workspace = true }
26+
tokio = { workspace = true }
27+
tracing = { workspace = true }
28+
tracing-subscriber = { workspace = true }
29+
30+
[dev-dependencies]
31+
tar = { workspace = true }
32+
tempfile = { workspace = true }
33+
xz2 = { workspace = true }
806 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pub mod api;
5+
pub mod error;
6+
pub mod node;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::types::api::{MovementAptosRestClient, MovementRestClient};
5+
use clap::Parser;
6+
7+
#[derive(Parser)]
8+
#[clap(
9+
name = "migration-api-validation",
10+
about = "Validates api conformity after movement migration."
11+
)]
12+
pub struct Command {
13+
#[clap(long = "movement", help = "The url of the Movement REST endpoint.")]
14+
pub movement_rest_api_url: String,
15+
#[clap(value_parser)]
16+
#[clap(
17+
long = "movement-aptos",
18+
help = "The url of the Movement Aptos REST endpoint."
19+
)]
20+
pub movement_aptos_rest_api_url: String,
21+
}
22+
23+
impl Command {
24+
pub async fn run(self) -> anyhow::Result<()> {
25+
let _movement_rest_client = MovementRestClient::new(&self.movement_rest_api_url)?;
26+
let _movement_aptos_rest_client =
27+
MovementAptosRestClient::new(&self.movement_aptos_rest_api_url)?;
28+
29+
Ok(())
30+
}
31+
}
32+
33+
#[test]
34+
fn verify_tool() {
35+
use clap::CommandFactory;
36+
Command::command().debug_assert()
37+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::checks::error::ValidationError;
5+
use crate::types::api::{MovementAptosRestClient, MovementRestClient};
6+
use aptos_rest_client::aptos_api_types::{EntryFunctionId, MoveType, ViewRequest};
7+
use serde_json::json;
8+
use std::collections::HashSet;
9+
use std::str::FromStr;
10+
11+
pub struct GlobalFeatureCheck;
12+
13+
impl GlobalFeatureCheck {
14+
pub async fn satisfies(
15+
movement_rest_client: &MovementRestClient,
16+
movement_aptos_rest_client: &MovementAptosRestClient,
17+
) -> Result<(), ValidationError> {
18+
let mut errors = vec![];
19+
let expected_active = HashSet::from([73]);
20+
let expected_inactive = HashSet::<u64>::new();
21+
22+
let mut aptos_request = ViewRequest {
23+
function: EntryFunctionId::from_str("0x1::features::is_enabled")
24+
.map_err(|e| ValidationError::Internal(e.into()))?,
25+
type_arguments: vec![MoveType::U64],
26+
arguments: vec![],
27+
};
28+
29+
let mut maptos_request = ViewRequest {
30+
function: EntryFunctionId::from_str("0x1::features::is_enabled")
31+
.map_err(|e| ValidationError::Internal(e.into()))?,
32+
type_arguments: vec![MoveType::U64],
33+
arguments: vec![],
34+
};
35+
36+
for feature_id in 1u64..=100 {
37+
aptos_request.arguments = vec![json!(feature_id)];
38+
maptos_request.arguments = vec![json!(feature_id)];
39+
40+
// Check feature for Aptos executor
41+
let aptos_active = movement_aptos_rest_client
42+
.view(&aptos_request, None)
43+
.await
44+
.map_err(|e| {
45+
ValidationError::Internal(
46+
format!("failed to get Aptos feature flag {}: {:?}", feature_id, e).into(),
47+
)
48+
})?
49+
.into_inner();
50+
51+
let aptos_active = aptos_active.get(0).ok_or_else(|| {
52+
ValidationError::Internal(
53+
format!(
54+
"failed to get Aptos feature flag {}: response is empty",
55+
feature_id
56+
)
57+
.into(),
58+
)
59+
})?;
60+
61+
let aptos_active = aptos_active.as_bool().ok_or_else(|| {
62+
ValidationError::Internal(
63+
format!(
64+
"failed to get Aptos feature flag {}: can't convert {:?} into a bool",
65+
feature_id, aptos_active
66+
)
67+
.into(),
68+
)
69+
})?;
70+
71+
// Check feature for Maptos executor
72+
let maptos_active = movement_rest_client
73+
.view(&maptos_request, None)
74+
.await
75+
.map_err(|e| {
76+
ValidationError::Internal(
77+
format!(
78+
"failed to get Movement feature flag {}: {:?}",
79+
feature_id, e
80+
)
81+
.into(),
82+
)
83+
})?
84+
.into_inner();
85+
86+
let maptos_active = maptos_active.get(0).ok_or_else(|| {
87+
ValidationError::Internal(
88+
format!(
89+
"failed to get Movement feature flag {}: response is empty",
90+
feature_id
91+
)
92+
.into(),
93+
)
94+
})?;
95+
96+
let maptos_active = maptos_active.as_bool().ok_or_else(|| {
97+
ValidationError::Internal(
98+
format!(
99+
"failed to get Movement feature flag {}: can't convert {:?} into a bool",
100+
feature_id, aptos_active
101+
)
102+
.into(),
103+
)
104+
})?;
105+
106+
if !expected_active.contains(&feature_id) {
107+
if !maptos_active {
108+
errors.push(format!(
109+
"Feature {}: Aptos={}, Maptos={} — expected to be active",
110+
feature_id, aptos_active, maptos_active
111+
));
112+
}
113+
} else if !expected_inactive.contains(&feature_id) {
114+
if maptos_active {
115+
errors.push(format!(
116+
"Feature {}: Aptos={}, Maptos={} — expected to be inactive",
117+
feature_id, aptos_active, maptos_active
118+
));
119+
}
120+
} else if aptos_active != maptos_active {
121+
errors.push(format!(
122+
"Feature {}: Aptos={}, Maptos={} — expected to match",
123+
feature_id, aptos_active, maptos_active
124+
));
125+
}
126+
}
127+
128+
if !errors.is_empty() {
129+
return Err(ValidationError::Unsatisfied(errors.join("\n").into()));
130+
}
131+
132+
Ok(())
133+
}
134+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum ValidationError {
6+
#[error("the criterion was not satisfied: {0}")]
7+
Unsatisfied(#[source] Box<dyn std::error::Error + Send + Sync>),
8+
#[error("criterion encountered an internal error: {0}")]
9+
Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
10+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::checks::node::global_storage_includes::GlobalStorageIncludes;
5+
use crate::types::storage::{MovementAptosStorage, MovementStorage};
6+
use clap::Parser;
7+
use std::path::PathBuf;
8+
9+
mod global_storage_includes;
10+
11+
#[derive(Parser)]
12+
#[clap(
13+
name = "migration-node-validation",
14+
about = "Validates data conformity after movement migration."
15+
)]
16+
pub struct Command {
17+
#[clap(long = "movement", help = "The path to the movement database.")]
18+
pub movement_db: PathBuf,
19+
#[clap(
20+
long = "movement-aptos",
21+
help = "The path to the movement Aptos database."
22+
)]
23+
pub movement_aptos_db: PathBuf,
24+
}
25+
26+
impl Command {
27+
pub async fn run(self) -> anyhow::Result<()> {
28+
let movement_storage = MovementStorage::open(&self.movement_db)?;
29+
let movement_aptos_storage = MovementAptosStorage::open(&self.movement_aptos_db)?;
30+
31+
GlobalStorageIncludes::satisfies(&movement_storage, &movement_aptos_storage)?;
32+
33+
Ok(())
34+
}
35+
}
36+
37+
#[test]
38+
fn verify_tool() {
39+
use clap::CommandFactory;
40+
Command::command().debug_assert()
41+
}

0 commit comments

Comments
 (0)