Skip to main content

fedimint_wallet_server/
db.rs

1use bitcoin::secp256k1::ecdsa::Signature;
2use bitcoin::{BlockHash, OutPoint, TxOut, Txid};
3use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
4use fedimint_core::encoding::{Decodable, Encodable};
5use fedimint_core::module::ModuleConsensusVersion;
6use fedimint_core::{PeerId, impl_db_lookup, impl_db_record};
7use fedimint_server_core::migration::{
8    ModuleHistoryItem, ServerModuleDbMigrationFnContext, ServerModuleDbMigrationFnContextExt as _,
9};
10use futures::StreamExt;
11use serde::Serialize;
12use strum_macros::EnumIter;
13
14use crate::common::{RecoveryItem, WalletInput};
15use crate::{
16    PEG_OUT_CHANGE_VOUT, PendingTransaction, SpendableUTXO, UnsignedTransaction, Wallet,
17    WalletOutputOutcome,
18};
19
20#[repr(u8)]
21#[derive(Clone, EnumIter, Debug)]
22pub enum DbKeyPrefix {
23    BlockHash = 0x30,
24    Utxo = 0x31,
25    BlockCountVote = 0x32,
26    FeeRateVote = 0x33,
27    UnsignedTransaction = 0x34,
28    PendingTransaction = 0x35,
29    PegOutTxSigCi = 0x36,
30    PegOutBitcoinOutPoint = 0x37,
31    PegOutNonce = 0x38,
32    ClaimedPegInOutpoint = 0x39,
33    ConsensusVersionVote = 0x40,
34    UnspentTxOut = 0x41,
35    ConsensusVersionVotingActivation = 0x42,
36    // Note: this key was added in 0.8, and it is not guaranteed
37    // to be present for all past processed blocks, unless Federation
38    // was started with fedimint 0.8 or later
39    BlockHashByHeight = 0x43,
40    RecoveryItem = 0x44,
41}
42
43impl std::fmt::Display for DbKeyPrefix {
44    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
45        write!(f, "{self:?}")
46    }
47}
48
49#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
50pub struct BlockHashKey(pub BlockHash);
51
52#[derive(Clone, Debug, Encodable, Decodable)]
53pub struct BlockHashKeyPrefix;
54
55impl_db_record!(
56    key = BlockHashKey,
57    value = (),
58    db_prefix = DbKeyPrefix::BlockHash,
59);
60impl_db_lookup!(key = BlockHashKey, query_prefix = BlockHashKeyPrefix);
61
62/// Note: only added in 0.8 and not backfilled. See
63/// [`DbKeyPrefix::BlockHashByHeight`]
64#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
65pub struct BlockHashByHeightKey(pub u32);
66
67#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
68pub struct BlockHashByHeightValue(pub BlockHash);
69
70#[derive(Clone, Debug, Encodable, Decodable)]
71pub struct BlockHashByHeightKeyPrefix;
72
73impl_db_record!(
74    key = BlockHashByHeightKey,
75    value = BlockHashByHeightValue,
76    db_prefix = DbKeyPrefix::BlockHashByHeight,
77);
78impl_db_lookup!(
79    key = BlockHashByHeightKey,
80    query_prefix = BlockHashByHeightKeyPrefix
81);
82
83#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize)]
84pub struct UTXOKey(pub bitcoin::OutPoint);
85
86#[derive(Clone, Debug, Encodable, Decodable)]
87pub struct UTXOPrefixKey;
88
89impl_db_record!(
90    key = UTXOKey,
91    value = SpendableUTXO,
92    db_prefix = DbKeyPrefix::Utxo,
93);
94impl_db_lookup!(key = UTXOKey, query_prefix = UTXOPrefixKey);
95
96#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
97pub struct UnsignedTransactionKey(pub Txid);
98
99#[derive(Clone, Debug, Encodable, Decodable)]
100pub struct UnsignedTransactionPrefixKey;
101
102impl_db_record!(
103    key = UnsignedTransactionKey,
104    value = UnsignedTransaction,
105    db_prefix = DbKeyPrefix::UnsignedTransaction,
106);
107impl_db_lookup!(
108    key = UnsignedTransactionKey,
109    query_prefix = UnsignedTransactionPrefixKey
110);
111
112#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
113pub struct PendingTransactionKey(pub Txid);
114
115#[derive(Clone, Debug, Encodable, Decodable)]
116pub struct PendingTransactionPrefixKey;
117
118impl_db_record!(
119    key = PendingTransactionKey,
120    value = PendingTransaction,
121    db_prefix = DbKeyPrefix::PendingTransaction,
122);
123impl_db_lookup!(
124    key = PendingTransactionKey,
125    query_prefix = PendingTransactionPrefixKey
126);
127
128#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
129pub struct PegOutTxSignatureCI(pub Txid);
130
131#[derive(Clone, Debug, Encodable, Decodable)]
132pub struct PegOutTxSignatureCIPrefix;
133
134impl_db_record!(
135    key = PegOutTxSignatureCI,
136    value = Vec<Signature>,
137    db_prefix = DbKeyPrefix::PegOutTxSigCi,
138);
139impl_db_lookup!(
140    key = PegOutTxSignatureCI,
141    query_prefix = PegOutTxSignatureCIPrefix
142);
143
144#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
145pub struct PegOutBitcoinTransaction(pub fedimint_core::OutPoint);
146
147#[derive(Clone, Debug, Encodable, Decodable)]
148pub struct PegOutBitcoinTransactionPrefix;
149
150impl_db_record!(
151    key = PegOutBitcoinTransaction,
152    value = WalletOutputOutcome,
153    db_prefix = DbKeyPrefix::PegOutBitcoinOutPoint,
154);
155
156impl_db_lookup!(
157    key = PegOutBitcoinTransaction,
158    query_prefix = PegOutBitcoinTransactionPrefix
159);
160
161#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
162pub struct BlockCountVoteKey(pub PeerId);
163
164#[derive(Clone, Debug, Encodable, Decodable)]
165pub struct BlockCountVotePrefix;
166
167impl_db_record!(
168    key = BlockCountVoteKey,
169    value = u32,
170    db_prefix = DbKeyPrefix::BlockCountVote
171);
172
173impl_db_lookup!(key = BlockCountVoteKey, query_prefix = BlockCountVotePrefix);
174
175#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
176pub struct FeeRateVoteKey(pub PeerId);
177
178#[derive(Clone, Debug, Encodable, Decodable)]
179pub struct FeeRateVotePrefix;
180
181impl_db_record!(
182    key = FeeRateVoteKey,
183    value = fedimint_core::Feerate,
184    db_prefix = DbKeyPrefix::FeeRateVote
185);
186
187impl_db_lookup!(key = FeeRateVoteKey, query_prefix = FeeRateVotePrefix);
188
189#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
190pub struct ConsensusVersionVoteKey(pub PeerId);
191
192#[derive(Clone, Debug, Encodable, Decodable)]
193pub struct ConsensusVersionVotePrefix;
194
195impl_db_record!(
196    key = ConsensusVersionVoteKey,
197    value = ModuleConsensusVersion,
198    db_prefix = DbKeyPrefix::ConsensusVersionVote
199);
200
201impl_db_lookup!(
202    key = ConsensusVersionVoteKey,
203    query_prefix = ConsensusVersionVotePrefix
204);
205
206#[derive(Clone, Debug, Encodable, Decodable)]
207pub struct PegOutNonceKey;
208
209impl_db_record!(
210    key = PegOutNonceKey,
211    value = u64,
212    db_prefix = DbKeyPrefix::PegOutNonce
213);
214
215#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize)]
216pub struct ClaimedPegInOutpointKey(pub OutPoint);
217
218#[derive(Clone, Debug, Encodable, Decodable)]
219pub struct ClaimedPegInOutpointPrefixKey;
220
221impl_db_record!(
222    key = ClaimedPegInOutpointKey,
223    value = (),
224    db_prefix = DbKeyPrefix::ClaimedPegInOutpoint,
225);
226impl_db_lookup!(
227    key = ClaimedPegInOutpointKey,
228    query_prefix = ClaimedPegInOutpointPrefixKey
229);
230
231/// Migrate to v1, backfilling all previously pegged-in outpoints
232pub async fn migrate_to_v1(
233    mut ctx: ServerModuleDbMigrationFnContext<'_, Wallet>,
234) -> Result<(), anyhow::Error> {
235    let outpoints = ctx
236        .get_typed_module_history_stream()
237        .await
238        .filter_map(|item| async {
239            match item {
240                ModuleHistoryItem::Input(input) => {
241                    let outpoint = input
242                        .maybe_v0_ref()
243                        .expect("can only support V0 wallet inputs")
244                        .0
245                        .outpoint();
246
247                    Some(outpoint)
248                }
249                ModuleHistoryItem::Output(_, _) | ModuleHistoryItem::ConsensusItem(_) => None,
250            }
251        })
252        .collect::<Vec<_>>()
253        .await;
254
255    let mut dbtx = ctx.dbtx();
256    for outpoint in outpoints {
257        dbtx.insert_new_entry(&ClaimedPegInOutpointKey(outpoint), &())
258            .await;
259    }
260
261    Ok(())
262}
263
264#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
265pub struct UnspentTxOutKey(pub bitcoin::OutPoint);
266
267#[derive(Clone, Debug, Encodable, Decodable)]
268pub struct UnspentTxOutPrefix;
269
270impl_db_record!(
271    key = UnspentTxOutKey,
272    value = TxOut,
273    db_prefix = DbKeyPrefix::UnspentTxOut,
274);
275impl_db_lookup!(key = UnspentTxOutKey, query_prefix = UnspentTxOutPrefix);
276
277#[derive(Clone, Debug, Encodable, Decodable, Serialize)]
278pub struct ConsensusVersionVotingActivationKey;
279
280#[derive(Clone, Debug, Encodable, Decodable)]
281pub struct ConsensusVersionVotingActivationPrefix;
282
283impl_db_record!(
284    key = ConsensusVersionVotingActivationKey,
285    value = (),
286    db_prefix = DbKeyPrefix::ConsensusVersionVotingActivation,
287);
288impl_db_lookup!(
289    key = ConsensusVersionVotingActivationKey,
290    query_prefix = ConsensusVersionVotingActivationPrefix
291);
292
293#[derive(Debug, Clone, Copy, Encodable, Decodable, Serialize)]
294pub struct RecoveryItemKey(pub u64);
295
296#[derive(Debug, Encodable, Decodable)]
297pub struct RecoveryItemKeyPrefix;
298
299impl_db_record!(
300    key = RecoveryItemKey,
301    value = RecoveryItem,
302    db_prefix = DbKeyPrefix::RecoveryItem,
303);
304impl_db_lookup!(key = RecoveryItemKey, query_prefix = RecoveryItemKeyPrefix);
305
306/// Migrate to v2, backfilling recovery items from module history
307pub async fn migrate_to_v2(
308    mut ctx: ServerModuleDbMigrationFnContext<'_, Wallet>,
309) -> Result<(), anyhow::Error> {
310    let mut recovery_items = Vec::new();
311    let mut stream = ctx.get_typed_module_history_stream().await;
312
313    while let Some(history_item) = stream.next().await {
314        if let ModuleHistoryItem::Input(input) = history_item {
315            let (outpoint, script) = match &input {
316                WalletInput::V0(input) => {
317                    (input.0.outpoint(), input.tx_output().script_pubkey.clone())
318                }
319                WalletInput::V1(input) => (input.outpoint, input.tx_out.script_pubkey.clone()),
320                WalletInput::Default { .. } => continue,
321            };
322            recovery_items.push(RecoveryItem::Input { outpoint, script });
323        }
324    }
325
326    drop(stream);
327
328    for (index, item) in recovery_items.into_iter().enumerate() {
329        ctx.dbtx()
330            .insert_new_entry(&RecoveryItemKey(index as u64), &item)
331            .await;
332    }
333
334    Ok(())
335}
336
337/// Migrate to v3, backfilling the change outputs of all peg-outs made so far.
338///
339/// Change outputs pay to the peg-in descriptor and so are indistinguishable
340/// from user deposits; going forward they are recorded as claimed when the
341/// peg-out is created, but transactions built before that change have no such
342/// record. Every peg-out we ever made is still listed under
343/// [`PegOutBitcoinTransaction`], which is never removed, so the set is
344/// recoverable in full.
345pub async fn migrate_to_v3(
346    mut ctx: ServerModuleDbMigrationFnContext<'_, Wallet>,
347) -> Result<(), anyhow::Error> {
348    let mut dbtx = ctx.dbtx();
349
350    let change_outpoints = dbtx
351        .find_by_prefix(&PegOutBitcoinTransactionPrefix)
352        .await
353        .map(|(_, outcome)| {
354            let WalletOutputOutcome::V0(outcome) = outcome else {
355                // Only V0 outcomes have ever been written, but an unknown variant
356                // carries no txid we could derive a change outpoint from.
357                return None;
358            };
359
360            Some(OutPoint {
361                txid: outcome.0,
362                vout: PEG_OUT_CHANGE_VOUT,
363            })
364        })
365        .collect::<Vec<_>>()
366        .await;
367
368    for outpoint in change_outpoints.into_iter().flatten() {
369        dbtx.insert_entry(&ClaimedPegInOutpointKey(outpoint), &())
370            .await;
371    }
372
373    Ok(())
374}