use super::*;
use crate::acl::Role;
use crate::test_support::build_signing_test_app_state;
use trust_tasks_rs::TypeUri;
use vta_sdk::trust_task_sign::HolderKey;
use vta_sdk::trust_tasks as uris;
use vti_voting::{
election::{Election, ElectionId},
rand::{SeedableRng, rngs::StdRng},
registrar::Registrar,
rules::{digest_multibase, manifest_digest},
service::{BallotBox, Caller as BoxCaller},
};
const COMMUNITY: &str = "did:webvh:QmCommunity:vtc.example.org";
const B58: &str = "z3ZeMb8yW1fRq2Rr5b6tHn7Kx9Lp2Vd";
fn claims(did: &str, role: Role, ctx: &str) -> AuthClaims {
AuthClaims {
did: did.to_string(),
role,
allowed_contexts: vec![ctx.to_string()],
session_id: "test-session".into(),
access_expires_at: 0,
issued_at: 0,
amr: Vec::new(),
acr: String::new(),
}
}
fn doc(uri: &str, payload: Value) -> TrustTask<Value> {
let uri: TypeUri = uri.parse().expect("voter uri");
TrustTask::new(format!("urn:uuid:{}", uuid::Uuid::new_v4()), uri, payload)
}
fn body(out: &TrustTaskOutcome) -> Value {
serde_json::from_slice(&out.body).expect("response is JSON")
}
fn ok(out: &TrustTaskOutcome) -> Value {
let b = body(out);
assert!(out.status.is_success(), "expected success, got {b}");
b["payload"].clone()
}
fn code(out: &TrustTaskOutcome) -> String {
body(out)
.pointer("/payload/code")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
fn box_key(seed: u8) -> (String, HolderKey) {
let public = ed25519_dalek::SigningKey::from_bytes(&[seed; 32])
.verifying_key()
.to_bytes();
let did = format!(
"did:key:{}",
vta_sdk::did_key::ed25519_multibase_pubkey(&public)
);
let key = HolderKey::from_did_key(
&did,
multibase::encode(multibase::Base::Base58Btc, [seed; 32]),
)
.unwrap();
(did, key)
}
async fn signed(uri: &str, issuer: &str, key: &HolderKey, payload: Value) -> Value {
let mut d: TrustTask<Value> = serde_json::from_value(json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": uri,
"issuer": issuer,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
}))
.unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut d, key)
.await
.unwrap();
serde_json::to_value(&d).unwrap()
}
fn manifest(id: &str, ballot_box: &str, open: bool, secrecy: &str) -> Value {
let now = chrono::Utc::now();
let (opens, closes) = if open {
(
now - chrono::Duration::hours(1),
now + chrono::Duration::days(1),
)
} else {
(
now - chrono::Duration::days(3),
now - chrono::Duration::days(1),
)
};
let t = |d: chrono::DateTime<chrono::Utc>| d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
json!({
"electionId": id, "community": COMMUNITY,
"questions": [{ "key": "budget", "type": "choice",
"options": [{ "key": "yes" }, { "key": "no" }],
"minSelections": 1, "maxSelections": 1, "optional": false }],
"secrecy": secrecy, "roll": { "rule": "members" },
"registrationOpens": t(opens - chrono::Duration::days(1)), "votingOpens": t(opens),
"votingCloses": t(closes), "revote": "replace",
"board": { "visibility": "public", "auditors": [] }, "ballotBox": ballot_box,
"binding": false, "allowEarlyClose": false, "resultsBeforeClose": false,
"textReleaseThreshold": 10,
})
}
fn ballot(manifest: &Value, choice: &str) -> Value {
json!({
"electionId": manifest["electionId"],
"manifestDigest": digest_multibase(&manifest_digest(manifest).unwrap()),
"answers": [{ "question": "budget", "choice": [choice] }],
})
}
fn register_doc(params: &Value, m: &Value) -> TrustTask<Value> {
doc(
uris::TASK_VTA_PCS_VOTER_REGISTER_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "parameters": params, "manifest": m }),
)
}
fn accept_doc(election: &str, params: &Value, pre: &str) -> TrustTask<Value> {
doc(
uris::TASK_VTA_PCS_VOTER_ACCEPT_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "electionId": election,
"parameters": params,
"response": { "electionId": election, "label": format!("voter/{election}"), "preCredential": pre } }),
)
}
fn vote_doc(params: &Value, m: &Value, b: Value) -> TrustTask<Value> {
doc(
uris::TASK_VTA_PCS_VOTE_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "parameters": params,
"manifest": m, "ballot": b }),
)
}
#[tokio::test]
async fn a_member_votes_through_the_vta_and_checks_the_receipt() {
let (state, _dir) = build_signing_test_app_state().await;
crate::contexts::create_context(&state.contexts_ks, "member", "member")
.await
.unwrap();
let auth = claims("did:key:z6MkVoter", Role::Admin, "member");
let mut rng = StdRng::seed_from_u64(0xB0);
let reg = Registrar::generate(COMMUNITY, &mut rng).unwrap();
let params = json!({ "suite": "ps-ddh-bls12381", "votingKey": reg.public_text().unwrap() });
let (box_did, box_signer) = box_key(5);
let m = manifest("agm-2027", &box_did, true, "open");
let digest = manifest_digest(&m).unwrap();
let first = ok(&handle_voter_register(&state, &auth, register_doc(¶ms, &m)).await);
assert_eq!(
ok(&handle_voter_register(&state, &auth, register_doc(¶ms, &m)).await),
first
);
let election = Election::new(COMMUNITY, ElectionId::new("agm-2027").unwrap(), digest).unwrap();
let id =
vti_vetting_pcs::scheme::point_from_text(first["request"]["id"].as_str().unwrap()).unwrap();
let request = serde_json::from_value(first["request"]["request"].clone()).unwrap();
let pre = reg
.issue_ticket(&election, &id, &request, &mut rng)
.unwrap();
let pre = vti_vetting_pcs::scheme::enc(&pre).unwrap();
assert_eq!(
ok(&handle_voter_accept(&state, &auth, accept_doc("agm-2027", ¶ms, &pre)).await)["ticketHeld"],
true
);
assert_eq!(
code(&handle_voter_accept(&state, &auth, accept_doc("agm-2027", ¶ms, &pre)).await),
"vta/pcs/voter/accept:noRequestInFlight"
);
assert_eq!(
code(&handle_voter_register(&state, &auth, register_doc(¶ms, &m)).await),
"vta/pcs/voter/register:ticketHeld"
);
let voted = ok(&handle_vote(&state, &auth, vote_doc(¶ms, &m, ballot(&m, "yes"))).await);
let cast_doc = voted["castDocument"].clone();
assert_eq!(cast_doc["recipient"], box_did);
assert_ne!(cast_doc["issuer"], json!(auth.did), "VTI-VOT-003");
let signer = vti_common::auth::verify_trust_task_proof_value(
&cast_doc,
&vti_common::auth::TrustTaskVmResolver::did_key_only(),
)
.await
.unwrap();
assert_eq!(json!(signer), cast_doc["issuer"]);
assert_eq!(voted["tag"], cast_doc["payload"]["cast"]["tag"]);
let mut bb = BallotBox::new(&box_did);
let now = chrono::Utc::now();
bb.register(
COMMUNITY,
&json!({ "community": COMMUNITY, "manifest": m,
"manifestDigest": digest_multibase(&digest), "parameters": params }),
now,
)
.unwrap();
let receipt_payload = bb.cast(&cast_doc["payload"], now).unwrap();
assert_eq!(receipt_payload["outcome"], "counted");
let receipt = signed(
"https://trusttasks.org/spec/voting/ballot/cast/0.1#response",
&box_did,
&box_signer,
receipt_payload.clone(),
)
.await;
let sel = json!({ "community": COMMUNITY, "electionId": "agm-2027" });
let head = bb.board_head(BoxCaller::Anonymous, &sel, now).unwrap();
let checkpoint = signed(
"https://trusttasks.org/spec/voting/board/head/0.1#response",
&box_did,
&box_signer,
head.clone(),
)
.await;
let mut q = sel.clone();
q["leafIndex"] = receipt_payload["leafIndex"].clone();
q["treeSize"] = head["head"]["treeSize"].clone();
let inclusion = bb.board_inclusion(BoxCaller::Anonymous, &q, now).unwrap();
let receipt_doc = |r: &Value, c: Option<&Value>| {
let mut p = json!({ "contextId": "member", "community": COMMUNITY,
"electionId": "agm-2027", "receipt": r });
if let Some(c) = c {
p["checkpoint"] = c.clone();
p["inclusion"] = inclusion.clone();
}
doc(uris::TASK_VTA_PCS_VOTER_RECEIPT_0_1, p)
};
assert_eq!(
ok(&handle_voter_receipt(&state, &auth, receipt_doc(&receipt, None)).await)["status"],
"kept"
);
assert_eq!(
ok(&handle_voter_receipt(&state, &auth, receipt_doc(&receipt, Some(&checkpoint))).await)["status"],
"included"
);
let (other, other_signer) = box_key(6);
let forged = signed(
"https://trusttasks.org/spec/voting/ballot/cast/0.1#response",
&other,
&other_signer,
receipt_payload,
)
.await;
assert_eq!(
code(&handle_voter_receipt(&state, &auth, receipt_doc(&forged, None)).await),
"vta/pcs/voter/receipt:receiptInvalid"
);
let mut unsigned = checkpoint.clone();
unsigned.as_object_mut().unwrap().remove("proof");
assert_eq!(
code(&handle_voter_receipt(&state, &auth, receipt_doc(&receipt, Some(&unsigned))).await),
"vta/pcs/voter/receipt:evidenceInvalid"
);
assert_eq!(
code(&handle_vote(&state, &auth, vote_doc(¶ms, &m, ballot(&m, "maybe"))).await),
"vta/pcs/vote:ballotInvalid"
);
let mut changed = m.clone();
changed["title"] = json!("changed");
let out = handle_vote(
&state,
&auth,
vote_doc(¶ms, &changed, ballot(&m, "yes")),
)
.await;
assert_eq!(code(&out), "vta/pcs/vote:manifestMismatch");
}
#[tokio::test]
async fn the_voter_tasks_refuse_with_the_codes_their_specifications_declare() {
let (state, _dir) = build_signing_test_app_state().await;
crate::contexts::create_context(&state.contexts_ks, "member", "member")
.await
.unwrap();
let auth = claims("did:key:z6MkVoter", Role::Admin, "member");
let mut rng = StdRng::seed_from_u64(0xB1);
let reg = Registrar::generate(COMMUNITY, &mut rng).unwrap();
let params = json!({ "suite": "ps-ddh-bls12381", "votingKey": reg.public_text().unwrap() });
let bad_key = json!({ "suite": "ps-ddh-bls12381", "votingKey": B58 });
let (box_did, _) = box_key(5);
let mut sealed = manifest("sealed-1", &box_did, true, "sealed");
sealed["trustees"] = json!({ "dids": ["did:key:z6MkT1", "did:key:z6MkT2"], "threshold": 2 });
ok(&handle_voter_register(&state, &auth, register_doc(¶ms, &sealed)).await);
let m = manifest("agm-2027", &box_did, true, "open");
let other_suite = json!({ "suite": "ps-ddh-bls12381-v9", "votingKey": params["votingKey"] });
assert_eq!(
code(&handle_voter_register(&state, &auth, register_doc(&other_suite, &m)).await),
"vta/pcs/voter/register:unsupportedSuite"
);
assert_eq!(
code(&handle_voter_register(&state, &auth, register_doc(&bad_key, &m)).await),
"vta/pcs/voter/register:parametersInvalid"
);
ok(&handle_voter_register(&state, &auth, register_doc(¶ms, &m)).await);
let mut changed = m.clone();
changed["title"] = json!("another manifest");
assert_eq!(
code(&handle_voter_register(&state, &auth, register_doc(¶ms, &changed)).await),
"vta/pcs/voter/register:electionChanged"
);
assert_eq!(
code(&handle_voter_accept(&state, &auth, accept_doc("none", ¶ms, B58)).await),
"vta/pcs/voter/accept:notFound"
);
assert_eq!(
code(&handle_voter_accept(&state, &auth, accept_doc("agm-2027", ¶ms, B58)).await),
"vta/pcs/voter/accept:credentialInvalid"
);
assert_eq!(
code(&handle_voter_accept(&state, &auth, accept_doc("agm-2027", &bad_key, B58)).await),
"vta/pcs/voter/accept:parametersInvalid"
);
let unknown = manifest("never-registered", &box_did, true, "open");
let out = handle_vote(
&state,
&auth,
vote_doc(¶ms, &unknown, ballot(&unknown, "yes")),
)
.await;
assert_eq!(code(&out), "vta/pcs/vote:notFound");
assert_eq!(
code(&handle_vote(&state, &auth, vote_doc(&bad_key, &m, ballot(&m, "yes"))).await),
"vta/pcs/vote:parametersInvalid"
);
let closed = manifest("closed-1", &box_did, false, "open");
ok(&handle_voter_register(&state, &auth, register_doc(¶ms, &closed)).await);
let out = handle_vote(
&state,
&auth,
vote_doc(¶ms, &closed, ballot(&closed, "yes")),
)
.await;
assert_eq!(code(&out), "vta/pcs/vote:notOpen");
let receipt = doc(
uris::TASK_VTA_PCS_VOTER_RECEIPT_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "electionId": "none",
"receipt": { "type": "https://trusttasks.org/spec/voting/ballot/cast/0.1#response" } }),
);
assert_eq!(
code(&handle_voter_receipt(&state, &auth, receipt).await),
"vta/pcs/voter/receipt:notFound"
);
}
#[tokio::test]
async fn the_voter_tasks_need_the_voter_capability() {
let (state, _dir) = build_signing_test_app_state().await;
let auth = claims("did:key:z6MkAgent", Role::Application, "member");
let out = handle_voter_register(
&state,
&auth,
doc(
uris::TASK_VTA_PCS_VOTER_REGISTER_0_1,
json!({ "contextId": "member", "community": COMMUNITY }),
),
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn the_ticket_request_is_signed_as_the_member_and_kept() {
let (state, _dir) = build_signing_test_app_state().await;
crate::contexts::create_context(&state.contexts_ks, "member", "member")
.await
.unwrap();
let auth = claims("did:key:z6MkVoter", Role::Admin, "member");
let persona = crate::operations::persona::test_fixture::did_key_persona(
&state.keys_ks,
&state.internal_ks,
&state.contexts_ks,
&state.acl_ks,
&state.seed_store,
&state.audit_sink,
"member",
"membership-key",
)
.await;
let mut rng = StdRng::seed_from_u64(0xB7);
let reg = Registrar::generate(COMMUNITY, &mut rng).unwrap();
let params = json!({ "suite": "ps-ddh-bls12381", "votingKey": reg.public_text().unwrap() });
let (box_did, _) = box_key(9);
let m = manifest("agm-2028", &box_did, true, "open");
let with_member = |did: &str| {
doc(
uris::TASK_VTA_PCS_VOTER_REGISTER_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "parameters": params,
"manifest": m, "member": did }),
)
};
let out = handle_voter_register(&state, &auth, with_member("did:key:z6MkNotHeldHere")).await;
assert_eq!(code(&out), "vta/pcs/voter/register:memberNotHeld");
let first = ok(&handle_voter_register(&state, &auth, with_member(&persona.did)).await);
let ticket = &first["ticketDocument"];
assert_eq!(
ticket["type"],
"https://trusttasks.org/spec/vtc/elections/tickets/issue/0.1"
);
assert_eq!(ticket["issuer"], persona.did.as_str());
assert_eq!(ticket["recipient"], COMMUNITY);
assert_eq!(ticket["payload"]["request"], first["request"]);
assert_eq!(ticket["payload"]["manifestDigest"], first["manifestDigest"]);
let signer = vta_sdk::trust_task_proof::verify_trust_task_proof_value(
ticket,
&vta_sdk::trust_task_proof::TrustTaskVmResolver::did_key_only(),
)
.await
.expect("the member's proof verifies");
assert_eq!(signer, persona.did);
assert_eq!(ticket["proof"]["proofPurpose"], "authentication");
let again = ok(&handle_voter_register(&state, &auth, with_member(&persona.did)).await);
assert_eq!(again["ticketDocument"], first["ticketDocument"]);
}
async fn unwrapped_rows(state: &AppState) -> Vec<String> {
state
.pcs_engine_ks
.prefix_iter_raw("")
.await
.unwrap()
.into_iter()
.filter(|(k, _)| !k.starts_with(b"secret:"))
.map(|(k, v)| {
format!(
"{} {}",
String::from_utf8_lossy(&k),
String::from_utf8_lossy(&v)
)
})
.collect()
}
#[tokio::test]
async fn the_vta_keeps_no_clear_pairing_and_discards_after_the_election() {
let (state, _dir) = build_signing_test_app_state().await;
crate::contexts::create_context(&state.contexts_ks, "member", "member")
.await
.unwrap();
let auth = claims("did:key:z6MkVoter", Role::Admin, "member");
let persona = crate::operations::persona::test_fixture::did_key_persona(
&state.keys_ks,
&state.internal_ks,
&state.contexts_ks,
&state.acl_ks,
&state.seed_store,
&state.audit_sink,
"member",
"membership-key",
)
.await;
let mut rng = StdRng::seed_from_u64(0xB8);
let reg = Registrar::generate(COMMUNITY, &mut rng).unwrap();
let params = json!({ "suite": "ps-ddh-bls12381", "votingKey": reg.public_text().unwrap() });
let (box_did, box_signer) = box_key(10);
let m = manifest("agm-2029", &box_did, true, "open");
let digest = manifest_digest(&m).unwrap();
let registered = ok(&handle_voter_register(
&state,
&auth,
doc(
uris::TASK_VTA_PCS_VOTER_REGISTER_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "parameters": params,
"manifest": m, "member": persona.did }),
),
)
.await);
assert!(
unwrapped_rows(&state)
.await
.iter()
.any(|r| r.contains(&persona.did)),
"kept for a resend until the ticket is held"
);
let election = Election::new(COMMUNITY, ElectionId::new("agm-2029").unwrap(), digest).unwrap();
let id =
vti_vetting_pcs::scheme::point_from_text(registered["request"]["id"].as_str().unwrap())
.unwrap();
let request = serde_json::from_value(registered["request"]["request"].clone()).unwrap();
let pre = vti_vetting_pcs::scheme::enc(
®.issue_ticket(&election, &id, &request, &mut rng)
.unwrap(),
)
.unwrap();
ok(&handle_voter_accept(&state, &auth, accept_doc("agm-2029", ¶ms, &pre)).await);
let voted = ok(&handle_vote(&state, &auth, vote_doc(¶ms, &m, ballot(&m, "no"))).await);
let mut bb = BallotBox::new(&box_did);
let now = chrono::Utc::now();
bb.register(
COMMUNITY,
&json!({ "community": COMMUNITY, "manifest": m,
"manifestDigest": digest_multibase(&digest), "parameters": params }),
now,
)
.unwrap();
let receipt_payload = bb.cast(&voted["castDocument"]["payload"], now).unwrap();
let receipt = signed(
"https://trusttasks.org/spec/voting/ballot/cast/0.1#response",
&box_did,
&box_signer,
receipt_payload.clone(),
)
.await;
let keep = || {
doc(
uris::TASK_VTA_PCS_VOTER_RECEIPT_0_1,
json!({ "contextId": "member", "community": COMMUNITY,
"electionId": "agm-2029", "receipt": receipt }),
)
};
assert_eq!(
ok(&handle_voter_receipt(&state, &auth, keep()).await)["status"],
"kept"
);
let entry_hash = receipt_payload["entryHash"].as_str().unwrap();
let tag = voted["tag"].as_str().unwrap();
for row in unwrapped_rows(&state).await {
assert!(
!row.contains(&persona.did),
"the member, in the clear: {row}"
);
assert!(
!row.contains(entry_hash),
"the cast's leaf, in the clear: {row}"
);
assert!(!row.contains(tag), "the cast's tag, in the clear: {row}");
}
let wrapped = |prefix: &'static str| {
let ks = state.pcs_engine_ks.clone();
async move { ks.prefix_iter_raw(prefix).await.unwrap().len() }
};
let closes: chrono::DateTime<chrono::Utc> =
m["votingCloses"].as_str().unwrap().parse().unwrap();
let sweep = |at| crate::operations::pcs_voter::sweep(&state.pcs_engine_ks, at);
assert_eq!(sweep(now).await.unwrap(), 0);
assert_eq!(wrapped("secret:voter:").await, 1);
assert_eq!(sweep(closes).await.unwrap(), 0);
assert_eq!(wrapped("secret:voter:").await, 0);
assert_eq!(wrapped("secret:voter-casts:").await, 1);
assert_eq!(
ok(&handle_voter_receipt(&state, &auth, keep()).await)["status"],
"kept"
);
let gone = closes + crate::operations::pcs_voter::RECEIPT_RETENTION;
assert_eq!(sweep(gone).await.unwrap(), 1);
assert_eq!(wrapped("secret:voter-casts:").await, 0);
assert_eq!(wrapped("voter:").await, 0);
assert!(
!handle_voter_receipt(&state, &auth, keep())
.await
.status
.is_success()
);
}
#[tokio::test]
async fn an_election_read_is_signed_as_the_member() {
let (state, _dir) = build_signing_test_app_state().await;
crate::contexts::create_context(&state.contexts_ks, "member", "member")
.await
.unwrap();
let auth = claims("did:key:z6MkVoter", Role::Admin, "member");
let persona = crate::operations::persona::test_fixture::did_key_persona(
&state.keys_ks,
&state.internal_ks,
&state.contexts_ks,
&state.acl_ks,
&state.seed_store,
&state.audit_sink,
"member",
"membership-key",
)
.await;
let read = |member: &str, which: &str, payload: Value| {
doc(
uris::TASK_VTA_PCS_VOTER_READ_0_1,
json!({ "contextId": "member", "community": COMMUNITY, "member": member,
"read": which, "payload": payload }),
)
};
let shown = ok(&handle_voter_read(
&state,
&auth,
read(&persona.did, "show", json!({ "electionId": "agm-2028" })),
)
.await);
let d = &shown["document"];
assert_eq!(
d["type"],
"https://trusttasks.org/spec/vtc/elections/show/0.1"
);
assert_eq!(d["issuer"], persona.did.as_str());
assert_eq!(d["recipient"], COMMUNITY);
assert_eq!(d["payload"], json!({ "electionId": "agm-2028" }));
let signer = vta_sdk::trust_task_proof::verify_trust_task_proof_value(
d,
&vta_sdk::trust_task_proof::TrustTaskVmResolver::did_key_only(),
)
.await
.expect("the member's proof verifies");
assert_eq!(signer, persona.did);
let listed = ok(&handle_voter_read(&state, &auth, read(&persona.did, "list", json!({}))).await);
assert_eq!(
listed["document"]["type"],
"https://trusttasks.org/spec/vtc/elections/list/0.1"
);
let out = handle_voter_read(&state, &auth, read(&persona.did, "show", json!({}))).await;
assert_eq!(code(&out), "malformedRequest");
let out = handle_voter_read(
&state,
&auth,
read("did:key:z6MkNotHeldHere", "list", json!({})),
)
.await;
assert_eq!(code(&out), "vta/pcs/voter/read:memberNotHeld");
}