feat(vault)!: implement full Shamir re-key flow and governance execution (§3.3–§3.5)
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

- Add `rekey.proto` with `ContributePassphrase` / `ContributeRecoveryPassphrase` / `RekeyResult`
- Wire `rekey` as a 4th vault stream payload in `vault.proto` and gRPC dispatch
- Add `RekeyRootKey` message to `Vault` actor: generates new random seal key, re-encrypts root key, writes new `root_key_history` row
- Add `StartRekey`, `ContributeRekey`, `ContributeRecoveryRekey` messages to `VaultCoordinator`; `finalize_rekey` uses threshold-1 fast path identical to bootstrap
- `execute_replace_operator` now UPDATEs `operator_identity.public_key` in-place (avoids FK constraint violation), deletes stale `operator` share row, then triggers `StartRekey`
- `execute_update_shamir_parameters` triggers `StartRekey` instead of warning stub
- `ProposalKind::ReplaceOperator` carries `old_operator_id`; encode/decode updated accordingly
- `GlobalActors::spawn` extracts `vault_coordinator` before `Ok(Self { … })` so it can be cloned into `ProposalManager::new`
- Add `handle_rekey` in session handlers forwarding passphrase contributions to `VaultCoordinator`
- Fix test: rename `replace_operator_inserts_identity_row` → `replace_operator_updates_pubkey_and_starts_rekey`, assert count stays 1 and pubkey is updated
This commit is contained in:
CleverWild
2026-06-14 15:11:11 +02:00
parent aff87c13ca
commit 7ab47ec563
14 changed files with 505 additions and 91 deletions

View File

@@ -733,7 +733,7 @@ async fn approve_one_off_transaction_stores_result() {
}
#[tokio::test]
async fn replace_operator_inserts_identity_row() {
async fn replace_operator_updates_pubkey_and_starts_rekey() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
@@ -751,7 +751,7 @@ async fn replace_operator_inserts_identity_row() {
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::ReplaceOperator { new_pubkey },
kind: ProposalKind::ReplaceOperator { old_operator_id: op_id, new_pubkey: new_pubkey.clone() },
initiator_id: op_id,
ttl_secs: None,
})
@@ -774,12 +774,22 @@ async fn replace_operator_inserts_identity_row() {
assert_eq!(outcome, VoteOutcome::QuorumApproved);
let mut conn = db.get().await.unwrap();
// The old identity row is updated in-place; count stays the same.
let count: i64 = operator_identity::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(count, 2); // original + new
assert_eq!(count, 1);
// Verify the public key was updated to the new one.
let stored_pubkey: Vec<u8> = operator_identity::table
.filter(operator_identity::id.eq(op_id))
.select(operator_identity::public_key)
.first(&mut conn)
.await
.unwrap();
assert_eq!(stored_pubkey, new_pubkey.clone());
}
#[tokio::test]
@@ -843,7 +853,7 @@ async fn key_rotation_requires_full_quorum() {
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::ReplaceOperator { new_pubkey },
kind: ProposalKind::ReplaceOperator { old_operator_id: 1, new_pubkey },
initiator_id: op1,
ttl_secs: None,
})
@@ -925,7 +935,7 @@ async fn recovery_vote_rejected_when_sleeping() {
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::ReplaceOperator { new_pubkey },
kind: ProposalKind::ReplaceOperator { old_operator_id: 1, new_pubkey },
initiator_id: op_id,
ttl_secs: None,
})
@@ -1072,7 +1082,7 @@ async fn recovery_operator_vote_contributes_to_replace_quorum() {
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::ReplaceOperator { new_pubkey },
kind: ProposalKind::ReplaceOperator { old_operator_id: 1, new_pubkey },
initiator_id: op_id,
ttl_secs: None,
})