#![allow(dead_code)]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use arch_program::sanitized::ArchMessage;
use arch_program::{pubkey::Pubkey, system_instruction};
use bitcoin::key::UntweakedKeypair;
use bitcoin::secp256k1::{Secp256k1, SecretKey, XOnlyPublicKey};
pub fn test_keypair(seed: u8) -> (UntweakedKeypair, Pubkey) {
let secp = Secp256k1::new();
let kp = UntweakedKeypair::from_secret_key(&secp, &SecretKey::from_slice(&[seed; 32]).unwrap());
let pk = Pubkey::from_slice(&XOnlyPublicKey::from_keypair(&kp).0.serialize());
(kp, pk)
}
pub fn verifies_under(digest: &[u8], key: &Pubkey, sig: [u8; 64]) -> bool {
arch_sdk::verify_message_bip322(
digest,
key.serialize(),
sig,
false,
bitcoin::Network::Bitcoin,
)
.is_ok()
|| arch_sdk::verify_message_bip322(
digest,
key.serialize(),
sig,
true,
bitcoin::Network::Bitcoin,
)
.is_ok()
}
pub struct ProxyFixture {
pub url: String,
pub token: String,
pub role_pubkey: Pubkey,
pub mock: Arc<mock_turnkey::MockTurnkey>,
pub proxy_requests: Arc<AtomicU64>,
pub force_503: Arc<AtomicBool>,
pub server: tokio::task::JoinHandle<()>,
pub tmpdir: std::path::PathBuf,
}
impl ProxyFixture {
pub fn proxy_requests(&self) -> u64 {
self.proxy_requests.load(Ordering::Relaxed)
}
}
impl Drop for ProxyFixture {
fn drop(&mut self) {
self.server.abort();
let _ = std::fs::remove_dir_all(&self.tmpdir);
}
}
pub async fn proxy_fixture(policy_allows_role: bool) -> ProxyFixture {
use arch_cosigner::authentication::token_hash;
use axum::response::IntoResponse;
let tmpdir = std::env::temp_dir().join(format!(
"sigeq-{}-{}",
std::process::id(),
rand::random::<u32>()
));
std::fs::create_dir_all(&tmpdir).unwrap();
let api_key = turnkey_api_key_stamper::TurnkeyP256ApiKey::generate();
let stamper_pub = hex::encode(api_key.compressed_public_key());
let cred = tmpdir.join("arb.stamper.json");
std::fs::write(
&cred,
format!(
r#"{{"publicKey":"{stamper_pub}","privateKey":"{}"}}"#,
hex::encode(api_key.private_key())
),
)
.unwrap();
let other_stamper =
hex::encode(turnkey_api_key_stamper::TurnkeyP256ApiKey::generate().compressed_public_key());
let allowed = if policy_allows_role {
vec![stamper_pub.clone()]
} else {
vec![other_stamper]
};
let (mock_key, xonly) = mock_turnkey::fresh_role_key(None, bitcoin::Network::Bitcoin, allowed);
let address = mock_key.address.clone();
let mut keys = vec![mock_key];
if !policy_allows_role {
let (dummy, _) = mock_turnkey::fresh_role_key(
None,
bitcoin::Network::Bitcoin,
vec![stamper_pub.clone()],
);
keys.push(dummy);
}
let mock = mock_turnkey::MockTurnkey::new("test-org", keys);
let (mock_url, _h) = mock.spawn().await;
let token = format!("arb-{}", rand::random::<u64>());
let toml = format!(
r#"
listen_addr = "127.0.0.1:0"
network = "bitcoin"
audit_log_path = "{audit}"
[turnkey]
base_url = "{mock_url}"
timeout_ms = 3000
retry_attempts = 0
retry_backoff_ms = 10
[roles.arb]
organization_id = "test-org"
sign_with = "{address}"
arch_pubkey_hex = "{pk}"
token_hashes = ["{th}"]
stamper = "file:{cred}"
"#,
audit = tmpdir.join("audit.jsonl").display(),
pk = hex::encode(xonly.serialize()),
th = hex::encode(token_hash(&token)),
cred = cred.display(),
);
let state =
arch_cosigner::AppState::build(arch_cosigner::Config::from_toml(&toml).unwrap()).unwrap();
let proxy_requests = Arc::new(AtomicU64::new(0));
let count_layer = {
let proxy_requests = Arc::clone(&proxy_requests);
axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let proxy_requests = Arc::clone(&proxy_requests);
async move {
proxy_requests.fetch_add(1, Ordering::Relaxed);
next.run(req).await
}
},
)
};
let force_503 = Arc::new(AtomicBool::new(false));
let unavailable_layer = {
let force_503 = Arc::clone(&force_503);
axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let force_503 = Arc::clone(&force_503);
async move {
if force_503.load(Ordering::Relaxed) {
return (
axum::http::StatusCode::SERVICE_UNAVAILABLE,
axum::Json(serde_json::json!({ "error": "service_unavailable" })),
)
.into_response();
}
next.run(req).await
}
},
)
};
let app = arch_cosigner::router(Arc::clone(&state))
.layer(unavailable_layer)
.layer(count_layer);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
ProxyFixture {
url: format!("http://{addr}"),
token,
role_pubkey: Pubkey::from_slice(&xonly.serialize()),
mock,
proxy_requests,
force_503,
server,
tmpdir,
}
}
pub fn transfer_message(from: Pubkey, seed: u8) -> ArchMessage {
let (_k, dest) = test_keypair(seed);
let ix = system_instruction::transfer(&from, &dest, 9000);
ArchMessage::new(&[ix], Some(from), arch_program::hash::Hash::from([7; 32]))
}