The daemon's tx_sanity_check requires ≥80% unique ring member indices
across all inputs (when ≥10000 outputs exist). Our DecoySelector picked
decoys independently per input, causing heavy overlap from the gamma
distribution favoring recent outputs. Sweeps with many inputs failed
on mainnet but passed on testnet (which has <10000 outputs, skipping
the check entirely).
Add build_ring_excluding() that avoids indices already used by prior
inputs in the same transaction, with soft fallback after many attempts
to prevent exhaustion on small output pools.
The rebuild step could produce a TX with slightly different weight than
the first build (signing randomness), leaving the fee short. Now loops
until fee >= weight * fee_per_byte, converging in 1-2 iterations.
- Add native_asset field to CLI FeeContext, derived from hard_fork_info() RPC
- Add --asset CLI arg to 14 command variants (default: auto-detect)
- Thread asset_override through all 12+ transfer commands in transfers.rs
- Update multisig transfer_multisig() with same dynamic asset pattern
- FFI: extend detect_fork_params() to return native asset, auto-detect
in do_transfer/do_stake/do_sweep when caller omits asset_type
- Fix asset-type-specific index conversion in CLI tx_common fetch_decoys
(was using global indices, matching prior FFI fix)
- Version bump to 1.0.7-r015
Add weight/estimated_weight/fee_per_byte to the JSON result from
walletTransfer, walletStake, and walletSweep so Dart can dlog() them.
On daemon rejection, the error string now includes the diagnostic
numbers (est_weight, actual_weight, fee, fee_needed, fpb) so fee
issues are immediately visible without a Rust log callback.
The FFI path (build_sign_maybe_broadcast) had three bugs not present in
the integration test code which builds transactions directly:
1. Legacy CryptoNote outputs always got secret_key_y=None, but TCLSAG
(rct_type >= SALVIUM_ONE) requires secret_key_y=Some([0u8;32]) even
for non-CARROT inputs. C++ sets carrot_ctkey.y = rct::zero(). This
caused "input N requires secret_key_y for TCLSAG but has None" on
sweeps.
2. STAKE transactions never set unlock_time (defaulted to 0). The daemon
requires unlock_time = current_height + stake_lock_period, causing
"Failed (unknown)" rejections. do_stake() now queries daemon height
and uses network_config().stake_lock_period.
3. estimate_tx_size() underestimated by ~86 bytes for CARROT transactions.
Missing: return_address_list (32*n_outputs), return_address_change_mask
(n_outputs), per-output ephemeral keys in extra (2+32*n_outputs).
Old flat estimates (24+40=64 bytes) replaced with accurate per-field
accounting (~150 bytes for 2-output CARROT TX).
Also adds FEE DIAGNOSTIC log line in FFI that prints estimated vs actual
serialized weight after signing, making fee issues immediately visible.
The fee pipeline had three critical bugs causing fee_too_low rejections:
1. Missing fees[] array — daemon returns [Fl,Fn,Fm,Fh] tiers via
get_fee_estimate RPC but we only captured fee (= fees[0]), losing
all priority tier data.
2. Wrong priority handling — we applied priority.multiplier() (1x/5x/25x/
1000x) on top of fees[0]. C++ wallet2 uses fees[priority_index] directly
with tiers already baked in (1x/4x/custom/custom).
3. Silent fallback to FEE_PER_BYTE=30 — real fee is ~500-1200/byte, so
the fallback guaranteed rejection. Now propagates errors instead.
Changes: add fees[] to FeeEstimate, add tier_index() to FeePriority,
remove priority multiplier from estimate_tx_fee (fee_per_byte now
includes priority), add calculate_fee_from_weight matching C++ exactly,
update all callers across CLI/FFI/tests.
The dynamic fee calculation used an inverted formula
(constant/base_reward) instead of the daemon's actual check_fee
formula (base_reward * 3000 / median²). This caused fee_too_low
rejections on all transaction types.
- Add get_dynamic_base_fee() matching C++ Blockchain::get_dynamic_base_fee()
- Add get_dynamic_base_fee_estimate_2021_scaling() for 4-tier fee tiers
- Add round_money_up() matching C++ cryptonote::round_money_up()
- Update calculate_required_fee/validate_fee to use median_block_weight
- CLI/FFI wallet: use daemon get_fee_estimate RPC (matches C++ wallet)
- Add SCALING_2021_FEE_ROUNDING_PLACES and REWARD_BLOCKS_WINDOW constants
fee_quantization_mask() was computing 10^PER_KB_FEE_QUANTIZATION_DECIMALS - 1
(= 10^8 - 1 = 99,999,999), rounding every fee UP to the nearest 100,000,000
atomic units — a minimum of 1 whole SAL per transaction. The C++ formula is
10^(DISPLAY_DECIMAL_POINT - PER_KB_FEE_QUANTIZATION_DECIMALS) = 10^(8-8) = 1,
meaning no quantization. Also fixed the rounding formula to match C++
calculate_fee_from_weight(). Added sanity tests that assert fees stay well
under 1 SAL for standard transactions.
The daemon rejects transactions with fee_too_low because the wallet was
using a hardcoded FEE_PER_BYTE=30, while the daemon dynamically computes
its minimum from the block reward. This replicates the C++ formula
(blockchain.cpp get_dynamic_base_fee_estimate) as a pure local function
and applies fee quantization matching the daemon's rounding.
- Add dynamic_fee_per_byte(base_reward, hf_version) and
fee_quantization_mask() to salvium-tx/fee.rs
- Update estimate_tx_fee() to accept explicit fee_per_byte with
quantization (round up to 10^8 boundary)
- Replace adjust_priority() with resolve_fee_context() in both CLI
(tx_common.rs) and FFI (transfer.rs), combining priority adjustment
and fee rate computation in a single RPC roundtrip using
BlockHeader.reward and BlockHeader.major_version
- Add fee_per_byte field to TxPipeline
- Thread fee_per_byte through all 13 CLI transfer commands, multisig
transfer, and 5 FFI entry points (transfer, stake, stake_dry_run,
sweep, transfer_dry_run)
- Update all test files to pass explicit fee_per_byte values
Automatically downgrade fee priority from Normal (5x) to Low (1x)
when the network isn't busy, saving users fees. Matches the C++
wallet2::adjust_priority logic: check mempool backlog and recent
block fullness (>80% threshold) before deciding.
- Add FeePriority::Default variant that resolves at runtime
- Add adjust_priority() in both FFI and CLI paths
- Refactor CLI TxPipeline to use shared NodePool from AppContext
- Wire adjustment into all transfer commands (FFI and CLI)
Stake transactions were adding the stake amount as both a destination
and amount_burnt, causing the builder to require 2x the intended
balance. Remove the destination — amount_burnt alone handles it.
Daemon rejection errors now report boolean flags (double_spend,
fee_too_low, invalid_input, etc.) instead of the often-empty reason
string.
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.
Store phase was still the bottleneck (~700-1800ms/batch) despite the
pipeline, because detect_spent_outputs issued ~51k individual DB queries
per batch (key image + ring member lookups for every on-chain tx).
Load all owned key images and global indices into HashSets at batch start,
check in-memory first, and only hit the DB on the rare cache hit (~0.01%).
Also adds PRAGMA synchronous=NORMAL and hoists get_stakes out of the
per-block loop.
Result: store times drop from 700-1800ms to 11-167ms, overall sync
throughput improves from ~490 to ~2073 blocks/s (4x).
The store phase (~65% of batch time) now runs on a std::thread while
the sync loop fetches and parses the next batch concurrently. Bounded
channel (capacity 2) provides backpressure. Block hash cache avoids
DB lock contention during reorg checks. scan_ctx updates (cn_subaddr
entries from STAKE/CONVERT/AUDIT) flow back via StoreResultMsg and
are merged before cloning for the next parse.
Distribute block fetch requests across up to 4 fastest nodes in the
NodePool, selected by latency-weighted racing. All configured nodes
are probed but only the top 4 are used for parallel fetching.
NodePool (salvium-rpc):
- Add max_fetch_nodes to PoolConfig (default 4)
- Add force_race() to probe all nodes on demand
- Add fetch_batch_distributed() with latency-weighted range splitting
- Add compute_assignments() with 6 unit tests
- Add DistributedBatchResult type
Sync engine (salvium-wallet):
- Call force_race() at sync start to populate latency data
- Replace all 3 fetch sites with fetch_batch_distributed
- Simplify PrefetchResult to use DistributedBatchResult
Explorer FFI (salvium-ffi):
- Add salvium_daemon_get_blocks_by_height (JSON heights → blocks)
- Add salvium_daemon_get_transactions (JSON hashes → tx hex)
- Add salvium_daemon_add_nodes (batch add from JSON array)
- Add salvium_daemon_force_race (probe all nodes)
CLI & bench:
- Add --nodes flag to salvium-wallet-cli and salvium-sync-bench
- Wire extra nodes into NodePool for sync commands
Docs:
- Document multi-node setup in wallet-sync-spec.md
- Document FFI block/tx fetching in explorer-spec.md
CARROT outputs need the commitment for spend key derivation, but store_found_outputs was hardcoding commitment: None. Now looks up the commitment from the parsed TxOutput by index. Existing wallets need a rescan to
populate the field.
Exposes two version queries: salvium_version() returns the Salvium protocol version (e.g. "1.0.7"), salvium_rs_version() returns the library version derived from the rXXX tag (e.g. "v0.1.2"). Both are auto-derived from
Cargo.toml at compile time.
The transfer pipeline was passing asset-type-specific asset_type to get_output_distribution and get_outs but using global output indices from the wallet DB, causing an index space mismatch that made the daemon return HTTP
500. Switched to empty asset_type (global index space) for both calls, matching the existing salvium-cli pattern. Removed unused is_global_out field from OutputRequest. Bumped version to r012.
Transfer/stake/sweep failed with "no suitable outputs" because the
default asset_type was "SAL" but HF10+ outputs are stored as "SAL1".
Remove hardcoded default, require the caller to specify asset_type
explicitly, and use select_outputs (not select_carrot_outputs) so
pre-fork UTXOs remain spendable. This also supports future token
types without hardcoding asset strings.
Correct camelCase field names in FFI source comments, fix build script
target description, bigint→number types for u64 params, address format
values (legacy not CryptoNote), verify_rct binary return format, and
stale line number references.
Change is_incoming logic in build_transaction_row so received change
outputs from our own spends don't mark the transaction as incoming.
Previously nearly every outgoing tx had is_incoming=true because
change outputs were present, causing the app to display 0.00 amounts.