mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-28 06:25:49 +00:00
5024 lines
170 KiB
C++
5024 lines
170 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2017 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.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <ripple/app/ledger/LedgerMaster.h>
|
|
#include <ripple/app/misc/HashRouter.h>
|
|
#include <ripple/app/tx/apply.h>
|
|
#include <ripple/app/tx/impl/XahauGenesis.h>
|
|
#include <ripple/json/json_reader.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/Indexes.h>
|
|
#include <ripple/protocol/STAccount.h>
|
|
#include <ripple/protocol/digest.h>
|
|
#include <ripple/protocol/jss.h>
|
|
#include <string>
|
|
#include <test/jtx.h>
|
|
#include <vector>
|
|
|
|
#define BEAST_REQUIRE(x) \
|
|
{ \
|
|
BEAST_EXPECT(!!(x)); \
|
|
if (!(x)) \
|
|
return; \
|
|
}
|
|
|
|
// Function to handle integer types
|
|
template <typename T>
|
|
std::string
|
|
maybe_to_string(T val, std::enable_if_t<std::is_integral_v<T>, int> = 0)
|
|
{
|
|
return std::to_string(val);
|
|
}
|
|
|
|
// Overload to handle non-integer types
|
|
template <typename T>
|
|
std::string
|
|
maybe_to_string(T val, std::enable_if_t<!std::is_integral_v<T>, int> = 0)
|
|
{
|
|
return val;
|
|
}
|
|
|
|
#define M(m) memo(maybe_to_string(m), "", "")
|
|
|
|
#define DEBUG_XGTEST 0
|
|
|
|
using namespace XahauGenesis;
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
/*
|
|
Accounts used in this test suite:
|
|
alice: AE123A8556F3CF91154711376AFB0F894F832B3D,
|
|
rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn bob:
|
|
F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F, rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK
|
|
carol: B389FBCED0AF9DCDFF62900BFAEFA3EB872D8A96,
|
|
rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW david:
|
|
0F4BFC99EC975E3F753927A69713889359C7100E, rpPtwXbmeXxznrUvnMuGhUqTu4Vvk4V98i
|
|
edward: 98D3AAD96D5D3F32C3723B6550A49DEE4DD9D4AC,
|
|
rNAnHhommqNJvV3mLieHADNJU6mfdRqXKp m1:
|
|
1EF5C53AC2B0E1CDEAF960833A9B7B6814879152, rsF66BeCcW5dxrDhPbCqm4ReU5MjkmshQo
|
|
m2: 0D20E8D13A89AA84F2334F57331903D21CCC866C,
|
|
rpURDnkgizBF1ksJr9DmDTNLhds2NBYNbC m6:
|
|
F41810565A673D2C45C63B7A51FEA139221DFB1B, rPEe68FWPYpfEYeL3TtdVGXR8WDET24h6e
|
|
m7: 52C947B84E2412B5BD2211639E2B9DEDF3ECB5F4,
|
|
r3YjXvPbu1srxx22tQefDwhXJS8qjCAgxg m8:
|
|
3392382F09E5EA935D5624D52919C683B661238A, rn6gdHFFtieGK748TioK8cZ8SPrHsWAfe3
|
|
m9: 62F73BE6E7718B3328DEB497F8298B1C42FFD3D8,
|
|
rwpHPvGwiJAFStN3KqVTeTLBTczFyK6Z6D m10:
|
|
971B25036F1EAAEEC7A0C299E15F60AB4C465B37, rNmyZrJEtbg5p84PSKGurNs1efdDNLhb8D
|
|
m11: B79B324A56425919D0BF64FA4560C7B616F08509,
|
|
rHjF2LuJKSRerNHtQMtyDUVXpQKmT18XuC m12:
|
|
8C3AA72EBB3172726BE9FDE2A91C9CE2C9F766E5, rD8T19Nz2gkAtKKEeLpXLkTb57AS7nJ45b
|
|
m13: 3B8292604D9CF9C679E70CCD0ED5C9DC1DF84607,
|
|
raRCHchPDpP8qAysxsmehC279GH613iGiM m14:
|
|
E77F5DFE960C4DA9524389F946BCD13AC26AB0D0, r4fsCASeoGzhD2ZeFgcJj1c2tmDsm1re3r
|
|
m15: D300D94BA59F55426889E144B423CFEE8F685450,
|
|
rLNgbTE2HVk5CJG7SHrpSD8gsFcwu5cG1a m16:
|
|
D9C6AC46D87BCFE374BE4F59C280CEC377E67788, rLiVdCesA2rV2NjC6HZPVK7k1VpvGwCbjx
|
|
m17: A620E154D56E38B12AD76A094A214A25AD8B87A1,
|
|
rG9QZQDR8XqCU1x2VARPuZwYxqcb3J9bYa m18:
|
|
7A5A72F059F6DCDD6E938DAFBCFFB0541E0A7481, rU9A8u622H6fxGQjux8uDgomks5D8QySfS
|
|
m19: F6C06C3D86A9D39FF813AEF6B839AD041651BE7D,
|
|
rPV6jx8QoQBCR1AcmVLDnu98QBu4QakwhU m20:
|
|
1B4D3113C2AB370293A0ACEA4D68C1B29A01A013, rsVM5ZaK9QMCgrW9UKyXLguESpDpsJnRbu
|
|
m21: 748B256D2BAD918A967F40F465692C7B9A01F836,
|
|
rBdNnJ4q9G3riJFcXjkwTgBu1MBR5pG4gD
|
|
*/
|
|
|
|
struct XahauGenesis_test : public beast::unit_test::suite
|
|
{
|
|
uint256 const acceptHookHash = ripple::sha512Half_s(ripple::Slice(
|
|
jtx::genesis::AcceptHook.data(),
|
|
jtx::genesis::AcceptHook.size()));
|
|
uint256 const governHookHash = ripple::sha512Half_s(ripple::Slice(
|
|
XahauGenesis::GovernanceHook.data(),
|
|
XahauGenesis::GovernanceHook.size()));
|
|
uint256 const rewardHookHash = ripple::sha512Half_s(ripple::Slice(
|
|
XahauGenesis::RewardHook.data(),
|
|
XahauGenesis::RewardHook.size()));
|
|
|
|
AccountID const genesisAccID = calcAccountID(
|
|
generateKeyPair(KeyType::secp256k1, generateSeed("masterpassphrase"))
|
|
.first);
|
|
|
|
// the test cases in this test suite are based on changing the state of the
|
|
// ledger before xahaugenesis is activated, to do this they call this
|
|
// templated function with an "execute-first" lambda
|
|
void
|
|
activate(
|
|
uint64_t lineno,
|
|
jtx::Env& env,
|
|
bool burnedViaTest =
|
|
false, // means the calling test already burned some of the genesis
|
|
bool skipTests = false,
|
|
bool const testFlag = false)
|
|
{
|
|
using namespace jtx;
|
|
|
|
auto isEnabled = [&](void) -> bool {
|
|
auto const obj = env.le(keylet::amendments());
|
|
if (!obj)
|
|
return false;
|
|
STVector256 amendments = obj->getFieldV256(sfAmendments);
|
|
return std::find(
|
|
amendments.begin(),
|
|
amendments.end(),
|
|
featureXahauGenesis) != amendments.end();
|
|
};
|
|
|
|
BEAST_EXPECT(!isEnabled());
|
|
|
|
uint32_t const startLgr =
|
|
env.app().getLedgerMaster().getClosedLedger()->info().seq + 1;
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout << "activate called. " << lineno << "L"
|
|
<< " burnedViaTest: "
|
|
<< (burnedViaTest ? "true" : "false")
|
|
<< " skipTests: " << (skipTests ? "true" : "false")
|
|
<< " testFlags: " << (testFlag ? "true" : "false")
|
|
<< " startLgr: " << startLgr << "\n";
|
|
}
|
|
|
|
// insert a ttAMENDMENT pseudo into the open ledger
|
|
env.app().openLedger().modify(
|
|
[&](OpenView& view, beast::Journal j) -> bool {
|
|
STTx tx(ttAMENDMENT, [&](auto& obj) {
|
|
obj.setAccountID(sfAccount, AccountID());
|
|
obj.setFieldH256(sfAmendment, featureXahauGenesis);
|
|
obj.setFieldU32(sfLedgerSequence, startLgr);
|
|
if (testFlag)
|
|
obj.setFieldU32(sfFlags, tfTestSuite);
|
|
});
|
|
|
|
uint256 txID = tx.getTransactionID();
|
|
auto s = std::make_shared<ripple::Serializer>();
|
|
tx.add(*s);
|
|
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
|
|
view.rawTxInsert(txID, std::move(s), nullptr);
|
|
|
|
return true;
|
|
});
|
|
|
|
// close the ledger
|
|
env.close();
|
|
|
|
BEAST_EXPECT(isEnabled());
|
|
|
|
if (skipTests)
|
|
return;
|
|
|
|
// sum the initial distribution balances, these should equal total coins
|
|
// in the closed ledger
|
|
std::vector<std::pair<std::string, XRPAmount>> const& l1membership =
|
|
testFlag ? XahauGenesis::TestL1Membership
|
|
: XahauGenesis::L1Membership;
|
|
|
|
XRPAmount total{GenesisAmount};
|
|
for (auto const& [node, amt] : l1membership)
|
|
total += amt;
|
|
|
|
// and the non gov distributions
|
|
std::vector<std::pair<std::string, XRPAmount>> const& ngdist = testFlag
|
|
? XahauGenesis::TestNonGovernanceDistribution
|
|
: XahauGenesis::NonGovernanceDistribution;
|
|
|
|
for (auto const& [node, amt] : ngdist)
|
|
total += amt;
|
|
|
|
BEAST_EXPECT(
|
|
burnedViaTest ||
|
|
env.app().getLedgerMaster().getClosedLedger()->info().drops ==
|
|
total);
|
|
|
|
// is the hook array present
|
|
auto genesisHooksLE = env.le(keylet::hook(genesisAccID));
|
|
BEAST_REQUIRE(!!genesisHooksLE);
|
|
auto genesisHookArray = genesisHooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(genesisHookArray.size() == 2);
|
|
|
|
// make sure the account root exists and has the correct balance and
|
|
// ownercount
|
|
auto genesisAccRoot = env.le(keylet::account(genesisAccID));
|
|
BEAST_REQUIRE(!!genesisAccRoot);
|
|
BEAST_EXPECT(
|
|
genesisAccRoot->getFieldAmount(sfBalance) ==
|
|
XahauGenesis::GenesisAmount);
|
|
BEAST_EXPECT(genesisAccRoot->getFieldU32(sfOwnerCount) == 2);
|
|
|
|
// ensure the definitions are correctly set
|
|
{
|
|
auto const govHash = ripple::sha512Half_s(
|
|
ripple::Slice(GovernanceHook.data(), GovernanceHook.size()));
|
|
auto const govKL = keylet::hookDefinition(govHash);
|
|
auto govSLE = env.le(govKL);
|
|
|
|
BEAST_EXPECT(!!govSLE);
|
|
BEAST_EXPECT(govSLE->getFieldH256(sfHookHash) == govHash);
|
|
|
|
auto const govVL = govSLE->getFieldVL(sfCreateCode);
|
|
BEAST_EXPECT(
|
|
govHash ==
|
|
ripple::sha512Half_s(
|
|
ripple::Slice(govVL.data(), govVL.size())));
|
|
BEAST_EXPECT(
|
|
govSLE->getFieldU64(sfReferenceCount) == 1 + testFlag
|
|
? XahauGenesis::TestL2Membership.size()
|
|
: XahauGenesis::L2Membership.size());
|
|
BEAST_EXPECT(
|
|
govSLE->getFieldH256(sfHookOn) ==
|
|
ripple::uint256("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFF"
|
|
"FFFFFFFFFFFFBFFFFF"));
|
|
BEAST_EXPECT(
|
|
govSLE->getFieldH256(sfHookNamespace) ==
|
|
ripple::uint256("0000000000000000000000000000000000000000000000"
|
|
"000000000000000000"));
|
|
BEAST_EXPECT(govSLE->getFieldU16(sfHookApiVersion) == 0);
|
|
auto const govFee = govSLE->getFieldAmount(sfFee);
|
|
|
|
BEAST_EXPECT(isXRP(govFee) && govFee > beast::zero);
|
|
BEAST_EXPECT(!govSLE->isFieldPresent(sfHookCallbackFee));
|
|
BEAST_EXPECT(govSLE->getFieldH256(sfHookSetTxnID) != beast::zero);
|
|
|
|
BEAST_EXPECT(
|
|
genesisHookArray[0].getFieldH256(sfHookHash) == govHash);
|
|
|
|
auto const rwdHash = ripple::sha512Half_s(
|
|
ripple::Slice(RewardHook.data(), RewardHook.size()));
|
|
auto const rwdKL = keylet::hookDefinition(rwdHash);
|
|
auto rwdSLE = env.le(rwdKL);
|
|
|
|
BEAST_EXPECT(!!rwdSLE);
|
|
BEAST_EXPECT(rwdSLE->getFieldH256(sfHookHash) == rwdHash);
|
|
|
|
auto const rwdVL = rwdSLE->getFieldVL(sfCreateCode);
|
|
BEAST_EXPECT(
|
|
rwdHash ==
|
|
ripple::sha512Half_s(
|
|
ripple::Slice(rwdVL.data(), rwdVL.size())));
|
|
BEAST_EXPECT(rwdSLE->getFieldU64(sfReferenceCount) == 1);
|
|
BEAST_EXPECT(
|
|
rwdSLE->getFieldH256(sfHookOn) ==
|
|
ripple::uint256("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFF"
|
|
"FFFFFFFFFFFFBFFFFF"));
|
|
BEAST_EXPECT(
|
|
rwdSLE->getFieldH256(sfHookNamespace) ==
|
|
ripple::uint256("0000000000000000000000000000000000000000000000"
|
|
"000000000000000000"));
|
|
BEAST_EXPECT(rwdSLE->getFieldU16(sfHookApiVersion) == 0);
|
|
auto const rwdFee = rwdSLE->getFieldAmount(sfFee);
|
|
BEAST_EXPECT(isXRP(rwdFee) && rwdFee > beast::zero);
|
|
BEAST_EXPECT(!rwdSLE->isFieldPresent(sfHookCallbackFee));
|
|
BEAST_EXPECT(rwdSLE->getFieldH256(sfHookSetTxnID) != beast::zero);
|
|
|
|
BEAST_EXPECT(
|
|
genesisHookArray[1].getFieldH256(sfHookHash) == rwdHash);
|
|
}
|
|
|
|
// check non-governance distributions
|
|
{
|
|
for (auto const& [rn, x] : ngdist)
|
|
{
|
|
const char first = rn.c_str()[0];
|
|
BEAST_EXPECT(
|
|
(first == 'r' && !!parseBase58<AccountID>(rn)) ||
|
|
first == 'n' &&
|
|
!!parseBase58<PublicKey>(TokenType::NodePublic, rn));
|
|
|
|
if (first == 'r')
|
|
{
|
|
AccountID id = *parseBase58<AccountID>(rn);
|
|
auto acc = env.le(keylet::account(id));
|
|
BEAST_EXPECT(!!acc);
|
|
auto bal = acc->getFieldAmount(sfBalance);
|
|
BEAST_EXPECT(bal == STAmount(x));
|
|
continue;
|
|
}
|
|
|
|
// node based addresses
|
|
auto const pk =
|
|
parseBase58<PublicKey>(TokenType::NodePublic, rn);
|
|
BEAST_EXPECT(!!pk);
|
|
|
|
AccountID id = calcAccountID(*pk);
|
|
auto acc = env.le(keylet::account(id));
|
|
BEAST_EXPECT(!!acc);
|
|
auto bal = acc->getFieldAmount(sfBalance);
|
|
BEAST_EXPECT(bal == STAmount(x));
|
|
}
|
|
}
|
|
|
|
// check distribution amounts and hook parameters
|
|
{
|
|
uint8_t member_count = 0;
|
|
std::vector<std::pair<std::vector<uint8_t>, std::vector<uint8_t>>>
|
|
params = XahauGenesis::GovernanceParameters;
|
|
for (auto const& [rn, x] : l1membership)
|
|
{
|
|
const char first = rn.c_str()[0];
|
|
BEAST_EXPECT(
|
|
(first == 'r' && !!parseBase58<AccountID>(rn)) ||
|
|
first == 'n' &&
|
|
!!parseBase58<PublicKey>(TokenType::NodePublic, rn));
|
|
|
|
if (first == 'r')
|
|
{
|
|
AccountID id = *parseBase58<AccountID>(rn);
|
|
auto acc = env.le(keylet::account(id));
|
|
BEAST_EXPECT(!!acc);
|
|
auto bal = acc->getFieldAmount(sfBalance);
|
|
BEAST_EXPECT(bal == STAmount(x));
|
|
|
|
params.emplace_back(
|
|
std::vector<uint8_t>{'I', 'S', member_count++},
|
|
std::vector<uint8_t>(id.data(), id.data() + 20));
|
|
continue;
|
|
}
|
|
|
|
// node based addresses
|
|
auto const pk =
|
|
parseBase58<PublicKey>(TokenType::NodePublic, rn);
|
|
BEAST_EXPECT(!!pk);
|
|
|
|
AccountID id = calcAccountID(*pk);
|
|
auto acc = env.le(keylet::account(id));
|
|
BEAST_EXPECT(!!acc);
|
|
auto bal = acc->getFieldAmount(sfBalance);
|
|
BEAST_EXPECT(bal == STAmount(x));
|
|
|
|
// initial member enumeration
|
|
params.emplace_back(
|
|
std::vector<uint8_t>{'I', 'S', member_count++},
|
|
std::vector<uint8_t>(id.data(), id.data() + 20));
|
|
}
|
|
|
|
// initial member count
|
|
params.emplace_back(
|
|
std::vector<uint8_t>{'I', 'M', 'C'},
|
|
std::vector<uint8_t>{member_count});
|
|
|
|
auto leParams = genesisHookArray[0].getFieldArray(sfHookParameters);
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout << "leParams.size() = " << leParams.size()
|
|
<< ", params.size() == " << params.size() << "\n";
|
|
|
|
std::cout << "leParams:\n";
|
|
for (auto const& p : leParams)
|
|
{
|
|
std::cout
|
|
<< "\t" << strHex(p.getFieldVL(sfHookParameterName))
|
|
<< " : " << strHex(p.getFieldVL(sfHookParameterValue))
|
|
<< "\n";
|
|
}
|
|
|
|
std::cout << "params:\n";
|
|
for (auto const& [k, v] : params)
|
|
std::cout << "\t" << strHex(k) << " : " << strHex(v)
|
|
<< "\n";
|
|
}
|
|
|
|
// check parameters
|
|
|
|
BEAST_EXPECT(leParams.size() == params.size());
|
|
|
|
// these should be recorded in the same order
|
|
std::set<std::vector<uint8_t>> keys_used;
|
|
int counter = 0;
|
|
for (auto& param : leParams)
|
|
{
|
|
auto key = param.getFieldVL(sfHookParameterName);
|
|
auto val = param.getFieldVL(sfHookParameterValue);
|
|
|
|
// no duplicates allowed
|
|
BEAST_EXPECT(keys_used.find(key) == keys_used.end());
|
|
|
|
BEAST_EXPECT(counter < leParams.size());
|
|
|
|
BEAST_EXPECT(params[counter].first == key);
|
|
BEAST_EXPECT(params[counter].second == val);
|
|
|
|
// add key to used set
|
|
keys_used.emplace(key);
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
// check fees object correctly recordsed activation seq
|
|
auto fees = env.le(keylet::fees());
|
|
BEAST_REQUIRE(!!fees);
|
|
BEAST_EXPECT(
|
|
fees->isFieldPresent(sfXahauActivationLgrSeq) &&
|
|
fees->getFieldU32(sfXahauActivationLgrSeq) == startLgr);
|
|
|
|
// ensure no signerlist
|
|
BEAST_EXPECT(!env.le(keylet::signers(genesisAccID)));
|
|
|
|
// ensure correctly blackholed
|
|
BEAST_EXPECT(
|
|
genesisAccRoot->isFieldPresent(sfRegularKey) &&
|
|
genesisAccRoot->getAccountID(sfRegularKey) == noAccount() &&
|
|
genesisAccRoot->getFieldU32(sfFlags) & lsfDisableMaster);
|
|
}
|
|
|
|
void
|
|
testPlainActivation(FeatureBitset features)
|
|
{
|
|
testcase("Test activation");
|
|
using namespace jtx;
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
activate(__LINE__, env, false, false, false);
|
|
}
|
|
|
|
void
|
|
testWithSignerList(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("Test signerlist");
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
Account const alice{"alice", KeyType::ed25519};
|
|
env.fund(XRP(1000), alice);
|
|
env.memoize(env.master);
|
|
env(signers(env.master, 1, {{alice, 1}}));
|
|
env.close();
|
|
|
|
activate(__LINE__, env, true, false, false);
|
|
}
|
|
|
|
void
|
|
testWithRegularKey(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("Test regkey");
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
env.memoize(env.master);
|
|
Account const alice("alice");
|
|
env.fund(XRP(10000), alice);
|
|
env(regkey(env.master, alice));
|
|
env.close();
|
|
|
|
activate(__LINE__, env, true, false, false);
|
|
}
|
|
|
|
void
|
|
setupGov(
|
|
jtx::Env& env,
|
|
std::vector<AccountID> const members,
|
|
std::map<AccountID, std::vector<AccountID>> const tables = {})
|
|
{
|
|
using namespace jtx;
|
|
|
|
auto const invoker = Account("invoker");
|
|
env.fund(XRP(10000), invoker);
|
|
env.close();
|
|
|
|
XahauGenesis::TestNonGovernanceDistribution.clear();
|
|
XahauGenesis::TestL1Membership.clear();
|
|
XahauGenesis::TestL2Membership.clear();
|
|
|
|
for (auto& m : members)
|
|
{
|
|
std::string acc = toBase58(m);
|
|
XahauGenesis::TestL1Membership.emplace_back(
|
|
acc, XRPAmount(10000000000));
|
|
}
|
|
|
|
for (auto& [t, members] : tables)
|
|
{
|
|
std::vector<std::string> membersStr;
|
|
for (auto& m : members)
|
|
membersStr.emplace_back(toBase58(m));
|
|
|
|
XahauGenesis::TestL2Membership.emplace_back(
|
|
toBase58(t), membersStr);
|
|
}
|
|
|
|
activate(__LINE__, env, true, false, true);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
XahauGenesis::TestNonGovernanceDistribution.clear();
|
|
XahauGenesis::TestL1Membership.clear();
|
|
XahauGenesis::TestL2Membership.clear();
|
|
|
|
Json::Value invoke;
|
|
invoke[jss::TransactionType] = "Invoke";
|
|
invoke[jss::Account] = invoker.human();
|
|
invoke[jss::Destination] = env.master.human();
|
|
env(invoke, fee(XRP(10)));
|
|
env.close();
|
|
env.close();
|
|
|
|
for (auto& [t, members] : tables)
|
|
{
|
|
// setup each L2 table
|
|
Json::Value invoke;
|
|
invoke[jss::TransactionType] = "Invoke";
|
|
invoke[jss::Account] = invoker.human();
|
|
invoke[jss::Destination] = toBase58(t);
|
|
env(invoke, fee(XRP(1)));
|
|
env.close();
|
|
if (DEBUG_XGTEST)
|
|
std::cout << "invoke: " << invoke << "\n";
|
|
}
|
|
}
|
|
|
|
inline static std::string
|
|
charToHex(uint8_t inp)
|
|
{
|
|
std::string ret("00");
|
|
ret.data()[0] = "0123456789ABCDEF"[inp >> 4];
|
|
ret.data()[1] = "0123456789ABCDEF"[(inp >> 0) & 0xFU];
|
|
return ret;
|
|
}
|
|
|
|
inline static std::vector<uint8_t>
|
|
vecFromAcc(jtx::Account const& acc)
|
|
{
|
|
// this was changed to a less efficient version
|
|
// to accomodate older compilers
|
|
std::vector<uint8_t> vec;
|
|
vec.reserve(20);
|
|
for (int i = 0; i < 20; ++i)
|
|
vec.push_back(acc.id().data()[i]);
|
|
return vec;
|
|
};
|
|
|
|
inline static Json::Value
|
|
vote(
|
|
uint16_t lineno,
|
|
jtx::Account const& acc,
|
|
jtx::Account const& table,
|
|
char topic1,
|
|
std::optional<char> topic2,
|
|
std::vector<uint8_t> data,
|
|
std::optional<uint8_t> layer = std::nullopt)
|
|
{
|
|
Json::Value txn(Json::objectValue);
|
|
|
|
txn[jss::HookParameters] = Json::arrayValue;
|
|
txn[jss::HookParameters][0u] = Json::objectValue;
|
|
|
|
txn[jss::HookParameters][0u][jss::HookParameter] = Json::objectValue;
|
|
txn[jss::HookParameters][0u][jss::HookParameter]
|
|
[jss::HookParameterName] = "54"; // 'T'
|
|
std::string val =
|
|
charToHex(topic1) + (topic2 ? charToHex(*topic2) : "");
|
|
if (DEBUG_XGTEST)
|
|
std::cout << "val: `" << val << "`\n";
|
|
txn[jss::HookParameters][0u][jss::HookParameter]
|
|
[jss::HookParameterValue] = val;
|
|
|
|
txn[jss::HookParameters][1u] = Json::objectValue;
|
|
txn[jss::HookParameters][1u][jss::HookParameter]
|
|
[jss::HookParameterName] = "56"; // 'V'
|
|
|
|
{
|
|
std::string strData;
|
|
strData.reserve(data.size() << 1U);
|
|
for (uint8_t c : data)
|
|
strData += charToHex(c);
|
|
|
|
txn[jss::HookParameters][1u][jss::HookParameter]
|
|
[jss::HookParameterValue] = strData;
|
|
}
|
|
|
|
uint8_t upto = 2u;
|
|
if (layer)
|
|
{
|
|
txn[jss::HookParameters][upto] = Json::objectValue;
|
|
txn[jss::HookParameters][upto][jss::HookParameter]
|
|
[jss::HookParameterName] = "4C";
|
|
txn[jss::HookParameters][upto][jss::HookParameter]
|
|
[jss::HookParameterValue] = charToHex(*layer);
|
|
upto++;
|
|
}
|
|
|
|
// add a line number for debugging
|
|
txn[jss::HookParameters][upto] = Json::objectValue;
|
|
txn[jss::HookParameters][upto][jss::HookParameter]
|
|
[jss::HookParameterName] = "44"; // D
|
|
{
|
|
std::string strData;
|
|
strData += charToHex((uint8_t)(lineno >> 8U));
|
|
strData += charToHex((uint8_t)(lineno & 0xFFU));
|
|
txn[jss::HookParameters][upto][jss::HookParameter]
|
|
[jss::HookParameterValue] = strData;
|
|
}
|
|
|
|
txn[jss::Account] = acc.human();
|
|
txn[jss::TransactionType] = "Invoke";
|
|
txn[jss::Destination] = table.human();
|
|
return txn;
|
|
};
|
|
|
|
std::vector<uint8_t> const null_acc_id{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
AccountID const null_acc;
|
|
|
|
inline static uint256
|
|
makeStateKey(
|
|
char voteType,
|
|
char topic1,
|
|
char topic2,
|
|
uint8_t layer,
|
|
AccountID const& id)
|
|
{
|
|
uint8_t data[32];
|
|
|
|
memset(data, 0, 32);
|
|
data[0] = voteType;
|
|
data[1] = topic1;
|
|
data[2] = topic2;
|
|
data[3] = layer;
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
data[12 + i] = id.data()[i];
|
|
|
|
return uint256::fromVoid(data);
|
|
}
|
|
|
|
uint8_t const member_count_key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 'M', 'C'};
|
|
|
|
void
|
|
testGovernanceL1(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("Test governance membership voting L1");
|
|
Env env{*this, envconfig(), features - featureXahauGenesis, nullptr};
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
auto const m1 = Account("m1");
|
|
auto const m2 = Account("m2");
|
|
|
|
auto const m6 = Account("m6");
|
|
auto const m7 = Account("m7");
|
|
auto const m8 = Account("m8");
|
|
auto const m9 = Account("m9");
|
|
auto const m10 = Account("m10");
|
|
auto const m11 = Account("m11");
|
|
auto const m12 = Account("m12");
|
|
auto const m13 = Account("m13");
|
|
auto const m14 = Account("m14");
|
|
auto const m15 = Account("m15");
|
|
auto const m16 = Account("m16");
|
|
auto const m17 = Account("m17");
|
|
auto const m18 = Account("m18");
|
|
auto const m19 = Account("m19");
|
|
auto const m20 = Account("m20");
|
|
auto const m21 = Account("m21");
|
|
|
|
env.fund(
|
|
XRP(10000),
|
|
alice,
|
|
bob,
|
|
carol,
|
|
david,
|
|
edward,
|
|
m1,
|
|
m2,
|
|
m6,
|
|
m7,
|
|
m8,
|
|
m9,
|
|
m10,
|
|
m11,
|
|
m12,
|
|
m13,
|
|
m14,
|
|
m15,
|
|
m16,
|
|
m17,
|
|
m18,
|
|
m19,
|
|
m20,
|
|
m21);
|
|
|
|
env.close();
|
|
|
|
std::vector<Account> initial_members{alice, bob, carol, david, edward};
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
auto doL1Vote = [&](uint64_t lineno,
|
|
Account const& acc,
|
|
char topic1,
|
|
char topic2,
|
|
std::vector<uint8_t> const& vote_data,
|
|
std::vector<uint8_t> const& old_data,
|
|
bool actioned = true,
|
|
bool const shouldFail = false) {
|
|
if (shouldFail)
|
|
actioned = false;
|
|
|
|
bool isZero = true;
|
|
for (auto& x : vote_data)
|
|
if (x != 0)
|
|
{
|
|
isZero = false;
|
|
break;
|
|
}
|
|
|
|
bool isOldDataZero = true;
|
|
for (auto& x : old_data)
|
|
if (x != 0)
|
|
{
|
|
isOldDataZero = false;
|
|
break;
|
|
}
|
|
|
|
uint8_t const key[32] = {
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
static_cast<uint8_t>(topic1 == 'S' ? 0 : topic1),
|
|
static_cast<uint8_t>(topic2)};
|
|
// check actioning prior to vote
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "topic vote precheck: " << lineno << "L\n"
|
|
<< "\tacc: " << acc.human()
|
|
<< " shouldAction: " << (actioned ? "true" : "false")
|
|
<< "\tshouldFail: " << (shouldFail ? "true" : "false")
|
|
<< "\n"
|
|
<< "\ttopic: " << topic1 << ","
|
|
<< (topic2 < 48 ? topic2 + '1' : topic2) << "\n"
|
|
<< "\tisOldDataZero: "
|
|
<< (isOldDataZero ? "true" : "false") << "\n"
|
|
<< "\tisVoteDataZero: " << (isZero ? "true" : "false")
|
|
<< "\n"
|
|
<< "\told_data: " << strHex(old_data) << "\n"
|
|
<< "\tlgr_data: "
|
|
<< (!entry ? "doesn't exist"
|
|
: strHex(entry->getFieldVL(sfHookStateData)))
|
|
<< "\n"
|
|
<< "\tnew_data: " << strHex(vote_data) << "\n"
|
|
<< "\t(isOldDataZero && !entry): "
|
|
<< (isOldDataZero && !entry ? "true" : "false") << "\n"
|
|
<< "\t(entry && entry->getFieldVL(sfHookStateData) == "
|
|
"old_data): "
|
|
<< (entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data
|
|
? "true"
|
|
: "false")
|
|
<< "\n"
|
|
<< ((isOldDataZero && !entry) ||
|
|
(entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data)
|
|
? ""
|
|
: "\tfailed: ^^^\n");
|
|
}
|
|
BEAST_EXPECT(
|
|
(isOldDataZero && !entry) ||
|
|
(entry && entry->getFieldVL(sfHookStateData) == old_data));
|
|
}
|
|
|
|
// perform and check vote
|
|
{
|
|
env(vote(lineno, acc, env.master, topic1, topic2, vote_data),
|
|
M(lineno),
|
|
fee(XRP(1)),
|
|
shouldFail ? ter(tecHOOK_REJECTED) : ter(tesSUCCESS));
|
|
env.close();
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
makeStateKey('V', topic1, topic2, 1, acc.id()),
|
|
beast::zero));
|
|
if (!shouldFail)
|
|
{
|
|
BEAST_REQUIRE(!!entry);
|
|
auto lgr_data = entry->getFieldVL(sfHookStateData);
|
|
BEAST_EXPECT(lgr_data.size() == vote_data.size());
|
|
BEAST_EXPECT(lgr_data == vote_data);
|
|
}
|
|
}
|
|
|
|
// check actioning
|
|
if (!shouldFail)
|
|
{
|
|
// if the vote count isn't high enough it will be hte old value
|
|
// if it's high enough it will be the new value
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
|
|
if (!actioned && isOldDataZero)
|
|
{
|
|
BEAST_EXPECT(
|
|
!entry ||
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
return;
|
|
}
|
|
|
|
if (actioned)
|
|
{
|
|
if (isZero)
|
|
BEAST_EXPECT(!entry);
|
|
else
|
|
{
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout << "new data: " << strHex(vote_data)
|
|
<< "\n";
|
|
std::cout << "lgr data: "
|
|
<< (!entry ? "<doesn't exist>"
|
|
: strHex(entry->getFieldVL(
|
|
sfHookStateData)))
|
|
<< "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == vote_data);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!!entry && DEBUG_XGTEST)
|
|
std::cout << "old data: "
|
|
<< strHex(entry->getFieldVL(sfHookStateData))
|
|
<< "\n";
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 100% vote for a different reward rate
|
|
{
|
|
// this will be the new reward rate
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x81U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x43U, 0x54U};
|
|
|
|
// this is the default reward rate
|
|
std::vector<uint8_t> const original_data{
|
|
0x55U, 0x55U, 0x40U, 0x25U, 0xA6U, 0xD7U, 0xCBU, 0x53U};
|
|
|
|
doL1Vote(
|
|
__LINE__, alice, 'R', 'R', vote_data, original_data, false);
|
|
doL1Vote(__LINE__, bob, 'R', 'R', vote_data, original_data, false);
|
|
doL1Vote(
|
|
__LINE__, carol, 'R', 'R', vote_data, original_data, false);
|
|
doL1Vote(
|
|
__LINE__, david, 'R', 'R', vote_data, original_data, false);
|
|
doL1Vote(
|
|
__LINE__, edward, 'R', 'R', vote_data, original_data, true);
|
|
|
|
// reverting a vote should not undo the action
|
|
doL1Vote(
|
|
__LINE__, carol, 'R', 'R', original_data, vote_data, false);
|
|
|
|
// submitting a null vote should delete the vote, and should not
|
|
// undo the action
|
|
std::vector<uint8_t> const null_data{0, 0, 0, 0, 0, 0, 0, 0};
|
|
doL1Vote(__LINE__, david, 'R', 'R', null_data, vote_data, false);
|
|
}
|
|
|
|
// 100% vote for a different reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
// this is the default reward delay
|
|
std::vector<uint8_t> const original_data{
|
|
0x00U, 0x80U, 0x6AU, 0xACU, 0xAFU, 0x3CU, 0x09U, 0x56U};
|
|
|
|
doL1Vote(
|
|
__LINE__, edward, 'R', 'D', vote_data, original_data, false);
|
|
doL1Vote(
|
|
__LINE__, david, 'R', 'D', vote_data, original_data, false);
|
|
doL1Vote(
|
|
__LINE__, carol, 'R', 'D', vote_data, original_data, false);
|
|
doL1Vote(__LINE__, bob, 'R', 'D', vote_data, original_data, false);
|
|
doL1Vote(__LINE__, alice, 'R', 'D', vote_data, original_data, true);
|
|
|
|
// reverting a vote should not undo the action
|
|
doL1Vote(
|
|
__LINE__, david, 'R', 'D', original_data, vote_data, false);
|
|
|
|
// submitting a null vote should delete the vote, and should not
|
|
// undo the action
|
|
std::vector<uint8_t> const null_data{0, 0, 0, 0, 0, 0, 0, 0};
|
|
doL1Vote(__LINE__, alice, 'R', 'D', null_data, vote_data, false);
|
|
}
|
|
|
|
// 100% vote to install the accept hook at hook position 7
|
|
// create a definition for accept hook first
|
|
{
|
|
Json::Value tx(Json::objectValue);
|
|
|
|
tx[jss::Account] = m21.human();
|
|
tx[jss::TransactionType] = "SetHook";
|
|
tx[jss::Hooks] = Json::arrayValue;
|
|
tx[jss::Hooks][0u] = Json::objectValue;
|
|
|
|
tx[jss::Hooks][0u][jss::Hook] = Json::objectValue;
|
|
|
|
tx[jss::Hooks][0u][jss::Hook][jss::CreateCode] =
|
|
strHex(jtx::genesis::AcceptHook);
|
|
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookApiVersion] = "0";
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookNamespace] =
|
|
"00000000000000000000000000000000000000000000000000000000000000"
|
|
"00";
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookOn] =
|
|
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFF"
|
|
"FF";
|
|
|
|
// we'll also make a reference anchor for reward and governance
|
|
// hooks here, so they aren't deleted from the ledger in subsequent
|
|
// tests
|
|
tx[jss::Hooks][1u] = Json::objectValue;
|
|
tx[jss::Hooks][1u][jss::Hook] = Json::objectValue;
|
|
tx[jss::Hooks][1u][jss::Hook][jss::HookHash] =
|
|
strHex(governHookHash);
|
|
|
|
tx[jss::Hooks][2u] = Json::objectValue;
|
|
tx[jss::Hooks][2u][jss::Hook] = Json::objectValue;
|
|
tx[jss::Hooks][2u][jss::Hook][jss::HookHash] =
|
|
strHex(rewardHookHash);
|
|
|
|
env(tx, M(__LINE__), fee(XRP(100)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
!!env.le(ripple::keylet::hookDefinition(acceptHookHash)));
|
|
|
|
uint8_t const* data = acceptHookHash.data();
|
|
std::vector<uint8_t> accept_data(data, data + 32);
|
|
|
|
std::vector<uint8_t> const null_data{
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
doL1Vote(__LINE__, alice, 'H', 7, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 7, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 7, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 7, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// check if the hook not was installed because we have not given it
|
|
// 100% votes yet
|
|
{
|
|
auto const hooks = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() == 2);
|
|
}
|
|
|
|
// now cast final vote
|
|
doL1Vote(__LINE__, david, 'H', 7, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it was installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash) &&
|
|
hooks->getFieldArray(sfHooks)[7].getFieldH256(sfHookHash) ==
|
|
acceptHookHash);
|
|
}
|
|
|
|
// now change a vote (note that the topic state is never recorded
|
|
// for hooks, so old data is null)
|
|
doL1Vote(__LINE__, carol, 'H', 7, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it's still installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash) &&
|
|
hooks->getFieldArray(sfHooks)[7].getFieldH256(sfHookHash) ==
|
|
acceptHookHash);
|
|
}
|
|
|
|
// now vote to delete it
|
|
doL1Vote(__LINE__, alice, 'H', 7, null_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 7, null_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 7, null_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 7, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it's still installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
!hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash));
|
|
}
|
|
|
|
// vote to place an invalid hook
|
|
std::vector<uint8_t> invalid_data{
|
|
0xFFU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
doL1Vote(__LINE__, alice, 'H', 1, invalid_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 1, invalid_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 1, invalid_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 1, invalid_data, null_data, false);
|
|
doL1Vote(
|
|
__LINE__, edward, 'H', 1, invalid_data, null_data, false, true);
|
|
|
|
uint8_t const* gdata = governHookHash.data();
|
|
std::vector<uint8_t> govern_data(gdata, gdata + 32);
|
|
uint8_t const* rdata = rewardHookHash.data();
|
|
std::vector<uint8_t> reward_data(rdata, rdata + 32);
|
|
|
|
// vote to put governance hook into position 2
|
|
doL1Vote(__LINE__, alice, 'H', 2, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 2, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 2, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 2, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 2, govern_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote to replace the hook at position 1 with accept
|
|
doL1Vote(__LINE__, alice, 'H', 1, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 1, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 1, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 1, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 1, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote to place reward hook at position 2
|
|
doL1Vote(__LINE__, alice, 'H', 0, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 0, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 0, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 0, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 0, reward_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// hooks array should now look like {governi, accept, reward,
|
|
// nothing ...}
|
|
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 3);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 3)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == acceptHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[2].getFieldH256(sfHookHash) == governHookHash);
|
|
}
|
|
}
|
|
|
|
// set hook 1 back to reward
|
|
doL1Vote(__LINE__, alice, 'H', 1, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 1, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 1, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 1, reward_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 1, reward_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// set hook 0 back to govern
|
|
doL1Vote(__LINE__, alice, 'H', 0, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 0, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 0, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 0, govern_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 0, govern_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// delete hook at 2
|
|
doL1Vote(__LINE__, alice, 'H', 2, null_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 2, null_data, null_data, false);
|
|
doL1Vote(__LINE__, carol, 'H', 2, null_data, null_data, false);
|
|
doL1Vote(__LINE__, david, 'H', 2, null_data, null_data, false);
|
|
doL1Vote(__LINE__, edward, 'H', 2, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// check we're back the way we were
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == governHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks.size() == 2 ||
|
|
!hooks[2].isFieldPresent(sfHookHash));
|
|
}
|
|
}
|
|
|
|
// change a vote, and ensure nothing changed
|
|
doL1Vote(__LINE__, alice, 'H', 0, reward_data, null_data, false);
|
|
env.close();
|
|
env.close();
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(env.master.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == governHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks.size() == 2 ||
|
|
!hooks[2].isFieldPresent(sfHookHash));
|
|
}
|
|
}
|
|
}
|
|
|
|
// four of the 5 vote to remove alice
|
|
{
|
|
std::vector<uint8_t> id = vecFromAcc(alice);
|
|
doL1Vote(__LINE__, bob, 'S', 0, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, carol, 'S', 0, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, david, 'S', 0, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, edward, 'S', 0, null_acc_id, id, true);
|
|
}
|
|
|
|
// check the membercount is now 4
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x04U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
// continue to remove members (david)
|
|
{
|
|
std::vector<uint8_t> id = vecFromAcc(david);
|
|
doL1Vote(__LINE__, bob, 'S', 3, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, carol, 'S', 3, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, edward, 'S', 3, null_acc_id, id, true);
|
|
}
|
|
|
|
{
|
|
// RH TODO: check all david's previous votes were correctly removed
|
|
// check expected hookstate count
|
|
}
|
|
|
|
// check the membercount is now 3
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x03U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
// continue to remove members (carol)
|
|
{
|
|
std::vector<uint8_t> id = vecFromAcc(carol);
|
|
doL1Vote(__LINE__, bob, 'S', 2, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, edward, 'S', 2, null_acc_id, id, true);
|
|
}
|
|
|
|
// check the membercount is now 2
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x02U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
// member count can't fall below 2
|
|
// try to vote out edward using 2/2 votes
|
|
{
|
|
std::vector<uint8_t> id = vecFromAcc(edward);
|
|
doL1Vote(__LINE__, bob, 'S', 4, null_acc_id, id, false);
|
|
doL1Vote(__LINE__, edward, 'S', 4, null_acc_id, id, false, true);
|
|
}
|
|
|
|
// check the membercount is now 2
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x02U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
// try to remove bob using 1/2 votes
|
|
{
|
|
std::vector<uint8_t> id = vecFromAcc(bob);
|
|
doL1Vote(__LINE__, bob, 'S', 1, null_acc_id, id, false, false);
|
|
}
|
|
|
|
// that shoul fail
|
|
// check the membercount is now 2
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x02U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
// replace edward with alice
|
|
{
|
|
std::vector<uint8_t> alice_id = vecFromAcc(alice);
|
|
std::vector<uint8_t> edward_id = vecFromAcc(edward);
|
|
doL1Vote(__LINE__, bob, 'S', 4, alice_id, edward_id, false);
|
|
doL1Vote(__LINE__, edward, 'S', 4, alice_id, edward_id, true);
|
|
// subsequent edward vote should fail because he's not a member
|
|
// anymore
|
|
doL1Vote(
|
|
__LINE__, edward, 'S', 4, null_acc_id, alice_id, false, true);
|
|
}
|
|
|
|
// check the membercount is now 2
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{0x02U};
|
|
BEAST_REQUIRE(!!entry);
|
|
BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
|
|
auto doSeatVote = [&](uint64_t lineno,
|
|
uint8_t seat_no,
|
|
uint8_t final_count,
|
|
AccountID const& candidate,
|
|
std::optional<AccountID const> previous,
|
|
std::vector<Account const*> const& voters,
|
|
bool actioned = true,
|
|
bool shouldFail = false) {
|
|
std::vector<uint8_t> previd{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
if (previous)
|
|
memcpy(previd.data(), previous->data(), 20);
|
|
|
|
std::vector<uint8_t> id{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
memcpy(id.data(), candidate.data(), 20);
|
|
|
|
int count = voters.size();
|
|
for (Account const* voter : voters)
|
|
doL1Vote(
|
|
lineno,
|
|
*voter,
|
|
'S',
|
|
seat_no,
|
|
id,
|
|
previd,
|
|
--count <= 0 && actioned,
|
|
shouldFail);
|
|
|
|
if (!shouldFail)
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{final_count};
|
|
|
|
BEAST_REQUIRE(!!entry);
|
|
if (entry->getFieldVL(sfHookStateData) != expected_data &&
|
|
DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "doSeatVote failed " << lineno << "L. entry data: `"
|
|
<< strHex(entry->getFieldVL(sfHookStateData)) << "` "
|
|
<< "expected data: `" << strHex(expected_data) << "`\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
};
|
|
|
|
// put edward into seat 0 previously occupied by alice
|
|
doSeatVote(__LINE__, 0, 3, edward.id(), {}, {&alice, &bob});
|
|
|
|
// at this point the governance table looks like
|
|
// 0 - edward
|
|
// 1 - bob
|
|
// 2 - empty
|
|
// 3 - empty
|
|
// 4 - alice
|
|
|
|
// fill seats 2,3 with accounts m6 and m7. this should take 2 votes only
|
|
// alice's vote alone is not enough
|
|
doSeatVote(__LINE__, 2, 3, m6.id(), {}, {&alice}, false);
|
|
// edward's vote makes 2/3 which is floor(0.8 * members)
|
|
doSeatVote(__LINE__, 2, 4, m6.id(), {}, {&edward});
|
|
|
|
// re-action should succeed
|
|
doSeatVote(__LINE__, 2, 4, m6.id(), m6, {&alice});
|
|
|
|
// re-voting for something else should not change the action outcome
|
|
doSeatVote(__LINE__, 2, 4, m11.id(), m6, {&alice}, false);
|
|
|
|
// check that m6 is still in seat 2
|
|
{
|
|
uint8_t key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
|
|
for (int i = 12; i < 32; ++i)
|
|
key[i] = m6.id().data()[i - 12];
|
|
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{0x02U});
|
|
}
|
|
|
|
// now we will need 3 votes to put m7 into seat 3
|
|
doSeatVote(__LINE__, 3, 4, m7.id(), {}, {&m6}, false);
|
|
doSeatVote(__LINE__, 3, 4, m7.id(), {}, {&alice}, false);
|
|
doSeatVote(__LINE__, 3, 5, m7.id(), {}, {&bob}, true);
|
|
|
|
// now we have: ed, bob, m6, m7, alice
|
|
// let's try moving ed to another seat, this should succeed
|
|
doSeatVote(
|
|
__LINE__, 5, 5, edward.id(), {}, {&alice, &bob, &edward, &m7});
|
|
|
|
// now lets move edward over the top of bob
|
|
doSeatVote(__LINE__, 1, 4, edward.id(), bob, {&alice, &bob, &m6, &m7});
|
|
|
|
// first put bob in where edward was
|
|
doSeatVote(__LINE__, 0, 5, bob.id(), {}, {&alice, &edward, &m6});
|
|
|
|
// now we have: bob, ed, m6, m7, alice
|
|
// we're going to fill the remaining seats up to 20
|
|
doSeatVote(__LINE__, 5, 6, carol.id(), {}, {&alice, &edward, &m6, &m7});
|
|
doSeatVote(__LINE__, 6, 7, david.id(), {}, {&alice, &edward, &m6, &m7});
|
|
|
|
// { bob, ed, m6, m7, alice, carol, david }
|
|
|
|
// quorum = floor(0.8*7) = 5
|
|
doSeatVote(
|
|
__LINE__, 7, 8, m8.id(), {}, {&alice, &edward, &m6, &m7, &carol});
|
|
|
|
// q = floor(0.8*8) = 6
|
|
doSeatVote(
|
|
__LINE__,
|
|
8,
|
|
9,
|
|
m9.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8});
|
|
|
|
// q = floor(0.8*9) = 7
|
|
doSeatVote(
|
|
__LINE__,
|
|
9,
|
|
10,
|
|
m10.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8, &m9});
|
|
|
|
// q = floor(0.8*10) = 8
|
|
doSeatVote(
|
|
__LINE__,
|
|
10,
|
|
11,
|
|
m11.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8, &m9, &m10});
|
|
|
|
// q = floor(0.8*11) = 8
|
|
doSeatVote(
|
|
__LINE__,
|
|
11,
|
|
12,
|
|
m12.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8, &m9, &m10});
|
|
|
|
// q = floor(0.8*12) = 9
|
|
doSeatVote(
|
|
__LINE__,
|
|
12,
|
|
13,
|
|
m13.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8, &m9, &m10, &m11});
|
|
|
|
// q = floor(0.8*13) = 10
|
|
doSeatVote(
|
|
__LINE__,
|
|
13,
|
|
14,
|
|
m14.id(),
|
|
{},
|
|
{&alice, &edward, &m6, &m7, &carol, &m8, &m9, &m10, &m11, &m12});
|
|
|
|
// q = floor(0.8*14) = 11
|
|
doSeatVote(
|
|
__LINE__,
|
|
14,
|
|
15,
|
|
m15.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13});
|
|
|
|
// q = floor(0.8*15) = 12
|
|
doSeatVote(
|
|
__LINE__,
|
|
15,
|
|
16,
|
|
m16.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14});
|
|
|
|
// q = floor(0.8*16) = 12
|
|
doSeatVote(
|
|
__LINE__,
|
|
16,
|
|
17,
|
|
m17.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14});
|
|
|
|
// q = floor(0.8*17) = 13
|
|
doSeatVote(
|
|
__LINE__,
|
|
17,
|
|
18,
|
|
m18.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15});
|
|
|
|
// q = floor(0.8*18) = 14
|
|
doSeatVote(
|
|
__LINE__,
|
|
18,
|
|
19,
|
|
m19.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16});
|
|
|
|
// q = floor(0.8*19) = 15
|
|
doSeatVote(
|
|
__LINE__,
|
|
19,
|
|
20,
|
|
m20.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16,
|
|
&m17});
|
|
|
|
// q = floor(0.8*20) = 16
|
|
// voting for seat 20 is invalid, only seats 0 to 19 are valid
|
|
doSeatVote(
|
|
__LINE__,
|
|
20,
|
|
21,
|
|
m21.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&carol,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16,
|
|
&m17,
|
|
&m18},
|
|
false,
|
|
true);
|
|
|
|
// voting for another random high seat
|
|
doSeatVote(__LINE__, 255, 21, m21.id(), {}, {&alice}, false, true);
|
|
doSeatVote(__LINE__, 101, 21, m21.id(), {}, {&alice}, false, true);
|
|
|
|
// RH TODO: check state count
|
|
|
|
// membership:
|
|
// { bob, ed, m6, m7, alice, carol, david, m8, ... m20 }
|
|
// check this is the case forward and reverse keys
|
|
|
|
auto const checkSeat = [&](uint8_t seat, Account const* acc) {
|
|
uint8_t key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
|
|
// seatno => accid
|
|
{
|
|
key[31] = seat;
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
|
|
if (!acc)
|
|
{
|
|
if (entry && DEBUG_XGTEST)
|
|
{
|
|
std::cout << "checkSeat failed, "
|
|
<< "seatno->accid present (but should be "
|
|
"empty) for seat: "
|
|
<< seat << "\n";
|
|
}
|
|
BEAST_EXPECT(!entry);
|
|
return;
|
|
}
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
if (!entry)
|
|
std::cout << "checkSeat failed, seatno->accid missing "
|
|
"for seat: "
|
|
<< seat << "\n";
|
|
else if (
|
|
entry->getFieldVL(sfHookStateData) != vecFromAcc(*acc))
|
|
std::cout << "checkSeat failed, seatno->accid "
|
|
"incorrect for seat: "
|
|
<< seat << "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == vecFromAcc(*acc));
|
|
}
|
|
|
|
// accid => seatno
|
|
{
|
|
for (int i = 12; i < 32; ++i)
|
|
key[i] = acc->id().data()[i - 12];
|
|
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
if (!entry)
|
|
std::cout << "checkSeat failed, accid->seatno missing "
|
|
"for seat: "
|
|
<< seat << "\n";
|
|
else if (
|
|
entry->getFieldVL(sfHookStateData) !=
|
|
std::vector<uint8_t>{seat})
|
|
std::cout << "checkSeat failed, accid->seatno "
|
|
"incorrect for seat: "
|
|
<< seat << "\n";
|
|
}
|
|
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{seat});
|
|
}
|
|
};
|
|
|
|
{
|
|
std::vector<Account const*> finalSeats{
|
|
&bob, &edward, &m6, &m7, &alice, &carol, &david,
|
|
&m8, &m9, &m10, &m11, &m12, &m13, &m14,
|
|
&m15, &m16, &m17, &m18, &m19, &m20};
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
checkSeat(i, finalSeats[i]);
|
|
}
|
|
|
|
// go back to the original 5 in their original seats: alice, bob, carol,
|
|
// david, edward
|
|
|
|
// set alice into position 0, this frees up spot 4 bringing count down
|
|
// to 19 floor (20*0.8) = 16
|
|
doSeatVote(
|
|
__LINE__,
|
|
0,
|
|
19,
|
|
alice.id(),
|
|
bob,
|
|
{&bob,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&alice,
|
|
&carol,
|
|
&david,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16},
|
|
true);
|
|
|
|
// now seating is:
|
|
// {alice, edward, m6, m7, blank, carol, david, m8, ... }
|
|
|
|
// edward into position 4, this frees up spot 1, and moves into a blank
|
|
// spot thus keeps count at 19 note floor(19*0.8) = 15
|
|
doSeatVote(
|
|
__LINE__,
|
|
4,
|
|
19,
|
|
edward.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&m16,
|
|
&carol,
|
|
&david,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15},
|
|
true);
|
|
|
|
// now seating is:
|
|
// {alice, blank, m6, m7, edward, carol, david, m8, ...}
|
|
|
|
// bob into position 1, the count becomes 20
|
|
doSeatVote(
|
|
__LINE__,
|
|
1,
|
|
20,
|
|
bob.id(),
|
|
{},
|
|
{&alice,
|
|
&edward,
|
|
&m6,
|
|
&m7,
|
|
&m16,
|
|
&carol,
|
|
&david,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15},
|
|
true);
|
|
|
|
// {alice, bob, m6, m7, edward, carol, david, m8, ...}
|
|
|
|
// carol into position 2, this frees up seat 5, bringing count to 19
|
|
doSeatVote(
|
|
__LINE__,
|
|
2,
|
|
19,
|
|
carol.id(),
|
|
m6,
|
|
{&alice,
|
|
&bob,
|
|
&m6,
|
|
&m7,
|
|
&m16,
|
|
&carol,
|
|
&david,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m17},
|
|
true);
|
|
|
|
// {alice, bob, carol, m7, edward, blank, david, m8, ...}
|
|
|
|
// david into position 3, this frees up seat 6, bringing count to 18
|
|
doSeatVote(
|
|
__LINE__,
|
|
3,
|
|
18,
|
|
david.id(),
|
|
m7,
|
|
{&alice,
|
|
&bob,
|
|
&m18,
|
|
&m17,
|
|
&m16,
|
|
&carol,
|
|
&david,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15},
|
|
true);
|
|
|
|
// {alice, bob, carol, david, edward, blank, blank, m8, ...}
|
|
|
|
// remove all higher members
|
|
|
|
// floor(18*0.8) = 14
|
|
doSeatVote(
|
|
__LINE__,
|
|
7,
|
|
17,
|
|
null_acc,
|
|
m8,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m8,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16},
|
|
true);
|
|
|
|
// floor(17*0.8) = 13
|
|
doSeatVote(
|
|
__LINE__,
|
|
8,
|
|
16,
|
|
null_acc,
|
|
m9,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m9,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16},
|
|
true);
|
|
|
|
// floor(16*0.8)=12
|
|
doSeatVote(
|
|
__LINE__,
|
|
9,
|
|
15,
|
|
null_acc,
|
|
m10,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m10,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16},
|
|
true);
|
|
|
|
// floor(15*0.8)=12
|
|
doSeatVote(
|
|
__LINE__,
|
|
10,
|
|
14,
|
|
null_acc,
|
|
m11,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m11,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16,
|
|
&m17},
|
|
true);
|
|
|
|
// floor(14*0.8)=11
|
|
doSeatVote(
|
|
__LINE__,
|
|
11,
|
|
13,
|
|
null_acc,
|
|
m12,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m12,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16,
|
|
&m17},
|
|
true);
|
|
|
|
// floor(13*0.8)=10
|
|
doSeatVote(
|
|
__LINE__,
|
|
12,
|
|
12,
|
|
null_acc,
|
|
m13,
|
|
{&alice,
|
|
&bob,
|
|
&carol,
|
|
&david,
|
|
&edward,
|
|
&m13,
|
|
&m14,
|
|
&m15,
|
|
&m16,
|
|
&m17},
|
|
true);
|
|
|
|
// floor(12*0.8)=9
|
|
doSeatVote(
|
|
__LINE__,
|
|
13,
|
|
11,
|
|
null_acc,
|
|
m14,
|
|
{&alice, &bob, &carol, &david, &edward, &m14, &m15, &m16, &m17},
|
|
true);
|
|
|
|
// floor(11*0.8)=8
|
|
doSeatVote(
|
|
__LINE__,
|
|
14,
|
|
10,
|
|
null_acc,
|
|
m15,
|
|
{&alice, &bob, &carol, &david, &edward, &m15, &m16, &m17},
|
|
true);
|
|
|
|
// floor(10*0.8)=8
|
|
doSeatVote(
|
|
__LINE__,
|
|
15,
|
|
9,
|
|
null_acc,
|
|
m16,
|
|
{&alice, &bob, &carol, &david, &edward, &m17, &m18, &m19},
|
|
true);
|
|
|
|
// floor(9*0.8)=7
|
|
doSeatVote(
|
|
__LINE__,
|
|
16,
|
|
8,
|
|
null_acc,
|
|
m17,
|
|
{&alice, &bob, &carol, &david, &edward, &m19, &m20},
|
|
true);
|
|
|
|
// floor(8*0.8)=6
|
|
doSeatVote(
|
|
__LINE__,
|
|
17,
|
|
7,
|
|
null_acc,
|
|
m18,
|
|
{&alice, &bob, &carol, &david, &edward, &m20},
|
|
true);
|
|
|
|
// floor(7*0.8)=5
|
|
doSeatVote(
|
|
__LINE__,
|
|
18,
|
|
6,
|
|
null_acc,
|
|
m19,
|
|
{&alice, &bob, &carol, &david, &edward},
|
|
true);
|
|
|
|
// floor(6*0.8)=4
|
|
doSeatVote(
|
|
__LINE__,
|
|
19,
|
|
5,
|
|
null_acc,
|
|
m20,
|
|
{&alice, &bob, &carol, &david},
|
|
true);
|
|
|
|
// check the seats are correct
|
|
{
|
|
std::vector<Account const*> finalSeats{
|
|
&alice, &bob, &carol, &david, &edward};
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
checkSeat(i, i < 5 ? finalSeats[i] : NULL);
|
|
}
|
|
}
|
|
|
|
void
|
|
testGovernanceL2(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("Test governance membership voting L2");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
auto const t1 = Account("t1");
|
|
auto const t2 = Account("t2");
|
|
auto const t3 = Account("t3");
|
|
|
|
auto const m6 = Account("m6");
|
|
auto const m7 = Account("m7");
|
|
auto const m8 = Account("m8");
|
|
auto const m9 = Account("m9");
|
|
auto const m10 = Account("m10");
|
|
auto const m11 = Account("m11");
|
|
auto const m12 = Account("m12");
|
|
auto const m13 = Account("m13");
|
|
auto const m14 = Account("m14");
|
|
auto const m15 = Account("m15");
|
|
auto const m16 = Account("m16");
|
|
auto const m17 = Account("m17");
|
|
auto const m18 = Account("m18");
|
|
auto const m19 = Account("m19");
|
|
auto const m20 = Account("m20");
|
|
auto const m21 = Account("m21");
|
|
|
|
auto const governHookHash = ripple::sha512Half_s(ripple::Slice(
|
|
XahauGenesis::GovernanceHook.data(),
|
|
XahauGenesis::GovernanceHook.size()));
|
|
|
|
// check tables correctly configured
|
|
auto checkL2Table = [&](AccountID const& tableID,
|
|
std::vector<Account const*> members) -> void {
|
|
auto const hookLE = env.le(keylet::hook(tableID));
|
|
BEAST_EXPECT(!!hookLE);
|
|
uint256 const ns = beast::zero;
|
|
uint8_t mc = 0;
|
|
if (hookLE)
|
|
{
|
|
auto const hooksArray = hookLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooksArray.size() == 1 &&
|
|
hooksArray[0].getFieldH256(sfHookHash) == governHookHash);
|
|
|
|
for (Account const* m : members)
|
|
{
|
|
auto const mVec = vecFromAcc(*m);
|
|
// forward key
|
|
|
|
uint8_t key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, mc};
|
|
|
|
auto const forward = env.le(
|
|
keylet::hookState(tableID, uint256::fromVoid(key), ns));
|
|
BEAST_EXPECT(
|
|
!!forward &&
|
|
forward->getFieldVL(sfHookStateData) == mVec);
|
|
|
|
// reverse key
|
|
|
|
for (int i = 12; i < 32; ++i)
|
|
key[i] = mVec[i - 12];
|
|
|
|
auto const reverse = env.le(
|
|
keylet::hookState(tableID, uint256::fromVoid(key), ns));
|
|
BEAST_EXPECT(
|
|
!!reverse &&
|
|
reverse->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{mc});
|
|
mc++;
|
|
}
|
|
}
|
|
|
|
// check membercount is correct
|
|
{
|
|
uint8_t key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 'M', 'C'};
|
|
auto const mcLE = env.le(
|
|
keylet::hookState(tableID, uint256::fromVoid(key), ns));
|
|
BEAST_EXPECT(
|
|
!!mcLE &&
|
|
mcLE->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{mc});
|
|
}
|
|
|
|
// check the hook didn't setup a reward rate or delay key
|
|
{
|
|
uint8_t key[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 'R', 'R'};
|
|
{
|
|
auto const le = env.le(
|
|
keylet::hookState(tableID, uint256::fromVoid(key), ns));
|
|
BEAST_EXPECT(!le);
|
|
}
|
|
key[31] = 'D';
|
|
{
|
|
auto const le = env.le(
|
|
keylet::hookState(tableID, uint256::fromVoid(key), ns));
|
|
BEAST_EXPECT(!le);
|
|
}
|
|
}
|
|
|
|
// check account has correct ownercount and is blackholed
|
|
auto const root = env.le(keylet::account(tableID));
|
|
BEAST_EXPECT(!!root);
|
|
if (root)
|
|
{
|
|
BEAST_EXPECT(root->getFieldU32(sfOwnerCount) == mc * 2 + 2);
|
|
BEAST_EXPECT(root->getFieldU32(sfFlags) & lsfDisableMaster);
|
|
BEAST_EXPECT(root->getAccountID(sfRegularKey) == noAccount());
|
|
}
|
|
};
|
|
|
|
auto doL1Vote = [&](uint64_t lineno,
|
|
Account const& acc,
|
|
char topic1,
|
|
char topic2,
|
|
std::vector<uint8_t> const& vote_data,
|
|
std::vector<uint8_t> const& old_data,
|
|
bool actioned = true,
|
|
bool const shouldFail = false) {
|
|
if (shouldFail)
|
|
actioned = false;
|
|
|
|
bool isZero = true;
|
|
for (auto& x : vote_data)
|
|
if (x != 0)
|
|
{
|
|
isZero = false;
|
|
break;
|
|
}
|
|
|
|
bool isOldDataZero = true;
|
|
for (auto& x : old_data)
|
|
if (x != 0)
|
|
{
|
|
isOldDataZero = false;
|
|
break;
|
|
}
|
|
|
|
uint8_t const key[32] = {
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
static_cast<uint8_t>(topic1 == 'S' ? 0 : topic1),
|
|
static_cast<uint8_t>(topic2)};
|
|
// check actioning prior to vote
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "topic vote precheck: " << lineno << "L\n"
|
|
<< "\tacc: " << acc.human()
|
|
<< " shouldAction: " << (actioned ? "true" : "false")
|
|
<< "\tshouldFail: " << (shouldFail ? "true" : "false")
|
|
<< "\n"
|
|
<< "\ttopic: " << topic1 << ","
|
|
<< (topic2 < 48 ? topic2 + '1' : topic2) << "\n"
|
|
<< "\tisOldDataZero: "
|
|
<< (isOldDataZero ? "true" : "false") << "\n"
|
|
<< "\tisVoteDataZero: " << (isZero ? "true" : "false")
|
|
<< "\n"
|
|
<< "\told_data: " << strHex(old_data) << "\n"
|
|
<< "\tlgr_data: "
|
|
<< (!entry ? "doesn't exist"
|
|
: strHex(entry->getFieldVL(sfHookStateData)))
|
|
<< "\n"
|
|
<< "\tnew_data: " << strHex(vote_data) << "\n"
|
|
<< "\t(isOldDataZero && !entry): "
|
|
<< (isOldDataZero && !entry ? "true" : "false") << "\n"
|
|
<< "\t(entry && entry->getFieldVL(sfHookStateData) == "
|
|
"old_data): "
|
|
<< (entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data
|
|
? "true"
|
|
: "false")
|
|
<< "\n"
|
|
<< ((isOldDataZero && !entry) ||
|
|
(entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data)
|
|
? ""
|
|
: "\tfailed: ^^^\n");
|
|
}
|
|
BEAST_EXPECT(
|
|
(isOldDataZero && !entry) ||
|
|
(entry && entry->getFieldVL(sfHookStateData) == old_data));
|
|
}
|
|
|
|
// perform and check vote
|
|
{
|
|
env(vote(lineno, acc, env.master, topic1, topic2, vote_data),
|
|
M(lineno),
|
|
fee(XRP(1)),
|
|
shouldFail ? ter(tecHOOK_REJECTED) : ter(tesSUCCESS));
|
|
env.close();
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
makeStateKey('V', topic1, topic2, 1, acc.id()),
|
|
beast::zero));
|
|
if (!shouldFail)
|
|
{
|
|
BEAST_REQUIRE(!!entry);
|
|
auto lgr_data = entry->getFieldVL(sfHookStateData);
|
|
BEAST_EXPECT(lgr_data.size() == vote_data.size());
|
|
BEAST_EXPECT(lgr_data == vote_data);
|
|
}
|
|
}
|
|
|
|
// check actioning
|
|
if (!shouldFail)
|
|
{
|
|
// if the vote count isn't high enough it will be hte old value
|
|
// if it's high enough it will be the new value
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(key), beast::zero));
|
|
|
|
if (!actioned && isOldDataZero)
|
|
{
|
|
BEAST_EXPECT(
|
|
!entry ||
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
return;
|
|
}
|
|
|
|
if (actioned)
|
|
{
|
|
if (isZero)
|
|
BEAST_EXPECT(!entry);
|
|
else
|
|
{
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout << "new data: " << strHex(vote_data)
|
|
<< "\n";
|
|
std::cout << "lgr data: "
|
|
<< (!entry ? "<doesn't exist>"
|
|
: strHex(entry->getFieldVL(
|
|
sfHookStateData)))
|
|
<< "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == vote_data);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!!entry && DEBUG_XGTEST)
|
|
std::cout << "old data: "
|
|
<< strHex(entry->getFieldVL(sfHookStateData))
|
|
<< "\n";
|
|
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
}
|
|
}
|
|
};
|
|
|
|
auto doL2Vote = [&](uint64_t lineno,
|
|
uint8_t layer,
|
|
Account const& table,
|
|
Account const& acc,
|
|
char topic1,
|
|
char topic2,
|
|
std::vector<uint8_t> const& vote_data,
|
|
std::vector<uint8_t> const& old_data,
|
|
bool actioned = true,
|
|
bool const shouldFail = false) {
|
|
if (shouldFail)
|
|
actioned = false;
|
|
|
|
bool isZero = true;
|
|
for (auto& x : vote_data)
|
|
if (x != 0)
|
|
{
|
|
isZero = false;
|
|
break;
|
|
}
|
|
|
|
bool isOldDataZero = true;
|
|
for (auto& x : old_data)
|
|
if (x != 0)
|
|
{
|
|
isOldDataZero = false;
|
|
break;
|
|
}
|
|
|
|
uint8_t const key[32] = {
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
static_cast<uint8_t>(topic1 == 'S' ? 0 : topic1),
|
|
static_cast<uint8_t>(topic2)};
|
|
// check actioning prior to vote
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
layer == 1 ? env.master.id() : table.id(),
|
|
uint256::fromVoid(key),
|
|
beast::zero));
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "topic L2 vote precheck: " << lineno << "L\n"
|
|
<< "\tlayer: " << std::string(1, '0' + layer)
|
|
<< " table: " << table.human() << "\n"
|
|
<< "\tacc: " << acc.human()
|
|
<< " shouldAction: " << (actioned ? "true" : "false")
|
|
<< "\tshouldFail: " << (shouldFail ? "true" : "false")
|
|
<< "\n"
|
|
<< "\ttopic: " << topic1 << ","
|
|
<< (topic2 < 48 ? topic2 + '1' : topic2) << "\n"
|
|
<< "\tisOldDataZero: "
|
|
<< (isOldDataZero ? "true" : "false") << "\n"
|
|
<< "\tisVoteDataZero: " << (isZero ? "true" : "false")
|
|
<< "\n"
|
|
<< "\told_data: " << strHex(old_data) << "\n"
|
|
<< "\tlgr_data: "
|
|
<< (!entry ? "doesn't exist"
|
|
: strHex(entry->getFieldVL(sfHookStateData)))
|
|
<< "\n"
|
|
<< "\tnew_data: " << strHex(vote_data) << "\n"
|
|
<< "\t(isOldDataZero && !entry): "
|
|
<< (isOldDataZero && !entry ? "true" : "false") << "\n"
|
|
<< "\t(entry && entry->getFieldVL(sfHookStateData) == "
|
|
"old_data): "
|
|
<< (entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data
|
|
? "true"
|
|
: "false")
|
|
<< "\n"
|
|
<< ((isOldDataZero && !entry) ||
|
|
(entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
old_data)
|
|
? ""
|
|
: "\tfailed: ^^^\n");
|
|
}
|
|
BEAST_EXPECT(
|
|
(isOldDataZero && !entry) ||
|
|
(entry && entry->getFieldVL(sfHookStateData) == old_data));
|
|
}
|
|
|
|
// perform and check vote
|
|
{
|
|
env(vote(lineno, acc, table, topic1, topic2, vote_data, layer),
|
|
M(lineno),
|
|
fee(XRP(1)),
|
|
shouldFail ? ter(tecHOOK_REJECTED) : ter(tesSUCCESS));
|
|
env.close();
|
|
auto entry = env.le(keylet::hookState(
|
|
table.id(),
|
|
makeStateKey('V', topic1, topic2, layer, acc.id()),
|
|
beast::zero));
|
|
if (!shouldFail)
|
|
{
|
|
BEAST_REQUIRE(!!entry);
|
|
auto lgr_data = entry->getFieldVL(sfHookStateData);
|
|
BEAST_EXPECT(lgr_data.size() == vote_data.size());
|
|
BEAST_EXPECT(lgr_data == vote_data);
|
|
}
|
|
}
|
|
|
|
// check actioning
|
|
if (!shouldFail)
|
|
{
|
|
// if the vote count isn't high enough it will be hte old value
|
|
// if it's high enough it will be the new value
|
|
auto entry = env.le(keylet::hookState(
|
|
layer == 1 ? env.master.id() : table.id(),
|
|
uint256::fromVoid(key),
|
|
beast::zero));
|
|
|
|
if (!actioned && isOldDataZero)
|
|
{
|
|
BEAST_EXPECT(
|
|
!entry ||
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
return;
|
|
}
|
|
|
|
if (actioned)
|
|
{
|
|
if (isZero)
|
|
BEAST_EXPECT(!entry);
|
|
else
|
|
{
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout << lineno << "L "
|
|
<< "new data: " << strHex(vote_data)
|
|
<< "\n"
|
|
<< "lgr data: "
|
|
<< (!entry ? "<doesn't exist>"
|
|
: strHex(entry->getFieldVL(
|
|
sfHookStateData)))
|
|
<< "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == vote_data);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< lineno << "L old data: "
|
|
<< (entry
|
|
? strHex(entry->getFieldVL(sfHookStateData))
|
|
: "<doesn't exist>")
|
|
<< " == " << strHex(old_data) << "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == old_data);
|
|
}
|
|
}
|
|
};
|
|
|
|
auto doL1SeatVote = [&](uint64_t lineno,
|
|
uint8_t seat_no,
|
|
uint8_t final_count,
|
|
AccountID const& candidate,
|
|
std::optional<AccountID const> previous,
|
|
std::vector<Account const*> const& voters,
|
|
bool actioned = true,
|
|
bool shouldFail = false) {
|
|
std::vector<uint8_t> previd{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
if (previous)
|
|
memcpy(previd.data(), previous->data(), 20);
|
|
|
|
std::vector<uint8_t> id{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
memcpy(id.data(), candidate.data(), 20);
|
|
|
|
int count = voters.size();
|
|
for (Account const* voter : voters)
|
|
doL1Vote(
|
|
lineno,
|
|
*voter,
|
|
'S',
|
|
seat_no,
|
|
id,
|
|
previd,
|
|
--count <= 0 && actioned,
|
|
shouldFail);
|
|
|
|
if (!shouldFail)
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{final_count};
|
|
|
|
BEAST_REQUIRE(!!entry);
|
|
if (entry->getFieldVL(sfHookStateData) != expected_data &&
|
|
DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "doSeatVote failed " << lineno << "L. entry data: `"
|
|
<< strHex(entry->getFieldVL(sfHookStateData)) << "` "
|
|
<< "expected data: `" << strHex(expected_data) << "`\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
};
|
|
|
|
auto doL2SeatVote = [&](uint64_t lineno,
|
|
uint8_t layer,
|
|
Account const& table,
|
|
uint8_t seat_no,
|
|
uint8_t final_count,
|
|
AccountID const& candidate,
|
|
std::optional<AccountID const> previous,
|
|
std::vector<Account const*> const& voters,
|
|
bool actioned = true,
|
|
bool shouldFail = false) {
|
|
std::vector<uint8_t> previd{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
if (previous)
|
|
memcpy(previd.data(), previous->data(), 20);
|
|
|
|
std::vector<uint8_t> id{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
memcpy(id.data(), candidate.data(), 20);
|
|
|
|
int count = voters.size();
|
|
for (Account const* voter : voters)
|
|
doL2Vote(
|
|
lineno,
|
|
layer,
|
|
table,
|
|
*voter,
|
|
'S',
|
|
seat_no,
|
|
id,
|
|
previd,
|
|
--count <= 0 && actioned,
|
|
shouldFail);
|
|
|
|
if (!shouldFail)
|
|
{
|
|
auto entry = env.le(keylet::hookState(
|
|
layer == 1 ? env.master.id() : table.id(),
|
|
uint256::fromVoid(member_count_key),
|
|
beast::zero));
|
|
std::vector<uint8_t> const expected_data{final_count};
|
|
|
|
BEAST_REQUIRE(!!entry);
|
|
if (entry->getFieldVL(sfHookStateData) != expected_data &&
|
|
DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "doSeatVote failed " << lineno << "L. entry data: `"
|
|
<< strHex(entry->getFieldVL(sfHookStateData)) << "` "
|
|
<< "expected data: `" << strHex(expected_data) << "`\n";
|
|
}
|
|
|
|
BEAST_EXPECT(
|
|
entry->getFieldVL(sfHookStateData) == expected_data);
|
|
}
|
|
};
|
|
|
|
env.fund(
|
|
XRP(10000),
|
|
alice,
|
|
bob,
|
|
carol,
|
|
david,
|
|
edward,
|
|
t1,
|
|
t2,
|
|
t3,
|
|
m6,
|
|
m7,
|
|
m8,
|
|
m9,
|
|
m10,
|
|
m11,
|
|
m12,
|
|
m13,
|
|
m14,
|
|
m15,
|
|
m16,
|
|
m17,
|
|
m18,
|
|
m19,
|
|
m20,
|
|
m21);
|
|
|
|
env.close();
|
|
|
|
setupGov(
|
|
env,
|
|
{alice.id(), bob.id(), t1.id(), t2.id()},
|
|
{{t1.id(), {m6, m7}}, {t2.id(), {m11, m12, m13, m14, m15}}});
|
|
|
|
env.close();
|
|
checkL2Table(t1.id(), std::vector<Account const*>{&m6, &m7});
|
|
checkL2Table(
|
|
t2.id(), std::vector<Account const*>{&m11, &m12, &m13, &m14, &m15});
|
|
|
|
// test voting from layer 2
|
|
// vote for a different reward rate
|
|
{
|
|
// this will be the new reward rate
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x54U};
|
|
|
|
// this is the default reward rate
|
|
std::vector<uint8_t> const original_data{
|
|
0x55U, 0x55U, 0x40U, 0x25U, 0xA6U, 0xD7U, 0xCBU, 0x53U};
|
|
|
|
doL1Vote(
|
|
__LINE__, alice, 'R', 'R', vote_data, original_data, false);
|
|
|
|
// trying to vote for a changed layer2 reward rate should fail
|
|
// because layer2 doesn't have one
|
|
doL2Vote(__LINE__, 2, t1, m6, 'R', 'R', vote_data, {}, false, true);
|
|
|
|
// but layer2 can vote for layer1 reward rate:
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m6, 'R', 'R', vote_data, original_data, false);
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m7, 'R', 'R', vote_data, original_data, false);
|
|
|
|
// a vote by the L2 table should have been emitted now
|
|
env.close();
|
|
env.close();
|
|
|
|
// check the vote counter
|
|
auto checkCounter = [&](uint64_t lineno, uint8_t c) {
|
|
uint8_t counter_key[32] = {
|
|
'C', 'R', 'R', 1, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x54U};
|
|
uint256 ns = beast::zero;
|
|
auto const counterLE = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(counter_key), ns));
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "Counter at " << lineno << "L: "
|
|
<< ((!!counterLE &&
|
|
counterLE->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{c})
|
|
? "pass "
|
|
: "failed ")
|
|
<< (!!counterLE
|
|
? strHex(counterLE->getFieldVL(sfHookStateData))
|
|
: "doesn't exist")
|
|
<< " vs " << std::string(1, (char)('0' + c)) << "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!counterLE &&
|
|
counterLE->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{c});
|
|
};
|
|
|
|
checkCounter(__LINE__, 2);
|
|
|
|
// vote with 60% of the second table
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m11,
|
|
'R',
|
|
'R',
|
|
vote_data,
|
|
original_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m12,
|
|
'R',
|
|
'R',
|
|
vote_data,
|
|
original_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m13,
|
|
'R',
|
|
'R',
|
|
vote_data,
|
|
original_data,
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
checkCounter(__LINE__, 3);
|
|
|
|
// final L1 vote should activate it
|
|
doL1Vote(__LINE__, bob, 'R', 'R', vote_data, original_data, true);
|
|
|
|
checkCounter(__LINE__, 4);
|
|
|
|
// reverting a L2 vote should emit a txn undoing the vote (not
|
|
// anymore Oct 20 2023)
|
|
/*
|
|
doL2Vote(__LINE__, 1, t2, m13, 'R', 'R', original_data, vote_data,
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
checkCounter(__LINE__, 3);
|
|
*/
|
|
}
|
|
|
|
// vote for a different reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
// this is the default reward delay
|
|
std::vector<uint8_t> const original_data{
|
|
0x00U, 0x80U, 0x6AU, 0xACU, 0xAFU, 0x3CU, 0x09U, 0x56U};
|
|
|
|
doL1Vote(
|
|
__LINE__, alice, 'R', 'D', vote_data, original_data, false);
|
|
|
|
// trying to vote for a changed layer2 reward rate should fail
|
|
// because layer2 doesn't have one
|
|
doL2Vote(__LINE__, 2, t1, m6, 'R', 'D', vote_data, {}, false, true);
|
|
|
|
// but layer2 can vote for layer1 reward rate:
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m6, 'R', 'D', vote_data, original_data, false);
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m7, 'R', 'D', vote_data, original_data, false);
|
|
|
|
// a vote by the L2 table should have been emitted now
|
|
env.close();
|
|
env.close();
|
|
|
|
// check the vote counter
|
|
auto checkCounter = [&](uint64_t lineno, uint8_t c) {
|
|
uint8_t counter_key[32] = {
|
|
'C', 'R', 'D', 1, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
uint256 ns = beast::zero;
|
|
auto const counterLE = env.le(keylet::hookState(
|
|
env.master.id(), uint256::fromVoid(counter_key), ns));
|
|
|
|
if (DEBUG_XGTEST)
|
|
{
|
|
std::cout
|
|
<< "Counter at " << lineno << "L: "
|
|
<< (!!counterLE
|
|
? strHex(counterLE->getFieldVL(sfHookStateData))
|
|
: "doesn't exist")
|
|
<< " vs " << std::string(1, (char)('0' + c)) << "\n";
|
|
}
|
|
BEAST_EXPECT(
|
|
!!counterLE &&
|
|
counterLE->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{c});
|
|
};
|
|
|
|
checkCounter(__LINE__, 2);
|
|
|
|
doL1Vote(__LINE__, bob, 'R', 'D', vote_data, original_data, false);
|
|
|
|
// vote with 60% of the second table
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m11,
|
|
'R',
|
|
'D',
|
|
vote_data,
|
|
original_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m12,
|
|
'R',
|
|
'D',
|
|
vote_data,
|
|
original_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__, 1, t2, m13, 'R', 'D', vote_data, original_data, true);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
checkCounter(__LINE__, 4);
|
|
|
|
// reverting a L2 vote should emit a txn undoing the vote (not
|
|
// anymore Oct 20 2023)
|
|
/*
|
|
doL2Vote(__LINE__, 1, t2, m11, 'R', 'D', original_data, vote_data,
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
checkCounter(__LINE__, 3);
|
|
*/
|
|
}
|
|
|
|
// vote an L1 seat in using L2 voting
|
|
{
|
|
// we'll vote claire into seat 4 using t2 as a voting party
|
|
// this requires 51% of t2
|
|
doL2SeatVote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
4,
|
|
4,
|
|
carol,
|
|
null_acc,
|
|
{&m11, &m12, &m13},
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// change our vote
|
|
doL2SeatVote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
4,
|
|
4,
|
|
null_acc,
|
|
null_acc,
|
|
{&m12, &m13, &m14},
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote at l1
|
|
doL1SeatVote(__LINE__, 4, 4, carol.id(), {}, {&alice, &bob}, false);
|
|
|
|
// change our vote again
|
|
doL2SeatVote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
4,
|
|
5,
|
|
carol,
|
|
null_acc,
|
|
{&m11, &m13, &m14},
|
|
true);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// check the member was voted in successfully
|
|
{
|
|
std::vector<uint8_t> key{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04U};
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(key.data()),
|
|
beast::zero));
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) == vecFromAcc(carol));
|
|
}
|
|
// check member count
|
|
{
|
|
std::vector<uint8_t> key{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 'M', 'C'};
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(key.data()),
|
|
beast::zero));
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{0x05U});
|
|
}
|
|
|
|
// now vote carol out
|
|
doL2SeatVote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
4,
|
|
5,
|
|
null_acc,
|
|
carol,
|
|
{&m11, &m12, &m13},
|
|
false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote at l1
|
|
doL1SeatVote(
|
|
__LINE__, 4, 4, null_acc, carol, {&alice, &carol, &bob}, true);
|
|
|
|
env.close();
|
|
|
|
// check the member was voted out successfully
|
|
{
|
|
std::vector<uint8_t> key{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04U};
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(key.data()),
|
|
beast::zero));
|
|
BEAST_EXPECT(!entry);
|
|
}
|
|
// check member count
|
|
{
|
|
std::vector<uint8_t> key{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 'M', 'C'};
|
|
auto entry = env.le(keylet::hookState(
|
|
env.master.id(),
|
|
uint256::fromVoid(key.data()),
|
|
beast::zero));
|
|
BEAST_EXPECT(
|
|
!!entry &&
|
|
entry->getFieldVL(sfHookStateData) ==
|
|
std::vector<uint8_t>{0x04U});
|
|
}
|
|
}
|
|
|
|
// t2 votes out seat 0
|
|
// floor(0.8*5) = 4
|
|
doL2SeatVote(
|
|
__LINE__,
|
|
2,
|
|
t2,
|
|
0,
|
|
4,
|
|
null_acc,
|
|
m11,
|
|
{&m12, &m13, &m14, &m15},
|
|
true);
|
|
|
|
// t2 votes out seat 3
|
|
// floor(0.8*4) = 3
|
|
// first try to use the newly voted out person as a vote
|
|
doL2SeatVote(__LINE__, 2, t2, 4, 3, null_acc, m15, {&m11}, false, true);
|
|
doL2SeatVote(
|
|
__LINE__, 2, t2, 4, 3, null_acc, m15, {&m12, &m13, &m14}, true);
|
|
|
|
// t2 votes out seat 1
|
|
// floor(0.8*3) = 2
|
|
doL2SeatVote(__LINE__, 2, t2, 1, 2, null_acc, m12, {&m12, &m13}, true);
|
|
|
|
// t2 = { empty, empty, m13, m14, empty }
|
|
|
|
// 2 seats is the minimum so trying to vote out another will fail
|
|
doL2SeatVote(
|
|
__LINE__, 2, t2, 2, 2, null_acc, m13, {&m13}, false, false);
|
|
doL2SeatVote(__LINE__, 2, t2, 2, 2, null_acc, m13, {&m14}, false, true);
|
|
|
|
// vote in some invalid seats
|
|
doL2SeatVote(
|
|
__LINE__, 2, t2, 20, 2, m14, null_acc, {&m13, &m14}, false, true);
|
|
|
|
doL2SeatVote(
|
|
__LINE__, 2, t2, 255, 2, m14, null_acc, {&m13, &m14}, false, true);
|
|
|
|
// vote in lots of members
|
|
doL2SeatVote(__LINE__, 2, t2, 0, 3, m11, null_acc, {&m13, &m14}, true);
|
|
|
|
doL2SeatVote(__LINE__, 2, t2, 1, 4, m12, null_acc, {&m13, &m14}, true);
|
|
|
|
doL2SeatVote(
|
|
__LINE__, 2, t2, 4, 5, m15, null_acc, {&m12, &m13, &m14}, true);
|
|
|
|
// t2 = { m11, m12, m13, m14, m15 }
|
|
|
|
{
|
|
Json::Value tx(Json::objectValue);
|
|
|
|
tx[jss::Account] = m21.human();
|
|
tx[jss::TransactionType] = "SetHook";
|
|
tx[jss::Hooks] = Json::arrayValue;
|
|
tx[jss::Hooks][0u] = Json::objectValue;
|
|
|
|
tx[jss::Hooks][0u][jss::Hook] = Json::objectValue;
|
|
|
|
tx[jss::Hooks][0u][jss::Hook][jss::CreateCode] =
|
|
strHex(jtx::genesis::AcceptHook);
|
|
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookApiVersion] = "0";
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookNamespace] =
|
|
"00000000000000000000000000000000000000000000000000000000000000"
|
|
"00";
|
|
tx[jss::Hooks][0u][jss::Hook][jss::HookOn] =
|
|
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFF"
|
|
"FF";
|
|
|
|
// we'll also make a reference anchor for reward and governance
|
|
// hooks here, so they aren't deleted from the ledger in subsequent
|
|
// tests
|
|
tx[jss::Hooks][1u] = Json::objectValue;
|
|
tx[jss::Hooks][1u][jss::Hook] = Json::objectValue;
|
|
tx[jss::Hooks][1u][jss::Hook][jss::HookHash] =
|
|
strHex(governHookHash);
|
|
|
|
tx[jss::Hooks][2u] = Json::objectValue;
|
|
tx[jss::Hooks][2u][jss::Hook] = Json::objectValue;
|
|
tx[jss::Hooks][2u][jss::Hook][jss::HookHash] =
|
|
strHex(rewardHookHash);
|
|
|
|
env(tx, M(__LINE__), fee(XRP(100)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
!!env.le(ripple::keylet::hookDefinition(acceptHookHash)));
|
|
|
|
uint8_t const* data = acceptHookHash.data();
|
|
std::vector<uint8_t> accept_data(data, data + 32);
|
|
|
|
std::vector<uint8_t> const null_data{
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 7, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 7, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 7, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 7, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// check if the hook not was installed because we have not given it
|
|
// 100% votes yet
|
|
{
|
|
auto const hooks = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() == 1);
|
|
}
|
|
|
|
// now cast final vote
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 7, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it was installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash) &&
|
|
hooks->getFieldArray(sfHooks)[7].getFieldH256(sfHookHash) ==
|
|
acceptHookHash);
|
|
}
|
|
|
|
// now change a vote (note that the topic state is never recorded
|
|
// for hooks, so old data is null)
|
|
doL2Vote(__LINE__, 2, t2, m11, 'H', 7, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it's still installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash) &&
|
|
hooks->getFieldArray(sfHooks)[7].getFieldH256(sfHookHash) ==
|
|
acceptHookHash);
|
|
}
|
|
|
|
// now vote to delete it
|
|
doL2Vote(__LINE__, 2, t2, m12, 'H', 7, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m13, 'H', 7, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m14, 'H', 7, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m15, 'H', 7, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// now check it's still installed
|
|
{
|
|
auto const hooks = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooks && hooks->getFieldArray(sfHooks).size() > 7 &&
|
|
!hooks->getFieldArray(sfHooks)[7].isFieldPresent(
|
|
sfHookHash));
|
|
}
|
|
|
|
// vote to place an invalid hook
|
|
std::vector<uint8_t> invalid_data{
|
|
0xFFU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 1, invalid_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 1, invalid_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 1, invalid_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 1, invalid_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
2,
|
|
t2,
|
|
m15,
|
|
'H',
|
|
1,
|
|
invalid_data,
|
|
null_data,
|
|
false,
|
|
true);
|
|
|
|
uint8_t const* gdata = governHookHash.data();
|
|
std::vector<uint8_t> govern_data(gdata, gdata + 32);
|
|
uint8_t const* rdata = rewardHookHash.data();
|
|
std::vector<uint8_t> reward_data(rdata, rdata + 32);
|
|
|
|
// vote to put governance hook into position 2
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 2, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 2, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 2, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 2, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 2, govern_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote to replace the hook at position 1 with accept
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 1, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 1, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 1, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 1, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 1, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// vote to place reward hook at position 2
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 0, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 0, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 0, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 0, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 0, reward_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// hooks array should now look like {govern, accept, reward, nothing
|
|
// ...}
|
|
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 3);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 3)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == acceptHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[2].getFieldH256(sfHookHash) == governHookHash);
|
|
}
|
|
}
|
|
|
|
// set hook 1 back to reward
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 1, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 1, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 1, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 1, reward_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 1, reward_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// set hook 0 back to govern
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 0, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m12, 'H', 0, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m13, 'H', 0, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m14, 'H', 0, govern_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m15, 'H', 0, govern_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// delete hook at 2
|
|
doL2Vote(__LINE__, 2, t2, m11, 'H', 2, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m12, 'H', 2, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m13, 'H', 2, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m14, 'H', 2, null_data, null_data, false);
|
|
doL2Vote(__LINE__, 2, t2, m15, 'H', 2, null_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
|
|
// check we're back the way we were
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == governHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks.size() == 2 ||
|
|
!hooks[2].isFieldPresent(sfHookHash));
|
|
}
|
|
}
|
|
|
|
// change a vote, and ensure nothing changed
|
|
doL2Vote(
|
|
__LINE__, 2, t2, m11, 'H', 0, reward_data, null_data, false);
|
|
env.close();
|
|
env.close();
|
|
{
|
|
auto const hooksLE = env.le(keylet::hook(t2.id()));
|
|
BEAST_EXPECT(
|
|
!!hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2);
|
|
|
|
if (hooksLE && hooksLE->getFieldArray(sfHooks).size() >= 2)
|
|
{
|
|
auto const hooks = hooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(
|
|
hooks[0].getFieldH256(sfHookHash) == governHookHash);
|
|
BEAST_EXPECT(
|
|
hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
|
|
BEAST_EXPECT(
|
|
hooks.size() == 2 ||
|
|
!hooks[2].isFieldPresent(sfHookHash));
|
|
}
|
|
}
|
|
|
|
// vote to install a hook on L1 from a L2 table
|
|
{
|
|
// alice, bob need to vote, because 100% L1 votes are needed to
|
|
// change a hook
|
|
doL1Vote(
|
|
__LINE__, alice, 'H', 2, accept_data, null_data, false);
|
|
doL1Vote(__LINE__, bob, 'H', 2, accept_data, null_data, false);
|
|
|
|
// 51% of t2 must vote
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m11,
|
|
'H',
|
|
2,
|
|
accept_data,
|
|
null_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m12,
|
|
'H',
|
|
2,
|
|
accept_data,
|
|
null_data,
|
|
false);
|
|
doL2Vote(
|
|
__LINE__,
|
|
1,
|
|
t2,
|
|
m13,
|
|
'H',
|
|
2,
|
|
accept_data,
|
|
null_data,
|
|
false);
|
|
|
|
// 51% of t1 must vote, but vote count cannot be less than 2
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m6, 'H', 2, accept_data, null_data, false);
|
|
doL2Vote(
|
|
__LINE__, 1, t1, m7, 'H', 2, accept_data, null_data, false);
|
|
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
|
|
// RH NOTE: idk why this ended up being so slow to finalize,
|
|
// probably multiple emitted txns settling
|
|
|
|
// check it was actioned correctly
|
|
auto genesisHooksLE = env.le(keylet::hook(genesisAccID));
|
|
BEAST_REQUIRE(!!genesisHooksLE);
|
|
auto genesisHookArray = genesisHooksLE->getFieldArray(sfHooks);
|
|
BEAST_EXPECT(genesisHookArray.size() >= 3);
|
|
auto const acceptHash = ripple::sha512Half_s(ripple::Slice(
|
|
jtx::genesis::AcceptHook.data(),
|
|
jtx::genesis::AcceptHook.size()));
|
|
BEAST_EXPECT(
|
|
genesisHookArray.size() >= 3 &&
|
|
genesisHookArray[2].isFieldPresent(sfHookHash) &&
|
|
genesisHookArray[2].getFieldH256(sfHookHash) == acceptHash);
|
|
}
|
|
}
|
|
}
|
|
|
|
Json::Value
|
|
claimReward(
|
|
jtx::Account const& account,
|
|
std::optional<jtx::Account> const& issuer = std::nullopt,
|
|
std::uint32_t flags = 0)
|
|
{
|
|
using namespace jtx;
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::ClaimReward;
|
|
jv[jss::Account] = account.human();
|
|
if (issuer)
|
|
jv[sfIssuer.jsonName] = issuer->human();
|
|
if (flags)
|
|
jv[jss::Flags] = flags;
|
|
return jv;
|
|
}
|
|
|
|
static std::pair<uint256, std::shared_ptr<SLE const>>
|
|
accountKeyAndSle(ReadView const& view, jtx::Account const& account)
|
|
{
|
|
auto const k = keylet::account(account);
|
|
return {k.key, view.read(k)};
|
|
}
|
|
|
|
static STAmount
|
|
rewardL1Amount(STAmount const& reward, std::uint64_t numValidators)
|
|
{
|
|
using namespace jtx;
|
|
return STAmount(reward.value().mantissa() / numValidators);
|
|
}
|
|
|
|
static STAmount
|
|
rewardUserAmount(
|
|
SLE const& acctSle,
|
|
std::uint32_t currentLedger,
|
|
double rate)
|
|
{
|
|
using namespace jtx;
|
|
std::uint64_t accumulator = acctSle.getFieldU64(sfRewardAccumulator);
|
|
std::uint32_t const lgrFirst = acctSle.getFieldU32(sfRewardLgrFirst);
|
|
std::uint32_t const lgrLast = acctSle.getFieldU32(sfRewardLgrLast);
|
|
STAmount const balance = acctSle.getFieldAmount(sfBalance);
|
|
std::uint32_t const elapsedSinceLast = currentLedger - lgrLast;
|
|
std::uint32_t const elapsed = currentLedger - lgrFirst;
|
|
|
|
if (balance.xrp().drops() > 0 && elapsedSinceLast > 0)
|
|
accumulator += (balance.xrp().drops() / 1'000'000) *
|
|
static_cast<std::uint64_t>(elapsedSinceLast);
|
|
|
|
auto const rewardSubtotal = accumulator / elapsed;
|
|
return STAmount(rate * rewardSubtotal);
|
|
}
|
|
|
|
void
|
|
updateTopic(
|
|
jtx::Env& env,
|
|
jtx::Account const& acc1,
|
|
jtx::Account const& acc2,
|
|
jtx::Account const& acc3,
|
|
jtx::Account const& acc4,
|
|
jtx::Account const& acc5,
|
|
char topic1,
|
|
char topic2,
|
|
std::vector<uint8_t> const& vote_data)
|
|
{
|
|
using namespace jtx;
|
|
env(vote(__LINE__, acc1, env.master, topic1, topic2, vote_data),
|
|
fee(XRP(1)),
|
|
ter(tesSUCCESS));
|
|
env(vote(__LINE__, acc2, env.master, topic1, topic2, vote_data),
|
|
fee(XRP(1)),
|
|
ter(tesSUCCESS));
|
|
env(vote(__LINE__, acc3, env.master, topic1, topic2, vote_data),
|
|
fee(XRP(1)),
|
|
ter(tesSUCCESS));
|
|
env(vote(__LINE__, acc4, env.master, topic1, topic2, vote_data),
|
|
fee(XRP(1)),
|
|
ter(tesSUCCESS));
|
|
env(vote(__LINE__, acc5, env.master, topic1, topic2, vote_data),
|
|
fee(XRP(1)),
|
|
ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
|
|
bool
|
|
hasUNLReport(jtx::Env const& env)
|
|
{
|
|
auto const slep = env.le(keylet::UNLReport());
|
|
return slep != nullptr;
|
|
}
|
|
|
|
void
|
|
activateUNLReport(
|
|
jtx::Env& env,
|
|
std::vector<PublicKey> ivlKeys,
|
|
std::vector<PublicKey> vlKeys)
|
|
{
|
|
// insert a ttUNL_REPORT pseudo into the open ledger
|
|
for (auto const& vlKey : vlKeys)
|
|
{
|
|
env.app().openLedger().modify(
|
|
[&](OpenView& view, beast::Journal j) -> bool {
|
|
STTx tx = unl::createUNLReportTx(
|
|
env.current()->seq() + 1, ivlKeys[0], vlKey);
|
|
uint256 txID = tx.getTransactionID();
|
|
auto s = std::make_shared<ripple::Serializer>();
|
|
tx.add(*s);
|
|
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
|
|
view.rawTxInsert(txID, std::move(s), nullptr);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// close the ledger
|
|
env.close();
|
|
}
|
|
|
|
bool
|
|
expectAccountFields(
|
|
jtx::Env const& env,
|
|
jtx::Account const& acct,
|
|
std::uint32_t const& ledgerFirst,
|
|
std::uint32_t const& ledgerLast,
|
|
STAmount const& accumulator,
|
|
NetClock::time_point const& timePoint)
|
|
{
|
|
auto const sle = env.le(keylet::account(acct));
|
|
auto const time = timePoint.time_since_epoch().count();
|
|
|
|
if (!sle->isFieldPresent(sfRewardLgrFirst) ||
|
|
sle->getFieldU32(sfRewardLgrFirst) != ledgerFirst)
|
|
{
|
|
return false;
|
|
}
|
|
if (!sle->isFieldPresent(sfRewardLgrLast) ||
|
|
sle->getFieldU32(sfRewardLgrLast) != ledgerLast)
|
|
{
|
|
return false;
|
|
}
|
|
if (!sle->isFieldPresent(sfRewardAccumulator) ||
|
|
sle->getFieldU64(sfRewardAccumulator) !=
|
|
accumulator.value().mantissa() / 1'000'000)
|
|
{
|
|
return false;
|
|
}
|
|
if (!sle->isFieldPresent(sfRewardTime) ||
|
|
sle->getFieldU32(sfRewardTime) != time)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
expectNoAccountFields(jtx::Env const& env, jtx::Account const& acct)
|
|
{
|
|
auto const sle = env.le(keylet::account(acct));
|
|
if (sle->isFieldPresent(sfRewardLgrFirst))
|
|
{
|
|
return false;
|
|
}
|
|
if (sle->isFieldPresent(sfRewardLgrLast))
|
|
{
|
|
return false;
|
|
}
|
|
if (sle->isFieldPresent(sfRewardAccumulator))
|
|
{
|
|
return false;
|
|
}
|
|
if (sle->isFieldPresent(sfRewardTime))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
NetClock::time_point
|
|
lastClose(jtx::Env& env)
|
|
{
|
|
return env.app()
|
|
.getLedgerMaster()
|
|
.getValidatedLedger()
|
|
->info()
|
|
.parentCloseTime;
|
|
}
|
|
|
|
void
|
|
validateTime(NetClock::time_point const& value, std::uint64_t ledgerTime)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
BEAST_EXPECT(value.time_since_epoch().count() == ledgerTime);
|
|
}
|
|
|
|
void
|
|
testLastCloseTime(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test last close time");
|
|
|
|
Env env{*this, envconfig(), features};
|
|
validateTime(lastClose(env), 0);
|
|
|
|
// last close = 0
|
|
env.close();
|
|
validateTime(lastClose(env), 0);
|
|
|
|
// last close + 10 = 10
|
|
env.close();
|
|
validateTime(lastClose(env), 10);
|
|
|
|
// last close + 10 = 20
|
|
env.close(10s);
|
|
validateTime(lastClose(env), 20);
|
|
|
|
// last close + 10 (^+10s) = 40
|
|
env.close();
|
|
validateTime(lastClose(env), 40);
|
|
|
|
// last close + 10 = 50
|
|
env.close();
|
|
validateTime(lastClose(env), 50);
|
|
}
|
|
|
|
void
|
|
testInvalidRate0(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("test claim reward rate is == 0");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward rate
|
|
{
|
|
// this will be the new reward rate
|
|
// 0
|
|
std::vector<uint8_t> const vote_data{
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'R', vote_data);
|
|
}
|
|
|
|
// opt in claim reward will fail
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testInvalidRateGreater1(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("test claim reward rate is > 1");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward rate
|
|
{
|
|
// this will be the new reward delay
|
|
// 2
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x00U, 0x8DU, 0x49U, 0xFDU, 0x1AU, 0x87U, 0x54U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'R', vote_data);
|
|
}
|
|
|
|
// opt in claim reward will fail
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testInvalidDelay0(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("test claim reward delay is == 0");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 0
|
|
std::vector<uint8_t> const vote_data{
|
|
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// opt in claim reward will fail
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testInvalidDelayNegative(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("test claim reward delay is < 0");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// -1
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x83U, 0x14U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// opt in claim reward will fail
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testInvalidBeforeTime(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
testcase("test claim reward before time");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// claim reward fails
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testValidWithoutUNLReport(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward valid without unl report");
|
|
|
|
Env env{
|
|
*this, envconfig(), supported_amendments() - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// validate no unl report
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 90);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 180);
|
|
|
|
// define pre close values
|
|
STAmount const preAlice = env.balance(alice);
|
|
STAmount const preBob = env.balance(bob);
|
|
STAmount const preCarol = env.balance(carol);
|
|
STAmount const preDavid = env.balance(david);
|
|
STAmount const preEdward = env.balance(edward);
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
auto const netReward = rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
|
|
// validate no govern rewards
|
|
BEAST_EXPECT(env.balance(alice).value() == preAlice.value());
|
|
BEAST_EXPECT(env.balance(bob).value() == preBob.value());
|
|
BEAST_EXPECT(env.balance(carol).value() == preCarol.value());
|
|
BEAST_EXPECT(env.balance(david).value() == preDavid.value());
|
|
BEAST_EXPECT(env.balance(edward).value() == preEdward.value());
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 220);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 310);
|
|
|
|
// define pre close values
|
|
STAmount const preAlice1 = env.balance(alice);
|
|
STAmount const preBob1 = env.balance(bob);
|
|
STAmount const preCarol1 = env.balance(carol);
|
|
STAmount const preDavid1 = env.balance(david);
|
|
STAmount const preEdward1 = env.balance(edward);
|
|
STAmount const preUser1 = env.balance(user);
|
|
NetClock::time_point const preTime1 = lastClose(env);
|
|
std::uint32_t const preLedger1 = env.current()->seq();
|
|
auto const [acct1, acctSle1] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
auto const netReward1 =
|
|
rewardUserAmount(*acctSle1, preLedger1, rateDrops);
|
|
|
|
// validate no govern rewards
|
|
BEAST_EXPECT(env.balance(alice).value() == preAlice1.value());
|
|
BEAST_EXPECT(env.balance(bob).value() == preBob1.value());
|
|
BEAST_EXPECT(env.balance(carol).value() == preCarol1.value());
|
|
BEAST_EXPECT(env.balance(david).value() == preDavid1.value());
|
|
BEAST_EXPECT(env.balance(edward).value() == preEdward1.value());
|
|
|
|
// validate account fields
|
|
STAmount const postUser1 = preUser1 + netReward1;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger1, preLedger1 + 1, postUser1, preTime1));
|
|
}
|
|
|
|
void
|
|
testValidWithUNLReport(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward valid with unl report");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// setup unl report
|
|
std::vector<std::string> const _ivlKeys = {
|
|
"ED74D4036C6591A4BDF9C54CEFA39B996A5DCE5F86D11FDA1874481CE9D5A1"
|
|
"CDC1",
|
|
};
|
|
std::vector<PublicKey> ivlKeys;
|
|
for (auto const& strPk : _ivlKeys)
|
|
{
|
|
auto pkHex = strUnHex(strPk);
|
|
ivlKeys.emplace_back(makeSlice(*pkHex));
|
|
}
|
|
std::vector<std::string> const _vlKeys = {
|
|
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
|
|
"2",
|
|
"02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3F"
|
|
"E",
|
|
"028949021029D5CC87E78BCF053AFEC0CAFD15108EC119EAAFEC466F5C095407B"
|
|
"F",
|
|
"027BAEF0CB02EA8B95F50DF4BC16C740B17B50C85F3757AA06A5DB6ADE0ED9210"
|
|
"6",
|
|
"0318E0D644F3D2911D7B7E1B0B17684E7E625A6C36AECCE851BD16A4AD628B213"
|
|
"6"};
|
|
std::vector<PublicKey> vlKeys;
|
|
for (auto const& strPk : _vlKeys)
|
|
{
|
|
auto pkHex = strUnHex(strPk);
|
|
vlKeys.emplace_back(makeSlice(*pkHex));
|
|
}
|
|
|
|
// activate unl report
|
|
activateUNLReport(env, ivlKeys, vlKeys);
|
|
|
|
// validate unl report
|
|
BEAST_EXPECT(hasUNLReport(env) == true);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 100);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 190);
|
|
|
|
// define pre close values
|
|
STAmount const preAlice = env.balance(alice);
|
|
STAmount const preBob = env.balance(bob);
|
|
STAmount const preCarol = env.balance(carol);
|
|
STAmount const preDavid = env.balance(david);
|
|
STAmount const preEdward = env.balance(edward);
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
STAmount const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
STAmount const l1Reward = rewardL1Amount(netReward, 20);
|
|
|
|
// validate govern rewards
|
|
BEAST_EXPECT(env.balance(alice) == preAlice + l1Reward);
|
|
BEAST_EXPECT(env.balance(bob) == preBob + l1Reward);
|
|
BEAST_EXPECT(env.balance(carol) == preCarol + l1Reward);
|
|
BEAST_EXPECT(env.balance(david) == preDavid + l1Reward);
|
|
BEAST_EXPECT(env.balance(edward) == preEdward + l1Reward);
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
}
|
|
|
|
void
|
|
testValidL1WithUNLReport(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward valid for L1 with unl report");
|
|
|
|
for (bool const withXahauV1 : {true, false})
|
|
{
|
|
FeatureBitset _features = features - featureXahauGenesis;
|
|
auto const amend = withXahauV1 ? _features : _features - fixXahauV1;
|
|
Env env{*this, envconfig(), amend};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// setup unl report
|
|
std::vector<std::string> const _ivlKeys = {
|
|
"ED74D4036C6591A4BDF9C54CEFA39B996A5DCE5F86D11FDA1874481CE9D5A1"
|
|
"CDC1",
|
|
};
|
|
std::vector<PublicKey> ivlKeys;
|
|
for (auto const& strPk : _ivlKeys)
|
|
{
|
|
auto pkHex = strUnHex(strPk);
|
|
ivlKeys.emplace_back(makeSlice(*pkHex));
|
|
}
|
|
std::vector<std::string> const _vlKeys = {
|
|
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97"
|
|
"C05"
|
|
"2",
|
|
"02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006"
|
|
"E3F"
|
|
"E",
|
|
"028949021029D5CC87E78BCF053AFEC0CAFD15108EC119EAAFEC466F5C0954"
|
|
"07B"
|
|
"F",
|
|
"027BAEF0CB02EA8B95F50DF4BC16C740B17B50C85F3757AA06A5DB6ADE0ED9"
|
|
"210"
|
|
"6",
|
|
"0318E0D644F3D2911D7B7E1B0B17684E7E625A6C36AECCE851BD16A4AD628B"
|
|
"213"
|
|
"6"};
|
|
std::vector<PublicKey> vlKeys;
|
|
for (auto const& strPk : _vlKeys)
|
|
{
|
|
auto pkHex = strUnHex(strPk);
|
|
vlKeys.emplace_back(makeSlice(*pkHex));
|
|
}
|
|
|
|
// activate unl report
|
|
activateUNLReport(env, ivlKeys, vlKeys);
|
|
|
|
// validate unl report
|
|
BEAST_EXPECT(hasUNLReport(env) == true);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(alice, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 90);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 180);
|
|
|
|
// define pre close values
|
|
STAmount const preAlice = env.balance(alice);
|
|
STAmount const preBob = env.balance(bob);
|
|
STAmount const preCarol = env.balance(carol);
|
|
STAmount const preDavid = env.balance(david);
|
|
STAmount const preEdward = env.balance(edward);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] =
|
|
accountKeyAndSle(*env.current(), alice);
|
|
|
|
// claim reward
|
|
auto const txResult =
|
|
withXahauV1 ? ter(tesSUCCESS) : ter(tecHOOK_REJECTED);
|
|
env(claimReward(alice, env.master), fee(feesXRP), txResult);
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
STAmount const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
STAmount const l1Reward =
|
|
withXahauV1 ? rewardL1Amount(netReward, 20) : STAmount(0);
|
|
|
|
// validate govern rewards
|
|
BEAST_EXPECT(env.balance(bob) == preBob + l1Reward);
|
|
BEAST_EXPECT(env.balance(carol) == preCarol + l1Reward);
|
|
BEAST_EXPECT(env.balance(david) == preDavid + l1Reward);
|
|
BEAST_EXPECT(env.balance(edward) == preEdward + l1Reward);
|
|
|
|
// validate account fields
|
|
STAmount const postAlice = preAlice + netReward + l1Reward;
|
|
bool const boolResult = withXahauV1 ? true : false;
|
|
BEAST_EXPECT(
|
|
expectAccountFields(
|
|
env, alice, preLedger, preLedger + 1, postAlice, preTime) ==
|
|
boolResult);
|
|
}
|
|
}
|
|
|
|
void
|
|
testOptInOptOut(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward optin optout");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// verify unl report does not exist
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 90);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 180);
|
|
|
|
// close claim ledger & time
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
STAmount const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
|
|
// opt out of claim rewards
|
|
env(claimReward(user, std::nullopt, 1), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(expectNoAccountFields(env, user));
|
|
|
|
// pre claim values
|
|
std::uint32_t const preLedger1 = env.current()->seq();
|
|
NetClock::time_point const preTime1 = lastClose(env);
|
|
|
|
// opt in for claim rewards
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// claim reward fails without suficient time passing
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env,
|
|
user,
|
|
preLedger1,
|
|
preLedger1 + 1,
|
|
env.balance(user),
|
|
preTime1));
|
|
}
|
|
|
|
void
|
|
testValidLowBalance(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward bal == 1");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.fund(XRP(200), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// verify unl report does not exist
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 90);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 180);
|
|
|
|
env(noop(user), fee(env.balance(user) - feesXRP), ter(tesSUCCESS));
|
|
|
|
// close claim ledger & time
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
STAmount const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
}
|
|
|
|
void
|
|
testValidElapsed1(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward elapsed_since_last == 1");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.fund(XRP(1000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// verify unl report does not exist
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
|
|
// close ledgers
|
|
env.close(100s);
|
|
validateTime(lastClose(env), 90);
|
|
|
|
// close claim ledger & time
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
STAmount const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
}
|
|
|
|
void
|
|
testInvalidElapsed0(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward elapsed_since_last == 0");
|
|
|
|
Env env{*this, envconfig(), features - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.fund(XRP(1000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xE3U, 0x37U, 0x79U, 0xC3U, 0x91U, 0x54U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// verify unl report does not exist
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
|
|
// sleep time
|
|
std::this_thread::sleep_for(std::chrono::seconds(5));
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
std::unique_ptr<Config>
|
|
makeGenesisConfig(
|
|
FeatureBitset features,
|
|
uint32_t networkID,
|
|
std::string fee,
|
|
std::string a_res,
|
|
std::string o_res,
|
|
uint32_t ledgerID)
|
|
{
|
|
using namespace jtx;
|
|
|
|
// IMPORT VL KEY
|
|
std::vector<std::string> const keys = {
|
|
"ED74D4036C6591A4BDF9C54CEFA39B996A"
|
|
"5DCE5F86D11FDA1874481CE9D5A1CDC1"};
|
|
|
|
Json::Value jsonValue;
|
|
Json::Reader reader;
|
|
|
|
std::string base_genesis = R"json({
|
|
"ledger": {
|
|
"accepted": true,
|
|
"accountState": [
|
|
{
|
|
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
|
"Balance": "100000000000000000",
|
|
"Flags": 0,
|
|
"LedgerEntryType": "AccountRoot",
|
|
"OwnerCount": 0,
|
|
"PreviousTxnID": "A92EF82C3C68F771927E3892A2F708F12CBD492EF68A860F042E4053C8EC6C8D",
|
|
"PreviousTxnLgrSeq": 0,
|
|
"Sequence": 1,
|
|
"index": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8"
|
|
},
|
|
{
|
|
"Amendments": [],
|
|
"Flags": 0,
|
|
"LedgerEntryType": "Amendments",
|
|
"index": "7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4"
|
|
},
|
|
{
|
|
"BaseFee": "A",
|
|
"Flags": 0,
|
|
"LedgerEntryType": "FeeSettings",
|
|
"ReferenceFeeUnits": 10,
|
|
"ReserveBase": 1000000,
|
|
"ReserveIncrement": 200000,
|
|
"XahauActivationLgrSeq": 0,
|
|
"index": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651"
|
|
},
|
|
{
|
|
"Account": "rHtp2PBLTDCGpuKPCn7JqqgRYV6Rkz7Mhc",
|
|
"Balance": "1000000000",
|
|
"Flags": 0,
|
|
"LedgerEntryType": "AccountRoot",
|
|
"OwnerCount": 0,
|
|
"PreviousTxnID": "A92EF82C3C68F771927E3892A2F708F12CBD492EF68A860F042E4053C8EC6C8D",
|
|
"PreviousTxnLgrSeq": 0,
|
|
"Sequence": 2,
|
|
"AccountIndex": 2,
|
|
"RewardAccumulator": "999",
|
|
"RewardLgrFirst": 18,
|
|
"RewardLgrLast": 19,
|
|
"RewardTime": 0,
|
|
"index": "67A4F2EA22654192D9782580A5DF8C907D1F71EA6C01922E4C534EDD0CFC4792"
|
|
}
|
|
],
|
|
"account_hash": "5DF3A98772FB73E782B8740E87885C6BAD9BA486422E3626DEF968AD2CB2C514",
|
|
"close_flags": 0,
|
|
"close_time": 0,
|
|
"close_time_human": "2000-Jan-01 00:00:00.000000",
|
|
"close_time_resolution": 10,
|
|
"closed": true,
|
|
"hash": "56DA0940767AC2F17F0E384F04816002403D0756432B9D503DDA20128A2AAF11",
|
|
"ledger_hash": "56DA0940767AC2F17F0E384F04816002403D0756432B9D503DDA20128A2AAF11",
|
|
"ledger_index": "0",
|
|
"parent_close_time": 0,
|
|
"parent_hash": "56DA0940767AC2F17F0E384F04816002403D0756432B9D503DDA20128A2AAF11",
|
|
"seqNum": "0",
|
|
"totalCoins": "0",
|
|
"total_coins": "0",
|
|
"transaction_hash": "9A77D1D1A4B36DA77B9C4DC63FDEB8F821741D157802F9C42A6ED86003D8B4A0",
|
|
"transactions": []
|
|
},
|
|
"ledger_current_index": 0,
|
|
"status": "success",
|
|
"validated": true
|
|
})json";
|
|
reader.parse(base_genesis, jsonValue);
|
|
|
|
foreachFeature(features, [&](uint256 const& feature) {
|
|
std::string featureName = featureToName(feature);
|
|
std::optional<uint256> featureHash =
|
|
getRegisteredFeature(featureName);
|
|
if (featureHash.has_value())
|
|
{
|
|
std::string hashString = to_string(featureHash.value());
|
|
jsonValue["ledger"]["accountState"][1u]["Amendments"].append(
|
|
hashString);
|
|
}
|
|
});
|
|
|
|
jsonValue["ledger_current_index"] = ledgerID;
|
|
jsonValue["ledger"]["ledger_index"] = to_string(ledgerID);
|
|
jsonValue["ledger"]["seqNum"] = to_string(ledgerID);
|
|
|
|
return envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->NETWORK_ID = networkID;
|
|
cfg->START_LEDGER = jsonValue.toStyledString();
|
|
cfg->START_UP = Config::LOAD_JSON;
|
|
Section config;
|
|
config.append(
|
|
{"reference_fee = " + fee,
|
|
"account_reserve = " + a_res,
|
|
"owner_reserve = " + o_res});
|
|
auto setup = setup_FeeVote(config);
|
|
cfg->FEES = setup;
|
|
|
|
for (auto const& strPk : keys)
|
|
{
|
|
auto pkHex = strUnHex(strPk);
|
|
if (!pkHex)
|
|
Throw<std::runtime_error>(
|
|
"Import VL Key '" + strPk + "' was not valid hex.");
|
|
|
|
auto const pkType = publicKeyType(makeSlice(*pkHex));
|
|
if (!pkType)
|
|
Throw<std::runtime_error>(
|
|
"Import VL Key '" + strPk +
|
|
"' was not a valid key type.");
|
|
|
|
cfg->IMPORT_VL_KEYS.emplace(strPk, makeSlice(*pkHex));
|
|
}
|
|
return cfg;
|
|
});
|
|
}
|
|
|
|
void
|
|
testInvalidElapsedNegative(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test claim reward elapsed_since_last < 0");
|
|
|
|
Env env{
|
|
*this,
|
|
makeGenesisConfig(
|
|
supported_amendments() - featureXahauGenesis,
|
|
21337,
|
|
"10",
|
|
"1000000",
|
|
"200000",
|
|
0),
|
|
supported_amendments() - featureXahauGenesis};
|
|
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
Account const user = Account("user");
|
|
env.memoize(user);
|
|
|
|
// setup governance
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
Account const carol = Account("carol");
|
|
Account const david = Account("david");
|
|
Account const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// verify unl report does not exist
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// claim reward fails - ledger time < 100
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
|
|
// close ledgers
|
|
validateTime(lastClose(env), 80);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), 170);
|
|
|
|
// claim reward fails - ledger elapsed < 0
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testCompoundInterest(FeatureBitset features)
|
|
{
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
testcase("test compound interest over 12 claims");
|
|
|
|
Env env{
|
|
*this, envconfig(), supported_amendments() - featureXahauGenesis};
|
|
|
|
double const rateDrops = 0.00333333333 * 1'000'000;
|
|
STAmount const feesXRP = XRP(1);
|
|
|
|
auto const user = Account("user");
|
|
env.fund(XRP(10000), user);
|
|
env.close();
|
|
|
|
// setup governance
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
auto const david = Account("david");
|
|
auto const edward = Account("edward");
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
|
env.close();
|
|
|
|
std::vector<AccountID> initial_members_ids{
|
|
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
|
|
|
setupGov(env, initial_members_ids);
|
|
|
|
// update reward delay
|
|
{
|
|
// this will be the new reward delay
|
|
// 100
|
|
std::vector<uint8_t> vote_data{
|
|
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
|
|
|
updateTopic(
|
|
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
|
}
|
|
|
|
// validate no unl report
|
|
BEAST_EXPECT(hasUNLReport(env) == false);
|
|
|
|
// opt in claim reward
|
|
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
STAmount const begBal = env.balance(user);
|
|
|
|
for (int i = 0; i < 12; ++i)
|
|
{
|
|
// close ledgers
|
|
std::uint64_t ledgerDiff = 90 + (i * 30) + (i * 90);
|
|
validateTime(lastClose(env), ledgerDiff);
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
env.close(10s);
|
|
}
|
|
validateTime(lastClose(env), ledgerDiff + 90);
|
|
|
|
// define pre close values
|
|
STAmount const preUser = env.balance(user);
|
|
NetClock::time_point const preTime = lastClose(env);
|
|
std::uint32_t const preLedger = env.current()->seq();
|
|
auto const [acct, acctSle] = accountKeyAndSle(*env.current(), user);
|
|
|
|
// claim reward
|
|
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// trigger emitted txn
|
|
env.close();
|
|
|
|
// calculate rewards
|
|
auto const netReward =
|
|
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
|
|
|
// validate account fields
|
|
STAmount const postUser = preUser + netReward;
|
|
BEAST_EXPECT(expectAccountFields(
|
|
env, user, preLedger, preLedger + 1, postUser, preTime));
|
|
}
|
|
|
|
STAmount const endBal = env.balance(user);
|
|
auto const gainloss = ((endBal - begBal) / begBal);
|
|
std::uint64_t const asPercent =
|
|
static_cast<std::int64_t>(gainloss * 100);
|
|
BEAST_EXPECT(asPercent == 4);
|
|
}
|
|
|
|
void
|
|
testRewardHookWithFeats(FeatureBitset features)
|
|
{
|
|
testLastCloseTime(features);
|
|
testInvalidRate0(features);
|
|
testInvalidRateGreater1(features);
|
|
testInvalidDelay0(features);
|
|
testInvalidDelayNegative(features);
|
|
testInvalidBeforeTime(features);
|
|
testValidWithoutUNLReport(features);
|
|
testValidWithUNLReport(features);
|
|
testValidL1WithUNLReport(features);
|
|
testOptInOptOut(features);
|
|
testValidLowBalance(features);
|
|
testValidElapsed1(features);
|
|
testInvalidElapsed0(features);
|
|
testInvalidElapsedNegative(features);
|
|
testCompoundInterest(features);
|
|
}
|
|
|
|
void
|
|
testGovernHookWithFeats(FeatureBitset features)
|
|
{
|
|
testPlainActivation(features);
|
|
testWithSignerList(features);
|
|
testWithRegularKey(features);
|
|
testGovernanceL1(features);
|
|
testGovernanceL2(features);
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
using namespace test::jtx;
|
|
auto const sa = supported_amendments();
|
|
testGovernHookWithFeats(sa);
|
|
testRewardHookWithFeats(sa);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(XahauGenesis, app, ripple);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|
|
#undef M
|