Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cardano]: Check if assetName is a UTF-8 string #4089

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions rust/tw_memory/src/ffi/tw_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// Copyright © 2017 Trust Wallet.

use crate::ffi::c_byte_array_ref::CByteArrayRef;
use crate::ffi::RawPtrTrait;
use std::ffi::{c_char, CStr, CString};

Expand All @@ -14,6 +15,13 @@ use std::ffi::{c_char, CStr, CString};
pub struct TWString(CString);

impl TWString {
pub unsafe fn is_utf8_string(bytes: *const u8, size: usize) -> bool {
let Some(bytes) = CByteArrayRef::new(bytes, size).to_vec() else {
return false;
};
String::from_utf8(bytes).is_ok()
}

/// Returns an empty `TWString` instance.
pub fn new() -> TWString {
TWString(CString::default())
Expand Down Expand Up @@ -70,6 +78,13 @@ pub unsafe extern "C" fn tw_string_utf8_bytes(str: *const TWString) -> *const c_
.unwrap_or_else(std::ptr::null)
}

/// Checks whether the C byte array is a UTF8 string.
/// \return true if the given C byte array is UTF-8 string, otherwise false.
#[no_mangle]
pub unsafe extern "C" fn tw_string_is_utf8_bytes(bytes: *const u8, size: usize) -> bool {
TWString::is_utf8_string(bytes, size)
}

/// Deletes a string created with a `TWStringCreate*` method and frees the memory.
/// \param str a `TWString` pointer.
#[no_mangle]
Expand Down
36 changes: 29 additions & 7 deletions src/Cardano/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
//
// Copyright © 2017 Trust Wallet.

#include <google/protobuf/stubs/strutil.h>

#include "Transaction.h"
#include "AddressV3.h"

#include "Cbor.h"
#include "Hash.h"
#include "HexCoding.h"
#include "Numeric.h"
#include "rust/Wrapper.h"

namespace TW::Cardano {

TokenAmount TokenAmount::fromProto(const Proto::TokenAmount& proto) {
std::string assetName;
Data assetName;
if (!proto.asset_name().empty()) {
assetName = proto.asset_name();
assetName = data(proto.asset_name());
} else if (!proto.asset_name_hex().empty()) {
auto assetNameData = parse_hex(proto.asset_name_hex());
assetName.assign(assetNameData.data(), assetNameData.data() + assetNameData.size());
Expand All @@ -29,13 +32,32 @@ Proto::TokenAmount TokenAmount::toProto() const {

Proto::TokenAmount tokenAmount;
tokenAmount.set_policy_id(policyId.data(), policyId.size());
tokenAmount.set_asset_name(assetName.data(), assetName.size());
tokenAmount.set_asset_name_hex(assetNameHex.data(), assetNameHex.size());
const auto amountData = store(amount);
tokenAmount.set_amount(amountData.data(), amountData.size());

if (const auto assetNameStr = assetNameToString(); assetNameStr.has_value()) {
tokenAmount.set_asset_name(assetNameStr.value().data(), assetNameStr.value().size());
}
return tokenAmount;
}

std::string TokenAmount::displayAssetName() const {
if (const auto assetNameStr = assetNameToString(); assetNameStr.has_value()) {
return std::move(assetNameStr.value());
}
return hex(assetName);
}

std::optional<std::string> TokenAmount::assetNameToString() const {
if (!Rust::tw_string_is_utf8_bytes(assetName.data(), assetName.size())) {
return std::nullopt;
}
std::string assetNameStr;
assetNameStr.assign(assetName.data(), assetName.data() + assetName.size());
return assetNameStr;
}

TokenBundle TokenBundle::fromProto(const Proto::TokenBundle& proto) {
TokenBundle ret;
const auto addFunctor = [&ret](auto&& cur) { ret.add(TokenAmount::fromProto(cur)); };
Expand Down Expand Up @@ -108,18 +130,18 @@ uint64_t TokenBundle::minAdaAmount() const {
}

std::unordered_set<std::string> policyIdRegistry;
std::unordered_set<std::string> assetNameRegistry;
std::unordered_set<Data, DataHash> assetNameRegistry;
uint64_t sumAssetNameLengths = 0;
for (const auto& t : bundle) {
policyIdRegistry.emplace(t.second.policyId);
if (t.second.assetName.length() > 0) {
if (!t.second.assetName.empty()) {
assetNameRegistry.emplace(t.second.assetName);
}
}

auto numPids = uint64_t(policyIdRegistry.size());
auto numAssets = uint64_t(assetNameRegistry.size());
for_each(assetNameRegistry.begin(), assetNameRegistry.end(), [&sumAssetNameLengths](auto&& a){ sumAssetNameLengths += a.length(); });
for_each(assetNameRegistry.begin(), assetNameRegistry.end(), [&sumAssetNameLengths](auto&& a){ sumAssetNameLengths += a.size(); });

return minAdaAmountHelper(numPids, numAssets, sumAssetNameLengths);
}
Expand Down Expand Up @@ -256,7 +278,7 @@ Cbor::Encode cborizeOutputAmounts(const Amount& amount, const TokenBundle& token
std::map<Cbor::Encode, Cbor::Encode> subTokensMap;
for (const auto& token : subTokens) {
subTokensMap.emplace(
Cbor::Encode::bytes(data(token.assetName)),
Cbor::Encode::bytes(token.assetName),
Cbor::Encode::uint(uint64_t(token.amount)) // 64 bits
);
}
Expand Down
9 changes: 6 additions & 3 deletions src/Cardano/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ typedef uint64_t Amount;
class TokenAmount {
public:
std::string policyId;
std::string assetName;
Data assetName;
uint256_t amount;

TokenAmount() = default;
TokenAmount(std::string policyId, std::string assetName, uint256_t amount)
TokenAmount(std::string policyId, Data assetName, uint256_t amount)
: policyId(std::move(policyId)), assetName(std::move(assetName)), amount(std::move(amount)) {}

static TokenAmount fromProto(const Proto::TokenAmount& proto);
Proto::TokenAmount toProto() const;
/// Key used in TokenBundle
std::string key() const { return policyId + "_" + assetName; }
std::string key() const { return policyId + "_" + displayAssetName(); }
std::string displayAssetName() const;
/// Tries to convert the `assetName` to a UTF-8 string. Returns `std::nullopt` otherwise.
std::optional<std::string> assetNameToString() const;
};

class TokenBundle {
Expand Down
10 changes: 10 additions & 0 deletions src/Data.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,14 @@ inline bool has_prefix(const Data& data, T& prefix) {
return std::equal(prefix.begin(), prefix.end(), data.begin(), data.begin() + std::min(data.size(), prefix.size()));
}

// Custom hash function for `Data` type.
struct DataHash {
std::size_t operator()(const Data& data) const {
// Create a string_view from the vector's data.
std::string_view ss(reinterpret_cast<const char*>(data.data()), data.size());
// Use the hash function for std::string_view
return std::hash<std::string_view>{}(ss);
}
};

} // namespace TW
78 changes: 76 additions & 2 deletions tests/chains/Cardano/SigningTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,14 @@ TEST(CardanoSigning, ExtraOutputPlan) {
const auto toAddress = AddressV3(txOutput1.address);
EXPECT_EQ(toAddress.string(), "addr1v9jxgu33wyunycmdddnh5a3edq6x2dt3xakkuun6wd6hsar8v9uhvee5w9erw7fnvauhswfhw44k673nv3n8sdmj89n82denweckuv34xvmnw6m9xeerq7rt8ymh5aesxaj8zu3e0y6k67tcd3nkzervxfenqer8ddjn27jkkrj");
EXPECT_EQ(txOutput1.tokenBundle.getByPolicyId(sundaeTokenPolicy)[0].amount, 3000000);
EXPECT_EQ(txOutput1.tokenBundle.getByPolicyId(sundaeTokenPolicy)[0].assetName, "CUBY");
EXPECT_EQ(txOutput1.tokenBundle.getByPolicyId(sundaeTokenPolicy)[0].assetName, data("CUBY"));
EXPECT_EQ(txOutput1.tokenBundle.getByPolicyId(sundaeTokenPolicy)[0].policyId, sundaeTokenPolicy);
}
{
// also test proto toProto / toProto
const auto toAddress = AddressV3("addr1q92cmkgzv9h4e5q7mnrzsuxtgayvg4qr7y3gyx97ukmz3dfx7r9fu73vqn25377ke6r0xk97zw07dqr9y5myxlgadl2s0dgke5");
std::vector<TokenAmount> tokenAmount;
tokenAmount.emplace_back(sundaeTokenPolicy, "CUBY", 3000000);
tokenAmount.emplace_back(sundaeTokenPolicy, data("CUBY"), 3000000);
const Proto::TxOutput txOutputProto = TxOutput(toAddress.data(), 2000000, TokenBundle(tokenAmount)).toProto();
EXPECT_EQ(txOutputProto.amount(), 2000000ul);
EXPECT_EQ(txOutputProto.address(), "addr1q92cmkgzv9h4e5q7mnrzsuxtgayvg4qr7y3gyx97ukmz3dfx7r9fu73vqn25377ke6r0xk97zw07dqr9y5myxlgadl2s0dgke5");
Expand Down Expand Up @@ -907,6 +907,80 @@ TEST(CardanoSigning, SignTransferTokenMaxAmount_620b71) {
EXPECT_EQ(hex(txid), "620b719338efb419b0e1417bfbe01fc94a62d5669a4b8cbbf4e32ecc1ca3b872");
}

TEST(CardanoSigning, SignTransferTokenAmountNonUtf8) {
const auto ownAddress = "addr1q83kuum4jhwu3gxdwftdv2vezr0etmt3tp7phw5assltzl6t4afzguegnkcrdzp79vdcqswly775f33jvtpayl280qeqts960l";
const auto privateKey = "009aba22621d98e008c266a8d19c493f5f80a3a4f55048a83168a9c856726852fc240e6e95d7dc4e8ea599d09d64f84fdbe951b2282f5e5ed374252d17be9507643b2d078e607b5327397f212e4f6607ff0b6dfc93bdc9ad2bd0a682887edb9f304a573e99c7c2022c925511f004c7c9b89e8569080d09e2c53dfb1d53726852d4735794e3d32eac2b17d4d7c94742a77b7400b66fa11eaeb6ae38ba2dea84612f0c38fd68b9751ed4cb4ac48fb5e19f985f809fff1cfe5303fbfd29aca43d66";
const auto gensTokenPolicy = "dda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fb";
// Non UTF-8 assetName according to https://github.com/cardano-foundation/CIPs/tree/master/CIP-0067
const auto gensTokenNameHex = "0014df1047454e53";
const auto currentSlot = 138'888'357ul;

Proto::SigningInput input;
auto* utxo1 = input.add_utxos();
const auto txHash1 = parse_hex("7b377e0cf7b83d67bb6919008c38e1a63be86c4831a93ad0cb45778b9f2f7e28");
utxo1->mutable_out_point()->set_tx_hash(txHash1.data(), txHash1.size());
utxo1->mutable_out_point()->set_output_index(4);
utxo1->set_address(ownAddress);
utxo1->set_amount(1'700'000ul);
// GENS token (asset1266q2ewhgul7jh3xqpvjzqarrepfjuler20akr).
auto* token1 = utxo1->add_token_amount();
token1->set_policy_id(gensTokenPolicy);
token1->set_asset_name_hex(gensTokenNameHex);
const auto tokenAmount1 = store(uint256_t(44'660'987ul));
token1->set_amount(tokenAmount1.data(), tokenAmount1.size());

const auto privateKeyData = parse_hex(privateKey);
input.add_private_key(privateKeyData.data(), privateKeyData.size());
input.mutable_transfer_message()->set_to_address("addr1q875r037fjeqveg6xv5wke922ff897eyrnshlj3ryp4mypzt4afzguegnkcrdzp79vdcqswly775f33jvtpayl280qeq7zgptp");
input.mutable_transfer_message()->set_change_address(ownAddress);
input.mutable_transfer_message()->set_amount(666ul); // doesn't matter, max is used
auto* toToken = input.mutable_transfer_message()->mutable_token_amount()->add_token();
toToken->set_policy_id(gensTokenPolicy);
toToken->set_asset_name_hex(gensTokenNameHex);
const auto toTokenAmount = store(uint256_t(666ul)); // doesn't matter, max is used
input.mutable_transfer_message()->set_use_max_amount(true);
input.set_ttl(currentSlot + 7200ul);

Proto::TransactionPlan plan;
ANY_PLAN(input, plan, TWCoinTypeCardano);

EXPECT_EQ(plan.error(), Common::Proto::SigningError::OK);
{
EXPECT_EQ(plan.available_amount(), 1'700'000ul);
EXPECT_EQ(plan.amount(), 1'700'000ul - 167'818ul);
EXPECT_EQ(plan.fee(), 167'818ul);
EXPECT_EQ(plan.change(), 0ul);
EXPECT_EQ(plan.utxos_size(), 1);
EXPECT_EQ(plan.available_tokens_size(), 1);

EXPECT_EQ(load(plan.available_tokens(0).amount()), 44'660'987ul);
// `assetName` must be empty as it's not a UTF-8 string.
EXPECT_EQ(plan.available_tokens(0).asset_name(), "");
EXPECT_EQ(plan.available_tokens(0).asset_name_hex(), gensTokenNameHex);

EXPECT_EQ(plan.output_tokens_size(), 1);
EXPECT_EQ(load(plan.output_tokens(0).amount()), 44'660'987ul);
// `assetName` must be empty as it's not a UTF-8 string.
EXPECT_EQ(plan.output_tokens(0).asset_name(), "");
EXPECT_EQ(plan.output_tokens(0).asset_name_hex(), gensTokenNameHex);
EXPECT_EQ(plan.change_tokens_size(), 0);
}

// set plan with specific fee, to match the real transaction
*input.mutable_plan() = plan;

Proto::SigningOutput output;
ANY_SIGN(input, TWCoinTypeCardano);

// https://cardanoscan.io/transaction/df89e81fbaec7485ba65ac3a2ffe4121a888f4937d085f3ad4f7e8e5192dea74
// curl -d '{"txHash":"620b71..b872","txBody":"83a400..08f6"}' -H "Content-Type: application/json" https://<cardano-node>/api/txs/submit
EXPECT_EQ(output.error(), Common::Proto::OK);
const auto encoded = data(output.encoded());
EXPECT_EQ(hex(encoded), "83a400818258207b377e0cf7b83d67bb6919008c38e1a63be86c4831a93ad0cb45778b9f2f7e2804018182583901fd41be3e4cb206651a3328eb64aa525272fb241ce17fca23206bb2044baf522473289db036883e2b1b8041df27bd44c63262c3d27d477832821a00176116a1581cdda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fba1480014df1047454e531a02a978fb021a00028f8a031a084760c5a10081825820748022805ee71f9fa31d06e60f14f0715a37c278c0690b565f26e1e1e83f930e5840386c5d05fb5cfdb11f1296e909a80314616cdd2779e5be5ea583e1a938ee8409f58b585c90248e1c0633638cc0f4517c03fdb59f17434267c2955e0fbbb3b609f6");
const auto txid = data(output.tx_id());
EXPECT_EQ(hex(txid), "df89e81fbaec7485ba65ac3a2ffe4121a888f4937d085f3ad4f7e8e5192dea74");
}

TEST(CardanoSigning, SignTransferTwoTokens) {
auto input = createSampleInput(7000000);
input.mutable_transfer_message()->set_amount(1500000);
Expand Down
24 changes: 12 additions & 12 deletions tests/chains/Cardano/TransactionTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ TEST(CardanoTransaction, minAdaAmount) {
}

{ // 1 policyId, 1 6-char asset name
const auto tb = TokenBundle({TokenAmount(policyId, "TOKEN1", 0)});
const auto tb = TokenBundle({TokenAmount(policyId, data("TOKEN1"), 0)});
EXPECT_EQ(tb.minAdaAmount(), 1444443ul);
}
{ // 2 policyId, 2 4-char asset names
auto tb = TokenBundle();
tb.add(TokenAmount("012345678901234567890POLICY1", "TOK1", 20));
tb.add(TokenAmount("012345678901234567890POLICY2", "TOK2", 20));
tb.add(TokenAmount("012345678901234567890POLICY1", data("TOK1"), 20));
tb.add(TokenAmount("012345678901234567890POLICY2", data("TOK2"), 20));
EXPECT_EQ(tb.minAdaAmount(), 1629628ul);
}
{ // 10 policyId, 10 6-char asset names
auto tb = TokenBundle();
for (auto i = 0; i < 10; ++i) {
std::string policyId1 = + "012345678901234567890123456" + std::to_string(i);
std::string name = "ASSET" + std::to_string(i);
Data name = data("ASSET" + std::to_string(i));
tb.add(TokenAmount(policyId1, name, 0));
}
EXPECT_EQ(tb.minAdaAmount(), 3370367ul);
Expand All @@ -96,9 +96,9 @@ TEST(CardanoTransaction, getPolicyIDs) {
const auto policyId1 = "012345678901234567890POLICY1";
const auto policyId2 = "012345678901234567890POLICY2";
const auto tb = TokenBundle({
TokenAmount(policyId1, "TOK1", 10),
TokenAmount(policyId2, "TOK2", 20),
TokenAmount(policyId2, "TOK3", 30), // duplicate policyId
TokenAmount(policyId1, data("TOK1"), 10),
TokenAmount(policyId2, data("TOK2"), 20),
TokenAmount(policyId2, data("TOK3"), 30), // duplicate policyId
});
ASSERT_EQ(tb.getPolicyIds().size(), 2ul);
EXPECT_TRUE(tb.getPolicyIds().contains(policyId1));
Expand All @@ -116,8 +116,8 @@ TEST(TWCardanoTransaction, minAdaAmount) {
}
{ // 2 policyId, 2 4-char asset names
auto bundle = TokenBundle();
bundle.add(TokenAmount("012345678901234567890POLICY1", "TOK1", 20));
bundle.add(TokenAmount("012345678901234567890POLICY2", "TOK2", 20));
bundle.add(TokenAmount("012345678901234567890POLICY1", data("TOK1"), 20));
bundle.add(TokenAmount("012345678901234567890POLICY2", data("TOK2"), 20));
const auto bundleProto = bundle.toProto();
const auto bundleProtoData = data(bundleProto.SerializeAsString());
EXPECT_EQ(TWCardanoMinAdaAmount(&bundleProtoData), 1629628ul);
Expand Down Expand Up @@ -145,16 +145,16 @@ TEST(TWCardanoTransaction, outputMinAdaAmount) {
}
{ // 1 NFT
auto bundle = TokenBundle();
bundle.add(TokenAmount("219820e6cb04316f41a337fea356480f412e7acc147d28f175f21b5e", "coolcatssociety4567", 1));
bundle.add(TokenAmount("219820e6cb04316f41a337fea356480f412e7acc147d28f175f21b5e", data("coolcatssociety4567"), 1));
const auto bundleProto = bundle.toProto();
const auto bundleProtoData = data(bundleProto.SerializeAsString());
const auto actual = WRAPS(TWCardanoOutputMinAdaAmount(toAddress.get(), &bundleProtoData, coinsPerUtxoByte.get()));
assertStringsEqual(actual, "1202490");
}
{ // 2 policyId, 2 4-char asset names
auto bundle = TokenBundle();
bundle.add(TokenAmount("8fef2d34078659493ce161a6c7fba4b56afefa8535296a5743f69587", "AADA", 20));
bundle.add(TokenAmount("6ac8ef33b510ec004fe11585f7c5a9f0c07f0c23428ab4f29c1d7d10", "MELD", 20));
bundle.add(TokenAmount("8fef2d34078659493ce161a6c7fba4b56afefa8535296a5743f69587", data("AADA"), 20));
bundle.add(TokenAmount("6ac8ef33b510ec004fe11585f7c5a9f0c07f0c23428ab4f29c1d7d10", data("MELD"), 20));
const auto bundleProto = bundle.toProto();
const auto bundleProtoData = data(bundleProto.SerializeAsString());
const auto actual = WRAPS(TWCardanoOutputMinAdaAmount(toAddress.get(), &bundleProtoData, coinsPerUtxoByte.get()));
Expand Down
Loading