mirror of
https://github.com/lux-rs/chains.git
synced 2026-10-30 20:53:50 +00:00
These were in luxfi/compute, a private repo whose licence is the same BSD-3-Clause as luxfi/node's. Nothing here needed protecting, and keeping it private broke the public Rust chain builds that reference it. Sibling paths lose a level: the chains sat three deep in compute and sit one deep here. The conformance generator loses a replace naming an absolute path on one machine, and takes luxfi/chains from the proxy. Co-authored-by: Hanzo Dev <[email protected]>
582 lines
23 KiB
Rust
582 lines
23 KiB
Rust
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
|
|
//! The operations F actually exposes — registering an encrypted value, granting
|
|
//! and withdrawing access to it, asking the committee to decrypt it, and rotating
|
|
//! the committee — and the threshold rules that make each of them safe.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
|
|
use lux_fhevm::clock::Time;
|
|
use lux_fhevm::error::Error;
|
|
use lux_fhevm::fhe;
|
|
use lux_fhevm::state::{
|
|
derive_handle, derive_permit_id, tally, STATUS_ACTIVE, STATUS_REVOKED,
|
|
};
|
|
use lux_fhevm::transaction::DEFAULT_REQUEST_WINDOW;
|
|
use lux_fhevm::vm::height_key;
|
|
|
|
/// One encrypted value from registration to a completed threshold decryption —
|
|
/// the whole reason F exists, end to end.
|
|
#[test]
|
|
fn a_confidential_value_goes_from_registration_to_a_completed_decryption() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[&owner, &grantee]);
|
|
|
|
// 1. The owner registers an encrypted value. F stores its digest, never it.
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
let ct = vm.ciphertext(&handle).expect("the ciphertext is registered");
|
|
assert_eq!(ct.meta.epoch, 0, "a ciphertext is bound to the epoch it was registered in");
|
|
|
|
// 2. The grantee, holding the permit, asks the committee to decrypt.
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
|
|
let rec = vm.decrypt(&request_id).expect("the request is recorded");
|
|
assert_eq!(rec.request.status, fhe::RequestStatus::Pending);
|
|
assert_eq!(rec.request.ciphertext_handle, handle);
|
|
assert_eq!(rec.permit_id, permit_id);
|
|
assert_eq!(rec.request.epoch, 0);
|
|
assert!(
|
|
rec.request.expiry > rec.request.created_at,
|
|
"an unbounded request would outlive the committee that can answer it"
|
|
);
|
|
assert_eq!(rec.request.expiry, rec.request.created_at + DEFAULT_REQUEST_WINDOW);
|
|
|
|
// 3. Two of three members attest the same result handle. F never sees the
|
|
// plaintext — the committee combines its shares off-chain and agrees on the
|
|
// handle of what came out.
|
|
let result = digest_of("decrypted-result-handle");
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, result, 1));
|
|
let rec = vm.decrypt(&request_id).unwrap();
|
|
assert_eq!(rec.request.status, fhe::RequestStatus::Pending, "one member is not a threshold");
|
|
assert_eq!(rec.attestations().len(), 1);
|
|
|
|
accept_one(&vm, &fulfill_tx(&members[1], request_id, result, 1));
|
|
let rec = vm.decrypt(&request_id).unwrap();
|
|
assert_eq!(rec.request.status, fhe::RequestStatus::Completed, "the threshold answers it");
|
|
assert_eq!(rec.request.result_handle, result);
|
|
assert_eq!(rec.attestations().len(), 2);
|
|
assert_ne!(rec.request.completed_at, 0);
|
|
|
|
// 4. The answer is final: a third attestation is refused.
|
|
let err = vm.submit_tx(&fulfill_tx(&members[2], request_id, result, 1)).unwrap_err();
|
|
assert!(matches!(err, Error::RequestClosed));
|
|
}
|
|
|
|
/// A lying member buys nothing but its own burnt fee. It attests first, and
|
|
/// wrong; the honest members still reach the threshold on the true result, because
|
|
/// votes are tallied per value rather than pinned by whoever spoke first.
|
|
#[test]
|
|
fn a_conflicting_attestation_cannot_stall_the_honest_majority() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[&owner, &grantee]);
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
|
|
let (liar, truth) = (digest_of("forged"), digest_of("true-result"));
|
|
|
|
accept_one(&vm, &fulfill_tx(&members[2], request_id, liar, 1));
|
|
assert_eq!(vm.decrypt(&request_id).unwrap().request.status, fhe::RequestStatus::Pending);
|
|
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, truth, 1));
|
|
accept_one(&vm, &fulfill_tx(&members[1], request_id, truth, 1));
|
|
|
|
let rec = vm.decrypt(&request_id).unwrap();
|
|
assert_eq!(rec.request.status, fhe::RequestStatus::Completed);
|
|
assert_eq!(rec.request.result_handle, truth, "the honest majority decides the answer");
|
|
assert_eq!(rec.attestations().len(), 3, "the false vote is counted against its own value");
|
|
|
|
assert!(vm.balance(&members[2].addr).unwrap() < TEST_FUND, "the liar paid for the privilege");
|
|
}
|
|
|
|
/// A stranger cannot answer a decryption request, and a member cannot vote twice.
|
|
#[test]
|
|
fn only_the_committee_answers_and_a_member_holds_one_vote() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let stranger = TestKey::new();
|
|
let (committee, members) = new_committee(3);
|
|
let mut all: Vec<&TestKey> = members.iter().collect();
|
|
all.extend_from_slice(&[&owner, &grantee, &stranger]);
|
|
let vm = new_test_vm(fund_all(&all), &committee, 2);
|
|
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
let result = digest_of("r");
|
|
|
|
assert!(matches!(
|
|
vm.submit_tx(&fulfill_tx(&stranger, request_id, result, 1)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
|
|
// Even the requester, who has every right to the answer, is not a member.
|
|
assert!(matches!(
|
|
vm.submit_tx(&fulfill_tx(&grantee, request_id, result, 2)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, result, 1));
|
|
|
|
// One member, one vote — but the vote is not spent by being cast. A second
|
|
// attestation REPLACES the first rather than adding to the tally, so a member
|
|
// can correct itself without ever counting twice.
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, digest_of("other"), 2));
|
|
let rec = vm.decrypt(&request_id).unwrap();
|
|
assert_eq!(rec.attestations().len(), 1, "one entry per member, however often it votes");
|
|
assert_eq!(rec.attestations()[0].value, digest_of("other"));
|
|
assert_eq!(tally(rec.attestations(), &result), 0, "the withdrawn vote counts for nothing");
|
|
assert_eq!(rec.request.status, fhe::RequestStatus::Pending);
|
|
}
|
|
|
|
#[test]
|
|
fn a_request_that_was_never_made_cannot_be_answered() {
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[]);
|
|
let err = vm
|
|
.submit_tx(&fulfill_tx(&members[0], digest_of("never-asked"), digest_of("r"), 1))
|
|
.unwrap_err();
|
|
assert!(matches!(err, Error::RequestNotFound));
|
|
}
|
|
|
|
/// A request outlives nothing: once its window closes, no attestation is accepted
|
|
/// and it can never complete.
|
|
#[test]
|
|
fn an_expired_request_can_never_be_answered_and_reads_as_expired() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[&owner, &grantee]);
|
|
|
|
let now = vm.clock.time();
|
|
vm.clock.set(now);
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
|
|
let expiry = now.unix() + 60;
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, expiry, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
|
|
// Still answerable inside the window.
|
|
{
|
|
let st = vm.state.read().unwrap();
|
|
fulfill_tx(&members[0], request_id, digest_of("r"), 1)
|
|
.check_auth(&st, expiry)
|
|
.expect("inside the window");
|
|
}
|
|
|
|
// One second past it, closed.
|
|
vm.clock.set(Time::from_unix(expiry + 1));
|
|
let err = vm.submit_tx(&fulfill_tx(&members[0], request_id, digest_of("r"), 1)).unwrap_err();
|
|
assert!(matches!(err, Error::RequestExpired));
|
|
|
|
// And the read surface says so, rather than reporting it as still pending.
|
|
let reply = lux_fhevm::service::call(
|
|
&vm,
|
|
"fchain.getDecrypt",
|
|
&serde_json::json!({ "requestId": hex_of(request_id) }),
|
|
)
|
|
.expect("the request reads");
|
|
assert_eq!(reply["request"]["status"], "expired");
|
|
}
|
|
|
|
/// The permit is the whole authority for a decryption request: it must exist, be
|
|
/// unrevoked, be unexpired, name THIS handle, name THIS grantee, and confer
|
|
/// decrypt specifically.
|
|
#[test]
|
|
fn a_permit_gates_a_decryption_on_every_one_of_its_terms() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let stranger = TestKey::new();
|
|
let (committee, members) = new_committee(1);
|
|
let mut all: Vec<&TestKey> = members.iter().collect();
|
|
all.extend_from_slice(&[&owner, &grantee, &stranger]);
|
|
let vm = new_test_vm(fund_all(&all), &committee, 1);
|
|
|
|
let now = vm.clock.time();
|
|
vm.clock.set(now);
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
|
|
// No such permit.
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, handle, digest_of("nope"), 0, 1))
|
|
.unwrap_err(),
|
|
Error::PermitNotFound
|
|
));
|
|
|
|
// The permit belongs to someone else.
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&stranger, TEST_SCHEME, handle, permit_id, 0, 1)).unwrap_err(),
|
|
Error::Unauthorized
|
|
));
|
|
|
|
// The permit is for another handle.
|
|
let other = register_tx(&owner, TEST_SCHEME, digest_of("other-value"), 3);
|
|
accept_one(&vm, &other);
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, other.subject, permit_id, 0, 1))
|
|
.unwrap_err(),
|
|
Error::PermitInvalid(_)
|
|
));
|
|
|
|
// The ciphertext was never registered.
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, digest_of("unregistered"), permit_id, 0, 1))
|
|
.unwrap_err(),
|
|
Error::CiphertextNotFound
|
|
));
|
|
|
|
// The permit confers compute but not decrypt.
|
|
accept_one(&vm, &grant_tx(&owner, handle, grantee.addr, fhe::PERMIT_OP_COMPUTE, 0, 4));
|
|
let compute_only =
|
|
derive_permit_id(&handle, &owner.addr, &grantee.addr, fhe::PERMIT_OP_COMPUTE, 0, 4);
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, handle, compute_only, 0, 1)).unwrap_err(),
|
|
Error::PermitInvalid(_)
|
|
));
|
|
|
|
// The permit has expired.
|
|
let expiry = now.unix() + 30;
|
|
accept_one(&vm, &grant_tx(&owner, handle, grantee.addr, fhe::PERMIT_OP_DECRYPT, expiry, 5));
|
|
let short =
|
|
derive_permit_id(&handle, &owner.addr, &grantee.addr, fhe::PERMIT_OP_DECRYPT, expiry, 5);
|
|
vm.clock.set(Time::from_unix(expiry + 1));
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, handle, short, 0, 1)).unwrap_err(),
|
|
Error::PermitExpired
|
|
));
|
|
vm.clock.set(now);
|
|
|
|
// The permit was revoked.
|
|
accept_one(&vm, &revoke_tx(&owner, permit_id, 6));
|
|
assert_eq!(vm.permit(&permit_id).unwrap().status, STATUS_REVOKED);
|
|
assert!(matches!(
|
|
vm.submit_tx(&request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1)).unwrap_err(),
|
|
Error::PermitRevoked
|
|
));
|
|
}
|
|
|
|
/// A capability over an encrypted value comes from its owner and from nobody else
|
|
/// — including the grantee, who cannot pass its access on, and cannot keep it
|
|
/// after the owner withdraws it.
|
|
#[test]
|
|
fn only_the_owner_grants_and_only_the_owner_revokes() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let stranger = TestKey::new();
|
|
let (committee, _) = new_committee(1);
|
|
let vm = new_test_vm(fund_all(&[&owner, &grantee, &stranger]), &committee, 1);
|
|
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
|
|
// A stranger cannot grant over someone else's ciphertext...
|
|
assert!(matches!(
|
|
vm.submit_tx(&grant_tx(&stranger, handle, stranger.addr, fhe::PERMIT_OP_DECRYPT, 0, 1))
|
|
.unwrap_err(),
|
|
Error::Unauthorized
|
|
));
|
|
|
|
// ...nor can the grantee pass its own access along.
|
|
assert!(matches!(
|
|
vm.submit_tx(&grant_tx(&grantee, handle, stranger.addr, fhe::PERMIT_OP_DECRYPT, 0, 1))
|
|
.unwrap_err(),
|
|
Error::Unauthorized
|
|
));
|
|
|
|
// Granting over a ciphertext nobody registered is refused.
|
|
assert!(matches!(
|
|
vm.submit_tx(&grant_tx(
|
|
&owner,
|
|
digest_of("ghost"),
|
|
grantee.addr,
|
|
fhe::PERMIT_OP_DECRYPT,
|
|
0,
|
|
3
|
|
))
|
|
.unwrap_err(),
|
|
Error::CiphertextNotFound
|
|
));
|
|
|
|
// A stranger cannot withdraw someone else's grant.
|
|
assert!(matches!(
|
|
vm.submit_tx(&revoke_tx(&stranger, permit_id, 1)).unwrap_err(),
|
|
Error::Unauthorized
|
|
));
|
|
|
|
// Revoking something that was never granted is refused.
|
|
assert!(matches!(
|
|
vm.submit_tx(&revoke_tx(&owner, digest_of("no-permit"), 3)).unwrap_err(),
|
|
Error::PermitNotFound
|
|
));
|
|
|
|
// The owner can, and once withdrawn it stays withdrawn.
|
|
accept_one(&vm, &revoke_tx(&owner, permit_id, 3));
|
|
assert!(matches!(
|
|
vm.submit_tx(&revoke_tx(&owner, permit_id, 4)).unwrap_err(),
|
|
Error::PermitRevoked
|
|
));
|
|
}
|
|
|
|
/// A handle names its content: the same body under the same scheme is the same
|
|
/// handle, so it cannot be registered twice, and a handle cannot be claimed by
|
|
/// someone who does not have the body to hash.
|
|
#[test]
|
|
fn a_handle_names_its_content_so_it_cannot_be_registered_twice_or_squatted() {
|
|
let a = TestKey::new();
|
|
let b = TestKey::new();
|
|
let (committee, _) = new_committee(1);
|
|
let vm = new_test_vm(fund_all(&[&a, &b]), &committee, 1);
|
|
|
|
let body = digest_of("the-encrypted-body");
|
|
accept_one(&vm, ®ister_tx(&a, TEST_SCHEME, body, 1));
|
|
|
|
// A different account cannot re-register the same content.
|
|
assert!(matches!(
|
|
vm.submit_tx(®ister_tx(&b, TEST_SCHEME, body, 1)).unwrap_err(),
|
|
Error::CiphertextExists
|
|
));
|
|
|
|
// The same content under a DIFFERENT scheme is a different value, and is
|
|
// registrable — a CKKS ciphertext and a BFV one are not the same object.
|
|
accept_one(&vm, ®ister_tx(&b, "bfv-n13", body, 1));
|
|
assert_eq!(vm.ciphertexts().len(), 2);
|
|
|
|
assert_eq!(
|
|
vm.ciphertext(&derive_handle(&body, TEST_SCHEME.as_bytes())).unwrap().scheme,
|
|
TEST_SCHEME
|
|
);
|
|
assert_eq!(vm.ciphertext(&derive_handle(&body, b"bfv-n13")).unwrap().scheme, "bfv-n13");
|
|
}
|
|
|
|
/// The committee installs its own successor by threshold vote, and the change
|
|
/// takes effect only when the threshold is reached.
|
|
#[test]
|
|
fn the_committee_installs_its_successor_only_at_the_threshold() {
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[]);
|
|
let (next, next_keys) = new_committee(3);
|
|
let next_pk = b"epoch-1-network-key";
|
|
|
|
// Fund the incoming committee so it can answer once seated.
|
|
{
|
|
let st = vm.state.read().unwrap();
|
|
for k in &next_keys {
|
|
lux_fhevm::fee::credit(st.db.as_ref(), &k.addr, TEST_FUND).expect("credited");
|
|
}
|
|
st.versdb.commit().expect("committed");
|
|
}
|
|
|
|
// One vote is not enough.
|
|
accept_one(&vm, &advance_tx(&members[0], 1, &next, 2, next_pk, 1));
|
|
assert_eq!(vm.current_epoch(), 0, "one member is not a threshold");
|
|
let cur = vm.epoch(0).unwrap();
|
|
assert_eq!(cur.attestations().len(), 1);
|
|
assert_eq!(cur.info.status, fhe::EpochStatus::Active);
|
|
|
|
// The second vote installs it, atomically with the block.
|
|
vm.submit_tx(&advance_tx(&members[1], 1, &next, 2, next_pk, 1)).expect("admitted");
|
|
let blk = accept_queued(&vm);
|
|
assert_eq!(vm.current_epoch(), 1);
|
|
|
|
let old = vm.epoch(0).unwrap();
|
|
assert_eq!(old.info.status, fhe::EpochStatus::Ended);
|
|
assert_eq!(old.info.end_time, blk.timestamp.unix());
|
|
|
|
let installed = vm.epoch(1).expect("the successor is seated");
|
|
assert_eq!(installed.info.status, fhe::EpochStatus::Active);
|
|
assert_eq!(installed.info.threshold, 2);
|
|
assert_eq!(installed.info.public_key, next_pk);
|
|
assert_eq!(installed.committee().len(), 3);
|
|
assert!(installed.attestations().is_empty(), "a fresh epoch starts with no votes cast");
|
|
|
|
// The outgoing committee no longer decides anything.
|
|
let (further, _) = new_committee(3);
|
|
assert!(matches!(
|
|
vm.submit_tx(&advance_tx(&members[2], 2, &further, 2, b"k", 1)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
|
|
// The incoming one does.
|
|
let st = vm.state.read().unwrap();
|
|
advance_tx(&next_keys[0], 2, &further, 2, b"k", 1)
|
|
.check_auth(&st, vm.clock.time().unix())
|
|
.expect("the seated committee decides");
|
|
}
|
|
|
|
/// Who may rotate the committee, and to what.
|
|
#[test]
|
|
fn an_epoch_advance_refuses_a_stranger_a_skip_and_a_second_vote() {
|
|
let stranger = TestKey::new();
|
|
let (committee, members) = new_committee(3);
|
|
let mut all: Vec<&TestKey> = members.iter().collect();
|
|
all.push(&stranger);
|
|
let vm = new_test_vm(fund_all(&all), &committee, 2);
|
|
let (next, _) = new_committee(3);
|
|
let pk = b"k";
|
|
|
|
// A stranger cannot propose a successor.
|
|
assert!(matches!(
|
|
vm.submit_tx(&advance_tx(&stranger, 1, &next, 2, pk, 1)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
|
|
// Nor can a member skip an epoch or re-elect the current one.
|
|
assert!(matches!(
|
|
vm.submit_tx(&advance_tx(&members[0], 2, &next, 2, pk, 1)).unwrap_err(),
|
|
Error::EpochMismatch(_)
|
|
));
|
|
assert!(matches!(
|
|
vm.submit_tx(&advance_tx(&members[0], 0, &next, 2, pk, 1)).unwrap_err(),
|
|
Error::EpochMismatch(_)
|
|
));
|
|
|
|
// One member, one vote — a member that moves to a rival proposal moves its
|
|
// single vote rather than splitting the tally in its own favour.
|
|
accept_one(&vm, &advance_tx(&members[0], 1, &next, 2, pk, 1));
|
|
let (rival, _) = new_committee(3);
|
|
accept_one(&vm, &advance_tx(&members[0], 1, &rival, 2, pk, 2));
|
|
|
|
let ep = vm.epoch(0).unwrap();
|
|
assert_eq!(ep.attestations().len(), 1, "one entry per member, however often it votes");
|
|
assert_eq!(
|
|
tally(ep.attestations(), &lux_fhevm::state::committee_digest(1, 2, pk, &next)),
|
|
0,
|
|
"the abandoned proposal keeps none of its support"
|
|
);
|
|
assert_eq!(
|
|
tally(ep.attestations(), &lux_fhevm::state::committee_digest(1, 2, pk, &rival)),
|
|
1
|
|
);
|
|
assert_eq!(vm.current_epoch(), 0, "and one member is still not a threshold");
|
|
}
|
|
|
|
/// An answer comes from the committee that was seated when the request was made. A
|
|
/// later committee holds different key shares and never saw the permit, so it must
|
|
/// not be able to answer.
|
|
#[test]
|
|
fn a_request_binds_to_the_epoch_that_was_seated_when_it_was_made() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[&owner, &grantee]);
|
|
let (next, next_keys) = new_committee(3);
|
|
{
|
|
let st = vm.state.read().unwrap();
|
|
for k in &next_keys {
|
|
lux_fhevm::fee::credit(st.db.as_ref(), &k.addr, TEST_FUND).expect("credited");
|
|
}
|
|
st.versdb.commit().expect("committed");
|
|
}
|
|
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
|
|
// Rotate the committee out from under the pending request.
|
|
accept_one(&vm, &advance_tx(&members[0], 1, &next, 2, b"k", 1));
|
|
accept_one(&vm, &advance_tx(&members[1], 1, &next, 2, b"k", 1));
|
|
assert_eq!(vm.current_epoch(), 1);
|
|
|
|
// The NEW committee cannot answer the OLD request.
|
|
assert!(matches!(
|
|
vm.submit_tx(&fulfill_tx(&next_keys[0], request_id, digest_of("r"), 1)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
|
|
// The committee that was seated when it was asked still can.
|
|
let result = digest_of("r");
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, result, 2));
|
|
accept_one(&vm, &fulfill_tx(&members[1], request_id, result, 2));
|
|
assert_eq!(vm.decrypt(&request_id).unwrap().request.status, fhe::RequestStatus::Completed);
|
|
|
|
// A request made now belongs to the new epoch.
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 2));
|
|
let fresh = vm.decrypt(&request_id_for(handle, &grantee.addr, 2)).unwrap();
|
|
assert_eq!(fresh.request.epoch, 1);
|
|
}
|
|
|
|
/// A chain whose genesis seated nobody refuses every threshold decision rather
|
|
/// than accepting one from anybody.
|
|
#[test]
|
|
fn a_chain_with_nobody_seated_answers_nothing() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let vm = new_test_vm(fund_all(&[&owner, &grantee]), &[], 0);
|
|
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
|
|
// There is no epoch record to name a committee, so nothing can answer.
|
|
assert!(matches!(
|
|
vm.submit_tx(&fulfill_tx(&owner, request_id, digest_of("r"), 3)).unwrap_err(),
|
|
Error::EpochNotFound
|
|
));
|
|
|
|
// And nobody can seat themselves.
|
|
let (next, _) = new_committee(3);
|
|
assert!(matches!(
|
|
vm.submit_tx(&advance_tx(&owner, 1, &next, 2, b"k", 3)).unwrap_err(),
|
|
Error::NotCommittee
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_stopping_chain_proposes_nothing_and_an_idle_one_proposes_nothing() {
|
|
let k = TestKey::new();
|
|
let (committee, _) = new_committee(1);
|
|
let vm = new_test_vm(fund_all(&[&k]), &committee, 1);
|
|
assert!(matches!(vm.build_block().unwrap_err(), Error::NoPendingTxs));
|
|
|
|
vm.submit_tx(®ister_tx(&k, TEST_SCHEME, digest_of("x"), 1)).expect("admitted");
|
|
vm.shutdown().expect("the chain stops");
|
|
assert!(matches!(vm.build_block().unwrap_err(), Error::VmShutdown));
|
|
}
|
|
|
|
/// The height index is validated when it is read, so a damaged entry surfaces as
|
|
/// an error instead of a truncated id.
|
|
#[test]
|
|
fn a_corrupt_height_index_entry_is_an_error_not_a_truncated_id() {
|
|
let (committee, _) = new_committee(1);
|
|
let vm = new_test_vm(Vec::new(), &committee, 1);
|
|
|
|
{
|
|
// A read lock is enough: the store is behind its own lock, so writing a row
|
|
// does not need exclusive access to the chain's caches.
|
|
let st = vm.state.read().unwrap();
|
|
st.db.put(&height_key(5), &[1, 2, 3]).expect("written");
|
|
st.versdb.commit().expect("committed");
|
|
}
|
|
|
|
assert!(matches!(vm.block_id_at(5).unwrap_err(), Error::InvalidPayload(_)));
|
|
}
|
|
|
|
/// The caches are a projection of the database and not the truth: rebuilding them
|
|
/// from disk yields the same chain.
|
|
#[test]
|
|
fn the_state_survives_a_reload_because_the_store_is_the_truth() {
|
|
let owner = TestKey::new();
|
|
let grantee = TestKey::new();
|
|
let (vm, _, members) = new_decrypt_vm(3, 2, &[&owner, &grantee]);
|
|
|
|
let (handle, permit_id) = seed_permit(&vm, &owner, &grantee, fhe::PERMIT_OP_DECRYPT, 0);
|
|
accept_one(&vm, &request_tx(&grantee, TEST_SCHEME, handle, permit_id, 0, 1));
|
|
let request_id = request_id_for(handle, &grantee.addr, 1);
|
|
accept_one(&vm, &fulfill_tx(&members[0], request_id, digest_of("r"), 1));
|
|
|
|
let before = vm.decrypt(&request_id).unwrap();
|
|
let (height, epoch) = (vm.height(), vm.current_epoch());
|
|
|
|
vm.load_state().expect("the caches rebuild");
|
|
|
|
assert_eq!(vm.height(), height);
|
|
assert_eq!(vm.current_epoch(), epoch);
|
|
let after = vm.decrypt(&request_id).expect("the request survives");
|
|
assert_eq!(after.attestations(), before.attestations());
|
|
assert_eq!(after.request.status, before.request.status);
|
|
assert!(vm.ciphertext(&handle).is_some());
|
|
assert_eq!(vm.permit(&permit_id).unwrap().status, STATUS_ACTIVE);
|
|
assert_eq!(vm.epoch(0).unwrap().committee().len(), 3);
|
|
}
|