feat(aquavm-air-cli): --near execution mode [fixes VM-322] (#672)

Adding the NEAR execution mode that executes AIR NEAR smart contract, measuring its gas consumption.
This commit is contained in:
Ivan Boldyrev 2023-08-17 18:40:29 +07:00 committed by GitHub
parent 7a8a460572
commit 0e80ee7908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 5081 additions and 258 deletions

View File

@ -14,7 +14,8 @@
"components": [ "components": [
"air", "air",
"air-interpreter", "air-interpreter",
"avm-client" "avm-client",
"avm-near-contract"
] ]
} }
], ],
@ -56,5 +57,8 @@
"tools/wasm/air-beautify-wasm": { "tools/wasm/air-beautify-wasm": {
"component": "air-beautify-wasm" "component": "air-beautify-wasm"
} }
"tools/wasm/air-near-contract": {
"component": "air-near-contract"
}
} }
} }

2463
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ air-lambda-parser = { version = "0.1.0", path = "../crates/air-lib/lambda/parser
air-trace-handler = { version = "0.4.0", path = "../crates/air-lib/trace-handler" } air-trace-handler = { version = "0.4.0", path = "../crates/air-lib/trace-handler" }
air-utils = { version = "0.1.1", path = "../crates/air-lib/utils" } air-utils = { version = "0.1.1", path = "../crates/air-lib/utils" }
polyplets = { version = "0.4.1", path = "../crates/air-lib/polyplets" } polyplets = { version = "0.4.1", path = "../crates/air-lib/polyplets" }
fluence-keypair = { version = "0.10.1" } fluence-keypair = { version = "0.10.1", default-features = false }
serde = { version = "1.0.164", features = [ "derive", "rc" ] } serde = { version = "1.0.164", features = [ "derive", "rc" ] }
serde_json = "1.0.95" serde_json = "1.0.95"
@ -49,7 +49,6 @@ tracing = "0.1.37"
air-test-utils = { path = "../crates/air-lib/test-utils" } air-test-utils = { path = "../crates/air-lib/test-utils" }
air-testing-framework = { path = "../crates/testing-framework" } air-testing-framework = { path = "../crates/testing-framework" }
fluence-app-service = "0.28.0" fluence-app-service = "0.28.0"
fluence-keypair = "0.10.1"
marine-rs-sdk = { version = "0.8.1", features = ["logger"] } marine-rs-sdk = { version = "0.8.1", features = ["logger"] }
# the feature just silence a warning in the criterion 0.3.x. # the feature just silence a warning in the criterion 0.3.x.

View File

@ -30,4 +30,4 @@ serde = "1.0.164"
log = "0.4.17" log = "0.4.17"
parking_lot = "0.12.1" parking_lot = "0.12.1"
tracing = "0.1.37" tracing = "0.1.37"
fluence-keypair = "0.10.1" fluence-keypair = { version = "0.10.1", default-features = false }

View File

@ -12,7 +12,7 @@ categories = ["wasm"]
[dependencies] [dependencies]
air-interpreter-cid = { version = "0.3.0", path = "../interpreter-cid" } air-interpreter-cid = { version = "0.3.0", path = "../interpreter-cid" }
fluence-keypair = "0.10.1" fluence-keypair = { version = "0.10.1", default-features = false }
serde = { version = "1.0.164", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.96" serde_json = "1.0.96"

View File

@ -30,9 +30,15 @@ fluence-keypair = "0.10.1"
bs58 = "0.5.0" bs58 = "0.5.0"
zeroize = "1.6.0" zeroize = "1.6.0"
# near
near-sdk = { version = "4.1.1", optional = true }
tokio = { version = "1", features = ["rt"], optional = true }
workspaces = { version = "0.7.0", optional = true }
[features] [features]
default = ["wasm"] default = ["wasm"]
wasm = ["air-test-utils"] wasm = ["air-test-utils"]
near = [ "dep:near-sdk", "dep:tokio", "dep:workspaces" ]
[[bin]] [[bin]]
name = "air" name = "air"

View File

@ -27,7 +27,9 @@ All common parameters are optional. Their position is always before the mode se
+ `--runner-tracing-params` defines tracing logging level for the runner. By default, it is equal to `warn`. + `--runner-tracing-params` defines tracing logging level for the runner. By default, it is equal to `warn`.
+ `--random-key` or `--ed25519-key keyfile` (required): use random signing key or load it from a file. + `--random-key` or `--ed25519-key keyfile` (required): use random signing key or load it from a file.
The important option is `--native`. It runs the AquaVM as the native code that can be profiled with any native profiler. As input data deserialization and serialization time can be comparable to particle execution time, and short execution times provides less reliable results, one can use `--repeat N` option to repeat particle execution several times. Execution result is not printed in this case, so you may run `--repeat 1` to suppress it. The important option is the execution mode `--native`. It runs the AquaVM as the native code that can be profiled with any native profiler. As input data deserialization and serialization time can be comparable to particle execution time, and short execution times provides less reliable results, one can use `--repeat N` option to repeat particle execution several times. Execution result is not printed in this case, so you may run `--repeat 1` to suppress it.
Another alternative execution mode is `--near`, enabled by with `near` feature flag. It runs a NEAR contract defined by the `--near-contract` option, measuring its gas consumption. The `workspaces` crate in sandbox mode is used for the most accurate NEAR gas measurement. The workspaces build script installs NEAR Sandbox automatically, but on the other machines you have to install it manually and set the NEAR_SANDBOX_BIN_PATH variable.
Run `air run --help` to see all common parameters. Run `air run --help` to see all common parameters.

View File

@ -16,6 +16,8 @@
mod data; mod data;
mod native; mod native;
#[cfg(feature = "near")]
mod near;
mod runner; mod runner;
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
mod wasm; mod wasm;
@ -56,6 +58,13 @@ pub(crate) struct Args {
)] )]
air_interpreter_path: PathBuf, air_interpreter_path: PathBuf,
#[clap(
long = "near-contract",
env = "AIR_NEAR_CONTRACT_PATH",
default_value = "tools/wasm/air-near-contract/target/wasm32-unknown-unknown/release/aqua_vm.wasm"
)]
air_near_contract_path: PathBuf,
#[clap(long, help = "Execute several times; great for native profiling")] #[clap(long, help = "Execute several times; great for native profiling")]
repeat: Option<u32>, repeat: Option<u32>,
#[clap(long, help = "Output JSON tracing info")] #[clap(long, help = "Output JSON tracing info")]
@ -104,9 +113,14 @@ impl Keys {
struct ModeArgs { struct ModeArgs {
#[arg(long)] #[arg(long)]
native: bool, native: bool,
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
#[arg(long)] #[arg(long)]
wasm: bool, wasm: bool,
#[cfg(feature = "near")]
#[arg(long)]
near: bool,
} }
impl From<ModeArgs> for Option<Mode> { impl From<ModeArgs> for Option<Mode> {
@ -120,14 +134,23 @@ impl From<ModeArgs> for Option<Mode> {
return Some(Mode::Wasm); return Some(Mode::Wasm);
} }
#[cfg(feature = "near")]
if value.near {
return Some(Mode::Near);
}
None None
} }
} }
enum Mode { enum Mode {
Native, Native,
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
Wasm, Wasm,
#[cfg(feature = "near")]
Near,
} }
pub(crate) fn run(args: Args) -> anyhow::Result<()> { pub(crate) fn run(args: Args) -> anyhow::Result<()> {
@ -146,6 +169,7 @@ pub(crate) fn run(args: Args) -> anyhow::Result<()> {
let mut runner = get_runner( let mut runner = get_runner(
args.mode.into(), args.mode.into(),
&args.air_interpreter_path, &args.air_interpreter_path,
&args.air_near_contract_path,
args.max_heap_size, args.max_heap_size,
)?; )?;
@ -195,6 +219,7 @@ pub(crate) fn run(args: Args) -> anyhow::Result<()> {
fn get_runner( fn get_runner(
mode: Option<Mode>, mode: Option<Mode>,
air_interpreter_wasm_path: &Path, air_interpreter_wasm_path: &Path,
_air_contract_wasm_path: &Path,
max_heap_size: Option<u64>, max_heap_size: Option<u64>,
) -> anyhow::Result<Box<dyn AirRunner>> { ) -> anyhow::Result<Box<dyn AirRunner>> {
let mode = mode.unwrap_or(Mode::Wasm); let mode = mode.unwrap_or(Mode::Wasm);
@ -204,6 +229,9 @@ fn get_runner(
} }
Mode::Wasm => self::wasm::create_wasm_avm_runner(air_interpreter_wasm_path, max_heap_size) Mode::Wasm => self::wasm::create_wasm_avm_runner(air_interpreter_wasm_path, max_heap_size)
.context("Failed to instantiate WASM AVM"), .context("Failed to instantiate WASM AVM"),
#[cfg(feature = "near")]
Mode::Near => self::near::create_near_runner(_air_contract_wasm_path)
.context("Failed to instantiate NEAR AVM"),
} }
} }
@ -211,6 +239,7 @@ fn get_runner(
fn get_runner( fn get_runner(
mode: Option<Mode>, mode: Option<Mode>,
_air_interpreter_wasm_path: &Path, _air_interpreter_wasm_path: &Path,
_air_contract_wasm_path: &Path,
_max_heap_size: Option<u64>, _max_heap_size: Option<u64>,
) -> anyhow::Result<Box<dyn AirRunner>> { ) -> anyhow::Result<Box<dyn AirRunner>> {
let mode = mode.unwrap_or(Mode::Native); let mode = mode.unwrap_or(Mode::Native);
@ -218,6 +247,9 @@ fn get_runner(
Mode::Native => { Mode::Native => {
self::native::create_native_avm_runner().context("Failed to instantiate a native AVM") self::native::create_native_avm_runner().context("Failed to instantiate a native AVM")
} }
#[cfg(feature = "near")]
Mode::Near => self::near::create_near_runner(_air_contract_wasm_path)
.context("Failed to instantiate NEAR AVM"),
} }
} }

View File

@ -0,0 +1,122 @@
/*
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use super::runner::AirRunner;
use air_interpreter_interface::RunParameters;
use avm_interface::raw_outcome::RawAVMOutcome;
use fluence_keypair::KeyPair;
use std::path::{Path, PathBuf};
pub(crate) struct NearRunner {
air_contract_wasm_path: PathBuf,
}
impl AirRunner for NearRunner {
fn call_tracing(
&mut self,
air: String,
prev_data: Vec<u8>,
current_data: Vec<u8>,
init_peer_id: String,
timestamp: u64,
ttl: u32,
current_peer_id: String,
call_results: avm_interface::CallResults,
_tracing_params: String,
_tracing_output_mode: u8,
keypair: &KeyPair,
particle_id: String,
) -> anyhow::Result<RawAVMOutcome> {
let key_format = keypair.key_format().into();
let secret_key_bytes = keypair.secret().expect("Failed to get secret key");
let run_parameters = RunParameters {
init_peer_id,
current_peer_id,
timestamp,
ttl,
key_format,
secret_key_bytes,
particle_id,
};
execute_on_near(
&self.air_contract_wasm_path,
air,
prev_data,
current_data,
run_parameters,
call_results,
)
}
}
pub(crate) fn create_near_runner(
air_contract_wasm_path: &Path,
) -> anyhow::Result<Box<dyn AirRunner>> {
let air_contract_wasm_path = air_contract_wasm_path.to_owned();
Ok(Box::new(NearRunner {
air_contract_wasm_path,
}))
}
fn execute_on_near(
path: &Path,
air_script: String,
prev_data: Vec<u8>,
current_data: Vec<u8>,
run_parameters: RunParameters,
call_results: avm_interface::CallResults,
) -> anyhow::Result<avm_interface::raw_outcome::RawAVMOutcome> {
use avm_interface::into_raw_result;
let run_parameters = serde_json::to_string(&run_parameters)?;
// some inner parts transformations
let raw_call_results = into_raw_result(call_results);
let raw_call_results = serde_json::to_vec(&raw_call_results)?;
let wasm = std::fs::read(path)?;
let result = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async move {
let worker = workspaces::sandbox().await?;
let contract = worker.dev_deploy(&wasm).await?;
contract
.call("execute_script")
.max_gas()
.args_borsh((
air_script,
prev_data,
current_data,
run_parameters,
raw_call_results,
))
.transact()
.await
})?;
eprintln!("total gas: {:e}", result.total_gas_burnt);
eprintln!("transaction gas: {:e}", result.outcome().gas_burnt);
let data: String = result.borsh()?;
serde_json::from_str(&data).map_err(Into::into)
}

2591
tools/wasm/air-near-contract/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
[package]
name = "air-near-contract"
version = "0.43.1"
description = "AIR interpreter as a NEAR contract"
authors = ["Fluence labs"]
edition = "2018"
license = "Apache-2.0"
keywords = ["fluence", "air", "webassembly", "programming-language", "near"]
categories = ["wasm"]
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
aquavm-air = { path = "../../../air" }
air-interpreter-interface = { path = "../../../crates/air-lib/interpreter-interface" }
near-sdk = "4.1.1"
serde = { version = "1.0.118", features = [ "derive", "rc" ] }
serde_json = "1.0.61"
hashbrown = { version = "0.14.0", default-features = false }
[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true
# it is required to be build in own workspace, as it has special profile
[workspace]
members = []
[patch.crates-io]
fluence-keypair = { git = "https://github.com/fluencelabs/trust-graph.git", branch = "lean-keypair" }
libp2p-identity = { git = "https://github.com/fluencelabs/rust-libp2p.git", branch = "rand-feature" }

View File

@ -0,0 +1,6 @@
#!/bin/sh
echo ">> Building contract"
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "1.69"

View File

@ -0,0 +1,63 @@
/*
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use air::execute_air;
use air::RunParameters;
use air_interpreter_interface::InterpreterOutcome;
use near_sdk::near_bindgen;
use near_sdk::borsh as borsh;
use borsh::BorshDeserialize;
use borsh::BorshSerialize;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, Default)]
pub struct Aqua {}
#[near_bindgen]
impl Aqua {
#[result_serializer(borsh)]
pub fn execute_script(
&self,
#[serializer(borsh)] air_script: String,
#[serializer(borsh)] prev_data: Vec<u8>,
#[serializer(borsh)] current_data: Vec<u8>,
#[serializer(borsh)] run_parameters: String,
#[serializer(borsh)] call_results: String,
) -> String {
let outcome = Self::execute(
air_script,
prev_data,
current_data,
run_parameters.into(),
call_results.into(),
);
serde_json::to_string(&outcome).unwrap()
}
fn execute(
air: String,
prev_data: Vec<u8>,
cur_data: Vec<u8>,
params: Vec<u8>,
call_results: Vec<u8>,
) -> InterpreterOutcome {
let params: RunParameters =
serde_json::from_slice(&params).expect("cannot parse RunParameters");
execute_air(air, prev_data, cur_data, params, call_results)
}
}