Skip to content

Commit

Permalink
Merge pull request #136 from cspr-rad/add-deploy-contract-cctl
Browse files Browse the repository at this point in the history
kairos-test-utils/cctl: add deploy contract function
  • Loading branch information
Avi-D-coder committed Jun 26, 2024
2 parents 37186e1 + af899f9 commit 341dba0
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 17 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion kairos-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn fixture_path(relative_path: &str) -> PathBuf {
#[tokio::test]
#[cfg_attr(not(feature = "cctl-tests"), ignore)]
async fn deposit_successful_with_ed25519() {
let network = cctl::CCTLNetwork::run(Option::None, Option::None, Option::None)
let network = cctl::CCTLNetwork::run(None, None, None, None)
.await
.unwrap();
let node = network
Expand Down
4 changes: 1 addition & 3 deletions kairos-server/tests/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ fn new_test_app_with_casper_node(casper_node_url: &Url) -> TestServer {
#[tokio::test]
#[cfg_attr(not(feature = "cctl-tests"), ignore)]
async fn test_signed_deploy_is_forwarded_if_sender_in_approvals() {
let network = CCTLNetwork::run(Option::None, Option::None, Option::None)
.await
.unwrap();
let network = CCTLNetwork::run(None, None, None, None).await.unwrap();
let node = network
.nodes
.first()
Expand Down
8 changes: 7 additions & 1 deletion kairos-test-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ test = false
bench = false

[features]
all-tests = ["cctl-tests"]
# FIXME enable cctl-tests once this crate is factored out in a separate repository
#all-tests = ["cctl-tests"]
all-tests = []
cctl-tests = []

[lib]
Expand All @@ -23,7 +25,11 @@ anyhow = "1"
backoff = { version = "0.4", features = ["tokio", "futures"]}
clap = { version = "4", features = ["derive"] }
casper-client.workspace = true
casper-types.workspace = true
casper-client-types.workspace = true
nom = "7"
hex = "0.4"
rand = "0.8"
sd-notify = "0.4"
tokio = { version = "1", features = [ "full", "tracing", "macros" ] }
tempfile = "3"
Expand Down
17 changes: 17 additions & 0 deletions kairos-test-utils/bin/cctld.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use casper_client_types::{runtime_args, RuntimeArgs};
use clap::Parser;
use kairos_test_utils::cctl;
use sd_notify::NotifyState;
use std::path::PathBuf;
use tokio::signal;

use crate::cctl::DeployableContract;

#[derive(Parser)]
pub struct Cli {
#[arg(short, long)]
pub working_dir: Option<PathBuf>,
#[arg(short, long)]
pub deploy_contract: Option<String>,
#[arg(short, long)]
pub chainspec_path: Option<PathBuf>,
#[arg(short, long)]
pub config_path: Option<PathBuf>,
Expand All @@ -17,8 +22,20 @@ pub struct Cli {
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let deploy_contract = cli.deploy_contract.map(|deploy_contracts_arg| {
match deploy_contracts_arg.split_once(':') {
Some((hash_name, path)) => DeployableContract {
hash_name: hash_name.to_string(),
// FIXME at some point we want to make this parametrizable
runtime_args: runtime_args! { "initial_trie_root" => Option::<[u8; 32]>::None },
path: PathBuf::from(&path),
},
None => panic!("Error parsing the provided deploy contracts argument."),
}
});
let _network = cctl::CCTLNetwork::run(
cli.working_dir,
deploy_contract,
cli.chainspec_path.as_deref(),
cli.config_path.as_deref(),
)
Expand Down
253 changes: 248 additions & 5 deletions kairos-test-utils/src/cctl.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
pub mod parsers;
use anyhow::anyhow;
use backoff::{future::retry, ExponentialBackoff};
use casper_client::{get_node_status, rpcs::results::ReactorState, Error, JsonRpcId, Verbosity};
use casper_client::{
get_account, get_deploy, get_node_status, get_state_root_hash, put_deploy, query_global_state,
rpcs::results::ReactorState,
types::{DeployBuilder, ExecutableDeployItem, StoredValue, TimeDiff, Timestamp},
Error, JsonRpcId, Verbosity,
};
use casper_client_types::{ExecutionResult, Key, PublicKey, RuntimeArgs, SecretKey};
use casper_types::ContractHash;
use hex::FromHex;
use rand::Rng;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -35,12 +45,23 @@ pub struct CCTLNetwork {
pub nodes: Vec<CasperNode>,
}

pub struct DeployableContract {
/// This is the named key under which the contract hash is located
pub hash_name: String,
pub runtime_args: RuntimeArgs,
pub path: PathBuf,
}

// max amount allowed to be used on gas fees
pub const MAX_GAS_FEE_PAYMENT_AMOUNT: u64 = 10_000_000_000_000;

impl CCTLNetwork {
pub async fn run(
working_dir: Option<PathBuf>,
contract_to_deploy: Option<DeployableContract>,
chainspec_path: Option<&Path>,
config_path: Option<&Path>,
) -> Result<CCTLNetwork, io::Error> {
) -> anyhow::Result<CCTLNetwork> {
let working_dir = working_dir
.map(|dir| {
std::fs::create_dir_all(&dir)
Expand Down Expand Up @@ -135,8 +156,40 @@ impl CCTLNetwork {
let output = std::str::from_utf8(output.stdout.as_slice()).unwrap();
tracing::info!("{}", output);

if let Some(contract_to_deploy) = contract_to_deploy {
let deployer_skey =
SecretKey::from_file(working_dir.join("assets/users/user-1/secret_key.pem"))?;
let deployer_pkey =
PublicKey::from_file(working_dir.join("assets/users/user-1/public_key.pem"))?;

let (hash_name, contract_hash) = deploy_contract(
&casper_node_rpc_url,
&deployer_skey,
&deployer_pkey,
&contract_to_deploy,
)
.await?;
let contracts_dir = working_dir.join("contracts");
fs::create_dir_all(&contracts_dir)?;
fs::write(
contracts_dir.join(hash_name),
// For a ContractHash contract- will always be the prefix
contract_hash
.to_formatted_string()
.strip_prefix("contract-")
.unwrap(),
)?
}
Ok(CCTLNetwork { working_dir, nodes })
}
/// Get the deployed contract hash for a hash_name that was passed to new_contract
/// https://docs.rs/casper-contract/latest/casper_contract/contract_api/storage/fn.new_contract.html
pub fn get_contract_hash_for(&self, hash_name: &str) -> ContractHash {
let contract_hash_path = self.working_dir.join("contracts").join(hash_name);
let contract_hash_string = fs::read_to_string(contract_hash_path).unwrap();
let contract_hash_bytes = <[u8; 32]>::from_hex(contract_hash_string).unwrap();
ContractHash::new(contract_hash_bytes)
}
}

impl Drop for CCTLNetwork {
Expand All @@ -150,15 +203,182 @@ impl Drop for CCTLNetwork {
}
}

/// Deploys a contract as the given user for the contract's defined hash name located at the path.
/// The hash name should be equal to the hash name passed to https://docs.rs/casper-contract/latest/casper_contract/contract_api/storage/fn.new_locked_contract.html
async fn deploy_contract(
casper_node_rpc_url: &str,
contract_deployer_skey: &SecretKey,
contract_deployer_pkey: &PublicKey,
DeployableContract {
hash_name,
runtime_args,
path,
}: &DeployableContract,
) -> anyhow::Result<(String, casper_client_types::ContractHash)> {
tracing::info!(
"Deploying contract {}: {}",
&hash_name,
path.to_str().unwrap()
);

let contract_bytes = fs::read(path)?;
let contract =
ExecutableDeployItem::new_module_bytes(contract_bytes.into(), runtime_args.clone());
let deploy = DeployBuilder::new(
// TODO ideally make the chain-name this configurable
"cspr-dev-cctl",
contract,
contract_deployer_skey,
)
.with_standard_payment(MAX_GAS_FEE_PAYMENT_AMOUNT) // max amount allowed to be used on gas fees
.with_timestamp(Timestamp::now())
.with_ttl(TimeDiff::from_millis(60_000)) // 1 min
.build()?;

tracing::info!("Submitting contract deploy");
let expected_rpc_id = JsonRpcId::Number(rand::thread_rng().gen::<i64>());
let deploy_hash = put_deploy(
expected_rpc_id.clone(),
casper_node_rpc_url,
Verbosity::High,
deploy,
)
.await
.map_err(Into::<anyhow::Error>::into)
.and_then(|response| {
if response.id == expected_rpc_id {
Ok(response.result.deploy_hash)
} else {
Err(anyhow!("JSON RPC Id missmatch"))
}
})?;

tracing::info!("Waiting for successful contract initialization");
retry(ExponentialBackoff::default(), || async {
let expected_rpc_id = JsonRpcId::Number(rand::thread_rng().gen::<i64>());
let response = get_deploy(
expected_rpc_id.clone(),
casper_node_rpc_url,
Verbosity::High,
deploy_hash,
false,
)
.await
.map_err(|err| match &err {
Error::ResponseIsHttpError { .. } | Error::FailedToGetResponse { .. } => {
backoff::Error::transient(anyhow!(err))
}
_ => backoff::Error::permanent(anyhow!(err)),
})?;
if response.id == expected_rpc_id {
match response.result.execution_results.first() {
Some(result) => match &result.result {
ExecutionResult::Failure { error_message, .. } => {
Err(backoff::Error::permanent(anyhow!(error_message.clone())))
}
ExecutionResult::Success { .. } => Ok(()),
},
Option::None => Err(backoff::Error::transient(anyhow!(
"No execution results there yet"
))),
}
} else {
Err(backoff::Error::permanent(anyhow!("JSON RPC Id missmatch")))
}
})
.await?;
tracing::info!("Contract was deployed successfully");

tracing::info!("Fetching deployed contract hash");
// Query global state
let expected_rpc_id = JsonRpcId::Number(rand::thread_rng().gen::<i64>());
let state_root_hash = get_state_root_hash(
expected_rpc_id.clone(),
casper_node_rpc_url,
Verbosity::High,
Option::None,
)
.await
.map_err(Into::<anyhow::Error>::into)
.and_then(|response| {
if response.id == expected_rpc_id {
response
.result
.state_root_hash
.ok_or(anyhow!("No state root hash present in response"))
} else {
Err(anyhow!("JSON RPC Id missmatch"))
}
})?;

let expected_rpc_id = JsonRpcId::Number(rand::thread_rng().gen::<i64>());
let account = get_account(
expected_rpc_id.clone(),
casper_node_rpc_url,
Verbosity::High,
Option::None,
contract_deployer_pkey.clone(),
)
.await
.map_err(Into::<anyhow::Error>::into)
.and_then(|response| {
if response.id == expected_rpc_id {
Ok(response.result.account)
} else {
Err(anyhow!("JSON RPC Id missmatch"))
}
})?;

let expected_rpc_id = JsonRpcId::Number(rand::thread_rng().gen::<i64>());
let account_key = Key::Account(*account.account_hash());
let contract_hash: casper_client_types::ContractHash = query_global_state(
expected_rpc_id.clone(),
casper_node_rpc_url,
Verbosity::High,
casper_client::rpcs::GlobalStateIdentifier::StateRootHash(state_root_hash), // fetches recent blocks state root hash
account_key,
vec![hash_name.clone()],
)
.await
.map_err(Into::<anyhow::Error>::into)
.and_then(|response| {
if response.id == expected_rpc_id {
match response.result.stored_value {
StoredValue::ContractPackage(contract_package) => Ok(*contract_package
.versions()
.next()
.expect("Expected at least one contract version")
.contract_hash()),
other => Err(anyhow!(
"Unexpected result type, type is not a CLValue: {:?}",
other
)),
}
} else {
Err(anyhow!("JSON RPC Id missmatch"))
}
})?;
tracing::info!(
"Successfully fetched the contract hash for {}: {}",
&hash_name,
&contract_hash
);
Ok::<(String, casper_client_types::ContractHash), anyhow::Error>((
hash_name.clone(),
contract_hash,
))
}

#[cfg(test)]
mod tests {
use super::*;
use casper_client_types::runtime_args;
use hex::FromHex;

#[cfg_attr(not(feature = "cctl-tests"), ignore)]
#[tokio::test]
async fn test_cctl_network_starts_and_terminates() {
let network = CCTLNetwork::run(Option::None, Option::None, Option::None)
.await
.unwrap();
let network = CCTLNetwork::run(None, None, None, None).await.unwrap();
for node in &network.nodes {
if node.state == NodeState::Running {
let node_status = get_node_status(
Expand All @@ -172,4 +392,27 @@ mod tests {
}
}
}

#[cfg_attr(not(feature = "cctl-tests"), ignore)]
#[tokio::test]
async fn test_cctl_deploys_a_contract_successfully() {
let contract_wasm_path =
PathBuf::from(env!("PATH_TO_WASM_BINARIES")).join("demo-contract-optimized.wasm");
let hash_name = "kairos_contract_package_hash";
let contract_to_deploy = DeployableContract {
hash_name: hash_name.to_string(),
runtime_args: runtime_args! { "initial_trie_root" => Option::<[u8; 32]>::None },
path: contract_wasm_path,
};
let network = CCTLNetwork::run(None, Some(contract_to_deploy), None, None)
.await
.unwrap();
let expected_contract_hash_path = network.working_dir.join("contracts").join(hash_name);
assert!(expected_contract_hash_path.exists());

let hash_string = fs::read_to_string(expected_contract_hash_path).unwrap();
let contract_hash_bytes = <[u8; 32]>::from_hex(hash_string).unwrap();
let contract_hash = ContractHash::new(contract_hash_bytes);
assert!(contract_hash.to_formatted_string().starts_with("contract-"))
}
}
Loading

0 comments on commit 341dba0

Please sign in to comment.