aboutsummaryrefslogtreecommitdiff
path: root/src/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet')
-rw-r--r--src/wallet/rpc/wallet.cpp4
-rw-r--r--src/wallet/scriptpubkeyman.cpp2
-rw-r--r--src/wallet/scriptpubkeyman.h6
-rw-r--r--src/wallet/spend.cpp10
-rw-r--r--src/wallet/test/coinselector_tests.cpp76
-rw-r--r--src/wallet/test/fuzz/coinselection.cpp3
-rw-r--r--src/wallet/wallet.cpp21
-rw-r--r--src/wallet/wallet.h7
-rw-r--r--src/wallet/walletdb.cpp4
9 files changed, 101 insertions, 32 deletions
diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp
index 164ce9afed..c635093344 100644
--- a/src/wallet/rpc/wallet.cpp
+++ b/src/wallet/rpc/wallet.cpp
@@ -69,6 +69,7 @@ static RPCHelpMan getwalletinfo()
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
{RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"},
{RPCResult::Type::BOOL, "blank", "Whether this wallet intentionally does not contain any keys, scripts, or descriptors"},
+ {RPCResult::Type::NUM_TIME, "birthtime", /*optional=*/true, "The start time for blocks scanning. It could be modified by (re)importing any descriptor with an earlier timestamp."},
RESULT_LAST_PROCESSED_BLOCK,
}},
},
@@ -132,6 +133,9 @@ static RPCHelpMan getwalletinfo()
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
obj.pushKV("blank", pwallet->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET));
+ if (int64_t birthtime = pwallet->GetBirthTime(); birthtime != UNKNOWN_TIME) {
+ obj.pushKV("birthtime", birthtime);
+ }
AppendLastProcessedBlock(obj, *pwallet);
return obj;
diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
index bc3327cdb2..f5f3a17ae7 100644
--- a/src/wallet/scriptpubkeyman.cpp
+++ b/src/wallet/scriptpubkeyman.cpp
@@ -709,7 +709,7 @@ void LegacyScriptPubKeyMan::UpdateTimeFirstKey(int64_t nCreateTime)
// Cannot determine birthday information, so set the wallet birthday to
// the beginning of time.
nTimeFirstKey = 1;
- } else if (!nTimeFirstKey || nCreateTime < nTimeFirstKey) {
+ } else if (nTimeFirstKey == UNKNOWN_TIME || nCreateTime < nTimeFirstKey) {
nTimeFirstKey = nCreateTime;
}
diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
index 7c0eca1475..7231486df0 100644
--- a/src/wallet/scriptpubkeyman.h
+++ b/src/wallet/scriptpubkeyman.h
@@ -51,6 +51,9 @@ public:
virtual bool IsLocked() const = 0;
};
+//! Constant representing an unknown spkm creation time
+static constexpr int64_t UNKNOWN_TIME = std::numeric_limits<int64_t>::max();
+
//! Default for -keypool
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
@@ -286,7 +289,8 @@ private:
WatchOnlySet setWatchOnly GUARDED_BY(cs_KeyStore);
WatchKeyMap mapWatchKeys GUARDED_BY(cs_KeyStore);
- int64_t nTimeFirstKey GUARDED_BY(cs_KeyStore) = 0;
+ // By default, do not scan any block until keys/scripts are generated/imported
+ int64_t nTimeFirstKey GUARDED_BY(cs_KeyStore) = UNKNOWN_TIME;
//! Number of pre-generated keys/scripts (part of the look-ahead process, used to detect payments)
int64_t m_keypool_size GUARDED_BY(cs_KeyStore){DEFAULT_KEYPOOL_SIZE};
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index 8314a2ddfa..e473c5a093 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -693,9 +693,12 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
// Maximum allowed weight
int max_inputs_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
- if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change, max_inputs_weight)}) {
- results.push_back(*bnb_result);
- } else append_error(bnb_result);
+ // SFFO frequently causes issues in the context of changeless input sets: skip BnB when SFFO is active
+ if (!coin_selection_params.m_subtract_fee_outputs) {
+ if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change, max_inputs_weight)}) {
+ results.push_back(*bnb_result);
+ } else append_error(bnb_result);
+ }
// As Knapsack and SRD can create change, also deduce change weight.
max_inputs_weight -= (coin_selection_params.change_output_size * WITNESS_SCALE_FACTOR);
@@ -1260,6 +1263,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
// accidental re-use.
reservedest.KeepDestination();
+ wallet.WalletLogPrintf("Coin Selection: Algorithm:%s, Waste Metric Score:%d\n", GetAlgorithmName(result.GetAlgo()), result.GetWaste());
wallet.WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
current_fee, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
feeCalc.est.pass.start, feeCalc.est.pass.end,
diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp
index 9569210ba0..2b7ae888d8 100644
--- a/src/wallet/test/coinselector_tests.cpp
+++ b/src/wallet/test/coinselector_tests.cpp
@@ -320,7 +320,6 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_output_size);
coin_selection_params_bnb.m_cost_of_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size) + coin_selection_params_bnb.m_change_fee;
coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size);
- coin_selection_params_bnb.m_subtract_fee_outputs = true;
{
std::unique_ptr<CWallet> wallet = NewWallet(m_node);
@@ -345,6 +344,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
CoinsResult available_coins;
+ coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
@@ -355,7 +355,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
PreSelectedInputs selected_input;
selected_input.Insert(select_coin, coin_selection_params_bnb.m_subtract_fee_outputs);
available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
- coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
+
LOCK(wallet->cs_wallet);
const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);
BOOST_CHECK(result10);
@@ -370,12 +370,14 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
- add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
+ CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
+ add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
expected_result.Clear();
- add_coin(10 * CENT, 2, expected_result);
+ add_coin(10 * CENT + input_fee, 2, expected_result);
CCoinControl coin_control;
const auto result11 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
BOOST_CHECK(EquivalentResult(expected_result, *result11));
@@ -385,13 +387,15 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
coin_selection_params_bnb.m_effective_feerate = CFeeRate(3000);
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(5000);
- add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
+ input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
+ add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
expected_result.Clear();
- add_coin(9 * CENT, 2, expected_result);
- add_coin(1 * CENT, 2, expected_result);
+ add_coin(9 * CENT + input_fee, 2, expected_result);
+ add_coin(1 * CENT + input_fee, 2, expected_result);
const auto result12 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
BOOST_CHECK(EquivalentResult(expected_result, *result12));
available_coins.Clear();
@@ -400,13 +404,15 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
- add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
- add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
+ input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
+ add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
+ add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
expected_result.Clear();
- add_coin(9 * CENT, 2, expected_result);
- add_coin(1 * CENT, 2, expected_result);
+ add_coin(9 * CENT + input_fee, 2, expected_result);
+ add_coin(1 * CENT + input_fee, 2, expected_result);
coin_control.m_allow_other_inputs = true;
COutput select_coin = available_coins.All().at(1); // pre select 9 coin
coin_control.Select(select_coin.outpoint);
@@ -449,6 +455,44 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
}
}
+BOOST_AUTO_TEST_CASE(bnb_sffo_restriction)
+{
+ // Verify the coin selection process does not produce a BnB solution when SFFO is enabled.
+ // This is currently problematic because it could require a change output. And BnB is specialized on changeless solutions.
+ std::unique_ptr<CWallet> wallet = NewWallet(m_node);
+ WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(300, uint256{})); // set a high block so internal UTXOs are selectable
+
+ FastRandomContext rand{};
+ CoinSelectionParams params{
+ rand,
+ /*change_output_size=*/ 31, // unused value, p2wpkh output size (wallet default change type)
+ /*change_spend_size=*/ 68, // unused value, p2wpkh input size (high-r signature)
+ /*min_change_target=*/ 0, // dummy, set later
+ /*effective_feerate=*/ CFeeRate(3000),
+ /*long_term_feerate=*/ CFeeRate(1000),
+ /*discard_feerate=*/ CFeeRate(1000),
+ /*tx_noinputs_size=*/ 0,
+ /*avoid_partial=*/ false,
+ };
+ params.m_subtract_fee_outputs = true;
+ params.m_change_fee = params.m_effective_feerate.GetFee(params.change_output_size);
+ params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee;
+ params.m_min_change_target = params.m_cost_of_change + 1;
+ // Add spendable coin at the BnB selection upper bound
+ CoinsResult available_coins;
+ add_coin(available_coins, *wallet, COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
+ add_coin(available_coins, *wallet, 0.5 * COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
+ add_coin(available_coins, *wallet, 0.5 * COIN, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
+ // Knapsack will only find a changeless solution on an exact match to the satoshi, SRD doesn’t look for changeless
+ // If BnB were run, it would produce a single input solution with the best waste score
+ auto result = WITH_LOCK(wallet->cs_wallet, return SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, COIN, /*coin_control=*/{}, params));
+ BOOST_CHECK(result.has_value());
+ BOOST_CHECK_NE(result->GetAlgo(), SelectionAlgorithm::BNB);
+ BOOST_CHECK(result->GetInputSet().size() == 2);
+ // We have only considered BnB, SRD, and Knapsack. Test needs to be reevaluated if new algo is added
+ BOOST_CHECK(result->GetAlgo() == SelectionAlgorithm::SRD || result->GetAlgo() == SelectionAlgorithm::KNAPSACK);
+}
+
BOOST_AUTO_TEST_CASE(knapsack_solver_test)
{
FastRandomContext rand{};
diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp
index 4caf96b18d..ade3ec3f60 100644
--- a/src/wallet/test/fuzz/coinselection.cpp
+++ b/src/wallet/test/fuzz/coinselection.cpp
@@ -116,7 +116,8 @@ FUZZ_TARGET(coinselection)
}
// Run coinselection algorithms
- auto result_bnb = SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT);
+ auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} :
+ SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT);
if (result_bnb) {
assert(result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0);
assert(result_bnb->GetSelectedValue() >= target);
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 162d7f9ec7..78febb8195 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -1080,6 +1080,9 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block);
AddToSpends(wtx, &batch);
+
+ // Update birth time when tx time is older than it.
+ MaybeUpdateBirthTime(wtx.GetTxTime());
}
if (!fInsertedNew)
@@ -1215,6 +1218,10 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
}
}
}
+
+ // Update birth time when tx time is older than it.
+ MaybeUpdateBirthTime(wtx.GetTxTime());
+
return true;
}
@@ -1763,11 +1770,11 @@ bool CWallet::ImportScriptPubKeys(const std::string& label, const std::set<CScri
return true;
}
-void CWallet::FirstKeyTimeChanged(const ScriptPubKeyMan* spkm, int64_t new_birth_time)
+void CWallet::MaybeUpdateBirthTime(int64_t time)
{
int64_t birthtime = m_birth_time.load();
- if (new_birth_time < birthtime) {
- m_birth_time = new_birth_time;
+ if (time < birthtime) {
+ m_birth_time = time;
}
}
@@ -3103,7 +3110,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
int64_t time = spk_man->GetTimeFirstKey();
if (!time_first_key || time < *time_first_key) time_first_key = time;
}
- if (time_first_key) walletInstance->m_birth_time = *time_first_key;
+ if (time_first_key) walletInstance->MaybeUpdateBirthTime(*time_first_key);
if (chain && !AttachChain(walletInstance, *chain, rescan_required, error, warnings)) {
return nullptr;
@@ -3494,10 +3501,12 @@ LegacyScriptPubKeyMan* CWallet::GetOrCreateLegacyScriptPubKeyMan()
void CWallet::AddScriptPubKeyMan(const uint256& id, std::unique_ptr<ScriptPubKeyMan> spkm_man)
{
+ // Add spkm_man to m_spk_managers before calling any method
+ // that might access it.
const auto& spkm = m_spk_managers[id] = std::move(spkm_man);
// Update birth time if needed
- FirstKeyTimeChanged(spkm.get(), spkm->GetTimeFirstKey());
+ MaybeUpdateBirthTime(spkm->GetTimeFirstKey());
}
void CWallet::SetupLegacyScriptPubKeyMan()
@@ -3530,7 +3539,7 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
for (const auto& spk_man : GetActiveScriptPubKeyMans()) {
spk_man->NotifyWatchonlyChanged.connect(NotifyWatchonlyChanged);
spk_man->NotifyCanGetAddressesChanged.connect(NotifyCanGetAddressesChanged);
- spk_man->NotifyFirstKeyTimeChanged.connect(std::bind(&CWallet::FirstKeyTimeChanged, this, std::placeholders::_1, std::placeholders::_2));
+ spk_man->NotifyFirstKeyTimeChanged.connect(std::bind(&CWallet::MaybeUpdateBirthTime, this, std::placeholders::_2));
}
}
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 9333493a6e..c7cd3b3959 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -678,8 +678,8 @@ public:
bool ImportPubKeys(const std::vector<CKeyID>& ordered_pubkeys, const std::map<CKeyID, CPubKey>& pubkey_map, const std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool ImportScriptPubKeys(const std::string& label, const std::set<CScript>& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
- /** Updates wallet birth time if 'new_birth_time' is below it */
- void FirstKeyTimeChanged(const ScriptPubKeyMan* spkm, int64_t new_birth_time);
+ /** Updates wallet birth time if 'time' is below it */
+ void MaybeUpdateBirthTime(int64_t time);
CFeeRate m_pay_tx_fee{DEFAULT_PAY_TX_FEE};
unsigned int m_confirm_target{DEFAULT_TX_CONFIRM_TARGET};
@@ -877,6 +877,9 @@ public:
/* Returns true if the wallet can give out new addresses. This means it has keys in the keypool or can generate new keys */
bool CanGetAddresses(bool internal = false) const;
+ /* Returns the time of the first created key or, in case of an import, it could be the time of the first received transaction */
+ int64_t GetBirthTime() const { return m_birth_time; }
+
/**
* Blocks until the wallet state is up-to-date to /at least/ the current
* chain at the time this function is entered
diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp
index 92eca46f05..3a40b7a34f 100644
--- a/src/wallet/walletdb.cpp
+++ b/src/wallet/walletdb.cpp
@@ -1401,13 +1401,13 @@ bool WalletBatch::EraseRecords(const std::unordered_set<std::string>& types)
}
// Make a copy of key to avoid data being deleted by the following read of the type
- Span key_data{key};
+ const SerializeData key_data{key.begin(), key.end()};
std::string type;
key >> type;
if (types.count(type) > 0) {
- if (!m_batch->Erase(key_data)) {
+ if (!m_batch->Erase(Span{key_data})) {
cursor.reset(nullptr);
m_batch->TxnAbort();
return false; // erase failed