//------------------------------------------------------------------------------ /* 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 #include #include #include #include #include #include #include #include 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(); 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> params = XahauGenesis::GovernanceParameters; for (auto const& [rn, x]: XahauGenesis::Distribution) { const char first = rn.c_str()[0]; BEAST_EXPECT( (first == 'r' && !!parseBase58(rn)) || first == 'n' && !!parseBase58(TokenType::NodePublic, rn)); if (first == 'r') { AccountID id = *parseBase58(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{'I', 'S', member_count++}, std::vector(id.data(), id.data() + 20)); continue; } // node based addresses auto const pk = parseBase58(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{'I', 'S', member_count++}, std::vector(id.data(), id.data() + 20)); } // initial member count params.emplace( std::vector{'I', 'M', 'C'}, std::vector{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> 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 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 topic2, std::vector data, std::optional 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 const& vote_data, std::vector 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 vote_data {0x00U,0x81U,0xC6U,0xA4U,0x7EU,0x8DU,0x43U,0x54U}; // this is the default reward rate std::vector 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 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 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 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 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 const expected_data {0x04U}; BEAST_REQUIRE(!!entry); BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data); } // continue to remove members (david) { std::vector 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 const expected_data {0x03U}; BEAST_REQUIRE(!!entry); BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data); } // continue to remove members (carol) { std::vector 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 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 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 const expected_data {0x02U}; BEAST_REQUIRE(!!entry); BEAST_EXPECT(entry->getFieldVL(sfHookStateData) == expected_data); } // try to remove bob using 1/2 votes { std::vector 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 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