// Copyright (c) 2025-2026, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gtest/gtest.h" #include "carrot_core/enote_utils.h" #include "carrot_impl/address_device_ram_borrowed.h" #include "carrot_impl/tx_builder_inputs.h" #include "carrot_mock_helpers.h" #include "fcmp_pp/prove.h" using namespace carrot; //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_legacy_to_legacy_v1_mainaddr) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), addr.address_spend_pubkey); const crypto::hash signable_tx_hash = crypto::rand(); // k_e, K_e crypto::secret_key enote_ephemeral_privkey; crypto::public_key enote_ephemeral_pubkey; crypto::generate_keys(enote_ephemeral_pubkey, enote_ephemeral_privkey); // 8 k_e K^0_v crypto::key_derivation kd; ASSERT_TRUE(crypto::generate_key_derivation(addr.address_view_pubkey, enote_ephemeral_privkey, kd)); // K_o = K^0_s + Hs1(8 k_e K^0_v, i) G const std::size_t local_output_index = 2; crypto::public_key onetime_address; crypto::derive_public_key(kd, local_output_index, addr.address_spend_pubkey, onetime_address); // a const rct::xmr_amount amount = crypto::rand(); // z const crypto::secret_key amount_blinding_factor = mock::gen_secret_key(); // C_a = z G + a H const amount_commitment_t amount_commitment = commit_carrot_amount(amount, amount_blinding_factor); const LegacyOutputOpeningHintV1 opening_hint{ .onetime_address = onetime_address, .ephemeral_tx_pubkey = enote_ephemeral_pubkey, .subaddr_index = {0, 0}, .amount = amount, .amount_blinding_factor = amount_blinding_factor, .local_output_index = local_output_index }; const crypto::key_image expected_key_image = keys.key_image_dev->derive_key_image(opening_hint); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set( {onetime_address}, {amount_commitment}, {true}, {amount_blinding_factor}, {mock::gen_secret_key()}, output_amount_blinding_factor, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_legacy_to_legacy_v1_subaddr) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // j const subaddress_index_extended subaddress_index = mock::gen_subaddress_index_extended(keys.default_derive_type); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress(subaddress_index); const crypto::hash signable_tx_hash = crypto::rand(); // k_e, K_e = k_e K^j_s const crypto::secret_key enote_ephemeral_privkey = mock::gen_secret_key(); crypto::public_key enote_ephemeral_pubkey; { ge_p3 Kjs; [[maybe_unused]] const int r = ge_frombytes_vartime(&Kjs, to_bytes(addr.address_spend_pubkey)); ge_p2 Ke; ge_scalarmult(&Ke, to_bytes(enote_ephemeral_privkey), &Kjs); ge_tobytes(to_bytes(enote_ephemeral_pubkey), &Ke); } // 8 k_e K^j_v crypto::key_derivation kd; ASSERT_TRUE(crypto::generate_key_derivation(addr.address_view_pubkey, enote_ephemeral_privkey, kd)); // K_o = K^j_s + Hs1(8 k_e K^0_v, i) G const std::size_t local_output_index = 0; crypto::public_key onetime_address; crypto::derive_public_key(kd, local_output_index, addr.address_spend_pubkey, onetime_address); // a const rct::xmr_amount amount = crypto::rand(); // z const crypto::secret_key amount_blinding_factor = mock::gen_secret_key(); // k^g_o const crypto::secret_key sender_extension_g = mock::gen_secret_key(); // C_a = z G + a H const amount_commitment_t amount_commitment = commit_carrot_amount(amount, amount_blinding_factor); const LegacyOutputOpeningHintV1 opening_hint{ .onetime_address = onetime_address, .ephemeral_tx_pubkey = enote_ephemeral_pubkey, .subaddr_index = subaddress_index.index, .amount = amount, .amount_blinding_factor = amount_blinding_factor, .local_output_index = local_output_index }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.key_image_dev->derive_key_image(opening_hint); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({opening_hint.onetime_address}, {amount_commitment}, {use_biased_htp}, {amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_legacy_v1_mainaddr_normal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; RCTOutputEnoteProposal output_enote_proposal; encrypted_payment_id_t encrypted_payment_id; get_output_proposal_normal_v1(normal_payment_proposal, tx_first_key_image, output_enote_proposal, encrypted_payment_id); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, encrypted_payment_id, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = encrypted_payment_id, .subaddr_index = {{0, 0}, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_legacy_v1_subaddr_normal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // j const subaddress_index subaddr_index = mock::gen_subaddress_index(); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress({subaddr_index}); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; RCTOutputEnoteProposal output_enote_proposal; encrypted_payment_id_t encrypted_payment_id; get_output_proposal_normal_v1(normal_payment_proposal, tx_first_key_image, output_enote_proposal, encrypted_payment_id); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, encrypted_payment_id, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = encrypted_payment_id, .subaddr_index = {subaddr_index, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_legacy_v1_subaddr_special) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // j const subaddress_index subaddr_index = mock::gen_subaddress_index(); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress({subaddr_index}); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::CHANGE, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_special_v1(selfsend_payment_proposal, keys.k_view_incoming_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {subaddr_index, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_legacy_v1_mainaddr_special) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::CHANGE, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_special_v1(selfsend_payment_proposal, keys.k_view_incoming_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {{0, 0}, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_mainaddr_normal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; RCTOutputEnoteProposal output_enote_proposal; encrypted_payment_id_t encrypted_payment_id; get_output_proposal_normal_v1(normal_payment_proposal, tx_first_key_image, output_enote_proposal, encrypted_payment_id); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, encrypted_payment_id, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = encrypted_payment_id, .subaddr_index = {{0, 0}, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_subaddr_normal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // j const subaddress_index subaddr_index = mock::gen_subaddress_index(); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress({subaddr_index}); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; RCTOutputEnoteProposal output_enote_proposal; encrypted_payment_id_t encrypted_payment_id; get_output_proposal_normal_v1(normal_payment_proposal, tx_first_key_image, output_enote_proposal, encrypted_payment_id); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, encrypted_payment_id, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = encrypted_payment_id, .subaddr_index = {subaddr_index, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_mainaddr_special) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::PAYMENT, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_special_v1(selfsend_payment_proposal, keys.k_view_incoming_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {{0, 0}, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_subaddr_special) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // j const subaddress_index subaddr_index = mock::gen_subaddress_index(); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress({subaddr_index}); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::CHANGE, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_special_v1(selfsend_payment_proposal, keys.k_view_incoming_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {subaddr_index, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_mainaddr_internal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::PAYMENT, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_internal_v1(selfsend_payment_proposal, keys.s_view_balance_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {{0, 0}, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_to_carrot_v1_subaddr_internal) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // j const subaddress_index subaddr_index = mock::gen_subaddress_index(); // (K^j_s, K^j_v) const CarrotDestinationV1 addr = keys.subaddress({subaddr_index}); const crypto::key_image tx_first_key_image = mock::gen_key_image(); const CarrotPaymentProposalSelfSendV1 selfsend_payment_proposal{ .destination_address_spend_pubkey = addr.address_spend_pubkey, .amount = crypto::rand(), .enote_type = CarrotEnoteType::CHANGE, .enote_ephemeral_privkey = mock::gen_secret_key() }; RCTOutputEnoteProposal output_enote_proposal; get_output_proposal_internal_v1(selfsend_payment_proposal, keys.s_view_balance_dev, tx_first_key_image, nullptr, nullptr, output_enote_proposal); const CarrotEnoteV1 &enote = output_enote_proposal.enote; // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_enote_set({enote}, gen_encrypted_payment_id(), keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotOutputOpeningHintV1 opening_hint{ .source_enote = enote, .encrypted_payment_id = std::nullopt, .subaddr_index = {subaddr_index, keys.default_derive_type} }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({enote.onetime_address}, {enote.amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_coinbase_to_legacy_v1) { mock::mock_carrot_and_legacy_keys keys; keys.generate(AddressDeriveType::PreCarrot); const cryptonote_hierarchy_address_device cn_addr_dev( std::make_shared(keys.k_view_incoming_dev), keys.legacy_acb.get_keys().m_account_address.m_spend_public_key); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; const uint64_t block_index = mock::gen_block_index(); CarrotCoinbaseEnoteV1 coinbase_enote; get_coinbase_enote_v1(normal_payment_proposal, block_index, coinbase_enote); const amount_commitment_t coinbase_enote_amount_commitment = commit_carrot_amount(coinbase_enote.amount, {{1}}); // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_coinbase_enote_set({coinbase_enote}, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotCoinbaseOutputOpeningHintV1 opening_hint{ .source_enote = coinbase_enote, .derive_type = keys.default_derive_type }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, coinbase_enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({coinbase_enote.onetime_address}, {coinbase_enote_amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_legacy_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.legacy_acb.get_keys().m_spend_secret_key, cn_addr_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_tx_builder, make_sal_proof_carrot_coinbase_to_carrot_v1) { mock::mock_carrot_and_legacy_keys keys; keys.generate(); // (K^0_s, K^0_v) const CarrotDestinationV1 addr = keys.cryptonote_address(); const CarrotPaymentProposalV1 normal_payment_proposal{ .destination = addr, .amount = crypto::rand(), .randomness = gen_janus_anchor() }; const uint64_t block_index = mock::gen_block_index(); CarrotCoinbaseEnoteV1 coinbase_enote; get_coinbase_enote_v1(normal_payment_proposal, block_index, coinbase_enote); const amount_commitment_t coinbase_enote_amount_commitment = commit_carrot_amount(coinbase_enote.amount, {{1}}); // scan enote to get sender extensions and calculate expected key image std::vector scan_results; mock::mock_scan_coinbase_enote_set({coinbase_enote}, keys, scan_results); ASSERT_EQ(1, scan_results.size()); const mock::mock_scan_result_t &scan_result = scan_results.front(); ASSERT_EQ(addr.address_spend_pubkey, scan_result.address_spend_pubkey); const CarrotCoinbaseOutputOpeningHintV1 opening_hint{ .source_enote = coinbase_enote, .derive_type = keys.default_derive_type }; const bool use_biased_htp = use_biased_hash_to_point(opening_hint); const crypto::key_image expected_key_image = keys.derive_key_image(addr.address_spend_pubkey, scan_result.sender_extension_g, scan_result.sender_extension_t, coinbase_enote.onetime_address, use_biased_htp); // fake output amount blinding factor in a hypothetical tx where we spent the aforementioned output const crypto::secret_key output_amount_blinding_factor = mock::gen_secret_key(); const crypto::hash signable_tx_hash = crypto::rand(); // make rerandomized outputs std::vector rerandomized_outputs; fcmp_pp::make_balanced_rerandomized_output_set({coinbase_enote.onetime_address}, {coinbase_enote_amount_commitment}, {use_biased_htp}, {scan_result.amount_blinding_factor}, {mock::gen_secret_key()}, {output_amount_blinding_factor}, rerandomized_outputs); ASSERT_EQ(1, rerandomized_outputs.size()); // make SA/L proof for spending aforementioned enote fcmp_pp::FcmpPpSalProof sal_proof; crypto::key_image actual_key_image; make_sal_proof_any_to_carrot_v1(signable_tx_hash, rerandomized_outputs.front(), opening_hint, keys.k_prove_spend, keys.k_generate_image, *keys.addr_dev, keys.s_view_balance_dev, keys.k_view_incoming_dev, sal_proof, actual_key_image); ASSERT_EQ(expected_key_image, actual_key_image); // verify SA/L EXPECT_TRUE(fcmp_pp::verify_sal(signable_tx_hash, rerandomized_outputs.front().input, actual_key_image, sal_proof)); } //----------------------------------------------------------------------------------------------------------------------