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

feat: support Concise Transaction Identifier (CTID) (XLS-37) #4418

Merged
merged 30 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5e4f8ea
add CTIM to tx rpc
RichardAH Feb 10, 2023
1daf5f7
Add NetworkID field to Transaction common fields, enforced when netwo…
RichardAH Dec 19, 2022
91b0aae
Update src/ripple/app/tx/impl/Transactor.cpp
RichardAH Dec 21, 2022
2752108
[FOLD] add const, missing include
RichardAH Dec 22, 2022
3782ab7
[FOLD] fix: apply clang-format patch
intelliot Feb 10, 2023
1aaac47
preliminary support for xls37 (Improved Concise Transaction Identifier)
RichardAH Feb 13, 2023
04be722
fix txn hash lookup from index
RichardAH Feb 13, 2023
fec9fb7
rename CTIM to CTID
RichardAH Feb 14, 2023
281f20b
ctid tests
dangell7 Feb 27, 2023
2dad662
add autofill + refactor
dangell7 Mar 21, 2023
f39a839
Merge branch 'develop' into ctim
dangell7 Apr 28, 2023
ccea8c9
clang-format
dangell7 Apr 28, 2023
3a96000
add `'` seperator to hex constants
dangell7 May 29, 2023
5058592
remove impossible validation
dangell7 May 29, 2023
4073a85
pre-increment `it`
dangell7 May 29, 2023
9decc31
add leading 0
dangell7 May 31, 2023
701af50
use boost_regex
dangell7 May 31, 2023
8625bb3
add leading 0
dangell7 May 31, 2023
0728556
`const` after declare variable
dangell7 May 31, 2023
802144b
clang-format
dangell7 May 31, 2023
7f7a93d
add extra check on std::optional
RichardAH Jun 6, 2023
55cbbbd
move guard
dangell7 Jun 27, 2023
38170b6
add test
dangell7 Jun 27, 2023
413388d
update test to match
dangell7 Jun 27, 2023
1f61853
Merge branch 'develop' into ctim
intelliot Jul 12, 2023
027949c
rename `txnIDfromIndex` -> `txnIdFromIndex`
dangell7 Jul 20, 2023
b2ddfde
revert UB guard
dangell7 Jul 20, 2023
80f308f
update test to match guard
dangell7 Jul 20, 2023
32a27c5
fix test naming
RichardAH Jul 20, 2023
61d2f5b
Merge branch 'develop' into ctim
intelliot Jul 26, 2023
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
1 change: 1 addition & 0 deletions Builds/CMake/RippledCore.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ if (tests)
src/test/app/HashRouter_test.cpp
src/test/app/LedgerHistory_test.cpp
src/test/app/LedgerLoad_test.cpp
src/test/app/LedgerMaster_test.cpp
src/test/app/LedgerReplay_test.cpp
src/test/app/LoadFeeTrack_test.cpp
intelliot marked this conversation as resolved.
Show resolved Hide resolved
src/test/app/Manifest_test.cpp
Expand Down
4 changes: 4 additions & 0 deletions src/ripple/app/ledger/LedgerMaster.h
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ class LedgerMaster : public AbstractFetchPackContainer
std::optional<LedgerIndex>
minSqlSeq();

// Iff a txn exists at the specified ledger and offset then return its txnid
std::optional<uint256>
txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex);
thejohnfreeman marked this conversation as resolved.
Show resolved Hide resolved

private:
void
setValidLedger(std::shared_ptr<Ledger const> const& l);
Expand Down
21 changes: 21 additions & 0 deletions src/ripple/app/ledger/impl/LedgerMaster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2369,4 +2369,25 @@ LedgerMaster::minSqlSeq()
return app_.getRelationalDatabase().getMinLedgerSeq();
}

std::optional<uint256>
LedgerMaster::txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex)
{
uint32_t first = 0, last = 0;

if (!getValidatedRange(first, last) || last < ledgerSeq)
return {};

auto const lgr = getLedgerBySeq(ledgerSeq);
if (!lgr || lgr->txs.empty())
intelliot marked this conversation as resolved.
Show resolved Hide resolved
return {};

for (auto it = lgr->txs.begin(); it != lgr->txs.end(); ++it)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-nit: prefer for(auto const& tx : lgr->txs) instead.

if (it->first && it->second &&
it->second->isFieldPresent(sfTransactionIndex) &&
it->second->getFieldU32(sfTransactionIndex) == txnIndex)
return it->first->getTransactionID();

return {};
}

} // namespace ripple
6 changes: 5 additions & 1 deletion src/ripple/net/impl/RPCCall.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,11 @@ class RPCParser
jvRequest[jss::max_ledger] = jvParams[2u + offset].asString();
}

jvRequest[jss::transaction] = jvParams[0u].asString();
if (jvParams[0u].asString().length() == 16)
jvRequest[jss::ctid] = jvParams[0u].asString();
else
jvRequest[jss::transaction] = jvParams[0u].asString();

return jvRequest;
}

Expand Down
2 changes: 1 addition & 1 deletion src/ripple/protocol/ErrorCodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ enum error_code_i {
rpcJSON_RPC = 2,
rpcFORBIDDEN = 3,

rpcWRONG_NETWORK = 4,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add "rpcWRONG_NETWORK" in unorderedErrorInfos ( ErrorCodes.cpp )

// Misc failure
// unused 4,
// unused 5,
rpcNO_PERMISSION = 6,
rpcNO_EVENTS = 7,
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/jss.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ JSS(converge_time_s); // out: NetworkOPs
JSS(cookie); // out: NetworkOPs
JSS(count); // in: AccountTx*, ValidatorList
JSS(counters); // in/out: retrieve counters
JSS(ctid); // in/out: Tx RPC
JSS(currency_a); // out: BookChanges
JSS(currency_b); // out: BookChanges
JSS(currentShard); // out: NodeToShardStatus
Expand Down
88 changes: 88 additions & 0 deletions src/ripple/rpc/CTID.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https:/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#ifndef RIPPLE_RPC_CTID_H_INCLUDED
#define RIPPLE_RPC_CTID_H_INCLUDED

#include <boost/algorithm/string/predicate.hpp>
#include <boost/regex.hpp>
#include <optional>
#include <regex>
#include <sstream>

namespace ripple {

namespace RPC {

inline std::optional<std::string>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following why this is a string instead of a uint64_t.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think because the intention is for a CTID to always be 16 hex digits, and there's no "easy" way to format that from a std::uint64_t (hence the ugly bit of code.

encodeCTID(
uint32_t ledger_seq,
uint16_t txn_index,
uint16_t network_id) noexcept
{
if (ledger_seq > 0x0FFF'FFFF)
return {};

uint64_t ctidValue =
((0xC000'0000ULL + static_cast<uint64_t>(ledger_seq)) << 32) +
(static_cast<uint64_t>(txn_index) << 16) + network_id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


std::stringstream buffer;
buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16)
<< ctidValue;
return {buffer.str()};
}

template <typename T>
inline std::optional<std::tuple<uint32_t, uint16_t, uint16_t>>
decodeCTID(const T ctid) noexcept
{
uint64_t ctidValue{0};
if constexpr (
std::is_same_v<T, std::string> || std::is_same_v<T, char*> ||
std::is_same_v<T, const char*> || std::is_same_v<T, std::string_view>)
{
std::string const ctidString(ctid);

if (ctidString.length() != 16)
return {};

if (!boost::regex_match(ctidString, boost::regex("^[0-9A-F]+$")))
return {};

ctidValue = std::stoull(ctidString, nullptr, 16);
Comment on lines +61 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider:

if (ctid.length() != 16)
    return {};

auto res = std::from_chars(ctid.begin(), ctid.end(), ctidValue, 16);

if (res.ptr != id.end() && res.ec != std::errc())
    return {};

}
else if constexpr (std::is_integral_v<T>)
ctidValue = ctid;
else
return {};

if ((ctidValue & 0xF000'0000'0000'0000ULL) != 0xC000'0000'0000'0000ULL)
return {};

uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFF'FFFUL;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing groups for 4 here (see comment above).

uint16_t txn_index = (ctidValue >> 16) & 0xFFFFU;
uint16_t network_id = ctidValue & 0xFFFFU;
return {{ledger_seq, txn_index, network_id}};
}

} // namespace RPC
} // namespace ripple

#endif
90 changes: 75 additions & 15 deletions src/ripple/rpc/handlers/Tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,17 @@
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
#include <ripple/rpc/CTID.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/GRPCHandlers.h>
#include <ripple/rpc/NFTSyntheticSerializer.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <charconv>
#include <regex>

namespace ripple {

// {
// transaction: <hex>
// }

static bool
isValidated(LedgerMaster& ledgerMaster, std::uint32_t seq, uint256 const& hash)
{
Expand All @@ -54,12 +53,14 @@ struct TxResult
Transaction::pointer txn;
std::variant<std::shared_ptr<TxMeta>, Blob> meta;
bool validated = false;
std::optional<std::string> ctid;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following why this is a string instead of a u64. (I'm not saying there isn't a reason, I'm just not seeing why it's done).

Copy link
Collaborator Author

@RichardAH RichardAH May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reasons:

  1. CTID should be displayed to users as a hex encoded string. (So that they can see the leading C and identify it easily). If it is to be placed into the JSON as a hex-encoded string why store it in an intermediate state first?
  2. Supposing we were ok with passing it to json as a decimal number, by convention numbers larger than 32 bits are still passed into JSON as a string. Not all parsing libraries / processors can safely understand these numbers.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Thanks @RichardAH!

TxSearched searchedAll;
};

struct TxArgs
{
uint256 hash;
std::optional<uint256> hash;
std::optional<std::pair<uint32_t, uint16_t>> ctid;
bool binary = false;
std::optional<std::pair<uint32_t, uint32_t>> ledgerRange;
};
Expand All @@ -73,11 +74,19 @@ doTxPostgres(RPC::Context& context, TxArgs const& args)
Throw<std::runtime_error>(
"Called doTxPostgres yet not in reporting mode");
}

TxResult res;
res.searchedAll = TxSearched::unknown;

if (!args.hash)
return {
res,
{rpcNOT_IMPL,
"Use of CTIDs on reporting mode is not currently supported."}};

JLOG(context.j.debug()) << "Fetching from postgres";
Transaction::Locator locator = Transaction::locate(args.hash, context.app);
Transaction::Locator locator =
Transaction::locate(*(args.hash), context.app);

std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
pair;
Expand Down Expand Up @@ -127,7 +136,7 @@ doTxPostgres(RPC::Context& context, TxArgs const& args)
else
{
res.meta = std::make_shared<TxMeta>(
args.hash, res.txn->getLedger(), *meta);
*(args.hash), res.txn->getLedger(), *meta);
}
res.validated = true;
return {res, rpcSUCCESS};
Expand Down Expand Up @@ -168,7 +177,7 @@ doTxPostgres(RPC::Context& context, TxArgs const& args)
}

std::pair<TxResult, RPC::Status>
doTxHelp(RPC::Context& context, TxArgs const& args)
doTxHelp(RPC::Context& context, TxArgs args)
{
if (context.app.config().reporting())
return doTxPostgres(context, args);
Expand Down Expand Up @@ -196,15 +205,25 @@ doTxHelp(RPC::Context& context, TxArgs const& args)
std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;

result.searchedAll = TxSearched::unknown;

std::variant<TxPair, TxSearched> v;

if (args.ctid)
{
args.hash = context.app.getLedgerMaster().txnIDfromIndex(
args.ctid->first, args.ctid->second);

if (args.hash)
range =
ClosedInterval<uint32_t>(args.ctid->first, args.ctid->second);
}

if (args.ledgerRange)
{
v = context.app.getMasterTransaction().fetch(args.hash, range, ec);
v = context.app.getMasterTransaction().fetch(*(args.hash), range, ec);
thejohnfreeman marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
v = context.app.getMasterTransaction().fetch(args.hash, ec);
v = context.app.getMasterTransaction().fetch(*(args.hash), ec);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments here.

}

if (auto e = std::get_if<TxSearched>(&v))
Expand All @@ -213,6 +232,9 @@ doTxHelp(RPC::Context& context, TxArgs const& args)
return {result, rpcTXN_NOT_FOUND};
}

if (!args.hash)
return {result, rpcTXN_NOT_FOUND};

auto [txn, meta] = std::get<TxPair>(v);

if (ec == rpcDB_DESERIALIZATION)
Expand Down Expand Up @@ -246,6 +268,15 @@ doTxHelp(RPC::Context& context, TxArgs const& args)
}
result.validated = isValidated(
context.ledgerMaster, ledger->info().seq, ledger->info().hash);

// compute outgoing CTID
uint32_t lgrSeq = ledger->info().seq;
uint32_t txnIdx = meta->getAsObject().getFieldU32(sfTransactionIndex);
uint32_t netID = context.app.config().NETWORK_ID;

if (txnIdx <= 0xFFFFU && netID < 0xFFFFU && lgrSeq < 0x0FFF'FFFFUL)
result.ctid =
RPC::encodeCTID(lgrSeq, (uint16_t)txnIdx, (uint16_t)netID);
Comment on lines +277 to +279
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend putting these checks in a single place (namely in encodeCTID) so that you don't need to check in "unrelated" places and keep the code cleaner.

}

return {result, rpcSUCCESS};
Expand Down Expand Up @@ -301,6 +332,9 @@ populateJsonResponse(
}
}
response[jss::validated] = result.validated;

if (result.ctid)
response[jss::ctid] = *(result.ctid);
}
return response;
}
Expand All @@ -313,13 +347,39 @@ doTxJson(RPC::JsonContext& context)

// Deserialize and validate JSON arguments

if (!context.params.isMember(jss::transaction))
TxArgs args;

if (context.params.isMember(jss::transaction) &&
context.params.isMember(jss::ctid))
// specifying both is ambiguous
return rpcError(rpcINVALID_PARAMS);

TxArgs args;
if (context.params.isMember(jss::transaction))
{
uint256 hash;
if (!hash.parseHex(context.params[jss::transaction].asString()))
return rpcError(rpcNOT_IMPL);
args.hash = hash;
}
else if (context.params.isMember(jss::ctid))
{
auto ctid = RPC::decodeCTID(context.params[jss::ctid].asString());
if (!ctid)
return rpcError(rpcINVALID_PARAMS);

if (!args.hash.parseHex(context.params[jss::transaction].asString()))
return rpcError(rpcNOT_IMPL);
auto const [lgr_seq, txn_idx, net_id] = *ctid;
if (net_id != context.app.config().NETWORK_ID)
{
std::stringstream out;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer to avoid std::stringstream. I'd recommend:

return RPC::make_error(rpcWRONG_NETWORK,
    "Wrong network. You should submit this request to a node "
    "running on NetworkID: " + std::to_string(net_id));

out << "Wrong network. You should submit this request to a node "
"running on NetworkID: "
<< net_id;
return RPC::make_error(rpcWRONG_NETWORK, out.str());
}
args.ctid = {lgr_seq, txn_idx};
}
else
return rpcError(rpcINVALID_PARAMS);

args.binary = context.params.isMember(jss::binary) &&
context.params[jss::binary].asBool();
Expand Down
1 change: 1 addition & 0 deletions src/ripple/rpc/impl/RPCHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <boost/algorithm/string/case_conv.hpp>
#include <regex>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded if you adopt my std::from_chars recommendation.


namespace ripple {
namespace RPC {
Expand Down
Loading