fix: Support deleted object in ledger_entry (#1483)

Fixes #1306
This commit is contained in:
Zhiyuan Wang
2024-07-15 13:07:09 -04:00
committed by GitHub
parent d6598f30f1
commit e16a9510f1
7 changed files with 405 additions and 4 deletions

View File

@@ -102,6 +102,19 @@ BackendInterface::fetchLedgerObject(
return dbObj;
}
std::optional<std::uint32_t>
BackendInterface::fetchLedgerObjectSeq(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context yield
) const
{
auto seq = doFetchLedgerObjectSeq(key, sequence, yield);
if (!seq)
LOG(gLog.trace()) << "Missed in db";
return seq;
}
std::vector<Blob>
BackendInterface::fetchLedgerObjects(
std::vector<ripple::uint256> const& keys,

View File

@@ -378,6 +378,19 @@ public:
std::optional<Blob>
fetchLedgerObject(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const;
/**
* @brief Fetches a specific ledger object sequence.
*
* Currently the real fetch happens in doFetchLedgerObjectSeq
*
* @param key The key of the object
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The sequence in unit32_t on success; nullopt otherwise
*/
std::optional<std::uint32_t>
fetchLedgerObjectSeq(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const;
/**
* @brief Fetches all ledger objects by their keys.
*
@@ -407,6 +420,17 @@ public:
virtual std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const = 0;
/**
* @brief The database-specific implementation for fetching a ledger object sequence.
*
* @param key The key to fetch for
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The sequence in unit32_t on success; nullopt otherwise
*/
virtual std::optional<std::uint32_t>
doFetchLedgerObjectSeq(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const = 0;
/**
* @brief The database-specific implementation for fetching ledger objects.
*

View File

@@ -567,6 +567,25 @@ public:
return std::nullopt;
}
std::optional<std::uint32_t>
doFetchLedgerObjectSeq(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const override
{
LOG(log_.debug()) << "Fetching ledger object for seq " << sequence << ", key = " << ripple::to_string(key);
if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res) {
if (auto const result = res->template get<Blob, std::uint32_t>(); result) {
auto [_ ,seq] = result.value();
return seq;
} else {
LOG(log_.debug()) << "Could not fetch ledger object sequence - no rows";
}
} else {
LOG(log_.error()) << "Could not fetch ledger object sequence: " << res.error();
}
return std::nullopt;
}
std::optional<TransactionAndMetadata>
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context yield) const override
{

View File

@@ -162,17 +162,26 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
return Error{*status};
auto const lgrInfo = std::get<ripple::LedgerHeader>(lgrInfoOrStatus);
auto const ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, lgrInfo.seq, ctx.yield);
auto output = LedgerEntryHandler::Output{};
auto ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, lgrInfo.seq, ctx.yield);
if (!ledgerObject || ledgerObject->empty()) {
if (not input.includeDeleted)
return Error{Status{"entryNotFound"}};
auto const deletedSeq = sharedPtrBackend_->fetchLedgerObjectSeq(key, lgrInfo.seq, ctx.yield);
if (!deletedSeq)
return Error{Status{"entryNotFound"}};
ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, deletedSeq.value() - 1, ctx.yield);
if (!ledgerObject || ledgerObject->empty())
return Error{Status{"entryNotFound"}};
output.deletedLedgerIndex = deletedSeq.value();
}
ripple::STLedgerEntry const sle{ripple::SerialIter{ledgerObject->data(), ledgerObject->size()}, key};
if (input.expectedType != ripple::ltANY && sle.getType() != input.expectedType)
return Error{Status{"unexpectedLedgerType"}};
auto output = LedgerEntryHandler::Output{};
output.index = ripple::strHex(key);
output.ledgerIndex = lgrInfo.seq;
output.ledgerHash = ripple::strHex(lgrInfo.hash);
@@ -220,6 +229,9 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, LedgerEntryHandl
{JS(index), output.index},
};
if (output.deletedLedgerIndex)
object["deleted_ledger_index"] = *(output.deletedLedgerIndex);
if (output.nodeBinary) {
object[JS(node_binary)] = *(output.nodeBinary);
} else {
@@ -337,6 +349,9 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
input.oracleNode = parseOracleFromJson(jv.at(JS(oracle)));
}
if (jsonObject.contains("include_deleted"))
input.includeDeleted = jv.at("include_deleted").as_bool();
return input;
}

View File

@@ -71,6 +71,7 @@ public:
std::string ledgerHash;
std::optional<boost::json::object> node;
std::optional<std::string> nodeBinary;
std::optional<uint32_t> deletedLedgerIndex;
bool validated = true;
};
@@ -103,6 +104,7 @@ public:
std::optional<uint32_t> chainClaimId;
std::optional<uint32_t> createAccountClaimId;
std::optional<ripple::uint256> oracleNode;
bool includeDeleted = false;
};
using Result = HandlerReturnType<Output>;
@@ -314,6 +316,7 @@ public:
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
}}},
{JS(ledger), check::Deprecated{}},
{"include_deleted", validation::Type<bool>{}},
};
return rpcSpec;

View File

@@ -154,6 +154,13 @@ struct MockBackend : public BackendInterface {
(const, override)
);
MOCK_METHOD(
std::optional<std::uint32_t>,
doFetchLedgerObjectSeq,
(ripple::uint256 const&, std::uint32_t const, boost::asio::yield_context),
(const, override)
);
MOCK_METHOD(
std::vector<LedgerObject>,
fetchLedgerDiff,

View File

@@ -61,6 +61,8 @@ constexpr static auto RANGEMIN = 10;
constexpr static auto RANGEMAX = 30;
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto TOKENID = "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA";
constexpr static auto NFTID = "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004";
constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD";
class RPCLedgerEntryTest : public HandlerBaseTest {};
@@ -2620,3 +2622,321 @@ TEST(RPCLedgerEntrySpecTest, DeprecatedFields)
EXPECT_EQ(warning.at("id").as_int64(), static_cast<int64_t>(rpc::WarningCode::warnRPC_DEPRECATED));
EXPECT_NE(warning.at("message").as_string().find("Field 'ledger' is deprecated."), std::string::npos) << warning;
}
// Same as BinaryFalse with include_deleted set to true
// Expected Result: same as BinaryFalse
TEST_F(RPCLedgerEntryTest, BinaryFalseIncludeDeleted)
{
static auto constexpr OUT = R"({
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"node": {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "100",
"Balance": "200",
"Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Flags": 0,
"LedgerEntryType": "PayChannel",
"OwnerNode": "0",
"PreviousTxnID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"PreviousTxnLgrSeq": 400,
"PublicKey": "020000000000000000000000000000000000000000000000000000000000000000",
"SettleDelay": 300,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"
}
})";
backend->setRange(RANGEMIN, RANGEMAX);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
// return valid ledger entry which can be deserialized
auto const ledgerEntry = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400);
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillRepeatedly(Return(ledgerEntry.getSerializer().peekData()));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(OUT));
});
}
// Test for object is deleted in the latest sequence
// Expected Result: return the latest object that is not deleted
TEST_F(RPCLedgerEntryTest, LedgerEntryDeleted)
{
static auto constexpr OUT = R"({
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"deleted_ledger_index": 30,
"node": {
"Amount": "123",
"Flags": 0,
"LedgerEntryType": "NFTokenOffer",
"NFTokenID": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004",
"NFTokenOfferNode": "0",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"
}
})";
backend->setRange(RANGEMIN, RANGEMAX);
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
// return valid ledger entry which can be deserialized
auto const offer = CreateNFTBuyOffer(NFTID, ACCOUNT);
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(std::optional<Blob>{}));
EXPECT_CALL(*backend, doFetchLedgerObjectSeq(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(uint32_t{RANGEMAX}));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX - 1, _))
.WillOnce(Return(offer.getSerializer().peekData()));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(OUT));
});
}
// Test for object not exist in database
// Expected Result: return entryNotFound error
TEST_F(RPCLedgerEntryTest, LedgerEntryNotExist)
{
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(std::optional<Blob>{}));
EXPECT_CALL(*backend, doFetchLedgerObjectSeq(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(uint32_t{RANGEMAX}));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX - 1, _))
.WillOnce(Return(std::optional<Blob>{}));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
auto const myerr = err.at("error").as_string();
EXPECT_EQ(myerr, "entryNotFound");
});
}
// Same as BinaryFalse with include_deleted set to false
// Expected Result: same as BinaryFalse
TEST_F(RPCLedgerEntryTest, BinaryFalseIncludeDeleteFalse)
{
static auto constexpr OUT = R"({
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"node": {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "100",
"Balance": "200",
"Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Flags": 0,
"LedgerEntryType": "PayChannel",
"OwnerNode": "0",
"PreviousTxnID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"PreviousTxnLgrSeq": 400,
"PublicKey": "020000000000000000000000000000000000000000000000000000000000000000",
"SettleDelay": 300,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"
}
})";
backend->setRange(RANGEMIN, RANGEMAX);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
// return valid ledger entry which can be deserialized
auto const ledgerEntry = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400);
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillRepeatedly(Return(ledgerEntry.getSerializer().peekData()));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"payment_channel": "{}",
"include_deleted": false
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(OUT));
});
}
// Test when an object is updated and include_deleted is set to true
// Expected Result: return the latest object that is not deleted (latest object in this test)
TEST_F(RPCLedgerEntryTest, ObjectUpdateIncludeDelete)
{
static auto constexpr OUT = R"({
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"node": {
"Balance": {
"currency": "USD",
"issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"value": "10"
},
"Flags": 0,
"HighLimit": {
"currency": "USD",
"issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"value": "200"
},
"LedgerEntryType": "RippleState",
"LowLimit": {
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "100"
},
"PreviousTxnID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"PreviousTxnLgrSeq": 123,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"
}
})";
backend->setRange(RANGEMIN, RANGEMAX);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
// return valid ledger entry which can be deserialized
auto const line1 = CreateRippleStateLedgerObject("USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123);
auto const line2 = CreateRippleStateLedgerObject("USD", ACCOUNT, 10, ACCOUNT2, 100, ACCOUNT, 200, TXNID, 123);
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillRepeatedly(Return(line1.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX - 1, _))
.WillRepeatedly(Return(line2.getSerializer().peekData()));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(OUT));
});
}
// Test when an object is deleted several sequence ago and include_deleted is set to true
// Expected Result: return the latest object that is not deleted
TEST_F(RPCLedgerEntryTest, ObjectDeletedPreviously)
{
static auto constexpr OUT = R"({
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"deleted_ledger_index": 26,
"node": {
"Amount": "123",
"Flags": 0,
"LedgerEntryType": "NFTokenOffer",
"NFTokenID": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004",
"NFTokenOfferNode": "0",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"index": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"
}
})";
backend->setRange(RANGEMIN, RANGEMAX);
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
// return valid ledger entry which can be deserialized
auto const offer = CreateNFTBuyOffer(NFTID, ACCOUNT);
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(std::optional<Blob>{}));
EXPECT_CALL(*backend, doFetchLedgerObjectSeq(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(uint32_t{RANGEMAX - 4}));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX - 5, _))
.WillOnce(Return(offer.getSerializer().peekData()));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(OUT));
});
}
// Test for object seq not exist in database
// Expected Result: return entryNotFound error
TEST_F(RPCLedgerEntryTest, ObjectSeqNotExist)
{
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerinfo));
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(std::optional<Blob>{}));
EXPECT_CALL(*backend, doFetchLedgerObjectSeq(ripple::uint256{INDEX1}, RANGEMAX, _))
.WillOnce(Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"index": "{}",
"include_deleted": true
}})",
INDEX1
));
auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
auto const myerr = err.at("error").as_string();
EXPECT_EQ(myerr, "entryNotFound");
});
}