Implement book_offers in new RPC framework (#542)

Fixes #547
This commit is contained in:
cyan317
2023-03-21 09:12:25 +00:00
committed by GitHub
parent b25ac5d707
commit edd2e9dd4b
17 changed files with 1788 additions and 37 deletions

View File

@@ -62,6 +62,7 @@ target_sources(clio PRIVATE
src/rpc/ngHandlers/Tx.cpp
src/rpc/ngHandlers/GatewayBalances.cpp
src/rpc/ngHandlers/LedgerEntry.cpp
src/rpc/ngHandlers/BookOffers.cpp
## RPC Methods
# Account
src/rpc/handlers/AccountChannels.cpp
@@ -126,7 +127,8 @@ if(BUILD_TESTS)
unittests/rpc/handlers/AccountChannelsTest.cpp
unittests/rpc/handlers/TxTest.cpp
unittests/rpc/handlers/GatewayBalancesTest.cpp
unittests/rpc/handlers/LedgerEntryTest.cpp)
unittests/rpc/handlers/LedgerEntryTest.cpp
unittests/rpc/handlers/BookOffersTest.cpp)
include(CMake/deps/gtest.cmake)
# if CODE_COVERAGE enable, add clio_test-ccov

View File

@@ -173,7 +173,6 @@ BackendInterface::fetchBookOffers(
ripple::uint256 const& book,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursor,
boost::asio::yield_context& yield) const
{
// TODO try to speed this up. This can take a few seconds. The goal is

View File

@@ -479,7 +479,6 @@ public:
ripple::uint256 const& book,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursor,
boost::asio::yield_context& yield) const;
/**

View File

@@ -1269,7 +1269,7 @@ postProcessOrderBook(
bool globalFreeze =
isGlobalFrozen(backend, ledgerSequence, book.out.account, yield) ||
isGlobalFrozen(backend, ledgerSequence, book.out.account, yield);
isGlobalFrozen(backend, ledgerSequence, book.in.account, yield);
auto rate = transferRate(backend, ledgerSequence, book.out.account, yield);
@@ -1312,14 +1312,13 @@ postProcessOrderBook(
}
else
{
bool zeroIfFrozen = true;
saOwnerFunds = accountHolds(
backend,
ledgerSequence,
uOfferOwnerID,
book.out.currency,
book.out.account,
zeroIfFrozen,
true,
yield);
if (saOwnerFunds < beast::zero)
@@ -1388,6 +1387,43 @@ postProcessOrderBook(
return jsonOffers;
}
// get book via currency type
std::variant<Status, ripple::Book>
parseBook(
ripple::Currency pays,
ripple::AccountID payIssuer,
ripple::Currency gets,
ripple::AccountID getIssuer)
{
if (isXRP(pays) && !isXRP(payIssuer))
return Status{
RippledError::rpcSRC_ISR_MALFORMED,
"Unneeded field 'taker_pays.issuer' for XRP currency "
"specification."};
if (!isXRP(pays) && isXRP(payIssuer))
return Status{
RippledError::rpcSRC_ISR_MALFORMED,
"Invalid field 'taker_pays.issuer', expected non-XRP "
"issuer."};
if (ripple::isXRP(gets) && !ripple::isXRP(getIssuer))
return Status{
RippledError::rpcDST_ISR_MALFORMED,
"Unneeded field 'taker_gets.issuer' for XRP currency "
"specification."};
if (!ripple::isXRP(gets) && ripple::isXRP(getIssuer))
return Status{
RippledError::rpcDST_ISR_MALFORMED,
"Invalid field 'taker_gets.issuer', expected non-XRP issuer."};
if (pays == gets && payIssuer == getIssuer)
return Status{RippledError::rpcBAD_MARKET, "badMarket"};
return ripple::Book{{pays, payIssuer}, {gets, getIssuer}};
}
std::variant<Status, ripple::Book>
parseBook(boost::json::object const& request)
{

View File

@@ -216,6 +216,13 @@ postProcessOrderBook(
std::uint32_t ledgerSequence,
boost::asio::yield_context& yield);
std::variant<Status, ripple::Book>
parseBook(
ripple::Currency pays,
ripple::AccountID payIssuer,
ripple::Currency gets,
ripple::AccountID getIssuer);
std::variant<Status, ripple::Book>
parseBook(boost::json::object const& request);

View File

@@ -22,6 +22,7 @@
#include <rpc/common/Validators.h>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <charconv>
#include <string_view>
@@ -200,4 +201,27 @@ CustomValidator CurrencyValidator = CustomValidator{
return MaybeError{};
}};
CustomValidator IssuerValidator = CustomValidator{
[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_string())
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS,
std::string(key) + "NotString"}};
ripple::AccountID issuer;
if (!ripple::to_issuer(issuer, value.as_string().c_str()))
return Error{RPC::Status{
// TODO: need to align with the error
RPC::RippledError::rpcINVALID_PARAMS,
fmt::format("Invalid field '{}', bad issuer.", key)}};
if (issuer == ripple::noAccount())
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS,
fmt::format(
"Invalid field '{}', bad issuer account "
"one.",
key)}};
return MaybeError{};
}};
} // namespace RPCng::validation

View File

@@ -485,4 +485,10 @@ extern CustomValidator Uint256HexStringValidator;
*/
extern CustomValidator CurrencyValidator;
/**
* @brief Provide a common used validator for issuer type
* It must be a hex string or base58 string
*/
extern CustomValidator IssuerValidator;
} // namespace RPCng::validation

View File

@@ -83,13 +83,9 @@ doBookOffers(Context const& context)
if (auto const status = getTaker(request, takerID); status)
return status;
ripple::uint256 marker = beast::zero;
if (auto const status = getHexMarker(request, marker); status)
return status;
auto start = std::chrono::system_clock::now();
auto [offers, retMarker] = context.backend->fetchBookOffers(
bookBase, lgrInfo.seq, limit, marker, context.yield);
auto [offers, _] = context.backend->fetchBookOffers(
bookBase, lgrInfo.seq, limit, context.yield);
auto end = std::chrono::system_clock::now();
gLog.warn() << "Time loading books: "
@@ -111,10 +107,6 @@ doBookOffers(Context const& context)
end2 - end)
.count()
<< " milliseconds - request = " << request;
if (retMarker)
response["marker"] = ripple::strHex(*retMarker);
return response;
}

View File

@@ -263,8 +263,8 @@ validateAndGetBooks(
auto book,
boost::asio::yield_context& yield) {
auto bookBase = getBookBase(book);
auto [offers, retMarker] = backend->fetchBookOffers(
bookBase, rng->maxSequence, 200, {}, yield);
auto [offers, _] = backend->fetchBookOffers(
bookBase, rng->maxSequence, 200, yield);
auto orderBook = postProcessOrderBook(
offers, book, takerID, *backend, rng->maxSequence, yield);

View File

@@ -0,0 +1,134 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and 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.
*/
//==============================================================================
#include <rpc/RPCHelpers.h>
#include <rpc/ngHandlers/BookOffers.h>
namespace RPCng {
BookOffersHandler::Result
BookOffersHandler::process(Input input, boost::asio::yield_context& yield) const
{
auto bookMaybe = RPC::parseBook(
input.paysCurrency, input.paysID, input.getsCurrency, input.getsID);
if (auto const status = std::get_if<RPC::Status>(&bookMaybe))
return Error{*status};
// check ledger
auto const range = sharedPtrBackend_->fetchLedgerRange();
auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq(
*sharedPtrBackend_,
yield,
input.ledgerHash,
input.ledgerIndex,
range->maxSequence);
if (auto const status = std::get_if<RPC::Status>(&lgrInfoOrStatus))
return Error{*status};
auto const lgrInfo = std::get<ripple::LedgerInfo>(lgrInfoOrStatus);
auto const book = std::get<ripple::Book>(bookMaybe);
auto const bookKey = getBookBase(book);
// TODO: Add perfomance metrics if needed in future
auto [offers, _] = sharedPtrBackend_->fetchBookOffers(
bookKey, lgrInfo.seq, input.limit, yield);
BookOffersHandler::Output output;
output.ledgerHash = ripple::strHex(lgrInfo.hash);
output.ledgerIndex = lgrInfo.seq;
output.offers = RPC::postProcessOrderBook(
offers,
book,
input.taker ? *(input.taker) : beast::zero,
*sharedPtrBackend_,
lgrInfo.seq,
yield);
return output;
}
void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
BookOffersHandler::Output const& output)
{
jv = boost::json::object{
{"ledger_hash", output.ledgerHash},
{"ledger_index", output.ledgerIndex},
{"offers", output.offers},
};
}
BookOffersHandler::Input
tag_invoke(
boost::json::value_to_tag<BookOffersHandler::Input>,
boost::json::value const& jv)
{
BookOffersHandler::Input input;
auto const& jsonObject = jv.as_object();
ripple::to_currency(
input.getsCurrency,
jv.at("taker_gets").as_object().at("currency").as_string().c_str());
ripple::to_currency(
input.paysCurrency,
jv.at("taker_pays").as_object().at("currency").as_string().c_str());
if (jv.at("taker_gets").as_object().contains("issuer"))
{
ripple::to_issuer(
input.getsID,
jv.at("taker_gets").as_object().at("issuer").as_string().c_str());
}
if (jv.at("taker_pays").as_object().contains("issuer"))
{
ripple::to_issuer(
input.paysID,
jv.at("taker_pays").as_object().at("issuer").as_string().c_str());
}
if (jsonObject.contains("ledger_hash"))
{
input.ledgerHash = jv.at("ledger_hash").as_string().c_str();
}
if (jsonObject.contains("ledger_index"))
{
if (!jsonObject.at("ledger_index").is_string())
{
input.ledgerIndex = jv.at("ledger_index").as_int64();
}
else if (jsonObject.at("ledger_index").as_string() != "validated")
{
input.ledgerIndex =
std::stoi(jv.at("ledger_index").as_string().c_str());
}
}
if (jsonObject.contains("taker"))
{
input.taker =
RPC::accountFromStringStrict(jv.at("taker").as_string().c_str());
}
if (jsonObject.contains("limit"))
{
input.limit = jv.at("limit").as_int64();
}
return input;
}
} // namespace RPCng

View File

@@ -0,0 +1,118 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and 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.
*/
//==============================================================================
#pragma once
#include <backend/BackendInterface.h>
#include <rpc/common/Types.h>
#include <rpc/common/Validators.h>
#include <boost/asio/spawn.hpp>
namespace RPCng {
class BookOffersHandler
{
std::shared_ptr<BackendInterface> sharedPtrBackend_;
public:
struct Output
{
std::string ledgerHash;
uint32_t ledgerIndex;
boost::json::array offers;
bool validated = true;
};
// the taker is not really used in both clio and rippled, both of them
// return all the offers regardless the funding status
struct Input
{
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
uint32_t limit = 50;
std::optional<ripple::AccountID> taker;
ripple::Currency paysCurrency;
ripple::Currency getsCurrency;
// accountID will be filled by input converter, if no issuer is given,
// will use XRP issuer
ripple::AccountID paysID = ripple::xrpAccount();
ripple::AccountID getsID = ripple::xrpAccount();
};
using Result = RPCng::HandlerReturnType<Output>;
BookOffersHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend)
: sharedPtrBackend_(sharedPtrBackend)
{
}
RpcSpecConstRef
spec() const
{
static const RpcSpec rpcSpec = {
{"taker_gets",
validation::Required{},
validation::Type<boost::json::object>{},
validation::Section{
{"currency",
validation::Required{},
validation::WithCustomError{
validation::CurrencyValidator,
RPC::Status(RPC::RippledError::rpcDST_AMT_MALFORMED)}},
{"issuer",
validation::WithCustomError{
validation::IssuerValidator,
RPC::Status(RPC::RippledError::rpcDST_ISR_MALFORMED)}}}},
{"taker_pays",
validation::Required{},
validation::Type<boost::json::object>{},
validation::Section{
{"currency",
validation::Required{},
validation::WithCustomError{
validation::CurrencyValidator,
RPC::Status(RPC::RippledError::rpcSRC_CUR_MALFORMED)}},
{"issuer",
validation::WithCustomError{
validation::IssuerValidator,
RPC::Status(RPC::RippledError::rpcSRC_ISR_MALFORMED)}}}},
{"taker", validation::AccountValidator},
{"limit",
validation::Type<uint32_t>{},
validation::Between{1, 100}},
{"ledger_hash", validation::Uint256HexStringValidator},
{"ledger_index", validation::LedgerIndexValidator}};
return rpcSpec;
}
Result
process(Input input, boost::asio::yield_context& yield) const;
};
void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
BookOffersHandler::Output const& output);
BookOffersHandler::Input
tag_invoke(
boost::json::value_to_tag<BookOffersHandler::Input>,
boost::json::value const& jv);
} // namespace RPCng

View File

@@ -171,15 +171,15 @@ TEST_F(RPCAccountCurrenciesHandlerTest, DefaultParameter)
// ACCOUNT can receive USD 10 from ACCOUNT2 and send USD 20 to ACCOUNT2, now
// the balance is 100, ACCOUNT can only send USD to ACCOUNT2
auto const line1 = CreateRippleStateLedgerObject(
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0);
// ACCOUNT2 can receive JPY 10 from ACCOUNT and send JPY 20 to ACCOUNT, now
// the balance is 100, ACCOUNT2 can only send JPY to ACCOUNT
auto const line2 = CreateRippleStateLedgerObject(
ACCOUNT, "JPY", ISSUER, 100, ACCOUNT2, 10, ACCOUNT, 20, TXNID, 123);
ACCOUNT, "JPY", ISSUER, 100, ACCOUNT2, 10, ACCOUNT, 20, TXNID, 123, 0);
// ACCOUNT can receive EUR 10 from ACCOUNT and send EUR 20 to ACCOUNT2, now
// the balance is 8, ACCOUNT can receive/send EUR to/from ACCOUNT2
auto const line3 = CreateRippleStateLedgerObject(
ACCOUNT, "EUR", ISSUER, 8, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
ACCOUNT, "EUR", ISSUER, 8, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0);
std::vector<Blob> bbs;
bbs.push_back(line1.getSerializer().peekData());
bbs.push_back(line2.getSerializer().peekData());
@@ -226,7 +226,7 @@ TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderHash)
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2);
std::vector<Blob> bbs;
auto const line1 = CreateRippleStateLedgerObject(
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0);
bbs.push_back(line1.getSerializer().peekData());
ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs));
@@ -272,7 +272,7 @@ TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderSeq)
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2);
std::vector<Blob> bbs;
auto const line1 = CreateRippleStateLedgerObject(
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0);
bbs.push_back(line1.getSerializer().peekData());
ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs));

File diff suppressed because it is too large Load Diff

View File

@@ -746,7 +746,15 @@ generateTestValuesForNormalPathTest()
}})",
INDEX1),
ripple::uint256{INDEX1},
CreateOfferLedgerObject(ACCOUNT, 100, 200, "USD", ACCOUNT2)},
CreateOfferLedgerObject(
ACCOUNT,
100,
200,
"USD",
"XRP",
ACCOUNT2,
ripple::toBase58(ripple::xrpAccount()),
INDEX1)},
NormalPathTestBundle{
"EscrowIndex",
fmt::format(
@@ -879,7 +887,8 @@ generateTestValuesForNormalPathTest()
ACCOUNT2,
20,
INDEX1,
123)},
123,
0)},
NormalPathTestBundle{
"Ticket",
fmt::format(
@@ -905,7 +914,15 @@ generateTestValuesForNormalPathTest()
}})",
ACCOUNT),
ripple::keylet::offer(account1, 2).key,
CreateOfferLedgerObject(ACCOUNT, 100, 200, "USD", ACCOUNT2)}};
CreateOfferLedgerObject(
ACCOUNT,
100,
200,
"USD",
"XRP",
ACCOUNT2,
ripple::toBase58(ripple::xrpAccount()),
INDEX1)}};
}
INSTANTIATE_TEST_CASE_P(

View File

@@ -152,6 +152,21 @@ struct SyncAsioContextTest : virtual public NoLoggerFixture
{
}
template <typename F>
void
runSpawn(F&& f)
{
auto called = false;
auto work = std::optional<boost::asio::io_context::work>{ctx};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
f(yield);
called = true;
work.reset();
});
ctx.run();
ASSERT_TRUE(called);
}
protected:
boost::asio::io_context ctx;
};

View File

@@ -101,7 +101,8 @@ CreateAccountRootObject(
int balance,
uint32_t ownerCount,
std::string_view previousTxnID,
uint32_t previousTxnSeq)
uint32_t previousTxnSeq,
uint32_t transferRate)
{
ripple::STObject accountRoot(ripple::sfAccount);
accountRoot.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT);
@@ -115,6 +116,7 @@ CreateAccountRootObject(
accountRoot.setFieldH256(
ripple::sfPreviousTxnID, ripple::uint256{previousTxnID});
accountRoot.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxnSeq);
accountRoot.setFieldU32(ripple::sfTransferRate, transferRate);
return accountRoot;
}
@@ -303,11 +305,12 @@ CreateRippleStateLedgerObject(
std::string_view highNodeAccountId,
int highLimit,
std::string_view previousTxnId,
uint32_t previousTxnSeq)
uint32_t previousTxnSeq,
uint32_t flag)
{
auto line = ripple::STObject(ripple::sfLedgerEntry);
line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE);
line.setFieldU32(ripple::sfFlags, 0);
line.setFieldU32(ripple::sfFlags, flag);
line.setFieldAmount(
ripple::sfBalance,
ripple::STAmount(GetIssue(currency, issuerId), balance));
@@ -327,22 +330,27 @@ CreateOfferLedgerObject(
std::string_view account,
int takerGets,
int takerPays,
std::string_view currency,
std::string_view issueId)
std::string_view getsCurrency,
std::string_view paysCurrency,
std::string_view getsIssueId,
std::string_view paysIssueId,
std::string_view dirId)
{
ripple::STObject offer(ripple::sfLedgerEntry);
offer.setFieldU16(ripple::sfLedgerEntryType, ripple::ltOFFER);
offer.setAccountID(ripple::sfAccount, GetAccountIDWithString(account));
offer.setFieldU32(ripple::sfSequence, 0);
offer.setFieldU32(ripple::sfFlags, 0);
ripple::Issue issue1 = GetIssue(currency, issueId);
ripple::Issue issue1 = GetIssue(getsCurrency, getsIssueId);
offer.setFieldAmount(
ripple::sfTakerGets, ripple::STAmount(issue1, takerGets));
ripple::Issue issue2 = GetIssue(paysCurrency, paysIssueId);
offer.setFieldAmount(
ripple::sfTakerPays, ripple::STAmount(takerPays, false));
ripple::sfTakerPays, ripple::STAmount(issue2, takerPays));
offer.setFieldH256(ripple::sfBookDirectory, ripple::uint256{});
offer.setFieldU64(ripple::sfBookNode, 0);
offer.setFieldU64(ripple::sfOwnerNode, 0);
offer.setFieldH256(ripple::sfBookDirectory, ripple::uint256{dirId});
offer.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
offer.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
return offer;

View File

@@ -79,7 +79,8 @@ CreateAccountRootObject(
int balance,
uint32_t ownerCount,
std::string_view previousTxnID,
uint32_t previousTxnSeq);
uint32_t previousTxnSeq,
uint32_t transferRate = 0);
/*
* Create a createoffer treansaction
@@ -168,15 +169,19 @@ CreateRippleStateLedgerObject(
std::string_view highNodeAccountId,
int highLimit,
std::string_view previousTxnId,
uint32_t previousTxnSeq);
uint32_t previousTxnSeq,
uint32_t flag = 0);
ripple::STObject
CreateOfferLedgerObject(
std::string_view account,
int takerGets,
int takerPays,
std::string_view currency,
std::string_view issueId);
std::string_view getsCurrency,
std::string_view payssCurrency,
std::string_view getsIssueId,
std::string_view paysIssueId,
std::string_view bookDirId);
ripple::STObject
CreateTicketLedgerObject(std::string_view rootIndex, uint32_t sequence);