1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
//! Vault Engine for CTV-Based Security Mechanisms
//!
//! Implements Bitcoin vaults using CTV covenants to enforce spending conditions:
//! - Time delays before withdrawal
//! - Multi-step spending (unvault → withdraw)
//! - Recovery paths for key loss scenarios
//! - Security layers against hot wallet compromises
use crate::payment::covenant::{CovenantEngine, CovenantProof};
use crate::payment::processor::PaymentError;
use crate::rpc::errors::STORAGE_NOT_AVAILABLE_MSG;
use crate::utils::current_timestamp;
use crate::Hash;
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
use std::sync::Arc;
use tracing::info;
/// Vault configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
/// Time delay in blocks before withdrawal allowed
pub withdrawal_delay_blocks: u32,
/// Recovery path script (alternative spending path)
pub recovery_script: Option<Vec<u8>>,
/// Maximum withdrawal amount per transaction
pub max_withdrawal: Option<u64>,
/// Whether to require unvaulting step
pub require_unvault: bool,
}
impl Default for VaultConfig {
fn default() -> Self {
Self {
withdrawal_delay_blocks: 144, // ~1 day on mainnet
recovery_script: None,
max_withdrawal: None,
require_unvault: true,
}
}
}
/// Vault lifecycle state
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum VaultLifecycle {
/// Funds deposited, waiting for unvault
Deposited,
/// Unvault transaction in mempool
Unvaulting,
/// Funds in unvault output, waiting for withdrawal delay
Unvaulted { unvaulted_at_block: u64 },
/// Withdrawal transaction in mempool
Withdrawing,
/// Funds successfully withdrawn
Withdrawn,
/// Funds recovered via recovery path
Recovered,
}
/// Vault state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultState {
pub vault_id: String,
pub deposit_tx_hash: Hash,
pub deposit_amount: u64,
pub unvault_tx_hash: Option<Hash>,
pub withdrawal_tx_hash: Option<Hash>,
pub recovery_tx_hash: Option<Hash>,
pub state: VaultLifecycle,
pub config: VaultConfig,
pub deposit_covenant: CovenantProof,
pub unvault_covenant: Option<CovenantProof>,
pub withdrawal_covenant: Option<CovenantProof>,
pub created_at: u64,
pub withdrawal_available_at: Option<u64>, // Block height
}
/// Vault Engine
pub struct VaultEngine {
covenant_engine: Arc<CovenantEngine>,
/// Storage for vault states (optional)
storage: Option<Arc<crate::storage::Storage>>,
/// In-memory vault states cache
vaults: Arc<std::sync::Mutex<std::collections::HashMap<String, VaultState>>>,
}
impl VaultEngine {
/// Create a new vault engine
pub fn new(covenant_engine: Arc<CovenantEngine>) -> Self {
Self {
covenant_engine,
storage: None,
vaults: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
}
}
/// Create vault engine with storage
pub fn with_storage(
covenant_engine: Arc<CovenantEngine>,
storage: Arc<crate::storage::Storage>,
) -> Self {
let engine = Self {
covenant_engine,
storage: Some(storage),
vaults: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
};
// Load existing vaults from storage
if let Err(e) = engine.load_all_vaults() {
tracing::warn!("Failed to load vaults from storage: {}", e);
}
engine
}
/// Load all vaults from storage
fn load_all_vaults(&self) -> Result<(), PaymentError> {
let storage = self
.storage
.as_ref()
.ok_or_else(|| PaymentError::ProcessingError(STORAGE_NOT_AVAILABLE_MSG.to_string()))?;
let vaults_tree = storage.open_tree("vaults").map_err(|e| {
PaymentError::ProcessingError(format!("Failed to open vaults tree: {}", e))
})?;
let mut vaults = self.vaults.lock().unwrap();
for result in vaults_tree.iter() {
let (key, value) = result.map_err(|e| {
PaymentError::ProcessingError(format!("Failed to read vault: {}", e))
})?;
let vault_id = String::from_utf8(key)
.map_err(|e| PaymentError::ProcessingError(format!("Invalid vault ID: {}", e)))?;
let vault_state: VaultState = bincode::deserialize(&value).map_err(|e| {
PaymentError::ProcessingError(format!("Failed to deserialize vault: {}", e))
})?;
vaults.insert(vault_id, vault_state);
}
Ok(())
}
/// Save vault state to storage
fn save_vault(&self, vault_state: &VaultState) -> Result<(), PaymentError> {
// Update in-memory cache
let mut vaults = self.vaults.lock().unwrap();
vaults.insert(vault_state.vault_id.clone(), vault_state.clone());
// Persist to storage if available
if let Some(storage) = &self.storage {
let vaults_tree = storage.open_tree("vaults").map_err(|e| {
PaymentError::ProcessingError(format!("Failed to open vaults tree: {}", e))
})?;
let key = vault_state.vault_id.as_bytes();
let value = bincode::serialize(vault_state).map_err(|e| {
PaymentError::ProcessingError(format!("Failed to serialize vault: {}", e))
})?;
vaults_tree.insert(key, &value).map_err(|e| {
PaymentError::ProcessingError(format!("Failed to save vault: {}", e))
})?;
}
Ok(())
}
/// Get vault state by ID
pub fn get_vault(&self, vault_id: &str) -> Result<Option<VaultState>, PaymentError> {
// Check in-memory cache first
let vaults = self.vaults.lock().unwrap();
if let Some(vault) = vaults.get(vault_id) {
return Ok(Some(vault.clone()));
}
// If not in cache, try loading from storage
if let Some(storage) = &self.storage {
let vaults_tree = storage.open_tree("vaults").map_err(|e| {
PaymentError::ProcessingError(format!("Failed to open vaults tree: {}", e))
})?;
if let Some(value) = vaults_tree.get(vault_id.as_bytes()).map_err(|e| {
PaymentError::ProcessingError(format!("Failed to read vault: {}", e))
})? {
let vault_state: VaultState = bincode::deserialize(&value).map_err(|e| {
PaymentError::ProcessingError(format!("Failed to deserialize vault: {}", e))
})?;
// Update cache
drop(vaults);
let mut vaults = self.vaults.lock().unwrap();
vaults.insert(vault_id.to_string(), vault_state.clone());
return Ok(Some(vault_state));
}
}
Ok(None)
}
/// Create a new vault with deposit covenant
///
/// # Arguments
///
/// * `vault_id` - Unique identifier for the vault
/// * `deposit_amount` - Amount to deposit into vault
/// * `withdrawal_script` - Script where funds can be withdrawn
/// * `config` - Vault configuration
///
/// # Returns
///
/// Vault state with deposit covenant
pub fn create_vault(
&self,
vault_id: &str,
deposit_amount: u64,
withdrawal_script: Vec<u8>,
config: VaultConfig,
) -> Result<VaultState, PaymentError> {
#[cfg(not(feature = "ctv"))]
{
return Err(PaymentError::FeatureNotEnabled(
"Vaults require CTV feature".to_string(),
));
}
#[cfg(feature = "ctv")]
{
use blvm_protocol::payment::PaymentOutput;
// Build the deposit covenant outputs.
//
// require_unvault = true → two-step: deposit → unvault → withdraw.
// The deposit covenant commits to an intermediate "vault holder" output.
// The holder script is OP_RETURN <32-byte vault_id digest>, which
// is unspendable on-chain but serves as a unique, verifiable commitment
// inside the covenant-proof system. When the operator calls unvault(),
// a real unvault covenant over the final destination is created.
//
// require_unvault = false → single-step: deposit directly to withdrawal_script.
let deposit_outputs = if config.require_unvault {
// Derive the vault-holder placeholder script: OP_RETURN <sha256(vault_id)>
let holder_hash: [u8; 32] = Sha256::digest(vault_id.as_bytes()).into();
let mut holder_script = Vec::with_capacity(34);
holder_script.push(0x6a); // OP_RETURN
holder_script.push(0x20); // PUSH 32 bytes
holder_script.extend_from_slice(&holder_hash);
vec![PaymentOutput {
script: holder_script,
amount: Some(deposit_amount),
}]
} else {
// Direct withdrawal (no unvault step)
vec![PaymentOutput {
script: withdrawal_script,
amount: Some(deposit_amount),
}]
};
let deposit_covenant =
self.covenant_engine
.create_payment_covenant(vault_id, &deposit_outputs, None)?;
let created_at = current_timestamp();
let vault_state = VaultState {
vault_id: vault_id.to_string(),
deposit_tx_hash: [0u8; 32], // Will be set when deposit tx is created
deposit_amount,
unvault_tx_hash: None,
withdrawal_tx_hash: None,
recovery_tx_hash: None,
state: VaultLifecycle::Deposited,
config,
deposit_covenant,
unvault_covenant: None,
withdrawal_covenant: None,
created_at,
withdrawal_available_at: None,
};
// Save vault state
self.save_vault(&vault_state)?;
Ok(vault_state)
}
}
/// Create unvaulting transaction (first step of withdrawal)
///
/// Moves funds from vault deposit to unvault output, which can then be
/// withdrawn after the time delay.
///
/// # Arguments
///
/// * `vault_state` - Current vault state
/// * `unvault_script` - Script for unvault output
///
/// # Returns
///
/// Updated vault state with unvault covenant
pub fn unvault(
&self,
vault_state: &VaultState,
unvault_script: Vec<u8>,
) -> Result<VaultState, PaymentError> {
#[cfg(not(feature = "ctv"))]
{
return Err(PaymentError::FeatureNotEnabled(
"Vaults require CTV feature".to_string(),
));
}
#[cfg(feature = "ctv")]
{
if !vault_state.config.require_unvault {
return Err(PaymentError::ProcessingError(
"Vault does not require unvaulting step".to_string(),
));
}
if vault_state.state != VaultLifecycle::Deposited {
return Err(PaymentError::ProcessingError(format!(
"Vault must be in Deposited state, current: {:?}",
vault_state.state
)));
}
use blvm_protocol::payment::PaymentOutput;
// Create unvault covenant that commits to withdrawal transaction
let unvault_outputs = vec![PaymentOutput {
script: unvault_script,
amount: Some(vault_state.deposit_amount),
}];
let unvault_covenant = self.covenant_engine.create_payment_covenant(
&format!("{}_unvault", vault_state.vault_id),
&unvault_outputs,
None,
)?;
let mut new_state = vault_state.clone();
new_state.unvault_covenant = Some(unvault_covenant);
new_state.state = VaultLifecycle::Unvaulting;
// Save vault state
self.save_vault(&new_state)?;
info!("Unvaulting created for vault: {}", vault_state.vault_id);
Ok(new_state)
}
}
/// Create withdrawal transaction (final step)
///
/// Withdraws funds from unvault output to final destination.
/// Requires that the withdrawal delay has passed.
///
/// # Arguments
///
/// * `vault_state` - Current vault state
/// * `withdrawal_script` - Final destination script
/// * `current_block_height` - Current blockchain height
///
/// # Returns
///
/// Updated vault state with withdrawal covenant
pub fn withdraw(
&self,
vault_state: &VaultState,
withdrawal_script: Vec<u8>,
current_block_height: u64,
) -> Result<VaultState, PaymentError> {
#[cfg(not(feature = "ctv"))]
{
return Err(PaymentError::FeatureNotEnabled(
"Vaults require CTV feature".to_string(),
));
}
#[cfg(feature = "ctv")]
{
// Check if withdrawal is allowed
if let Some(available_at) = vault_state.withdrawal_available_at {
if current_block_height < available_at {
return Err(PaymentError::ProcessingError(format!(
"Withdrawal not yet available. Available at block {}, current: {}",
available_at, current_block_height
)));
}
}
// Check state
let state_valid = if vault_state.config.require_unvault {
matches!(vault_state.state, VaultLifecycle::Unvaulted { .. })
} else {
matches!(vault_state.state, VaultLifecycle::Deposited)
};
if !state_valid {
return Err(PaymentError::ProcessingError(format!(
"Vault state invalid for withdrawal. Current: {:?}",
vault_state.state
)));
}
// Check max withdrawal if configured
if let Some(max) = vault_state.config.max_withdrawal {
if vault_state.deposit_amount > max {
return Err(PaymentError::ProcessingError(format!(
"Withdrawal amount {} exceeds maximum {}",
vault_state.deposit_amount, max
)));
}
}
use blvm_protocol::payment::PaymentOutput;
// Create withdrawal covenant
let withdrawal_outputs = vec![PaymentOutput {
script: withdrawal_script,
amount: Some(vault_state.deposit_amount),
}];
let withdrawal_covenant = self.covenant_engine.create_payment_covenant(
&format!("{}_withdrawal", vault_state.vault_id),
&withdrawal_outputs,
None,
)?;
let mut new_state = vault_state.clone();
new_state.withdrawal_covenant = Some(withdrawal_covenant);
new_state.state = VaultLifecycle::Withdrawing;
// Save vault state
self.save_vault(&new_state)?;
info!("Withdrawal created for vault: {}", vault_state.vault_id);
Ok(new_state)
}
}
/// Use recovery path to access funds
///
/// Allows recovery of funds if primary keys are lost, using the
/// recovery script specified in vault configuration.
///
/// # Arguments
///
/// * `vault_state` - Current vault state
/// * `recovery_script` - Recovery destination script
///
/// # Returns
///
/// Updated vault state with recovery transaction
pub fn recover(
&self,
vault_state: &VaultState,
recovery_script: Vec<u8>,
) -> Result<VaultState, PaymentError> {
#[cfg(not(feature = "ctv"))]
{
return Err(PaymentError::FeatureNotEnabled(
"Vaults require CTV feature".to_string(),
));
}
#[cfg(feature = "ctv")]
{
if vault_state.config.recovery_script.is_none() {
return Err(PaymentError::ProcessingError(
"Vault does not have recovery path configured".to_string(),
));
}
// Recovery can be used from any state except already recovered/withdrawn
match vault_state.state {
VaultLifecycle::Withdrawn | VaultLifecycle::Recovered => {
return Err(PaymentError::ProcessingError(
"Vault already withdrawn or recovered".to_string(),
));
}
_ => {}
}
use blvm_protocol::payment::PaymentOutput;
// Create recovery covenant
let recovery_outputs = vec![PaymentOutput {
script: recovery_script,
amount: Some(vault_state.deposit_amount),
}];
let _recovery_covenant = self.covenant_engine.create_payment_covenant(
&format!("{}_recovery", vault_state.vault_id),
&recovery_outputs,
None,
)?;
let mut new_state = vault_state.clone();
new_state.state = VaultLifecycle::Recovered;
// Save vault state
self.save_vault(&new_state)?;
info!("Recovery initiated for vault: {}", vault_state.vault_id);
Ok(new_state)
}
}
/// Check if withdrawal is eligible based on block height
///
/// # Arguments
///
/// * `vault_state` - Current vault state
/// * `current_block_height` - Current blockchain height
///
/// # Returns
///
/// `true` if withdrawal is allowed, `false` otherwise
pub fn check_withdrawal_eligibility(
vault_state: &VaultState,
current_block_height: u64,
) -> bool {
if let Some(available_at) = vault_state.withdrawal_available_at {
current_block_height >= available_at
} else {
// If no delay configured, always eligible (for non-unvault vaults)
!vault_state.config.require_unvault
}
}
/// Update vault state after unvault transaction is confirmed
///
/// Sets the withdrawal_available_at block height based on delay.
///
/// # Arguments
///
/// * `vault_state` - Current vault state
/// * `unvault_tx_hash` - Hash of confirmed unvault transaction
/// * `unvault_block_height` - Block height where unvault was confirmed
///
/// # Returns
///
/// Updated vault state
pub fn mark_unvaulted(
&self,
vault_state: &VaultState,
unvault_tx_hash: Hash,
unvault_block_height: u64,
) -> Result<VaultState, PaymentError> {
let mut new_state = vault_state.clone();
new_state.unvault_tx_hash = Some(unvault_tx_hash);
new_state.withdrawal_available_at =
Some(unvault_block_height + vault_state.config.withdrawal_delay_blocks as u64);
new_state.state = VaultLifecycle::Unvaulted {
unvaulted_at_block: unvault_block_height,
};
// Save vault state
self.save_vault(&new_state)?;
info!(
"Vault {} unvaulted at block {}, withdrawal available at block {}",
vault_state.vault_id,
unvault_block_height,
new_state.withdrawal_available_at.unwrap()
);
Ok(new_state)
}
}
impl Default for VaultEngine {
fn default() -> Self {
Self::new(Arc::new(CovenantEngine::new()))
}
}