From ef746a09343ac89f23406610517db6ce48e19478 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Sun, 30 Jun 2024 19:14:02 +0800 Subject: [PATCH] chore: improve UTXOs burning task --- src/ck-doge-minter/ck-doge-minter.did | 9 +- src/ck-doge-minter/src/api_init.rs | 18 +-- src/ck-doge-minter/src/api_query.rs | 10 +- src/ck-doge-minter/src/api_update.rs | 26 +++- src/ck-doge-minter/src/lib.rs | 2 +- src/ck-doge-minter/src/store.rs | 168 ++++++++++++--------- src/ck-doge-minter/src/{job.rs => task.rs} | 0 src/ck-doge-minter/src/types.rs | 1 + 8 files changed, 143 insertions(+), 91 deletions(-) rename src/ck-doge-minter/src/{job.rs => task.rs} (100%) diff --git a/src/ck-doge-minter/ck-doge-minter.did b/src/ck-doge-minter/ck-doge-minter.did index 5a41d48..51d04f6 100644 --- a/src/ck-doge-minter/ck-doge-minter.did +++ b/src/ck-doge-minter/ck-doge-minter.did @@ -1,6 +1,7 @@ type BurnInput = record { fee_rate : nat64; address : text; amount : nat64 }; type BurnOutput = record { tip_height : nat64; + block_index : nat64; txid : blob; instructions : nat64; }; @@ -37,10 +38,15 @@ type Result_3 = variant { Ok : State; Err }; type Result_4 = variant { Ok : vec MintedUtxo; Err : text }; type Result_5 = variant { Ok : MintOutput; Err : text }; type State = record { + tokens_minted_count : nat64; ecdsa_key_name : opt text; managers : vec principal; + burning_utxos : vec record { + nat64; + record { principal; blob; nat64; nat64; text }; + }; chain : text; - utxos_retry_burning_queue : vec record { nat64; blob; nat64; nat64; nat8 }; + tokens_burned_count : nat64; collected_utxos : nat64; tokens_burned : nat64; accounts : nat64; @@ -66,5 +72,6 @@ service : (opt MinterArgs) -> { list_collected_utxos : (nat64, nat16) -> (vec CollectedUtxo) query; list_minted_utxos : (opt principal) -> (Result_4) query; mint_ckdoge : () -> (Result_5); + retry_burn_ckdoge : (nat64, opt nat64) -> (Result_1); validate_admin_set_managers : (vec principal) -> (Result); } diff --git a/src/ck-doge-minter/src/api_init.rs b/src/ck-doge-minter/src/api_init.rs index ea82dfa..4d8695d 100644 --- a/src/ck-doge-minter/src/api_init.rs +++ b/src/ck-doge-minter/src/api_init.rs @@ -2,7 +2,7 @@ use candid::{CandidType, Principal}; use serde::Deserialize; use std::time::Duration; -use crate::{job, store}; +use crate::{store, task}; #[derive(Clone, Debug, CandidType, Deserialize)] pub enum MinterArgs { @@ -40,12 +40,12 @@ fn init(args: Option) { ic_cdk::spawn(store::state::init_ecdsa_public_key()) }); - ic_cdk_timers::set_timer_interval(Duration::from_secs(job::FINALIZE_BURNING_INTERVAL), || { - ic_cdk::spawn(job::finalize_burning()) + ic_cdk_timers::set_timer_interval(Duration::from_secs(task::FINALIZE_BURNING_INTERVAL), || { + ic_cdk::spawn(task::finalize_burning()) }); - ic_cdk_timers::set_timer_interval(Duration::from_secs(job::CLEAR_UTXOS_INTERVAL), || { - ic_cdk::spawn(job::collect_and_clear_utxos()) + ic_cdk_timers::set_timer_interval(Duration::from_secs(task::CLEAR_UTXOS_INTERVAL), || { + ic_cdk::spawn(task::collect_and_clear_utxos()) }); } @@ -83,11 +83,11 @@ fn post_upgrade(args: Option) { _ => {} } - ic_cdk_timers::set_timer_interval(Duration::from_secs(job::FINALIZE_BURNING_INTERVAL), || { - ic_cdk::spawn(job::finalize_burning()) + ic_cdk_timers::set_timer_interval(Duration::from_secs(task::FINALIZE_BURNING_INTERVAL), || { + ic_cdk::spawn(task::finalize_burning()) }); - ic_cdk_timers::set_timer_interval(Duration::from_secs(job::CLEAR_UTXOS_INTERVAL), || { - ic_cdk::spawn(job::collect_and_clear_utxos()) + ic_cdk_timers::set_timer_interval(Duration::from_secs(task::CLEAR_UTXOS_INTERVAL), || { + ic_cdk::spawn(task::collect_and_clear_utxos()) }); } diff --git a/src/ck-doge-minter/src/api_query.rs b/src/ck-doge-minter/src/api_query.rs index 70606d8..6a79266 100644 --- a/src/ck-doge-minter/src/api_query.rs +++ b/src/ck-doge-minter/src/api_query.rs @@ -1,6 +1,6 @@ use candid::{CandidType, Principal}; use dogecoin::canister; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use crate::{ is_authenticated, is_controller_or_manager, minter_account, store, types, user_account, @@ -17,6 +17,8 @@ pub struct State { pub tokens_minted: u64, pub tokens_burned: u64, + pub tokens_minted_count: u64, + pub tokens_burned_count: u64, pub accounts: u64, pub collected_utxos: u64, pub burned_utxos: u64, @@ -25,7 +27,7 @@ pub struct State { pub managers: BTreeSet, // manager info pub ecdsa_key_name: Option, - pub utxos_retry_burning_queue: Vec<(u64, canister::Address, u64, u64, u8)>, + pub burning_utxos: BTreeMap, pub minter_address: Option, pub minter_subaddress: Option, } @@ -37,6 +39,8 @@ fn get_state() -> Result { chain: s.chain_params().chain_name.to_string(), tokens_minted: s.tokens_minted, tokens_burned: s.tokens_burned, + tokens_minted_count: s.tokens_minted_count, + tokens_burned_count: s.tokens_burned_count, accounts: store::state::get_accounts_len(), collected_utxos: store::state::get_collected_utxos_len(), burned_utxos: store::state::get_burned_utxos_len(), @@ -48,7 +52,7 @@ fn get_state() -> Result { if is_controller_or_manager().is_ok() { res.ecdsa_key_name = Some(s.ecdsa_key_name.clone()); - res.utxos_retry_burning_queue = s.utxos_retry_burning_queue.clone().into(); + res.burning_utxos = s.burning_utxos.clone(); res.minter_address = s.get_address(&minter_account()).map(|v| v.to_string()).ok(); res.minter_subaddress = s .get_address(&user_account(&ic_cdk::id())) diff --git a/src/ck-doge-minter/src/api_update.rs b/src/ck-doge-minter/src/api_update.rs index 70dfefa..d34b98a 100644 --- a/src/ck-doge-minter/src/api_update.rs +++ b/src/ck-doge-minter/src/api_update.rs @@ -1,4 +1,4 @@ -use crate::{is_authenticated, store, types}; +use crate::{is_authenticated, is_controller_or_manager, store, types}; #[ic_cdk::update(guard = "is_authenticated")] async fn mint_ckdoge() -> Result { @@ -11,12 +11,22 @@ async fn mint_ckdoge() -> Result { #[ic_cdk::update(guard = "is_authenticated")] async fn burn_ckdoge(args: types::BurnInput) -> Result { - let res = - store::burn_ckdoge(ic_cdk::caller(), args.address, args.amount, args.fee_rate).await?; + store::burn_ckdoge(ic_cdk::caller(), args.address, args.amount, args.fee_rate).await +} - Ok(types::BurnOutput { - txid: res.txid, - tip_height: res.tip_height, - instructions: ic_cdk::api::performance_counter(1), - }) +#[ic_cdk::update(guard = "is_authenticated")] +async fn retry_burn_ckdoge( + block_index: u64, + fee_rate: Option, +) -> Result { + store::retry_burn_utxos( + block_index, + fee_rate, + if is_controller_or_manager().is_ok() { + None + } else { + Some(ic_cdk::caller()) + }, + ) + .await } diff --git a/src/ck-doge-minter/src/lib.rs b/src/ck-doge-minter/src/lib.rs index ff74f8b..2e0e596 100644 --- a/src/ck-doge-minter/src/lib.rs +++ b/src/ck-doge-minter/src/lib.rs @@ -15,9 +15,9 @@ mod api_query; mod api_update; mod chain; mod ecdsa; -mod job; mod ledger; mod store; +mod task; mod types; static ANONYMOUS: Principal = Principal::anonymous(); diff --git a/src/ck-doge-minter/src/store.rs b/src/ck-doge-minter/src/store.rs index f048825..587e58e 100644 --- a/src/ck-doge-minter/src/store.rs +++ b/src/ck-doge-minter/src/store.rs @@ -22,7 +22,6 @@ use std::{ cell::RefCell, collections::{BTreeMap, BTreeSet, VecDeque}, ops, - time::Duration, }; use crate::{ @@ -58,9 +57,14 @@ pub struct State { pub managers: BTreeSet, - // VecDeque<(block_index, receiver, amount, fee_rate, retry)> #[serde(default)] - pub utxos_retry_burning_queue: VecDeque<(u64, canister::Address, u64, u64, u8)>, + pub tokens_minted_count: u64, + #[serde(default)] + pub tokens_burned_count: u64, + + // BTreeMap + #[serde(default)] + pub burning_utxos: BTreeMap, // VecDeque<(block_index, timestamp_ms)> #[serde(default)] @@ -385,6 +389,10 @@ pub async fn mint_ckdoge(caller: Principal) -> Result { } .await; + state::with_mut(|s| { + s.tokens_minted_count = s.tokens_minted_count.saturating_add(1); + }); + match res { Ok(_) => Ok(total_amount), Err(err) => { @@ -406,7 +414,7 @@ pub async fn burn_ckdoge( address: String, amount: u64, fee_rate: u64, -) -> Result { +) -> Result { if amount < DUST_LIMIT * 10 { return Err("amount is too small".to_string()); } @@ -466,6 +474,7 @@ pub async fn burn_ckdoge( state::with_mut(|s| { s.tokens_burned = s.tokens_burned.saturating_add(amount); + s.tokens_burned_count = s.tokens_burned_count.saturating_add(1); }); COLLECTED_UTXOS_HEAP.with(|r| { @@ -476,25 +485,96 @@ pub async fn burn_ckdoge( } }); - match burn_utxos(block_index, receiver.clone(), amount, fee_rate, utxos).await { - Ok(res) => Ok(res), + state::with_mut(|s| { + s.burning_utxos.insert( + block_index, + ( + caller, + receiver.clone().into(), + amount, + fee_rate, + "".to_string(), + ), + ) + }); + + match burn_utxos(block_index, receiver, amount, fee_rate, utxos).await { + Ok(res) => { + state::with_mut(|s| s.burning_utxos.remove(&block_index)); + + Ok(types::BurnOutput { + block_index, + txid: res.txid, + tip_height: res.tip_height, + instructions: ic_cdk::api::performance_counter(1), + }) + } Err(err) => { state::with_mut(|s| { - s.utxos_retry_burning_queue.push_back(( - block_index, - receiver.into(), - amount, - fee_rate, - 0, - )); + s.burning_utxos + .entry(block_index) + .and_modify(|v| v.4.clone_from(&err)); }); + Err(format!( + "burn_utxos failed: {err}, block_index: {block_index}" + )) + } + } +} - // retry burn utxos after 30 seconds - ic_cdk_timers::set_timer( - Duration::from_secs(30), - || ic_cdk::spawn(retry_burn_utxos()), - ); - Err(err) +// we can retry burn utxos if it failed in the previous burn_ckdoge call +pub async fn retry_burn_utxos( + block_index: u64, + new_fee_rate: Option, + caller: Option, +) -> Result { + let (owner, receiver, amount, fee_rate, _) = + state::with(|s| s.burning_utxos.get(&block_index).cloned()) + .ok_or_else(|| format!("no burning utxos found for block_index: {block_index}"))?; + + if let Some(caller) = caller { + if caller != owner { + return Err("caller is not the owner of the burning utxos".to_string()); + } + } + + let new_fee_rate = new_fee_rate.unwrap_or(fee_rate); + + let utxos = COLLECTED_UTXOS_HEAP.with(|r| { + let m = r.borrow(); + let mut utxos: Vec<(UtxoState, Principal)> = vec![]; + for (utxo, v) in m.iter() { + if v.1 == block_index && v.2 == 0 { + utxos.push((utxo.clone(), v.0)); + } + } + utxos + }); + + if utxos.is_empty() { + return Err(format!("no utxos found for block_index: {block_index}")); + } + + match burn_utxos(block_index, receiver.into(), amount, new_fee_rate, utxos).await { + Ok(res) => { + state::with_mut(|s| s.burning_utxos.remove(&block_index)); + + Ok(types::BurnOutput { + block_index, + txid: res.txid, + tip_height: res.tip_height, + instructions: ic_cdk::api::performance_counter(1), + }) + } + Err(err) => { + state::with_mut(|s| { + s.burning_utxos + .entry(block_index) + .and_modify(|v| v.4.clone_from(&err)); + }); + Err(format!( + "burn_utxos failed: {err}, block_index: {block_index}" + )) } } } @@ -548,56 +628,6 @@ pub fn list_burned_utxos(start_index: u64, take: usize) -> Vec = vec![]; - for (utxo, v) in m.iter() { - if v.1 == block_index && v.2 == 0 { - utxos.push((utxo.clone(), v.0)); - } - } - utxos - }); - - if burn_utxos( - block_index, - receiver.clone().into(), - amount, - fee_rate, - utxos, - ) - .await - .is_err() - && retry < 3 - { - state::with_mut(|s| { - s.utxos_retry_burning_queue.push_back(( - block_index, - receiver, - amount, - fee_rate, - retry + 1, - )); - }); - } - - // retry burn utxos after 30 seconds - ic_cdk_timers::set_timer( - Duration::from_secs(30), - || ic_cdk::spawn(retry_burn_utxos()), - ); - } -} - pub async fn collect_and_clear_utxos() -> Result { let minter = ic_cdk::id(); let acc = user_account(&minter); diff --git a/src/ck-doge-minter/src/job.rs b/src/ck-doge-minter/src/task.rs similarity index 100% rename from src/ck-doge-minter/src/job.rs rename to src/ck-doge-minter/src/task.rs diff --git a/src/ck-doge-minter/src/types.rs b/src/ck-doge-minter/src/types.rs index c9b1b2a..c405ca5 100644 --- a/src/ck-doge-minter/src/types.rs +++ b/src/ck-doge-minter/src/types.rs @@ -47,6 +47,7 @@ pub struct BurnInput { #[derive(CandidType, Clone, Debug, Serialize, Deserialize)] pub struct BurnOutput { + pub block_index: u64, pub txid: Txid, pub tip_height: u64, pub instructions: u64,