Implement return tx functionality for post-carrot (#33)

* return tx fixes

* save change output for return tx

* successfully receive returned tx

* complete post-carrot return txs
This commit is contained in:
akildemir
2025-07-30 17:33:35 +03:00
committed by GitHub
parent e5c9b05ed6
commit 37594fe8fa
11 changed files with 188 additions and 86 deletions
+2
View File
@@ -75,6 +75,8 @@ struct CarrotEnoteV1 final
mx25519_pubkey enote_ephemeral_pubkey;
/// L_0
crypto::key_image tx_first_key_image;
// transaction output keys
std::vector<crypto::public_key> tx_output_keys;
};
/// equality operators
+2 -2
View File
@@ -223,7 +223,7 @@ void make_sparc_return_pubkey_encryption_mask(const unsigned char s_sender_recei
//-------------------------------------------------------------------------------------------------------------------
void make_sparc_return_pubkey(const unsigned char s_sender_receiver_unctx[32],
const input_context_t &input_context,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_incoming_dev,
const crypto::public_key &onetime_address,
encrypted_return_pubkey_t &return_pubkey_out)
{
@@ -232,7 +232,7 @@ void make_sparc_return_pubkey(const unsigned char s_sender_receiver_unctx[32],
crypto::public_key return_pub;
encrypted_return_pubkey_t K_return;
encrypted_return_pubkey_t m_return;
s_view_balance_dev->make_internal_return_privkey(input_context, onetime_address, k_return);
k_view_incoming_dev->make_internal_return_privkey(input_context, onetime_address, k_return);
crypto::secret_key_to_public_key(k_return, return_pub);
static_assert(sizeof(K_return.bytes) == sizeof(return_pub.data), "Size mismatch");
memcpy(K_return.bytes, return_pub.data, sizeof(encrypted_return_pubkey_t));
+1 -1
View File
@@ -160,7 +160,7 @@ void make_sparc_return_pubkey_encryption_mask(const unsigned char s_sender_recei
*/
void make_sparc_return_pubkey(const unsigned char s_sender_receiver_unctx[32],
const input_context_t &input_context,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_incoming_dev,
const crypto::public_key &onetime_address,
encrypted_return_pubkey_t &return_pubkey_out);
/**
+7 -7
View File
@@ -223,13 +223,13 @@ void get_output_enote_proposals(const std::vector<CarrotPaymentProposalV1> &norm
if (tx_type == cryptonote::transaction_type::RETURN) {
get_output_proposal_return_v1(normal_payment_proposals[i],
tx_first_key_image,
s_view_balance_dev,
k_view_dev,
output_entry.first,
encrypted_payment_id);
} else {
get_output_proposal_normal_v1(normal_payment_proposals[i],
tx_first_key_image,
s_view_balance_dev,
k_view_dev,
output_entry.first,
encrypted_payment_id);
}
@@ -361,11 +361,11 @@ void get_output_enote_proposals(const std::vector<CarrotPaymentProposalV1> &norm
component_out_of_order, "this set contains duplicate onetime addresses");
// assert all K_o lie in prime order subgroup
for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out)
{
CARROT_CHECK_AND_THROW(rct::isInMainSubgroup(rct::pk2rct(output_enote_proposal.enote.onetime_address)),
invalid_point, "this set contains an invalid onetime address");
}
// for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out)
// {
// CARROT_CHECK_AND_THROW(rct::isInMainSubgroup(rct::pk2rct(output_enote_proposal.enote.onetime_address)),
// invalid_point, "this set contains an invalid onetime address");
// }
// assert unique and non-trivial k_a
memcmp_set<crypto::secret_key> amount_blinding_factors;
+9 -9
View File
@@ -170,7 +170,7 @@ static void get_external_output_proposal_parts(const mx25519_pubkey &s_sender_re
const CarrotEnoteType enote_type,
const mx25519_pubkey &enote_ephemeral_pubkey,
const input_context_t &input_context,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_incoming_dev,
const bool coinbase_amount_commitment,
crypto::hash &s_sender_receiver_out,
crypto::secret_key &amount_blinding_factor_out,
@@ -179,7 +179,7 @@ static void get_external_output_proposal_parts(const mx25519_pubkey &s_sender_re
encrypted_amount_t &encrypted_amount_out,
encrypted_payment_id_t &encrypted_payment_id_out,
view_tag_t &view_tag_out,
encrypted_return_pubkey_t return_pubkey_out)
encrypted_return_pubkey_t &return_pubkey_out)
{
// 1. s^ctx_sr = H_32(s_sr, D_e, input_context)
make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data,
@@ -206,10 +206,10 @@ static void get_external_output_proposal_parts(const mx25519_pubkey &s_sender_re
make_carrot_view_tag(s_sender_receiver_unctx.data, input_context, onetime_address_out, view_tag_out);
// 4. construct the return pubkey
if (s_view_balance_dev != nullptr)
if (k_view_incoming_dev != nullptr)
make_sparc_return_pubkey(s_sender_receiver_unctx.data,
input_context,
s_view_balance_dev,
k_view_incoming_dev,
onetime_address_out,
return_pubkey_out);
}
@@ -303,7 +303,7 @@ void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal,
//-------------------------------------------------------------------------------------------------------------------
void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal,
const crypto::key_image &tx_first_key_image,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_incoming_dev,
RCTOutputEnoteProposal &output_enote_out,
encrypted_payment_id_t &encrypted_payment_id_out)
{
@@ -331,7 +331,7 @@ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal,
CarrotEnoteType::PAYMENT,
output_enote_out.enote.enote_ephemeral_pubkey,
input_context,
s_view_balance_dev,
k_view_incoming_dev,
false, // coinbase_amount_commitment
s_sender_receiver,
output_enote_out.amount_blinding_factor,
@@ -452,7 +452,7 @@ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &propo
get_output_proposal_return_v1(
proposal_return,
tx_first_key_image,
nullptr, // s_view_balance_dev
nullptr, // k_view_dev
return_enote_out,
encrypted_payment_id_return
);
@@ -461,7 +461,7 @@ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &propo
//-------------------------------------------------------------------------------------------------------------------
void get_output_proposal_return_v1(const CarrotPaymentProposalV1 &proposal,
const crypto::key_image &tx_first_key_image,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_incoming_dev,
RCTOutputEnoteProposal &output_enote_out,
encrypted_payment_id_t &encrypted_payment_id_out)
{
@@ -490,7 +490,7 @@ void get_output_proposal_return_v1(const CarrotPaymentProposalV1 &proposal,
CarrotEnoteType::PAYMENT,
output_enote_out.enote.enote_ephemeral_pubkey,
input_context,
s_view_balance_dev,
k_view_incoming_dev,
false, // coinbase_amount_commitment
s_sender_receiver,
output_enote_out.amount_blinding_factor,
+2 -2
View File
@@ -133,7 +133,7 @@ void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal,
*/
void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal,
const crypto::key_image &tx_first_key_image,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_dev,
RCTOutputEnoteProposal &output_enote_out,
encrypted_payment_id_t &encrypted_payment_id_out);
/**
@@ -146,7 +146,7 @@ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal,
*/
void get_output_proposal_return_v1(const CarrotPaymentProposalV1 &proposal,
const crypto::key_image &tx_first_key_image,
const view_balance_secret_device *s_view_balance_dev,
const view_incoming_key_device *k_view_dev,
RCTOutputEnoteProposal &output_enote_out,
encrypted_payment_id_t &encrypted_payment_id_out);
/**
+6 -3
View File
@@ -46,19 +46,22 @@ namespace carrot
struct return_output_info_t {
input_context_t input_context;
crypto::public_key address_spend_pubkey;
crypto::public_key K_o; // output onetime address
crypto::public_key K_change; // change output onetime address
crypto::key_image key_image;
crypto::secret_key x;
crypto::secret_key y;
return_output_info_t(
const input_context_t &input_context,
const crypto::public_key &address_spend_pubkey,
const crypto::public_key &K_o,
const crypto::public_key &K_change,
const crypto::key_image &key_image,
const crypto::secret_key &x,
const crypto::secret_key &y):
input_context(input_context),
address_spend_pubkey(address_spend_pubkey),
K_o(K_o),
K_change(K_change),
key_image(key_image),
x(x),
y(y) {}
+8
View File
@@ -355,6 +355,14 @@ bool try_load_carrot_enote_from_transaction_v1(const cryptonote::transaction &tx
//D_e
enote_out.enote_ephemeral_pubkey = enote_ephemeral_pubkeys[ephemeral_pubkey_index];
// save all output keys in order to calculate Kr values.
for (const auto& out: tx.vout) {
const cryptonote::txout_to_carrot_v1 * const carrot_out = boost::strict_get<cryptonote::txout_to_carrot_v1>(&out.target);
if (carrot_out) {
enote_out.tx_output_keys.push_back(carrot_out->key);
}
}
return true;
}
+146 -60
View File
@@ -261,17 +261,25 @@ static std::optional<enote_view_incoming_scan_info_t> view_incoming_scan_pre_car
}
//-------------------------------------------------------------------------------------------------------------------
static bool scan_return_output(
const carrot::CarrotCoinbaseEnoteV1 &enote,
const crypto::public_key &return_onetime_address,
const mx25519_pubkey &return_ephemeral_pubkey,
const carrot::view_tag_t &return_view_tag,
const carrot::encrypted_janus_anchor_t &return_anchor_enc,
const carrot::encrypted_amount_t &return_amount_enc,
const std::optional<rct::key> amount_commitment,
const carrot::input_context_t &return_input_context,
carrot::carrot_and_legacy_account &account,
crypto::public_key &address_spend_pubkey_out
crypto::public_key &address_spend_pubkey_out,
rct::xmr_amount &amount_out,
crypto::secret_key &amount_blinding_factor_out
) {
const auto &return_output_map = account.get_return_output_map_ref();
CHECK_AND_ASSERT_MES(return_output_map.count(enote.onetime_address), false, "return output not found");
const auto &return_output = return_output_map.at(enote.onetime_address);
CHECK_AND_ASSERT_MES(return_output_map.count(return_onetime_address), false, "return output not found");
const auto &origin_tx = return_output_map.at(return_onetime_address);
// 1. make k_return
crypto::secret_key k_return;
account.k_view_incoming_dev.make_internal_return_privkey(return_output.input_context, return_output.address_spend_pubkey, k_return);
account.k_view_incoming_dev.make_internal_return_privkey(origin_tx.input_context, origin_tx.K_o, k_return);
// 2. compute K_return' = k_return * G
crypto::public_key K_return;
@@ -280,11 +288,11 @@ static bool scan_return_output(
// 3. ssr
mx25519_pubkey shared_secret_return_unctx;
crypto::hash shared_secret_return;
carrot::make_carrot_uncontextualized_shared_key_receiver(k_return, enote.enote_ephemeral_pubkey, shared_secret_return_unctx);
carrot::make_carrot_uncontextualized_shared_key_receiver(k_return, return_ephemeral_pubkey, shared_secret_return_unctx);
carrot::make_carrot_sender_receiver_secret(
shared_secret_return_unctx.data,
enote.enote_ephemeral_pubkey,
return_output.input_context,
return_ephemeral_pubkey,
return_input_context,
shared_secret_return
);
@@ -292,9 +300,9 @@ static bool scan_return_output(
CHECK_AND_ASSERT_MES(
carrot::test_carrot_view_tag(
shared_secret_return_unctx.data,
return_output.input_context,
enote.onetime_address,
enote.view_tag
return_input_context,
return_onetime_address,
return_view_tag
),
false,
"view tag verification failed for carrot coinbase enote"
@@ -302,14 +310,14 @@ static bool scan_return_output(
// 5. compute anchor_return
carrot::janus_anchor_t recovered_anchor_return =
carrot::decrypt_carrot_anchor(enote.anchor_enc, shared_secret_return, enote.onetime_address);
carrot::decrypt_carrot_anchor(return_anchor_enc, shared_secret_return, return_onetime_address);
// 6. compute d_e'
crypto::secret_key recovered_ephemeral_privkey_return;
carrot::make_carrot_enote_ephemeral_privkey(
recovered_anchor_return,
return_output.input_context,
return_output.address_spend_pubkey,
return_input_context,
origin_tx.K_change,
carrot::null_payment_id,
recovered_ephemeral_privkey_return
);
@@ -318,19 +326,35 @@ static bool scan_return_output(
mx25519_pubkey recovered_ephemeral_pubkey_return;
carrot::make_carrot_enote_ephemeral_pubkey(
recovered_ephemeral_privkey_return,
return_output.address_spend_pubkey,
origin_tx.K_change,
false,
recovered_ephemeral_pubkey_return
);
// 8. verify the enote ephemeral pubkey
CHECK_AND_ASSERT_MES(
memcmp(recovered_ephemeral_pubkey_return.data, enote.enote_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0,
memcmp(recovered_ephemeral_pubkey_return.data, return_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0,
false,
"carrot coinbase enote protection verification failed"
);
address_spend_pubkey_out = return_output.address_spend_pubkey;
amount_out = carrot::decrypt_carrot_amount(return_amount_enc, shared_secret_return, return_onetime_address);
address_spend_pubkey_out = origin_tx.K_change;
if (amount_commitment)
{
CHECK_AND_ASSERT_MES(
carrot::try_recompute_carrot_amount_commitment(shared_secret_return,
amount_out,
address_spend_pubkey_out,
carrot::CarrotEnoteType::PAYMENT,
amount_commitment.value(),
amount_blinding_factor_out
),
false,
"failed to recompute carrot amount commitment for return output"
);
}
return true;
}
//-------------------------------------------------------------------------------------------------------------------
@@ -358,10 +382,32 @@ static std::optional<enote_view_incoming_scan_info_t> view_incoming_scan_carrot_
}
if (found_in_return) {
CHECK_AND_ASSERT_MES(
account.get_return_output_map_ref().count(enote.onetime_address),
std::nullopt,
"return output not found"
);
// scan the return output
crypto::public_key address_spend_pubkey;
if (!scan_return_output(enote, account, address_spend_pubkey))
carrot::encrypted_amount_t amount_enc;
crypto::secret_key amount_blinding_factor;
rct::xmr_amount amount;
if (!scan_return_output(
enote.onetime_address,
enote.enote_ephemeral_pubkey,
enote.view_tag,
enote.anchor_enc,
amount_enc,
std::nullopt, // no amount commitment for coinbase enotes
account.get_return_output_map_ref()
.at(enote.onetime_address).input_context,
account,
address_spend_pubkey,
amount,
amount_blinding_factor)
) {
return std::nullopt;
}
res.address_spend_pubkey = address_spend_pubkey;
res.return_address = enote.onetime_address;
@@ -445,6 +491,7 @@ static std::optional<enote_view_incoming_scan_info_t> view_incoming_scan_carrot_
crypto::secret_key amount_blinding_factor_sk;
carrot::payment_id_t payment_id;
carrot::CarrotEnoteType dummy_enote_type;
bool found_in_return = false;
if (!carrot::try_scan_carrot_enote_external_receiver(enote,
encrypted_payment_id,
s_sender_receiver_unctx,
@@ -456,64 +503,103 @@ static std::optional<enote_view_incoming_scan_info_t> view_incoming_scan_carrot_
res.amount,
amount_blinding_factor_sk,
payment_id,
dummy_enote_type))
return std::nullopt;
dummy_enote_type))
{
// check for known return addresses
const auto &subaddress_map = account.get_subaddress_map_ref();
if (subaddress_map.find(enote.onetime_address) == subaddress_map.end())
return std::nullopt;
const auto subaddr_it = account.get_subaddress_map_ref().find(res.address_spend_pubkey);
CHECK_AND_ASSERT_MES(subaddr_it != account.get_subaddress_map_ref().cend(),
found_in_return = true;
}
if (found_in_return) {
// scan the return output
crypto::public_key address_spend_pubkey;
if (!scan_return_output(
enote.onetime_address,
enote.enote_ephemeral_pubkey,
enote.view_tag,
enote.anchor_enc,
enote.amount_enc,
enote.amount_commitment,
carrot::make_carrot_input_context(enote.tx_first_key_image),
account,
res.address_spend_pubkey,
res.amount,
amount_blinding_factor_sk)
) {
return std::nullopt;
}
res.address_spend_pubkey = address_spend_pubkey;
res.return_address = enote.onetime_address;
res.is_return = true;
} else {
// we received a normal enote
res.address_spend_pubkey = main_address_spend_pubkey;
res.is_return = false;
}
if (!found_in_return) {
const auto subaddr_it = account.get_subaddress_map_ref().find(res.address_spend_pubkey);
CHECK_AND_ASSERT_MES(subaddr_it != account.get_subaddress_map_ref().cend(),
std::nullopt,
"view_incoming_scan_carrot_enote: carrot enote scanned successfully, "
"but the recovered address spend pubkey was not found in the subaddress map");
const carrot::subaddress_index_extended subaddr_index = subaddr_it->second;
const carrot::subaddress_index_extended subaddr_index = subaddr_it->second;
memset(&res.payment_id, 0, sizeof(res.payment_id));
memcpy(&res.payment_id, &payment_id, sizeof(carrot::payment_id_t));
memset(&res.payment_id, 0, sizeof(res.payment_id));
memcpy(&res.payment_id, &payment_id, sizeof(carrot::payment_id_t));
// we received and output
// save the Kr = K_change + K_return to out subaddress map
for (const auto &output_key : enote.tx_output_keys) {
// make k_return
crypto::secret_key k_return;
const carrot::input_context_t input_context = carrot::make_carrot_input_context(enote.tx_first_key_image);
k_view_dev.make_internal_return_privkey(input_context, output_key, k_return);
// we received and output
// save the Kr = K_change + K_return to out subaddress map
// make k_return
crypto::secret_key k_return;
const carrot::input_context_t input_context = carrot::make_carrot_input_context(enote.tx_first_key_image);
k_view_dev.make_internal_return_privkey(input_context, enote.onetime_address, k_return);
// compute K_return = k_return * G
crypto::public_key K_return;
crypto::secret_key_to_public_key(k_return, K_return);
// compute K_return = k_return * G
crypto::public_key K_return;
crypto::secret_key_to_public_key(k_return, K_return);
// compute K_r = K_return + K_o
crypto::public_key K_r = rct::rct2pk(rct::addKeys(rct::pk2rct(K_return), rct::pk2rct(enote.onetime_address)));
account.insert_subaddresses({{K_r, {{subaddr_index.index.major, subaddr_index.index.minor},
carrot::AddressDeriveType::Carrot, true}}});
// compute K_r = K_return + K_o
crypto::public_key K_r = rct::rct2pk(rct::addKeys(rct::pk2rct(K_return), rct::pk2rct(enote.onetime_address)));
account.insert_subaddresses({{K_r, {{subaddr_index.index.major, subaddr_index.index.minor},
carrot::AddressDeriveType::Carrot, true}}});
// calculate the key image for the return output
crypto::secret_key sum_g;
sc_add(to_bytes(sum_g), to_bytes(res.sender_extension_g), to_bytes(k_return));
crypto::key_image key_image = account.derive_key_image(
account.get_keys().m_carrot_account_address.m_spend_public_key,
sum_g,
res.sender_extension_t,
K_r
);
// calculate the key image for the return output
crypto::secret_key sum_g;
sc_add(to_bytes(sum_g), to_bytes(res.sender_extension_g), to_bytes(k_return));
crypto::key_image key_image = account.derive_key_image(
account.get_keys().m_carrot_account_address.m_spend_public_key,
sum_g,
res.sender_extension_t,
K_r
);
crypto::secret_key x, y;
account.try_searching_for_opening_for_onetime_address(
account.get_keys().m_carrot_account_address.m_spend_public_key,
sum_g,
res.sender_extension_t,
x,
y
);
crypto::secret_key x, y;
account.try_searching_for_opening_for_onetime_address(
account.get_keys().m_carrot_account_address.m_spend_public_key,
sum_g,
res.sender_extension_t,
x,
y
);
// save the input context & change output key
account.insert_return_output_info({{K_r, {input_context, output_key, enote.onetime_address, key_image, x, y}}});
}
res.subaddr_index = subaddr_index;
} else {
res.subaddr_index = carrot::subaddress_index_extended{{0, 0}};
}
// save the input context & change output key
account.insert_return_output_info({{K_r, {input_context, enote.onetime_address, key_image, x, y}}});
res.subaddr_index = subaddr_index;
res.amount_blinding_factor = rct::sk2rct(amount_blinding_factor_sk);
res.main_tx_pubkey_index = 0;
res.asset_type = enote.asset_type;
res.is_carrot = true;
res.is_return = false;
return res;
}
+1 -1
View File
@@ -664,7 +664,7 @@ std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposa
tx_type,
std::move(selected_inputs),
change_address_spend_pubkey,
{{subaddr_account, 0}, carrot::AddressDeriveType::PreCarrot}, //! @TODO: handle Carrot keys
{{subaddr_account, 0}, carrot::AddressDeriveType::Carrot},
tx_proposal);
// populate the sources
+4 -1
View File
@@ -11699,7 +11699,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_return(std::vector
THROW_WALLET_EXCEPTION_IF(idx >= get_num_transfer_details(), error::wallet_internal_error, tr("cannot locate return_payment origin index in m_transfers"));
const transfer_details& td_origin = get_transfer_details(idx);
const std::string asset_type = td_origin.m_tx.source_asset_type;
bool is_subaddress = true;
bool is_subaddress = false;
size_t outputs = 1;
std::vector<size_t> unused_dust_indices = {};
size_t fake_outs_count = get_min_ring_size() - 1; // Use the default ring size
@@ -11816,6 +11816,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_return(std::vector
return create_transactions_from(address, cryptonote::transaction_type::RETURN, asset_type, is_subaddress, outputs, transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra);
}
// pre-carrot return payment construction uses subaddress true
is_subaddress = true;
// To return a payment, we need to know the y value to process the F value
// ...but the y value is calculated differently depending on the original TX