Files
xahaud/src/test/app/XahauGenesis_test.cpp
2023-09-04 14:59:15 +00:00

693 lines
25 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/tx/apply.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/app/tx/impl/XahauGenesis.h>
#include <string>
#include <test/jtx.h>
#include <vector>
using namespace XahauGenesis;
namespace ripple {
namespace test {
struct XahauGenesis_test : public beast::unit_test::suite
{
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(jtx::Env& env, bool burnedViaTest = false, bool skipTests = false, bool 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;
// 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
XRPAmount total { GenesisAmount };
for (auto const& [node, amt] : Distribution)
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);
BEAST_EXPECT(govSLE->getFieldH256(sfHookOn) ==
ripple::uint256("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFF"));
BEAST_EXPECT(govSLE->getFieldH256(sfHookNamespace) ==
ripple::uint256("0000000000000000000000000000000000000000000000000000000000000000"));
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("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFFFFFFFBFFFFF"));
BEAST_EXPECT(rwdSLE->getFieldH256(sfHookNamespace) ==
ripple::uint256("0000000000000000000000000000000000000000000000000000000000000000"));
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 distribution amounts and hook parameters
{
uint8_t member_count = 0;
std::map<std::vector<uint8_t>, std::vector<uint8_t>> params = XahauGenesis::GovernanceParameters;
for (auto const& [rn, x]: XahauGenesis::Distribution)
{
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(
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(
std::vector<uint8_t>{'I', 'S', member_count++},
std::vector<uint8_t>(id.data(), id.data() + 20));
}
// initial member count
params.emplace(
std::vector<uint8_t>{'I', 'M', 'C'},
std::vector<uint8_t>{member_count});
// check parameters
auto leParams = genesisHookArray[0].getFieldArray(sfHookParameters);
BEAST_EXPECT(leParams.size() == params.size());
// these should be recorded in the same order
std::set<std::vector<uint8_t>> keys_used;
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());
// should be in our precomputed params
BEAST_EXPECT(params.find(key) != params.end());
// value should match
BEAST_EXPECT(params[key] == val);
// add key to used set
keys_used.emplace(key);
}
}
// 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()
{
testcase("Test activation");
using namespace jtx;
Env env{*this, envconfig(), supported_amendments() - featureXahauGenesis, nullptr,
beast::severities::kWarning
//beast::severities::kTrace
};
activate(env);
}
void
testWithSignerList()
{
using namespace jtx;
testcase("Test signerlist");
Env env{*this, envconfig(), supported_amendments() - featureXahauGenesis, nullptr,
beast::severities::kWarning
//beast::severities::kTrace
};
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(env, true);
}
void
testWithRegularKey()
{
using namespace jtx;
testcase("Test regkey");
Env env{*this, envconfig(), supported_amendments() - featureXahauGenesis, nullptr,
beast::severities::kWarning
//beast::severities::kTrace
};
env.memoize(env.master);
Account const alice("alice");
env.fund(XRP(10000), alice);
env(regkey(env.master, alice));
env.close();
activate(env, true);
}
void
setupGov(jtx::Env& env, std::vector<AccountID> const members)
{
using namespace jtx;
auto const invoker = Account("invoker");
env.fund(XRP(10000), invoker);
env.close();
XahauGenesis::TestDistribution.clear();
for (auto& m: members)
{
std::string acc = toBase58(m);
XahauGenesis::TestDistribution[acc] = XRPAmount(10000000000);
}
activate(env, true, true, true);
XahauGenesis::TestDistribution.clear();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = invoker.human();
invoke[jss::Destination] = env.master.human();
env(invoke, fee(XRP(1)));
env.close();
}
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;
}
void
testGovernance()
{
using namespace jtx;
testcase("Test governance hook");
Env env{*this, envconfig(), supported_amendments() - featureXahauGenesis, nullptr,
beast::severities::kTrace
};
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();
setupGov(env, {alice.id(), bob.id(), carol.id(), david.id(), edward.id()});
auto vote = [&](
Account const& acc,
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) : "");
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;
if (layer)
{
txn[jss::HookParameters][2u] = Json::objectValue;
txn[jss::HookParameters][2u][jss::HookParameter][jss::HookParameterName] = "4C";
txn[jss::HookParameters][2u][jss::HookParameter][jss::HookParameterValue] = charToHex(*layer);
}
txn[jss::Account] = acc.human();
txn[jss::TransactionType] = "Invoke";
txn[jss::Destination] = env.master.human();
return txn;
};
auto makeStateKey =
[&](char voteType, char topic1, char topic2, uint8_t layer, AccountID const& id) -> uint256
{
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);
};
auto doL1Vote = [&](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;
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, topic1, topic2};
// check actioning prior to vote
{
auto entry = env.le(keylet::hookState(env.master.id(), uint256::fromVoid(key), beast::zero));
BEAST_EXPECT((old_data.empty() && !entry) ||
(entry && entry->getFieldVL(sfHookStateData) == old_data));
}
// perform and check vote
{
env(vote(acc, topic1, topic2, vote_data), 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 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));
bool isZero = true;
for (auto& x: vote_data)
if (x != 0)
{
isZero = false;
break;
}
if (!actioned && old_data.empty())
{
BEAST_EXPECT(!entry);
return;
}
if (actioned)
{
if (isZero)
BEAST_EXPECT(!entry);
else
BEAST_EXPECT(!!entry && entry->getFieldVL(sfHookStateData) == vote_data);
}
else
{
std::cout << "old data: " << strHex(entry->getFieldVL(sfHookStateData)) << "\n";
BEAST_EXPECT(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 {0x00U,0xE4U,0x61U,0xEEU,0x78U,0x90U,0x83U,0x54U};
doL1Vote(alice, 'R', 'R', vote_data, original_data, false);
doL1Vote(bob, 'R', 'R', vote_data, original_data, false);
doL1Vote(carol, 'R', 'R', vote_data, original_data, false);
doL1Vote(david, 'R', 'R', vote_data, original_data, false);
doL1Vote(edward, 'R', 'R', vote_data, original_data, true);
// reverting a vote should not undo the action
doL1Vote(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(david, 'R', 'R', null_data, vote_data, false);
}
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'};
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};
// four of the 5 vote to remove alice
{
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};
std::vector<uint8_t> id;
id.reserve(20);
memcpy(id.data(), alice.id().data(), 20);
doL1Vote(bob, 'S', 0, null_acc_id, id, false);
doL1Vote(carol, 'S', 0, null_acc_id, id, false);
doL1Vote(david, 'S', 0, null_acc_id, id, false);
doL1Vote(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;
id.reserve(20);
memcpy(id.data(), david.id().data(), 20);
doL1Vote(bob, 'S', 3, null_acc_id, id, false);
doL1Vote(carol, 'S', 3, null_acc_id, id, false);
doL1Vote(edward, 'S', 3, null_acc_id, id, true);
}
// 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;
id.reserve(20);
memcpy(id.data(), carol.id().data(), 20);
doL1Vote(bob, 'S', 2, null_acc_id, id, false);
doL1Vote(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;
id.reserve(20);
memcpy(id.data(), edward.id().data(), 20);
doL1Vote(bob, 'S', 4, null_acc_id, id, true);
doL1Vote(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;
id.reserve(20);
memcpy(id.data(), bob.id().data(), 20);
doL1Vote(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);
}
}
// auto hooksArray =
// RH TODO:
// governance hook tests:
// last member tries to remove themselves
// try to add more than 20 members
// add a member normally
// remove a member normally
// add a member normally then re-action by adding another vote
// add a member normally then ensure it's not undone by removing one of the votes
// remove a member normally then re-action by adding another vote
// remove a member normally then ensure it's not undone when removing one of the votes
// action a hook change
// action a hook change to a non-existent hook
// action a reward rate change
// action a reward delay change
// L2 versions of all of the above
// reward hook tests:
// test claim reward before time
// test claim reward after time
// test claim reward when UNL report empty
// test claim reward when UNL report full
// genesis mint tests:
// test send from non-genesis account emitted txn
// test send from non-genesis account non-emitted txn
// test send from genesis account emitted txn
// test send from genesis account non-emitted txn
// test send to account that doesn't exist
// test send an overflow amount
// test set governance flags
// test no-destinations specified
// unl report test:
// test several validators all get on the list
// test several validators all vote different vl import keys
// test badly behaved validators dont get on the list
// test no validators on list
// test whole unl on list
// account counter
// test import created accounts get a sequence
// test payment created accounts get a sequence
// test genesis mint created accounts get a sequence
// test rpc
void
run() override
{
using namespace test::jtx;
//testPlainActivation();
//testWithSignerList();
//testWithRegularKey();
testGovernance();
}
};
BEAST_DEFINE_TESTSUITE(XahauGenesis, app, ripple);
} // namespace test
} // namespace ripple