Skip to content

Commit

Permalink
[Cardano]: Check if assetName is a UTF-8 string (#4089)
Browse files Browse the repository at this point in the history
* [Cardano]: Check if `assetName` is a UTF-8 string

* Treat `assetName` as a byte array by default

* [Cardano]: Add TokenAmount transfer with assetName CIP-0067

* [CI] Trigger CI

* [Cardano]: Add missing transaction link
  • Loading branch information
satoshiotomakan authored Nov 4, 2024
1 parent f6d8a10 commit 5071656
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 24 deletions.
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

0 comments on commit 5071656

Please sign in to comment.