use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use thruput_json_rpc_types::transaction::TxHash;
use thruput_xdr::contract::{Contract, ContractBlob};
use thruput_xdr::types::{ContractAddress, SHA256Hash};
use tokio::sync::{Mutex, MutexGuard};
use xdr_common::BytesM;
use crate::error::{Result, TestkitError};
use crate::nonce::NonceNamespace;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
Rv32,
}
#[derive(Debug)]
pub(crate) enum ArtifactSpec {
Path {
name: String,
path: PathBuf,
kind: Option<ArtifactKind>,
},
Bytes { name: String, bytes: Vec<u8> },
}
impl ArtifactSpec {
fn name(&self) -> &str {
match self {
Self::Path { name, .. } | Self::Bytes { name, .. } => name,
}
}
}
#[derive(Debug)]
pub(crate) struct ArtifactEntry {
bytes: Vec<u8>,
hash: SHA256Hash,
registered: AtomicBool,
pending_registration: Mutex<Option<PendingRegistration>>,
}
#[derive(Debug, Clone)]
pub(crate) struct PendingRegistration {
pub(crate) tx_hash: TxHash,
pub(crate) tx_hex: String,
pub(crate) owner: RegistrationOwner,
pub(crate) effects: Vec<u8>,
pub(crate) address: ContractAddress,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RegistrationOwner {
pub(crate) address: ContractAddress,
pub(crate) namespace: NonceNamespace,
}
impl ArtifactEntry {
pub(crate) fn hash(&self) -> SHA256Hash {
self.hash
}
pub(crate) fn registered(&self) -> bool {
self.registered.load(Ordering::Acquire)
}
pub(crate) fn mark_registered(&self) {
self.registered.store(true, Ordering::Release);
}
pub(crate) async fn registration_guard(&self) -> MutexGuard<'_, Option<PendingRegistration>> {
self.pending_registration.lock().await
}
pub(crate) async fn finish_registration(&self, tx_hash: TxHash, succeeded: bool) {
let mut pending = self.pending_registration.lock().await;
if pending.as_ref().map(|attempt| attempt.tx_hash) != Some(tx_hash) {
return;
}
if succeeded {
self.mark_registered();
}
*pending = None;
}
pub(crate) fn contract(&self) -> Contract {
contract_from_bytes(self.bytes.clone())
}
}
#[derive(Debug)]
pub(crate) struct Registry {
entries: HashMap<String, Arc<ArtifactEntry>>,
}
impl Registry {
pub(crate) fn from_specs(specs: Vec<ArtifactSpec>) -> Result<Self> {
let mut entries = HashMap::with_capacity(specs.len());
for spec in specs {
let name = spec.name().to_owned();
if entries.contains_key(&name) {
return Err(TestkitError::DuplicateArtifact { name });
}
entries.insert(name, Arc::new(load_entry(spec)?));
}
Ok(Self { entries })
}
pub(crate) fn get(&self, name: &str) -> Result<Arc<ArtifactEntry>> {
self.entries
.get(name)
.map(Arc::clone)
.ok_or_else(|| TestkitError::UnknownArtifact {
name: name.to_owned(),
})
}
}
pub(crate) fn contract_from_bytes(bytes: Vec<u8>) -> Contract {
Contract::kRv32 {
rv32_contract: ContractBlob {
data: BytesM(bytes),
},
}
}
pub(crate) fn contract_hash(contract: &Contract) -> Result<SHA256Hash> {
thruput_client::contract_hash(contract).map_err(TestkitError::encode)
}
fn load_entry(spec: ArtifactSpec) -> Result<ArtifactEntry> {
let bytes = match spec {
ArtifactSpec::Bytes { bytes, .. } => bytes,
ArtifactSpec::Path { name, path, kind } => {
if kind.is_none() {
validate_inferred_kind(&path)?;
}
std::fs::read(&path).map_err(|source| TestkitError::ArtifactLoad {
name,
path: path.display().to_string(),
detail: source.to_string(),
})?
}
};
let hash = contract_hash(&contract_from_bytes(bytes.clone()))?;
Ok(ArtifactEntry {
bytes,
hash,
registered: AtomicBool::new(false),
pending_registration: Mutex::new(None),
})
}
fn validate_inferred_kind(path: &Path) -> Result<()> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("rv32a") => Ok(()),
_ => Err(TestkitError::UnknownArtifactExtension {
path: path.display().to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use thruput_json_rpc_types::{base58::Base58, transaction::TxHash};
use thruput_xdr::contract::Contract;
use thruput_xdr::types::{ContractAddress, SHA256Hash};
use crate::nonce::NonceNamespace;
use super::{
ArtifactKind, ArtifactSpec, RegistrationOwner, Registry, contract_from_bytes,
validate_inferred_kind,
};
#[test]
fn raw_artifact_bytes_build_an_rv32_contract() {
let bytes = vec![1, 2, 3];
let contract = contract_from_bytes(bytes.clone());
match contract {
Contract::kRv32 { rv32_contract } => assert_eq!(rv32_contract.data.0, bytes),
other => panic!("expected RV32 contract, got {other:?}"),
}
}
#[test]
fn inferred_artifact_kind_requires_rv32_extension() {
assert!(validate_inferred_kind(Path::new("counter.rv32a")).is_ok());
assert!(validate_inferred_kind(Path::new("counter.wasm")).is_err());
assert!(validate_inferred_kind(Path::new("counter")).is_err());
}
#[cfg(unix)]
#[test]
fn inferred_artifact_kind_rejects_non_utf8_extension() {
use std::os::unix::ffi::OsStringExt;
let path = PathBuf::from(std::ffi::OsString::from_vec(b"counter.\xff".to_vec()));
assert!(validate_inferred_kind(&path).is_err());
}
#[test]
fn explicit_rv32_kind_overrides_filename_extension() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/artifact.rs");
let registry = Registry::from_specs(vec![ArtifactSpec::Path {
name: "counter".to_owned(),
path,
kind: Some(ArtifactKind::Rv32),
}]);
assert!(registry.is_ok());
}
#[tokio::test]
async fn registration_resolution_only_clears_the_matching_transaction() {
let registry = Registry::from_specs(vec![ArtifactSpec::Bytes {
name: "counter".to_owned(),
bytes: vec![1, 2, 3],
}])
.expect("bug: in-memory artifact should load");
let entry = registry.get("counter").expect("bug: artifact exists");
let pending_hash = TxHash(Base58(SHA256Hash::from([0x11; 32])));
let other_hash = TxHash(Base58(SHA256Hash::from([0x22; 32])));
*entry.registration_guard().await = Some(super::PendingRegistration {
tx_hash: pending_hash,
tx_hex: "0x01".to_owned(),
effects: Vec::new(),
owner: RegistrationOwner {
address: [0; 32].into(),
namespace: NonceNamespace::for_run("seed", 0),
},
address: [0; 32].into(),
});
entry.finish_registration(other_hash, true).await;
assert_eq!(
entry
.registration_guard()
.await
.as_ref()
.map(|attempt| attempt.tx_hash),
Some(pending_hash)
);
assert!(!entry.registered());
entry.finish_registration(pending_hash, true).await;
assert!(entry.registration_guard().await.is_none());
assert!(entry.registered());
}
#[test]
fn registration_owner_distinguishes_repeated_runs_using_the_same_account() {
let address = ContractAddress::from([0x42; 32]);
assert_ne!(
RegistrationOwner {
address,
namespace: NonceNamespace::for_run("seed", 0),
},
RegistrationOwner {
address,
namespace: NonceNamespace::for_run("seed", 1),
}
);
}
}