use crate::proto::auth_config::AuthType;
use crate::testing::mocks::{
MockGuardianService, create_mock_account_state, create_mock_delta, start_mock_server,
};
use crate::{
AccountRef, AccountState, AuthConfig, ClientError, ConfigureResponse, FalconKeyStore,
GetAccountByKeyCommitmentResponse, GetCanonicalNonceResponse, GetDeltaHistoryResponse,
GetDeltaProposalResponse, GetDeltaProposalsResponse, GetDeltaResponse, GetDeltaSinceResponse,
GetStateResponse, GuardianClient, HistoryEntry, HistoryNote, HistoryNoteAsset, MAX_SESSION_TTL,
MIN_SESSION_TTL, MidenFalconRpoAuth, PushDeltaProposalResponse, PushDeltaResponse,
SignDeltaProposalResponse, Signer, StartSessionOptions,
};
use guardian_shared::ProposalSignature as JsonProposalSignature;
use guardian_shared::hex::FromHex;
use miden_protocol::account::AccountId;
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
use std::sync::Arc;
use tonic::Status;
fn create_test_account_id() -> AccountId {
AccountId::from_hex("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b").unwrap()
}
fn create_test_signer() -> Arc<dyn Signer> {
Arc::new(FalconKeyStore::new(SecretKey::new()))
}
fn guardian_auth_status(code: &str, retryable: bool) -> Status {
let details = serde_json::json!({
"code": code,
"message": "test",
"meta": { "retryable": retryable }
})
.to_string()
.into_bytes();
Status::with_details(tonic::Code::Unauthenticated, "test", details.into())
}
#[tokio::test]
async fn test_get_pubkey_success() {
let service = MockGuardianService::default().with_get_pubkey(Ok("test_pubkey_123".to_string()));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint).await.unwrap();
let result = client.get_pubkey(None).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().0, "test_pubkey_123");
}
#[tokio::test]
async fn test_get_pubkey_error() {
let service =
MockGuardianService::default().with_get_pubkey(Err(Status::internal("Server error")));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint).await.unwrap();
let result = client.get_pubkey(None).await;
assert!(result.is_err());
match result.unwrap_err() {
ClientError::Status(_) => {}
e => panic!("Expected Status error, got: {:?}", e),
}
}
#[tokio::test]
async fn rate_limit_rejection_round_trips_retry_classification() {
let details = serde_json::json!({
"code": "rate_limit_exceeded",
"message": "Too many requests — please try again shortly.",
"meta": { "retryable": true, "retry_after_secs": 1 }
})
.to_string()
.into_bytes();
let mut status = Status::with_details(
tonic::Code::ResourceExhausted,
"Too many requests — please try again shortly.",
details.into(),
);
status.metadata_mut().insert("retry-after", 1.into());
let service = MockGuardianService::default().with_get_pubkey(Err(status));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint).await.unwrap();
let err = client.get_pubkey(None).await.unwrap_err();
assert_eq!(err.guardian_code().as_deref(), Some("rate_limit_exceeded"));
assert!(err.is_retryable());
assert_eq!(err.retry_after(), Some(std::time::Duration::from_secs(1)));
}
#[tokio::test]
async fn test_configure_success() {
let service = MockGuardianService::default().with_configure(Ok(ConfigureResponse {
success: true,
message: "Account configured".to_string(),
ack_pubkey: "test_pubkey_123".to_string(),
ack_commitment: String::new(),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let initial_state = serde_json::json!({"balance": 1000});
let auth_config = AuthConfig {
auth_type: Some(AuthType::MidenFalconRpo(crate::proto::MidenFalconRpoAuth {
cosigner_commitments: vec!["0xabc".to_string()],
})),
};
let result = client
.configure(&account_id, auth_config, initial_state)
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.message, "Account configured");
assert_eq!(response.ack_pubkey, "test_pubkey_123");
}
#[tokio::test]
async fn test_configure_server_error() {
let service = MockGuardianService::default().with_configure(Ok(ConfigureResponse {
success: false,
message: "Account already exists".to_string(),
ack_pubkey: String::new(),
ack_commitment: String::new(),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let initial_state = serde_json::json!({"balance": 1000});
let auth_config = AuthConfig {
auth_type: Some(AuthType::MidenFalconRpo(crate::proto::MidenFalconRpoAuth {
cosigner_commitments: vec!["0xabc".to_string()],
})),
};
let result = client
.configure(&account_id, auth_config, initial_state)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ClientError::ServerError(msg) => {
assert_eq!(msg, "Account already exists");
}
e => panic!("Expected ServerError, got: {:?}", e),
}
}
#[tokio::test]
async fn test_push_delta_proposal_success() {
let mock_delta = create_mock_delta();
let service =
MockGuardianService::default().with_push_delta_proposal(Ok(PushDeltaProposalResponse {
success: true,
message: String::new(),
commitment: "proposal_commitment_123".to_string(),
delta: Some(mock_delta.clone()),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let delta_payload = serde_json::json!({"tx_summary": {}, "signatures": []});
let result = client
.push_delta_proposal(&account_id, 1, delta_payload)
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.commitment, "proposal_commitment_123");
assert!(response.delta.is_some());
}
#[tokio::test]
async fn test_get_delta_proposals_success() {
let mock_delta1 = create_mock_delta();
let mut mock_delta2 = create_mock_delta();
mock_delta2.nonce = 2;
let service =
MockGuardianService::default().with_get_delta_proposals(Ok(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![mock_delta1, mock_delta2],
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_delta_proposals(&account_id).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.proposals.len(), 2);
}
#[tokio::test]
async fn test_get_delta_proposals_empty() {
let service =
MockGuardianService::default().with_get_delta_proposals(Ok(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![],
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_delta_proposals(&account_id).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.proposals.len(), 0);
}
#[tokio::test]
async fn test_get_delta_proposal_success() {
let mock_delta = create_mock_delta();
let service =
MockGuardianService::default().with_get_delta_proposal(Ok(GetDeltaProposalResponse {
success: true,
message: String::new(),
proposal: Some(mock_delta),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client
.get_delta_proposal(&account_id, "proposal_commitment_123")
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert!(response.proposal.is_some());
}
#[tokio::test]
async fn test_sign_delta_proposal_success() {
let mock_delta = create_mock_delta();
let service =
MockGuardianService::default().with_sign_delta_proposal(Ok(SignDeltaProposalResponse {
success: true,
message: "Signature added".to_string(),
delta: Some(mock_delta),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let signature = JsonProposalSignature::Falcon {
signature: "0xabcd".to_string(),
};
let result = client
.sign_delta_proposal(&account_id, "commitment_123", signature)
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.message, "Signature added");
assert!(response.delta.is_some());
}
#[tokio::test]
async fn test_push_delta_success() {
let mock_delta = create_mock_delta();
let service = MockGuardianService::default().with_push_delta(Ok(PushDeltaResponse {
success: true,
message: "Delta pushed".to_string(),
delta: Some(mock_delta),
ack_sig: Some("0xsig".to_string()),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let delta_payload = serde_json::json!({"updates": []});
let result = client
.push_delta(&account_id, 1, "0x123", delta_payload)
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert_eq!(response.message, "Delta pushed");
assert!(response.delta.is_some());
}
#[tokio::test]
async fn test_get_delta_success() {
let mock_delta = create_mock_delta();
let service = MockGuardianService::default().with_get_delta(Ok(GetDeltaResponse {
success: true,
message: String::new(),
delta: Some(mock_delta),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_delta(&account_id, 1).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert!(response.delta.is_some());
assert_eq!(response.delta.unwrap().nonce, 1);
}
#[tokio::test]
async fn test_get_delta_not_found() {
let service = MockGuardianService::default().with_get_delta(Ok(GetDeltaResponse {
success: false,
message: "Delta not found".to_string(),
delta: None,
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_delta(&account_id, 999).await;
assert!(result.is_err());
match result.unwrap_err() {
ClientError::ServerError(msg) => {
assert_eq!(msg, "Delta not found");
}
e => panic!("Expected ServerError, got: {:?}", e),
}
}
#[tokio::test]
async fn test_get_delta_since_success() {
let mock_delta = create_mock_delta();
let service = MockGuardianService::default().with_get_delta_since(Ok(GetDeltaSinceResponse {
success: true,
message: String::new(),
merged_delta: Some(mock_delta),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_delta_since(&account_id, 1).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert!(response.merged_delta.is_some());
}
#[tokio::test]
async fn test_get_delta_history_success() {
let service =
MockGuardianService::default().with_get_delta_history(Ok(GetDeltaHistoryResponse {
success: true,
message: String::new(),
entries: vec![HistoryEntry {
nonce: 3,
status: "canonical".to_string(),
timestamp: "2026-08-01T12:00:03Z".to_string(),
new_commitment: Some("0xnew0003".to_string()),
input_notes: vec![],
output_notes: vec![HistoryNote {
note_id: "0xnote".to_string(),
tag: "p2id".to_string(),
note_type: "public".to_string(),
assets: vec![HistoryNoteAsset {
asset_id: "0xfaucet".to_string(),
kind: "fungible".to_string(),
amount: Some("100".to_string()),
}],
sender: None,
recipient: Some("0xrecipient".to_string()),
}],
decode_warnings: vec![],
}],
next_cursor: Some("cursor-token".to_string()),
}));
let requests = service.get_delta_history_requests_handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let response = client
.get_delta_history(&account_id, Some(10), Some("prev-cursor".to_string()))
.await
.expect("get_delta_history should succeed");
assert!(response.success);
assert_eq!(response.entries.len(), 1);
assert_eq!(response.entries[0].nonce, 3);
assert_eq!(response.entries[0].output_notes[0].tag, "p2id");
assert_eq!(response.next_cursor.as_deref(), Some("cursor-token"));
let recorded = requests.lock().unwrap();
assert_eq!(recorded.len(), 1);
let (request, timestamp, signature) = &recorded[0];
assert_eq!(request.account_id, account_id.to_string());
assert_eq!(request.limit, Some(10));
assert_eq!(request.cursor.as_deref(), Some("prev-cursor"));
assert!(*timestamp > 0, "auth timestamp metadata must be attached");
assert!(
signature.starts_with("0x") && signature.len() > 2,
"auth signature metadata must be attached"
);
}
#[tokio::test]
async fn test_get_state_success() {
let mock_state = create_mock_account_state();
let service = MockGuardianService::default().with_get_state(Ok(GetStateResponse {
success: true,
message: String::new(),
state: Some(mock_state),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_state(&account_id).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.success);
assert!(response.state.is_some());
assert!(response.state.unwrap().state_json.contains("balance"));
}
#[tokio::test]
async fn replay_rejections_are_retried_with_fresh_timestamp_and_signature_each_attempt() {
let service = MockGuardianService::default()
.with_get_state(Err(guardian_auth_status("authentication_replay", true)))
.with_get_state(Err(guardian_auth_status("authentication_replay", true)));
let recorded_headers = service.get_state_auth_headers_handle();
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
let response = client
.get_state(&create_test_account_id())
.await
.expect("replay rejections within the retry budget must be retried to success");
assert!(response.success);
let attempts = recorded_headers.lock().unwrap().clone();
assert_eq!(attempts.len(), 3, "one initial attempt plus two retries");
assert!(
attempts[0].0 < attempts[1].0 && attempts[1].0 < attempts[2].0,
"every attempt must mint a strictly increasing timestamp: {attempts:?}"
);
let signatures: std::collections::HashSet<&String> =
attempts.iter().map(|(_, signature)| signature).collect();
assert_eq!(
signatures.len(),
3,
"every attempt must carry a fresh signature over the new timestamp"
);
}
#[tokio::test]
async fn replay_retries_stop_at_the_bounded_budget() {
let service = MockGuardianService::default()
.with_get_state(Err(guardian_auth_status("authentication_replay", true)))
.with_get_state(Err(guardian_auth_status("authentication_replay", true)))
.with_get_state(Err(guardian_auth_status("authentication_replay", true)));
let recorded_headers = service.get_state_auth_headers_handle();
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
let error = client
.get_state(&create_test_account_id())
.await
.expect_err("a replay condition outlasting the retry budget must surface");
assert!(error.is_replay_rejection());
assert_eq!(
recorded_headers.lock().unwrap().len(),
3,
"one initial attempt plus exactly two retries"
);
}
#[tokio::test]
async fn terminal_authentication_failure_is_not_retried() {
let service = MockGuardianService::default()
.with_get_state(Err(guardian_auth_status("authentication_failed", false)));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
let error = client
.get_state(&create_test_account_id())
.await
.expect_err("a terminal authentication failure must not be retried");
assert_eq!(
error.guardian_code().as_deref(),
Some("authentication_failed")
);
assert!(!error.is_replay_rejection());
}
#[tokio::test]
async fn test_get_canonical_nonce_success() {
let service =
MockGuardianService::default().with_get_canonical_nonce(Ok(GetCanonicalNonceResponse {
success: true,
message: String::new(),
account_id: "0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b".to_string(),
nonce: 9,
commitment: "0x123".to_string(),
error_code: String::new(),
}));
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let response = client
.get_canonical_nonce(&account_id)
.await
.expect("canonical nonce should resolve");
assert_eq!(response.nonce, 9);
assert_eq!(response.commitment, "0x123");
assert_eq!(handle.calls(), vec!["get_canonical_nonce".to_string()]);
}
#[tokio::test]
async fn test_get_canonical_nonce_server_error() {
let service =
MockGuardianService::default().with_get_canonical_nonce(Ok(GetCanonicalNonceResponse {
success: false,
message: "State not found".to_string(),
account_id: String::new(),
nonce: 0,
commitment: String::new(),
error_code: "state_not_found".to_string(),
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let err = client
.get_canonical_nonce(&create_test_account_id())
.await
.expect_err("unsuccessful response should be an error");
assert!(err.to_string().contains("State not found"), "{err}");
}
#[tokio::test]
async fn test_get_canonical_nonce_defaults_to_the_persistent_state() {
let service = MockGuardianService::default();
let handle = service.handle();
handle.set_persistent_get_state(GetStateResponse {
success: true,
message: String::new(),
state: Some(AccountState {
commitment: "0xfeed".to_string(),
..create_mock_account_state()
}),
});
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let response = client
.get_canonical_nonce(&create_test_account_id())
.await
.expect("derived head should resolve");
assert_eq!(response.commitment, "0xfeed");
assert_eq!(response.nonce, 0, "non-account blobs derive nonce 0");
}
#[tokio::test]
async fn test_get_state_not_found() {
let service = MockGuardianService::default().with_get_state(Ok(GetStateResponse {
success: false,
message: "State not found".to_string(),
state: None,
}));
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let account_id = create_test_account_id();
let result = client.get_state(&account_id).await;
assert!(result.is_err());
match result.unwrap_err() {
ClientError::ServerError(msg) => {
assert_eq!(msg, "State not found");
}
e => panic!("Expected ServerError, got: {:?}", e),
}
}
#[tokio::test]
async fn test_signer_pubkey_hex_without_signer() {
let service = MockGuardianService::default();
let endpoint = start_mock_server(service).await.unwrap();
let client = GuardianClient::connect(endpoint).await.unwrap();
let result = client.signer_pubkey_hex();
assert!(result.is_err());
match result.unwrap_err() {
ClientError::InvalidResponse(msg) => {
assert!(msg.contains("no signer configured"));
}
e => panic!("Expected InvalidResponse, got: {:?}", e),
}
}
#[tokio::test]
async fn test_signer_pubkey_hex_with_signer() {
let service = MockGuardianService::default();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let expected_pubkey = signer.public_key_hex();
let client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let result = client.signer_pubkey_hex();
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected_pubkey);
}
fn lookup_test_signer() -> (Arc<dyn Signer>, String) {
let signer = Arc::new(FalconKeyStore::new(SecretKey::new())) as Arc<dyn Signer>;
let commitment_hex = signer.commitment_hex();
(signer, commitment_hex)
}
#[tokio::test]
async fn test_lookup_account_by_key_commitment_single_match() {
let service = MockGuardianService::default().with_get_account_by_key_commitment(Ok(
GetAccountByKeyCommitmentResponse {
accounts: vec![AccountRef {
account_id: "0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b".to_string(),
}],
},
));
let endpoint = start_mock_server(service).await.unwrap();
let (signer, commitment_hex) = lookup_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let response = client
.lookup_account_by_key_commitment(&commitment_hex)
.await
.expect("happy path returns Ok");
assert_eq!(response.accounts.len(), 1);
assert_eq!(
response.accounts[0].account_id,
"0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b"
);
}
#[tokio::test]
async fn test_lookup_account_by_key_commitment_empty_result() {
let service = MockGuardianService::default().with_get_account_by_key_commitment(Ok(
GetAccountByKeyCommitmentResponse { accounts: vec![] },
));
let endpoint = start_mock_server(service).await.unwrap();
let (signer, commitment_hex) = lookup_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let response = client
.lookup_account_by_key_commitment(&commitment_hex)
.await
.expect("empty list is a successful response");
assert!(response.accounts.is_empty());
}
#[tokio::test]
async fn test_lookup_account_by_key_commitment_multi_match() {
let service = MockGuardianService::default().with_get_account_by_key_commitment(Ok(
GetAccountByKeyCommitmentResponse {
accounts: vec![
AccountRef {
account_id: "0xaaaaaaaaaaaaaa012aaaaaaaaaaaaa".to_string(),
},
AccountRef {
account_id: "0xbbbbbbbabbbbbb013bbbbbbbbbbbbb".to_string(),
},
],
},
));
let endpoint = start_mock_server(service).await.unwrap();
let (signer, commitment_hex) = lookup_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let response = client
.lookup_account_by_key_commitment(&commitment_hex)
.await
.expect("multi-match returns all entries");
assert_eq!(response.accounts.len(), 2);
let ids: Vec<&str> = response
.accounts
.iter()
.map(|a| a.account_id.as_str())
.collect();
assert!(ids.contains(&"0xaaaaaaaaaaaaaa012aaaaaaaaaaaaa"));
assert!(ids.contains(&"0xbbbbbbbabbbbbb013bbbbbbbbbbbbb"));
}
#[tokio::test]
async fn test_lookup_account_by_key_commitment_unauthenticated_error_propagates() {
let service = MockGuardianService::default().with_get_account_by_key_commitment(Err(
Status::unauthenticated(
"submitted public key does not derive to the queried key_commitment",
),
));
let endpoint = start_mock_server(service).await.unwrap();
let (signer, commitment_hex) = lookup_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let result = client
.lookup_account_by_key_commitment(&commitment_hex)
.await;
let err = result.expect_err("unauthenticated must propagate as ClientError");
match err {
ClientError::Status(status) => {
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
other => panic!("expected Status error, got {other:?}"),
}
}
#[tokio::test]
async fn test_lookup_account_by_key_commitment_rejects_invalid_commitment_hex() {
let service = MockGuardianService::default();
let endpoint = start_mock_server(service).await.unwrap();
let (signer, _commitment_hex) = lookup_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer);
let result = client.lookup_account_by_key_commitment("not-hex").await;
let err = result.expect_err("malformed hex must fail locally");
match err {
ClientError::InvalidResponse(msg) => {
assert!(msg.contains("Invalid key_commitment hex"), "{msg}");
}
other => panic!("expected InvalidResponse, got {other:?}"),
}
}
const TEST_GUARDIAN_COMMITMENT: &str =
"0x0500000000000000060000000000000007000000000000000800000000000000";
fn session_options() -> StartSessionOptions {
StartSessionOptions {
network: "devnet".to_string(),
ttl: std::time::Duration::from_secs(3600),
}
}
#[tokio::test]
async fn test_start_session_signs_grant_with_wallet_and_signs_requests_with_session_key() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let grants = service.create_session_requests_handle();
let request_auth = service.get_state_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer.clone());
let info = client.start_session(session_options()).await.unwrap();
let request = grants.lock().unwrap()[0].clone();
let fields = request.grant.clone().expect("grant fields");
assert_eq!(request.scheme, "falcon");
assert_eq!(
request.public_key, None,
"Falcon embeds the key in the signature"
);
assert_eq!(fields.network, "devnet");
assert_eq!(fields.guardian_commitment, TEST_GUARDIAN_COMMITMENT);
assert_eq!(fields.signer_commitment, signer.commitment_hex());
assert_eq!(fields.session_public_key, info.session_public_key);
assert_eq!(fields.expires_at - fields.issued_at, 3600);
let session_key = guardian_shared::session_grant::parse_session_public_key(
&hex::decode(fields.session_public_key.trim_start_matches("0x")).unwrap(),
)
.unwrap();
assert_eq!(fields.origin, "", "a native client names no origin");
let grant = guardian_shared::session_grant::SessionGrant::new(
signer.commitment(),
&session_key,
"",
fields.issued_at,
fields.expires_at,
miden_protocol::Word::from_hex(TEST_GUARDIAN_COMMITMENT).unwrap(),
"devnet",
)
.unwrap();
let signature =
miden_protocol::crypto::dsa::falcon512_poseidon2::Signature::from_hex(&request.signature)
.unwrap();
assert!(signature.public_key().verify(grant.to_word(), &signature));
client.get_state(&create_test_account_id()).await.unwrap();
assert_eq!(
request_auth.lock().unwrap().last().unwrap(),
&("session".to_string(), info.session_public_key.clone())
);
}
#[tokio::test]
async fn test_configure_keeps_using_the_wallet_while_a_session_is_active() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let configure_auth = service.configure_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
client.start_session(session_options()).await.unwrap();
let auth = AuthConfig {
auth_type: Some(AuthType::MidenFalconRpo(MidenFalconRpoAuth {
cosigner_commitments: vec![],
})),
};
client
.configure(&create_test_account_id(), auth, serde_json::json!({}))
.await
.unwrap();
assert_eq!(configure_auth.lock().unwrap().as_slice(), [String::new()]);
}
#[tokio::test]
async fn test_push_delta_and_abandon_keep_using_the_wallet_while_a_session_is_active() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let formats = service.wallet_route_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
client.start_session(session_options()).await.unwrap();
let account_id = create_test_account_id();
client
.push_delta(&account_id, 1, "0x00", serde_json::json!({}))
.await
.unwrap();
client.abandon_candidate(&account_id, 1).await.unwrap();
assert_eq!(
formats.lock().unwrap().as_slice(),
[
("push_delta".to_string(), String::new()),
("abandon_delta_candidate".to_string(), String::new()),
]
);
}
#[tokio::test]
async fn test_revoke_all_sessions_is_wallet_signed_and_drops_the_active_session() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let calls = service.revoke_all_sessions_calls_handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer.clone());
client.start_session(session_options()).await.unwrap();
assert_eq!(client.revoke_all_sessions().await.unwrap(), 2);
assert!(client.session().is_none());
let (request, pubkey, signature, timestamp) = calls.lock().unwrap()[0].clone();
assert_eq!(request.signer_commitment, signer.commitment_hex());
assert_eq!(pubkey, signer.public_key_hex());
let digest = guardian_shared::session_grant::SessionRevokeAllMessage::new(
signer.commitment(),
timestamp,
)
.to_word();
let signature =
miden_protocol::crypto::dsa::falcon512_poseidon2::Signature::from_hex(&signature).unwrap();
assert!(signature.public_key().verify(digest, &signature));
}
#[tokio::test]
async fn test_an_ended_session_is_dropped_and_the_wallet_takes_over() {
for code in ["session_revoked", "session_expired"] {
let service = MockGuardianService::default()
.with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()))
.with_get_state(Err(guardian_auth_status(code, false)));
let request_auth = service.get_state_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer.clone());
client.start_session(session_options()).await.unwrap();
let error = client
.get_state(&create_test_account_id())
.await
.expect_err("the server ended the session");
assert!(error.is_ended_session(), "{code}");
assert!(client.session().is_none(), "{code}: session dropped");
client.get_state(&create_test_account_id()).await.unwrap();
assert_eq!(
request_auth.lock().unwrap().last().unwrap(),
&(String::new(), signer.public_key_hex()),
"{code}: the wallet signs the next request"
);
}
}
#[tokio::test]
async fn test_a_session_not_accepted_by_guardian_is_dropped() {
let service = MockGuardianService::default()
.with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()))
.with_get_state(Err(guardian_auth_status("authentication_failed", false)));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
client.start_session(session_options()).await.unwrap();
client
.get_state(&create_test_account_id())
.await
.expect_err("Guardian no longer knows the session key");
assert!(client.session().is_none());
}
#[tokio::test]
async fn test_a_session_survives_an_account_its_wallet_does_not_cosign() {
let service = MockGuardianService::default()
.with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()))
.with_get_state(Err(guardian_auth_status("authorization_failed", false)));
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
client.start_session(session_options()).await.unwrap();
client
.get_state(&create_test_account_id())
.await
.expect_err("the wallet does not cosign this account");
assert!(
client.session().is_some(),
"the session still works for the wallet's other accounts"
);
}
#[tokio::test]
async fn test_a_session_only_signs_for_the_wallet_that_granted_it() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let request_auth = service.get_state_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(create_test_signer());
client.start_session(session_options()).await.unwrap();
let other = create_test_signer();
let mut client = client.with_signer(other.clone());
assert!(
client.session().is_none(),
"the session belongs to the first wallet"
);
client.get_state(&create_test_account_id()).await.unwrap();
assert_eq!(
request_auth.lock().unwrap().last().unwrap(),
&(String::new(), other.public_key_hex())
);
}
#[tokio::test]
async fn test_start_session_rejects_a_lifetime_guardian_never_accepts() {
let mut client = GuardianClient::connect(
start_mock_server(MockGuardianService::default())
.await
.unwrap(),
)
.await
.unwrap()
.with_signer(create_test_signer());
for ttl in [
MIN_SESSION_TTL,
MIN_SESSION_TTL + std::time::Duration::from_millis(500),
MAX_SESSION_TTL + std::time::Duration::from_secs(1),
] {
let options = StartSessionOptions {
ttl,
..session_options()
};
assert!(client.start_session(options).await.is_err(), "{ttl:?}");
}
}
#[tokio::test]
async fn test_revoke_session_signs_logout_with_session_key_then_falls_back_to_wallet() {
let service =
MockGuardianService::default().with_get_pubkey(Ok(TEST_GUARDIAN_COMMITMENT.to_string()));
let logouts = service.revoke_session_auth_handle();
let request_auth = service.get_state_auth_formats_handle();
let endpoint = start_mock_server(service).await.unwrap();
let signer = create_test_signer();
let mut client = GuardianClient::connect(endpoint)
.await
.unwrap()
.with_signer(signer.clone());
let info = client.start_session(session_options()).await.unwrap();
assert!(client.revoke_session().await.unwrap());
assert!(client.session().is_none());
let (pubkey, signature, timestamp) = logouts.lock().unwrap()[0].clone();
assert_eq!(pubkey, info.session_public_key);
let key = hex::decode(pubkey.trim_start_matches("0x")).unwrap();
let key = guardian_shared::session_grant::parse_session_public_key(&key).unwrap();
let digest =
guardian_shared::session_grant::SessionLogoutMessage::new(&key, timestamp).to_word();
let signature = hex::decode(signature.trim_start_matches("0x")).unwrap();
guardian_shared::session_key::verify(&key, digest, &signature).unwrap();
client.get_state(&create_test_account_id()).await.unwrap();
assert_eq!(
request_auth.lock().unwrap().last().unwrap(),
&(String::new(), signer.public_key_hex())
);
assert!(
!client.revoke_session().await.unwrap(),
"no session left to revoke"
);
}