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

Patch CTID #4738

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 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
6 changes: 5 additions & 1 deletion src/ripple/app/ledger/TransactionMaster.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ class TransactionMaster

// return value: true = we had the transaction already
bool
inLedger(uint256 const& hash, std::uint32_t ledger);
inLedger(
uint256 const& hash,
std::uint32_t ledger,
std::optional<uint32_t> tseq,
std::optional<uint32_t> netID);

void
canonicalize(std::shared_ptr<Transaction>* pTransaction);
Expand Down
8 changes: 6 additions & 2 deletions src/ripple/app/ledger/impl/TransactionMaster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ TransactionMaster::TransactionMaster(Application& app)
}

bool
TransactionMaster::inLedger(uint256 const& hash, std::uint32_t ledger)
TransactionMaster::inLedger(
uint256 const& hash,
std::uint32_t ledger,
std::optional<uint32_t> tseq,
std::optional<uint32_t> netID)
{
auto txn = mCache.fetch(hash);

if (!txn)
return false;

txn->setStatus(COMMITTED, ledger);
txn->setStatus(COMMITTED, ledger, tseq, netID);
return true;
}

Expand Down
15 changes: 15 additions & 0 deletions src/ripple/app/misc/NetworkOPs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
#include <ripple/resource/Fees.h>
#include <ripple/resource/ResourceManager.h>
#include <ripple/rpc/BookChanges.h>
#include <ripple/rpc/CTID.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/ServerHandler.h>
#include <ripple/rpc/impl/RPCHelpers.h>
Expand Down Expand Up @@ -3122,6 +3123,20 @@ NetworkOPsImp::transJson(
jvObj[jss::meta], *ledger, transaction, meta->get());
}

// add CTID where the needed data for it exists
if (auto const& lookup = ledger->txRead(transaction->getTransactionID());
lookup.second && lookup.second->isFieldPresent(sfTransactionIndex))
{
uint32_t const txnSeq = lookup.second->getFieldU32(sfTransactionIndex);
uint32_t netID = app_.config().NETWORK_ID;
if (transaction->isFieldPresent(sfNetworkID))
netID = transaction->getFieldU32(sfNetworkID);

if (std::optional<std::string> ctid =
RPC::encodeCTID(ledger->info().seq, txnSeq, netID);
ctid)
jvObj[jss::ctid] = *ctid;
}
if (!ledger->open())
jvObj[jss::ledger_hash] = to_string(ledger->info().hash);

Expand Down
8 changes: 7 additions & 1 deletion src/ripple/app/misc/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ class Transaction : public std::enable_shared_from_this<Transaction>,
}

void
setStatus(TransStatus status, std::uint32_t ledgerSeq);
setStatus(
TransStatus status,
std::uint32_t ledgerSeq,
std::optional<uint32_t> transactionSeq = std::nullopt,
std::optional<uint32_t> networkID = std::nullopt);

void
setStatus(TransStatus status)
Expand Down Expand Up @@ -388,6 +392,8 @@ class Transaction : public std::enable_shared_from_this<Transaction>,
uint256 mTransactionID;

LedgerIndex mLedgerIndex = 0;
std::optional<uint32_t> mTxnSeq;
std::optional<uint32_t> mNetworkID;
TransStatus mStatus = INVALID;
TER mResult = temUNCERTAIN;
bool mApplying = false;
Expand Down
13 changes: 10 additions & 3 deletions src/ripple/app/misc/impl/AccountTxPaging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ convertBlobsToTxResult(

auto tr = std::make_shared<Transaction>(txn, reason, app);

tr->setStatus(Transaction::sqlTransactionStatus(status));
tr->setLedger(ledger_index);

auto metaset =
std::make_shared<TxMeta>(tr->getID(), tr->getLedger(), rawMeta);

// if properly formed meta is available we can use it to generate ctid
if (metaset->getAsObject().isFieldPresent(sfTransactionIndex))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@scottschurr Can you look at this? Wouldn't sfTransactionIndex always exist? If I do account_tx on a non validated ledger I get "Ledger not validated." SetStatus values are optional. So can we just call this every time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Or how do I test account_tx with a tx that doesnt have a sfTransactionIndex?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@dangell7, thanks for asking. I can't immediately think of a situation where you could retrieve metadata for a transaction from a ledger and not have sfTransactionIndex filled in. However I prefer the conservative approach of verifying that the field is present before accessing it. Bugs happen. Doing checks like this can prevent a bug elsewhere from turning catastrophic.

Taking a step back, we ran for a few years without code coverage as part of CI. And, thanks to some great work by good folks, we started getting code coverage reports again recently. But we're still learning how to best apply the tool. There will be code that can't be exercised by reasonable tests (testing asserts anyone?). We have a new microscope and we've discovered that there are tiny mites living on our skin. Over time we'll learn to relax and know we don't have to check that each individual mite is harmless.

Having said that, if I were bound and determined to exercise the else condition here, I'd doing it by calling the convertBlobsToTxResult() function directly. That would allow me to pass in whatever I wanted in the rawMeta argument. This test would pass in rawMeta that omitted an sfTransactionIndex. I'd guess that getting that test in place might take a day or so of effort.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Awesome. Thank you for this response. I do like the code coverage stuff. I will take a look at how to get some more coverage on this. convertBlobsToTxResult sounds pretty simple. Thanks

tr->setStatus(
Transaction::sqlTransactionStatus(status),
ledger_index,
metaset->getAsObject().getFieldU32(sfTransactionIndex),
app.config().NETWORK_ID);
else
tr->setStatus(Transaction::sqlTransactionStatus(status), ledger_index);

to.emplace_back(std::move(tr), metaset);
};

Expand Down
25 changes: 24 additions & 1 deletion src/ripple/app/misc/impl/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <ripple/rpc/CTID.h>

namespace ripple {

Expand Down Expand Up @@ -59,10 +60,18 @@ Transaction::Transaction(
//

void
Transaction::setStatus(TransStatus ts, std::uint32_t lseq)
Transaction::setStatus(
TransStatus ts,
std::uint32_t lseq,
std::optional<std::uint32_t> tseq,
std::optional<std::uint32_t> netID)
{
mStatus = ts;
mLedgerIndex = lseq;
if (tseq)
mTxnSeq = tseq;
if (netID)
mNetworkID = netID;
}

TransStatus
Expand Down Expand Up @@ -190,6 +199,20 @@ Transaction::getJson(JsonOptions options, bool binary) const
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}

// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);

if (mTxnSeq && netID)
{
std::optional<std::string> const ctid =
RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
}
}

return ret;
Expand Down
6 changes: 5 additions & 1 deletion src/ripple/app/rdb/backend/detail/impl/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,11 @@ saveValidatedLedger(
seq, acceptedLedgerTx->getEscMeta()) +
";");

app.getMasterTransaction().inLedger(transactionID, seq);
app.getMasterTransaction().inLedger(
transactionID,
seq,
acceptedLedgerTx->getTxnSeq(),
app.config().NETWORK_ID);
}

tr.commit();
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/ErrorCodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
{rpcNOT_SYNCED, "notSynced", "Not synced to the network.", 503},
{rpcNO_EVENTS, "noEvents", "Current transport does not support events.", 405},
{rpcNO_NETWORK, "noNetwork", "Not synced to the network.", 503},
{rpcWRONG_NETWORK, "wrongNetwork", "Wrong network.", 503},
{rpcNO_PERMISSION, "noPermission", "You don't have permission for this command.", 401},
{rpcNO_PF_REQUEST, "noPathRequest", "No pathfinding request in progress.", 404},
{rpcOBJECT_NOT_FOUND, "objectNotFound", "The requested object was not found.", 404},
Expand Down
20 changes: 13 additions & 7 deletions src/ripple/rpc/CTID.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,24 @@ namespace ripple {

namespace RPC {

// CTID stands for Concise Transaction ID.
//
// The CTID comes from XLS-15d: Concise Transaction Identifier #34
//
// https://github.com/XRPLF/XRPL-Standards/discussions/34
//
// The Concise Transaction ID provides a way to identify a transaction
// that includes which network the transaction was submitted to.

inline std::optional<std::string>
encodeCTID(
uint32_t ledger_seq,
uint16_t txn_index,
uint16_t network_id) noexcept
encodeCTID(uint32_t ledgerSeq, uint32_t txnIndex, uint32_t networkID) noexcept
{
if (ledger_seq > 0x0FFF'FFFF)
if (ledgerSeq > 0x0FFF'FFFF || txnIndex > 0xFFFF || networkID > 0xFFFF)
return {};

uint64_t ctidValue =
((0xC000'0000ULL + static_cast<uint64_t>(ledger_seq)) << 32) +
(static_cast<uint64_t>(txn_index) << 16) + network_id;
((0xC000'0000ULL + static_cast<uint64_t>(ledgerSeq)) << 32) +
(static_cast<uint64_t>(txnIndex) << 16) + networkID;

std::stringstream buffer;
buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16)
Expand Down
18 changes: 11 additions & 7 deletions src/ripple/rpc/handlers/Tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,17 @@ doTxHelp(RPC::Context& context, TxArgs args)
context.ledgerMaster.getCloseTimeBySeq(txn->getLedger());

// 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);
if (meta->getAsObject().isFieldPresent(sfTransactionIndex))
{
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, (uint32_t)txnIdx, (uint32_t)netID);
}
}

return {result, rpcSUCCESS};
Expand Down
89 changes: 64 additions & 25 deletions src/test/rpc/Transaction_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,20 +567,10 @@ class Transaction_test : public beast::unit_test::suite
BEAST_EXPECT(!RPC::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU));

// Test case 3: txn_index greater than 0xFFFF
// this test case is impossible in c++ due to the type, left in for
// completeness
auto const expected3 = std::optional<std::string>("CFFFFFFF0000FFFF");
BEAST_EXPECT(
RPC::encodeCTID(0x0FFF'FFFF, (uint16_t)0x10000, 0xFFFF) ==
expected3);
BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF));

// Test case 4: network_id greater than 0xFFFF
// this test case is impossible in c++ due to the type, left in for
// completeness
auto const expected4 = std::optional<std::string>("CFFFFFFFFFFF0000");
BEAST_EXPECT(
RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, (uint16_t)0x1000'0U) ==
expected4);
BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U));

// Test case 5: Valid input values
auto const expected51 =
Expand Down Expand Up @@ -648,10 +638,11 @@ class Transaction_test : public beast::unit_test::suite

using namespace test::jtx;

// test that the ctid AND the hash are in the response
// Use a Concise Transaction Identifier to request a transaction.
for (uint32_t netID : {11111, 65535, 65536})
{
Env env{*this, makeNetworkConfig(11111)};
uint32_t netID = env.app().config().NETWORK_ID;
Env env{*this, makeNetworkConfig(netID)};
BEAST_EXPECT(netID == env.app().config().NETWORK_ID);

auto const alice = Account("alice");
auto const bob = Account("bob");
Expand All @@ -661,19 +652,62 @@ class Transaction_test : public beast::unit_test::suite
env(pay(alice, bob, XRP(10)));
env.close();

auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
auto const ctid = RPC::encodeCTID(startLegSeq, 0, netID);
if (netID > 0xFFFF)
{
// Concise transaction IDs do not support a network ID > 0xFFFF.
BEAST_EXPECT(ctid == std::nullopt);
continue;
}

Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = ctid;
jsonTx[jss::ctid] = *ctid;
jsonTx[jss::id] = 1;
auto jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
auto const jrr =
env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::ctid] == ctid);
BEAST_EXPECT(jrr[jss::hash]);
BEAST_EXPECT(jrr.isMember(jss::hash));
}

// test that if the network is 65535 the ctid is not in the response
// Using a hash to request the transaction, test the network ID
// boundary where the CTID is (not) in the response.
for (uint32_t netID : {2, 1024, 65535, 65536})
{
Env env{*this, makeNetworkConfig(65535)};
Env env{*this, makeNetworkConfig(netID)};
BEAST_EXPECT(netID == env.app().config().NETWORK_ID);

auto const alice = Account("alice");
auto const bob = Account("bob");

env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();

auto const ledgerSeq = env.current()->info().seq;

env(noop(alice), ter(tesSUCCESS));
env.close();

Json::Value params;
params[jss::id] = 1;
auto const hash = env.tx()->getJson(JsonOptions::none)[jss::hash];
params[jss::transaction] = hash;
auto const jrr =
env.rpc("json", "tx", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::hash] == hash);

BEAST_EXPECT(jrr.isMember(jss::ctid) == (netID <= 0xFFFF));
if (jrr.isMember(jss::ctid))
{
auto const ctid = RPC::encodeCTID(ledgerSeq, 0, netID);
BEAST_EXPECT(jrr[jss::ctid] == *ctid);
}
}

// test the wrong network ID was submitted
{
Env env{*this, makeNetworkConfig(21337)};
uint32_t netID = env.app().config().NETWORK_ID;

auto const alice = Account("alice");
Expand All @@ -684,14 +718,19 @@ class Transaction_test : public beast::unit_test::suite
env(pay(alice, bob, XRP(10)));
env.close();

auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID + 1);
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = ctid;
jsonTx[jss::id] = 1;
auto jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(!jrr[jss::ctid]);
BEAST_EXPECT(jrr[jss::hash]);
auto const jrr =
env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "wrongNetwork");
BEAST_EXPECT(jrr[jss::error_code] == rpcWRONG_NETWORK);
BEAST_EXPECT(
jrr[jss::error_message] ==
"Wrong network. You should submit this request to a node "
"running on NetworkID: 21338");
}
}

Expand Down
Loading