mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 01:37:00 +00:00
535 lines
22 KiB
C++
535 lines
22 KiB
C++
#include <test/jtx.h>
|
|
#include <test/jtx/TrustedPublisherServer.h>
|
|
|
|
#include <xrpld/app/main/BasicApp.h>
|
|
#include <xrpld/app/misc/ValidatorSite.h>
|
|
#include <xrpld/core/ConfigSections.h>
|
|
|
|
#include <xrpl/beast/unit_test.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <set>
|
|
|
|
namespace xrpl {
|
|
|
|
namespace test {
|
|
|
|
class ValidatorRPC_test : public beast::unit_test::suite
|
|
{
|
|
using Validator = TrustedPublisherServer::Validator;
|
|
|
|
public:
|
|
void
|
|
testPrivileges()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
for (bool const isAdmin : {true, false})
|
|
{
|
|
for (std::string cmd : {"validators", "validator_list_sites"})
|
|
{
|
|
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
|
|
env.set_retries(isAdmin ? 5 : 0);
|
|
auto const jrr = env.rpc(cmd)[jss::result];
|
|
if (isAdmin)
|
|
{
|
|
BEAST_EXPECT(!jrr.isMember(jss::error));
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
}
|
|
else
|
|
{
|
|
// The current HTTP/S ServerHandler returns an HTTP 403
|
|
// error code here rather than a noPermission JSON error.
|
|
// The JSONRPCClient just eats that error and returns null
|
|
// result.
|
|
BEAST_EXPECT(jrr.isNull());
|
|
}
|
|
}
|
|
|
|
{
|
|
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
BEAST_EXPECT(jrr[jss::info].isMember(jss::validator_list) == isAdmin);
|
|
}
|
|
|
|
{
|
|
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
BEAST_EXPECT(jrr[jss::state].isMember(jss::validator_list_expires) == isAdmin);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testStaticUNL()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
std::set<std::string> const keys = {
|
|
"n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7",
|
|
"n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj"};
|
|
Env env{
|
|
*this,
|
|
envconfig([&keys](std::unique_ptr<Config> cfg) {
|
|
for (auto const& key : keys)
|
|
cfg->section(SECTION_VALIDATORS).append(key);
|
|
return cfg;
|
|
}),
|
|
};
|
|
|
|
// Server info reports maximum expiration since not dynamic
|
|
{
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::info][jss::validator_list][jss::expiration] == "never");
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::state][jss::validator_list_expires].asUInt() ==
|
|
NetClock::time_point::max().time_since_epoch().count());
|
|
}
|
|
// All our keys are in the response
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::validator_list][jss::expiration] == "never");
|
|
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == keys.size());
|
|
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == keys.size());
|
|
BEAST_EXPECT(jrr[jss::publisher_lists].size() == 0);
|
|
BEAST_EXPECT(jrr[jss::local_static_keys].size() == keys.size());
|
|
for (auto const& jKey : jrr[jss::local_static_keys])
|
|
{
|
|
BEAST_EXPECT(keys.count(jKey.asString()) == 1);
|
|
}
|
|
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
|
|
}
|
|
// No validator sites configured
|
|
{
|
|
auto const jrr = env.rpc("validator_list_sites")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::validator_sites].size() == 0);
|
|
}
|
|
// Negative UNL empty
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::NegativeUNL].isNull());
|
|
}
|
|
// Negative UNL update
|
|
{
|
|
hash_set<PublicKey> disabledKeys;
|
|
auto k1 = randomKeyPair(KeyType::ed25519).first;
|
|
auto k2 = randomKeyPair(KeyType::ed25519).first;
|
|
disabledKeys.insert(k1);
|
|
disabledKeys.insert(k2);
|
|
env.app().validators().setNegativeUNL(disabledKeys);
|
|
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
auto& jrrnUnl = jrr[jss::NegativeUNL];
|
|
auto jrrnUnlSize = jrrnUnl.size();
|
|
BEAST_EXPECT(jrrnUnlSize == 2);
|
|
for (std::uint32_t x = 0; x < jrrnUnlSize; ++x)
|
|
{
|
|
auto parsedKey =
|
|
parseBase58<PublicKey>(TokenType::NodePublic, jrrnUnl[x].asString());
|
|
BEAST_EXPECT(parsedKey);
|
|
if (parsedKey)
|
|
BEAST_EXPECT(disabledKeys.find(*parsedKey) != disabledKeys.end());
|
|
}
|
|
|
|
disabledKeys.clear();
|
|
env.app().validators().setNegativeUNL(disabledKeys);
|
|
auto const jrrUpdated = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(jrrUpdated[jss::NegativeUNL].isNull());
|
|
}
|
|
}
|
|
|
|
void
|
|
testDynamicUNL()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
auto toStr = [](PublicKey const& publicKey) {
|
|
return toBase58(TokenType::NodePublic, publicKey);
|
|
};
|
|
|
|
// Validator keys that will be in the published list
|
|
std::vector<Validator> validators = {
|
|
TrustedPublisherServer::randomValidator(), TrustedPublisherServer::randomValidator()};
|
|
std::set<std::string> expectedKeys;
|
|
for (auto const& val : validators)
|
|
expectedKeys.insert(toStr(val.masterPublic));
|
|
|
|
// Manage single-thread io_context for server.
|
|
BasicApp worker{1};
|
|
using namespace std::chrono_literals;
|
|
NetClock::time_point const validUntil{3600s};
|
|
NetClock::time_point const validFrom2{validUntil - 60s};
|
|
NetClock::time_point const validUntil2{validFrom2 + 3600s};
|
|
auto server = make_TrustedPublisherServer(
|
|
worker.get_io_context(),
|
|
validators,
|
|
validUntil,
|
|
{{validFrom2, validUntil2}},
|
|
false,
|
|
1,
|
|
false);
|
|
|
|
//----------------------------------------------------------------------
|
|
// Publisher list site unavailable v1
|
|
{
|
|
// Publisher site information
|
|
using namespace std::string_literals;
|
|
std::string siteURI = "http://"s + getEnvLocalhostAddr() + ":1234/validators";
|
|
|
|
Env env{
|
|
*this,
|
|
envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
|
|
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
|
|
.append(strHex(server->publisherPublic()));
|
|
return cfg;
|
|
}),
|
|
};
|
|
|
|
env.app().validatorSites().start();
|
|
env.app().validatorSites().join();
|
|
|
|
{
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::info][jss::validator_list][jss::expiration] == "unknown");
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::state][jss::validator_list_expires].asInt() == 0);
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::validation_quorum].asUInt() ==
|
|
std::numeric_limits<std::uint32_t>::max());
|
|
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
|
|
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == 0);
|
|
BEAST_EXPECT(jrr[jss::validator_list][jss::expiration] == "unknown");
|
|
|
|
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
|
|
{
|
|
auto jp = jrr[jss::publisher_lists][0u];
|
|
BEAST_EXPECT(jp[jss::available] == false);
|
|
BEAST_EXPECT(jp[jss::list].size() == 0);
|
|
BEAST_EXPECT(!jp.isMember(jss::seq));
|
|
BEAST_EXPECT(!jp.isMember(jss::expiration));
|
|
BEAST_EXPECT(!jp.isMember(jss::version));
|
|
BEAST_EXPECT(jp[jss::pubkey_publisher] == strHex(server->publisherPublic()));
|
|
}
|
|
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validator_list_sites")[jss::result];
|
|
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
|
|
{
|
|
auto js = jrr[jss::validator_sites][0u];
|
|
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
|
|
BEAST_EXPECT(js[jss::uri] == siteURI);
|
|
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
|
|
BEAST_EXPECT(js[jss::last_refresh_status] == "invalid");
|
|
}
|
|
}
|
|
}
|
|
// Publisher list site unavailable v2
|
|
{
|
|
// Publisher site information
|
|
using namespace std::string_literals;
|
|
std::string siteURI = "http://"s + getEnvLocalhostAddr() + ":1234/validators2";
|
|
|
|
Env env{
|
|
*this,
|
|
envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
|
|
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
|
|
.append(strHex(server->publisherPublic()));
|
|
return cfg;
|
|
}),
|
|
};
|
|
|
|
env.app().validatorSites().start();
|
|
env.app().validatorSites().join();
|
|
|
|
{
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::info][jss::validator_list][jss::expiration] == "unknown");
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::state][jss::validator_list_expires].asInt() == 0);
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::validation_quorum].asUInt() ==
|
|
std::numeric_limits<std::uint32_t>::max());
|
|
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
|
|
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == 0);
|
|
BEAST_EXPECT(jrr[jss::validator_list][jss::expiration] == "unknown");
|
|
|
|
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
|
|
{
|
|
auto jp = jrr[jss::publisher_lists][0u];
|
|
BEAST_EXPECT(jp[jss::available] == false);
|
|
BEAST_EXPECT(jp[jss::list].size() == 0);
|
|
BEAST_EXPECT(!jp.isMember(jss::seq));
|
|
BEAST_EXPECT(!jp.isMember(jss::expiration));
|
|
BEAST_EXPECT(!jp.isMember(jss::version));
|
|
BEAST_EXPECT(jp[jss::pubkey_publisher] == strHex(server->publisherPublic()));
|
|
}
|
|
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validator_list_sites")[jss::result];
|
|
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
|
|
{
|
|
auto js = jrr[jss::validator_sites][0u];
|
|
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
|
|
BEAST_EXPECT(js[jss::uri] == siteURI);
|
|
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
|
|
BEAST_EXPECT(js[jss::last_refresh_status] == "invalid");
|
|
}
|
|
}
|
|
}
|
|
//----------------------------------------------------------------------
|
|
// Publisher list site available
|
|
server->start();
|
|
// Publisher list site available v1
|
|
{
|
|
std::stringstream uri;
|
|
uri << "http://" << server->local_endpoint() << "/validators";
|
|
auto siteURI = uri.str();
|
|
|
|
Env env{
|
|
*this,
|
|
envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
|
|
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
|
|
.append(strHex(server->publisherPublic()));
|
|
return cfg;
|
|
}),
|
|
};
|
|
|
|
env.app().validatorSites().start();
|
|
env.app().validatorSites().join();
|
|
hash_set<NodeID> startKeys;
|
|
for (auto const& val : validators)
|
|
startKeys.insert(calcNodeID(val.masterPublic));
|
|
|
|
env.app().validators().updateTrusted(
|
|
startKeys,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
{
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::info][jss::validator_list][jss::expiration] == to_string(validUntil));
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::state][jss::validator_list_expires].asUInt() ==
|
|
validUntil.time_since_epoch().count());
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == 2);
|
|
BEAST_EXPECT(jrr[jss::validator_list][jss::expiration] == to_string(validUntil));
|
|
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
|
|
|
|
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == expectedKeys.size());
|
|
for (auto const& jKey : jrr[jss::trusted_validator_keys])
|
|
{
|
|
BEAST_EXPECT(expectedKeys.count(jKey.asString()) == 1);
|
|
}
|
|
|
|
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
|
|
{
|
|
auto jp = jrr[jss::publisher_lists][0u];
|
|
BEAST_EXPECT(jp[jss::available] == true);
|
|
if (BEAST_EXPECT(jp[jss::list].size() == 2))
|
|
{
|
|
// check entries
|
|
std::set<std::string> foundKeys;
|
|
for (auto const& k : jp[jss::list])
|
|
{
|
|
foundKeys.insert(k.asString());
|
|
}
|
|
BEAST_EXPECT(foundKeys == expectedKeys);
|
|
}
|
|
BEAST_EXPECT(jp[jss::seq].asUInt() == 1);
|
|
BEAST_EXPECT(jp[jss::pubkey_publisher] == strHex(server->publisherPublic()));
|
|
BEAST_EXPECT(jp[jss::expiration] == to_string(validUntil));
|
|
BEAST_EXPECT(jp[jss::version] == 1);
|
|
}
|
|
auto jsk = jrr[jss::signing_keys];
|
|
BEAST_EXPECT(jsk.size() == 2);
|
|
for (auto const& val : validators)
|
|
{
|
|
BEAST_EXPECT(jsk.isMember(toStr(val.masterPublic)));
|
|
BEAST_EXPECT(jsk[toStr(val.masterPublic)] == toStr(val.signingPublic));
|
|
}
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validator_list_sites")[jss::result];
|
|
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
|
|
{
|
|
auto js = jrr[jss::validator_sites][0u];
|
|
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
|
|
BEAST_EXPECT(js[jss::uri] == siteURI);
|
|
BEAST_EXPECT(js[jss::last_refresh_status] == "accepted");
|
|
// The actual time of the update will vary run to run, so
|
|
// just verify the time is there
|
|
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
|
|
}
|
|
}
|
|
}
|
|
// Publisher list site available v2
|
|
{
|
|
std::stringstream uri;
|
|
uri << "http://" << server->local_endpoint() << "/validators2";
|
|
auto siteURI = uri.str();
|
|
|
|
Env env{
|
|
*this,
|
|
envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
|
|
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
|
|
.append(strHex(server->publisherPublic()));
|
|
return cfg;
|
|
}),
|
|
};
|
|
|
|
env.app().validatorSites().start();
|
|
env.app().validatorSites().join();
|
|
hash_set<NodeID> startKeys;
|
|
for (auto const& val : validators)
|
|
startKeys.insert(calcNodeID(val.masterPublic));
|
|
|
|
env.app().validators().updateTrusted(
|
|
startKeys,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
{
|
|
auto const jrr = env.rpc("server_info")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::info][jss::validator_list][jss::expiration] == to_string(validUntil2));
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("server_state")[jss::result];
|
|
BEAST_EXPECT(
|
|
jrr[jss::state][jss::validator_list_expires].asUInt() ==
|
|
validUntil2.time_since_epoch().count());
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validators")[jss::result];
|
|
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == 2);
|
|
BEAST_EXPECT(jrr[jss::validator_list][jss::expiration] == to_string(validUntil2));
|
|
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
|
|
|
|
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == expectedKeys.size());
|
|
for (auto const& jKey : jrr[jss::trusted_validator_keys])
|
|
{
|
|
BEAST_EXPECT(expectedKeys.count(jKey.asString()) == 1);
|
|
}
|
|
|
|
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
|
|
{
|
|
auto jp = jrr[jss::publisher_lists][0u];
|
|
BEAST_EXPECT(jp[jss::available] == true);
|
|
if (BEAST_EXPECT(jp[jss::list].size() == 2))
|
|
{
|
|
// check entries
|
|
std::set<std::string> foundKeys;
|
|
for (auto const& k : jp[jss::list])
|
|
{
|
|
foundKeys.insert(k.asString());
|
|
}
|
|
BEAST_EXPECT(foundKeys == expectedKeys);
|
|
}
|
|
BEAST_EXPECT(jp[jss::seq].asUInt() == 1);
|
|
BEAST_EXPECT(jp[jss::pubkey_publisher] == strHex(server->publisherPublic()));
|
|
BEAST_EXPECT(jp[jss::expiration] == to_string(validUntil));
|
|
BEAST_EXPECT(jp[jss::version] == 2);
|
|
if (BEAST_EXPECT(jp.isMember(jss::remaining)) &&
|
|
BEAST_EXPECT(jp[jss::remaining].isArray()) &&
|
|
BEAST_EXPECT(jp[jss::remaining].size() == 1))
|
|
{
|
|
auto const& r = jp[jss::remaining][0u];
|
|
if (BEAST_EXPECT(r[jss::list].size() == 2))
|
|
{
|
|
// check entries
|
|
std::set<std::string> foundKeys;
|
|
for (auto const& k : r[jss::list])
|
|
{
|
|
foundKeys.insert(k.asString());
|
|
}
|
|
BEAST_EXPECT(foundKeys == expectedKeys);
|
|
}
|
|
BEAST_EXPECT(r[jss::seq].asUInt() == 2);
|
|
BEAST_EXPECT(r[jss::effective] == to_string(validFrom2));
|
|
BEAST_EXPECT(r[jss::expiration] == to_string(validUntil2));
|
|
}
|
|
}
|
|
auto jsk = jrr[jss::signing_keys];
|
|
BEAST_EXPECT(jsk.size() == 2);
|
|
for (auto const& val : validators)
|
|
{
|
|
BEAST_EXPECT(jsk.isMember(toStr(val.masterPublic)));
|
|
BEAST_EXPECT(jsk[toStr(val.masterPublic)] == toStr(val.signingPublic));
|
|
}
|
|
}
|
|
{
|
|
auto const jrr = env.rpc("validator_list_sites")[jss::result];
|
|
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
|
|
{
|
|
auto js = jrr[jss::validator_sites][0u];
|
|
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
|
|
BEAST_EXPECT(js[jss::uri] == siteURI);
|
|
BEAST_EXPECT(js[jss::last_refresh_status] == "accepted");
|
|
// The actual time of the update will vary run to run, so
|
|
// just verify the time is there
|
|
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
test_validation_create()
|
|
{
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
auto result = env.rpc("validation_create");
|
|
BEAST_EXPECT(result.isMember(jss::result) && result[jss::result][jss::status] == "success");
|
|
result =
|
|
env.rpc("validation_create", "BAWL MAN JADE MOON DOVE GEM SON NOW HAD ADEN GLOW TIRE");
|
|
BEAST_EXPECT(result.isMember(jss::result) && result[jss::result][jss::status] == "success");
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
testPrivileges();
|
|
testStaticUNL();
|
|
testDynamicUNL();
|
|
test_validation_create();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(ValidatorRPC, rpc, xrpl);
|
|
|
|
} // namespace test
|
|
} // namespace xrpl
|