Fix CARROT commitment storage, add spend-time fallback, fix sync height race
1. Pipeline refactor regression: store_found_output_row passed outputs: vec![]
so commitment and per-output unlock_time were never stored. Add both fields
to FoundOutputInfo and forward from parsed TxOutput to storage.
2. Spend-time fallback: recompute commitment from pedersen_commit(amount, mask)
when not stored, matching C++ wallet2 approach. Existing wallets work without
resync.
3. Progress callback reported batch_end (dispatched to store worker) while
sync_height() read from DB (committed by store worker), causing the app to
see height jump backwards. Now both report committed_height.
This commit is contained in:
@@ -81,9 +81,19 @@ impl<'a> TxPipeline<'a> {
|
||||
.as_deref()
|
||||
.ok_or("CARROT output missing shared_secret")?,
|
||||
)?;
|
||||
let commitment = hex_to_32(
|
||||
output.commitment.as_deref().ok_or("CARROT output missing commitment")?,
|
||||
)?;
|
||||
let commitment = if let Some(c) = output.commitment.as_deref() {
|
||||
hex_to_32(c)?
|
||||
} else {
|
||||
// Fallback: recompute from mask + amount (matches C++ wallet2).
|
||||
let mask_hex = output.mask.as_deref().ok_or("CARROT output missing mask")?;
|
||||
let mask = hex_to_32(mask_hex)?;
|
||||
let amount =
|
||||
output.amount.parse::<u64>().map_err(|e| format!("bad amount: {e}"))?;
|
||||
let c = salvium_crypto::pedersen_commit(&amount.to_le_bytes(), &mask);
|
||||
let mut arr = [0u8; 32];
|
||||
arr.copy_from_slice(&c[..32]);
|
||||
arr
|
||||
};
|
||||
let (sk_x, sk_y) = salvium_crypto::carrot_scan::derive_carrot_spend_keys(
|
||||
&carrot_prove_spend,
|
||||
&keys.carrot.generate_image_key,
|
||||
|
||||
@@ -644,9 +644,19 @@ async fn build_sign_maybe_broadcast(
|
||||
.ok_or("CARROT output missing shared_secret")?,
|
||||
)?;
|
||||
|
||||
let commitment = hex_to_32(
|
||||
output_row.commitment.as_deref().ok_or("CARROT output missing commitment")?,
|
||||
)?;
|
||||
let commitment = if let Some(c) = output_row.commitment.as_deref() {
|
||||
hex_to_32(c)?
|
||||
} else {
|
||||
// Fallback: recompute commitment from mask + amount (matches C++ wallet2 approach).
|
||||
let mask_hex = output_row.mask.as_deref().ok_or("CARROT output missing mask")?;
|
||||
let mask = hex_to_32(mask_hex)?;
|
||||
let amount =
|
||||
output_row.amount.parse::<u64>().map_err(|e| format!("bad amount: {e}"))?;
|
||||
let c = salvium_crypto::pedersen_commit(&amount.to_le_bytes(), &mask);
|
||||
let mut arr = [0u8; 32];
|
||||
arr.copy_from_slice(&c[..32]);
|
||||
arr
|
||||
};
|
||||
|
||||
// Adjust keys for subaddress outputs.
|
||||
let (adj_gik, adj_psk) = salvium_crypto::subaddress::carrot_adjust_keys_for_subaddress(
|
||||
|
||||
@@ -55,7 +55,7 @@ impl ScanContext {
|
||||
}
|
||||
|
||||
/// A single transaction output ready for scanning.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TxOutput {
|
||||
/// Output index within the transaction.
|
||||
pub index: u32,
|
||||
|
||||
@@ -127,7 +127,7 @@ struct StoreResultMsg {
|
||||
/// Block hashes from the committed batch (for reorg checking without DB lock).
|
||||
block_hashes: Vec<(u64, String)>,
|
||||
/// The highest block height committed in this batch.
|
||||
_committed_height: u64,
|
||||
committed_height: u64,
|
||||
/// Number of outputs found in this batch.
|
||||
outputs_found: usize,
|
||||
/// Number of parse errors in this batch.
|
||||
@@ -205,6 +205,7 @@ impl SyncEngine {
|
||||
}
|
||||
|
||||
let mut current = sync_height as u64;
|
||||
let mut committed_height = sync_height as u64;
|
||||
let mut total_outputs_found = 0usize;
|
||||
let mut total_parse_errors = 0usize;
|
||||
let mut total_empty_blobs = 0usize;
|
||||
@@ -245,6 +246,7 @@ impl SyncEngine {
|
||||
apply_store_result(
|
||||
scan_ctx,
|
||||
&mut block_hash_cache,
|
||||
&mut committed_height,
|
||||
&mut total_outputs_found,
|
||||
&mut total_parse_errors,
|
||||
&mut total_empty_blobs,
|
||||
@@ -316,6 +318,7 @@ impl SyncEngine {
|
||||
apply_store_result(
|
||||
scan_ctx,
|
||||
&mut block_hash_cache,
|
||||
&mut committed_height,
|
||||
&mut total_outputs_found,
|
||||
&mut total_parse_errors,
|
||||
&mut total_empty_blobs,
|
||||
@@ -397,6 +400,7 @@ impl SyncEngine {
|
||||
apply_store_result(
|
||||
scan_ctx,
|
||||
&mut block_hash_cache,
|
||||
&mut committed_height,
|
||||
&mut total_outputs_found,
|
||||
&mut total_parse_errors,
|
||||
&mut total_empty_blobs,
|
||||
@@ -473,10 +477,12 @@ impl SyncEngine {
|
||||
current = batch_end;
|
||||
|
||||
// ── 5. Progress event (one per batch) ───────────────────────
|
||||
// Report committed_height (what the store worker has persisted)
|
||||
// so it stays consistent with salvium_wallet_sync_height().
|
||||
if let Some(tx) = event_tx {
|
||||
let _ = tx
|
||||
.send(SyncEvent::Progress {
|
||||
current_height: current,
|
||||
current_height: committed_height,
|
||||
target_height: top_block,
|
||||
outputs_found: total_outputs_found,
|
||||
parse_errors: total_parse_errors,
|
||||
@@ -504,6 +510,7 @@ impl SyncEngine {
|
||||
if let Err(e) = apply_store_result(
|
||||
scan_ctx,
|
||||
&mut block_hash_cache,
|
||||
&mut committed_height,
|
||||
&mut total_outputs_found,
|
||||
&mut total_parse_errors,
|
||||
&mut total_empty_blobs,
|
||||
@@ -546,6 +553,10 @@ struct FoundOutputInfo {
|
||||
block_height: u64,
|
||||
tx_type: u8,
|
||||
unlock_time: u64,
|
||||
/// Pedersen commitment (outPk) for this output — needed for CARROT spend key derivation.
|
||||
commitment: Option<[u8; 32]>,
|
||||
/// Per-output unlock_time from the on-chain output target struct.
|
||||
output_unlock_time: u64,
|
||||
}
|
||||
|
||||
/// Data from a regular (non-coinbase) transaction carried from the parallel
|
||||
@@ -645,6 +656,7 @@ fn parse_and_scan_block(
|
||||
{
|
||||
let found = scanner::scan_transaction(&scan_ctx, &scan_data);
|
||||
for fo in &found {
|
||||
let tx_out = scan_data.outputs.get(fo.output_index as usize);
|
||||
outputs.push((
|
||||
fo.clone(),
|
||||
FoundOutputInfo {
|
||||
@@ -655,6 +667,10 @@ fn parse_and_scan_block(
|
||||
block_height: height,
|
||||
tx_type: scan_data.tx_type,
|
||||
unlock_time: scan_data.unlock_time,
|
||||
commitment: tx_out.and_then(|o| o.commitment),
|
||||
output_unlock_time: tx_out
|
||||
.map(|o| o.unlock_time)
|
||||
.unwrap_or(scan_data.unlock_time),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -714,6 +730,7 @@ fn parse_and_scan_block(
|
||||
{
|
||||
let found = scanner::scan_transaction(&scan_ctx, &scan_data);
|
||||
for fo in &found {
|
||||
let tx_out = scan_data.outputs.get(fo.output_index as usize);
|
||||
outputs.push((
|
||||
fo.clone(),
|
||||
FoundOutputInfo {
|
||||
@@ -724,6 +741,10 @@ fn parse_and_scan_block(
|
||||
block_height: height,
|
||||
tx_type: scan_data.tx_type,
|
||||
unlock_time: scan_data.unlock_time,
|
||||
commitment: tx_out.and_then(|o| o.commitment),
|
||||
output_unlock_time: tx_out
|
||||
.map(|o| o.unlock_time)
|
||||
.unwrap_or(scan_data.unlock_time),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -781,6 +802,7 @@ fn parse_and_scan_block(
|
||||
let (tx_pub_key, tx_type, unlock_time) = if let Some(ref sd) = scan_result {
|
||||
let found = scanner::scan_transaction(&scan_ctx, sd);
|
||||
for fo in &found {
|
||||
let tx_out = sd.outputs.get(fo.output_index as usize);
|
||||
found_pairs.push((
|
||||
fo.clone(),
|
||||
FoundOutputInfo {
|
||||
@@ -791,6 +813,10 @@ fn parse_and_scan_block(
|
||||
block_height: height,
|
||||
tx_type: sd.tx_type,
|
||||
unlock_time: sd.unlock_time,
|
||||
commitment: tx_out.and_then(|o| o.commitment),
|
||||
output_unlock_time: tx_out
|
||||
.map(|o| o.unlock_time)
|
||||
.unwrap_or(sd.unlock_time),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -1073,7 +1099,7 @@ fn store_worker_loop(
|
||||
let _ = result_tx.send(StoreResultMsg {
|
||||
new_cn_subaddr_entries: new_entries,
|
||||
block_hashes: batch_result.block_hashes,
|
||||
_committed_height: batch_result.max_height,
|
||||
committed_height: batch_result.max_height,
|
||||
outputs_found: batch_result.outputs_found,
|
||||
parse_errors: batch_result.parse_errors,
|
||||
empty_blobs: batch_result.empty_blobs,
|
||||
@@ -1090,7 +1116,7 @@ fn store_worker_loop(
|
||||
let _ = result_tx.send(StoreResultMsg {
|
||||
new_cn_subaddr_entries: Vec::new(),
|
||||
block_hashes: Vec::new(),
|
||||
_committed_height: 0,
|
||||
committed_height: 0,
|
||||
outputs_found: 0,
|
||||
parse_errors: 0,
|
||||
empty_blobs: 0,
|
||||
@@ -1109,6 +1135,7 @@ fn store_worker_loop(
|
||||
fn apply_store_result(
|
||||
scan_ctx: &mut ScanContext,
|
||||
block_hash_cache: &mut std::collections::HashMap<u64, String>,
|
||||
committed_height: &mut u64,
|
||||
total_outputs_found: &mut usize,
|
||||
total_parse_errors: &mut usize,
|
||||
total_empty_blobs: &mut usize,
|
||||
@@ -1118,6 +1145,11 @@ fn apply_store_result(
|
||||
return Err(WalletError::Sync(format!("store worker error: {}", e)));
|
||||
}
|
||||
|
||||
// Update committed height to reflect what the store worker has actually persisted.
|
||||
if result.committed_height > *committed_height {
|
||||
*committed_height = result.committed_height;
|
||||
}
|
||||
|
||||
// Merge new cn_subaddress_map entries.
|
||||
for &(ko, major, minor) in &result.new_cn_subaddr_entries {
|
||||
if !scan_ctx.cn_subaddress_map.iter().any(|(k, _, _)| *k == ko) {
|
||||
@@ -1155,11 +1187,16 @@ fn store_found_output_row(
|
||||
info: &FoundOutputInfo,
|
||||
) -> Result<(), WalletError> {
|
||||
// Build a minimal ScanTxData for store_found_outputs compatibility.
|
||||
// Include a stub TxOutput at the correct index so commitment and
|
||||
// per-output unlock_time are available during storage.
|
||||
let mut outputs = vec![TxOutput::default(); found.output_index as usize + 1];
|
||||
outputs[found.output_index as usize].commitment = info.commitment;
|
||||
outputs[found.output_index as usize].unlock_time = info.output_unlock_time;
|
||||
let scan_data = ScanTxData {
|
||||
tx_hash: info.tx_hash,
|
||||
tx_pub_key: info.tx_pub_key,
|
||||
additional_pubkeys: vec![],
|
||||
outputs: vec![], // not needed for storage
|
||||
outputs,
|
||||
is_coinbase: info.is_coinbase,
|
||||
block_height: info.block_height,
|
||||
first_key_image: None,
|
||||
|
||||
Reference in New Issue
Block a user