From ab242660c489c2404b794a347968bcbc81595561 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 29 Apr 2021 05:47:02 -0500 Subject: [PATCH] New RPC. (#299) * New RPC docs. Specifies HTTP, digest auth, switches over to using objects as parameters, removes dated methods, removes complex and niche methods, corrects handling of BIP 32... * Port the Transactions/Consensus/Merit/Network RPC modules. Without the matching macro, this is mostly meaningless. Updates the docs accordingly. Notably removed is the ability to broadcast DataDiffs/SendDiffs. This is due to the fact they're ordered and sequential, and at the same time, the protocol doesn't alloow requesting specific Elems. This means broadcasting Elem 5 will forever be doomed if Elem 4 didn't propagate. * Have the Wallet mnemonic affect both the MinerWallet and Wallet. Simply a documentation edit. * Add watch wallet/offline signing RPC routes. Also adds a note on the behavior of network_connect. * Redo the RPC macro/supporting code. * Re-enable the Merit RPC. Previous commit disabled all but Transactions/Network (and technically system which doesn't actually exist as a module). This re-enables Merit, leaving Consensus/Personal. * Fix multiple modules/replying. Misplaced break/reply wasn't called/reply didn't provide the surrounding object. * Add requireAuth Previously commented; now not in order to ensure it's not forgotten. The GUI is always authed; the HTTP RPC's authed status is currently hardcoded to true. Moves system_quit to its own module in order to enable access to requireAuth, a simulated pragma. While an auth check could've been used where it was originally, removing hardcoded methods is beneficial. Correctly extends the RPC macro with requireAuth, and enables directly taking the request/reply function as arguments. Adds a missing method not found error. Would previously only trigger if the module was found and the method wasn't. Corrects the reply function to not supply the outer object, yet rather the macro generated reply call (as having it in the reply function itself broke errors). * Remove unnecessary imports. Also remove exports, adding missing imports in the relevant files. Also includes a set of lines met for the previous commit. * Don't expect work to be performed by signTransactionTemplate. As one of the fields is named `type`, a keyword, the RPC macro will now handle arguments defined with "_JSON" as fields without such a string. Adds needed length checks to the Claim/Send/Data parsers. * Clarify and clean RPC docs a bit. * Re-enable Consensus/Transactions. * Misc tweaks. newException -> newLoggedException. Majority are in relevant code. Some are from master. Removes unused import when compiling with nogui (threadpool). * Misc tweaks. newException -> newLoggedException. Majority are in relevant code. Some are from master. Removes unused import when compiling with nogui (threadpool). * Stub out missing RPC routes. Solely personal has work to do. Also removes signTransactionTemplate; an independent binary without the bloat of the node should do that. * Tweaks. Adds a getMeritHolderNick route to the personal module. Fixes a bug in parsing BLSPublicKeys from JSON (length checked against the hex length despite already parsing out of hex). * Update the Python tests to the new RPC. The two methods calling personal_data no longer do so, as personal_data isn't impolemented yet. Also cleans NodeThresholdTest a bit, correcting a name differing from its file. The one test using personal_send (AddressTest) still fails due to the unimplemented nature of the RPC route. * Move the Address test off personal_send. Now all Python RPC tests should pass. * Stub out Python test files. Some functions share tests, due to scope (consensus_getSendDifficulty + consensus_getDataDifficulty). Such test files have the list of routes they're intended to test written out. Every test has an empty string at the top in order to trigger Pylint. Ensures that they won't be missed. * Stub tests expecting specific HTTP errors. * Update the Mint RPC definition Corrects the definition as the existing one was invalid, and updates it The Python JSON impl is also updated, yet that was never used in the vectors, hence the lack of regen * Stub test for invalid non-Nim-native RPC types * Remove argon from getTransaction. In response to https://github.com/MerosCrypto/Meros/issues/289. * Add a test for GetTransaction. Correct Python's JSON serialization in relation to the RPC edits made for https://github.com/MerosCrypto/Meros/issues/289, which already wasn't used when loading the JSON back into Python. * Stub a test for integer bounds. * Test publishTransaction. Fixes a bug in publishTransactionWithoutWork. * Remove stray print. * Test getSendDifficulty/getDataDifficulty. * Test getBlockTemplate. Closes https://github.com/MerosCrypto/Meros/issues/278. Fixes a bug where old templates were always dropped, even if they were for the current block (part of #278). Fixes a crash where the RPC took in a header of indescriminate length, which parseBlockHeader didn't check. * Augment getBlockTemplate with a MeritRemoval inclusion test. * Augment publishTransaction's test with broadcast behavior checks. * Change merit_getBlock's id field to block. * Tweaks to Merit RPC docs. No functional changes. * Test getBlock. Adds the packets field to the JSON, as Python needs it. While it was possible to remove it from Python, which was considered as this field has been previously excluded for being of little value, it is part of the header. If we ever add a getBlockHeader function, its value greatly increases. Fixes an oversight on the range checks in retrieveFromJSON. * Remove a test's expectation packets wasn't in Meros's JSON for a Block. Missed byproduct of the last commit. * Misc cleanup. * Rename RPC HTTP tests. Adds chunked encoding test. * Rough HTTP code. Closer to scribbles than code. Does a lot of HTTP work; more than's needed. The existing HTTP error code tests should now be all that are used * Working HTTP draft. Works against curl. Has no where near the functionality it should. Further stubs tests. * Update Python to use HTTP. * Update existing tests for https://github.com/MerosCrypto/Meros/issues/278. * Correct default HTTP socket close behavior. It's not keep-alive by default. Also removes the easily worked around 100 requests per second rate limit. * Support authorization. Updates the RPC and config object. Does not update Python except the system_quit call. Also doesn't bother to spawn the RPC when it and the GUI are disabled. * Correct Connection header parsing. I assumed it'd just be keep-alive. It's actually a comma separated list which is generally just keep-alive. Moves to a default policy of close. While that was the case for 1.0, 1.1 defaults to keep-alive, when Meros expected close. While keep-alive will be supported, it's more efficient from a FD perspective to regain that slot ASAP. Also utilizes a pair of logWarn statements for exceptions when shutting down. * Clean the HTTP code and re-enable headers * Have Python auth against the RPC by default * Add missing auth token argument to 232's custom process spawn. * Test auth requirement in every standing RPC test. Also provides an improvement to the GetBlockTemplate test. * Basic batch request support. Still needs further testing. Prelim req to chunked encoding, AKA the reason I'm moving on for now. * Test chunked encoding. Tests it using batched reqs, which isn't actually a requirement (no idea why I thought it would be), yet this the only usage that makes sense AFAIK. Some chunks are not valid JSON on their own, so the test stands as valid. * Support chunked encoding. Meant for the last commit. * Fix 100-continue support. Also handles the body no matter the content type (as curl defaults to x-www-form-urlencoded). Increases documentation. * Remove expectation of spaces in header definitions. * Multiple bug fixes + cleanup. Fixes Authorization, which broke in the previous commit. Returns HTTP 417 instead of 400 if the message body is too long. Fixes support for HTTP 100. Fixes readLine, as the version shipped with chronos only support \r\n. THe new version supports all three. Corrects RPC recv to not raise. Tests HTTP 100 support. * Test and fix support for all newline types. * Drop HTTP pipelining. * Drop GET and HEAD. Technically breaks HTTP compliance, yet they're not needed and should never come up. Not worth having. * Stub support for Range headers. Just acknowledges their existence and relevance. Our response would just be a 416. * Remove aspects/tests no longer required to be done before merging. See https://github.com/MerosCrypto/Meros/issues/292. * Test authorization. * Fix https://github.com/MerosCrypto/Meros/issues/162. Start of the new personal RPC. Provides a non-compliant personal_data which can be used as the starting point. * Add a pair of lines meant for the last commit. * Remove multi-Account functionality from the spec. See https://github.com/MerosCrypto/Meros/issues/292. * Work on testing seed management. * Update the Multiline Expansion Checker. False handling of a variable named defData. * Add warnings about the BIP 32 implementation. * Implement new seed management. Fixes https://github.com/MerosCrypto/Meros/issues/293. Tests seed handling/HD derivation/several RPC routes, yet the Seed Test isn't complete; it still needs to test addresses (included as they're derived from the seed; due to their scope, it may best to use its own test, as originally planned). Also lints the public domain ed25519 implementation added. * Implement HD public key derivation. * Implement getAddress for specific address. Also updates personal_data to handle hex data and the arguments of getTransactionTemplate. * Theoretically implement getAddress. Further extends SeedTest. * Extend the seed test with getAddress (without arguments; unused) checks. Uses the ed25519 RFC's signature code, ported to the existing ref impl's internal functions and cleaned up a bit (done due to an unknown issue with the original). Slightly edits the private key management code of BLS.py in how it converts a 32-byte to a 48-byte value. Randomly complained it couldn't rjust with a byte char, despite working in the past and in my local python console. Deletes the independent getAddress test for being irrelevant. * Rename setMnemonic to setWallet. It requires the password as well. Originally named setMnemonic as the password isn't saved, so it only sets the Mnemonic in the DB, as well as for parity with getMnemonic. * Misc improvements to the SeedTest. * Correct key handling for Datas. Adds a note to SeedTest to test it. Also fixes an argument name in the personal_data RPC route. * Rename the setPublicKey impl. Still not functional. Just a housekeeping task. * Finish SeedTest's actual seed test sections. Doesn't test the relevant RPC routes have the proper access lock, nor other invalid cases. Does finish testing all expected behavior for seed/address management. * Test invalid RPC calls related to seed management Includes a Nim bug fix meant for the last commit Also includes a new requirement all mnemonics must have 24 words (256 bits of entropy). Previously, they were generated as such, yet you could set one of any size. * Split up and clean the Seed test It's now Derivation and GetAddress test, with the latter also handling getMeritHolderNick and consistency when rebooting/loading the wallet. * Finish batch testing. Fixes bugs in how they were handled, as well as discovers a DoS vector where non-object requests caused a panic when used as the objects they're expected to be. They should be fixed now, yet a new test has been added to test them exclusively. Also removes a dated error case claiming we don't support batch requests. Also fixes a deprecation warning in the RPC folder, yet not other instances through out the codebase. * Test the Data RPC route. Also includes a lint fix for the batch test. * Test invalid request objects. * Misc lint. Also adds back the finalized property to getStatus. * Variety of getUTXOs test. 3 are complete, 1 is in progress, 2 are solely described. Fixes getStatus's re-added finalized field. * Finish the getUTXOs reorg test. * getUTXOs immediately spent test. Also fixes minor type errors in the reorg test. * Test broadcast. * Clean the GetUTXOs tests. Also includes a pair of minor tweaks to the Broadcast test. * Address discovery when reloading seeds (prototype). * Implement personal_getUTXOs. Test it and address recovery. * Delete the JSONRPC test. The error cases are already tested by the InvalidRequest test. The success cases are implicitly tested everywhere, and RPC.py has been expanded to properly check the jsonrpc/ID field. * Integer bound test. Corrects when the RPC should check the response's ID. Fixes a bug in boundary checking where a number outside of the uint16 range was accepted as a uint16. * getAccountKey -> getAccount. Also returns the chain code which is needed for further derivation. * Correct BIP 44 derivation. We swapped the internal/external account chains. 0, AKA change = false, is external. 1, AKA change = true, is internal. Also adds a getter for change address public keys to the Personal tests' Lib file + shifts function boundaries a bit. * WatchWallet support. * Fix creating Claims for Mints to the node's Wallet. Previously used the older API to get what key to mint to. Development had occurred by commenting out this block. Now uses the new API. Always mints to the first valid address on the wallet in order to not use further keys, as there's no added anonimity benefits given they can be linked to a single Merit Holder key. * Update usage of MuSig. Now formalized, using all keys (not just unique keys) for closer spec accuracy, a H of Blake2b-512, and a DST for Hagg. Adds a Python implementation. Cleans the code which declares Blake2b instances. * Change account chain management. * Misc fixes to WatchWallet functionality. * Tweaks. Has HTTP validate the range of the content length. Removes unused exports from Util. Has removeFromSpendable remove all occurrences, not just the first; stops edge cases where the same UTXO is reported as spendable twice. Also has a trade off on performance (more expensive generally, significantly less expensive rarely). Updates one test to have an empty string statement so pylint flags it. Updates another with a note on something it has to do. * Implement private key aggregation to match a MuSig public key. Doesn't implement MuSig, which is secure for MPC, yet rather a naive version that works given full knowledge of all private keys. * Use KeyIndex; a struct for the change bool and index uint32. Also documents the scalar -> extended private key code when aggregating private keys. * Generator for the WatchWallet test. Extremely subject to change. * Have EdPrivateKey aggregate not aggregate when there's a single unique key. * getTransactionTemplate prototype. * Rewrite personal_send to wrap personal_getTransactionTemplate. p_gTT does all the required logic/formatting for UTXO selection and output generation. There's no reason to duplicate it. This does parse the JSON output though, so moving the core logic to its own function, making p_gTT just a `%*` call, and making personal_send not call another RPC function, would probably be optimal. personal_send previously wouldn't compile/was disabled, so this is a major step forward. Tests for both p_gTT and personal_send coming soon. * Ed25519 aggregation fixes. Cleans up the unique key check; fixes a bug where the first key was included twice when adding a Send. * Test personal_send. * Test every personal method requires auth. Previously, some methods weren't checked as they weren't implemented. Not that all are... Requires moving the auth check to earlier in the function, which is a good thing in a couple of ways. Doesn't waste time arg parsing if not authed/less chance for something which changes data to happen. * Fix change address recovery. The wrong account chain was used. I have no idea how this was undetected for so long. The Personal Send test should've caught this. * Shift error codes so none use 0. NotEnoughMeros, a status which therefore used a positive error code (or rather, a non-negative error code), used 0. As 0 generally means success, it has been moved to 1. Shifts Spam to 2. * Add change specification to getTransactionTemplate. Functionality was previously missing. Also adds errors for no outputs/0 value outputs. * WatchWallet test. * Test transactions_getBalance. Just hooks into the transactions_getUTXOs test. * Test parsing of string based RPC types. Also removes the GetPeersTest to be done later. * Test fixes. They still don't pass under the most recent changes, yet they now compile and have some runtime fixes. * Support hex strings prefixed with "0x" in the RPC. * Don't remove spent UTXOs from spendable. Slower, yet enables getting the full history of a key's interactions. Will be useful in the planned personal_getTransactionHistory mentioned in https://github.com/MerosCrypto/Meros/issues/292. Updates the broken spendable test to the latest policies. It now passes. * Fix Data detection when loading an account. Apparently, the fix for #162 broke at some point; the WalletDB used the first address under account 0, yet it checked the Datas for account 0 proper. * Fix the TGU generator. Verified the old Claim and the new Claim in the same Block; didn't generate enough Blocks for a reorg. Remove a dated requirement to add back to spendable (since we now keep in spendable). Expands the spendable test a bit. * Tweak the WalletDB test. Doesn't expand it to match the revamp done as part of this branch; does a minimal addition of equality checks and adds a note it should be expanded. https://github.com/MerosCrypto/Meros/issues/292 takes that burden. * Case insensitive header support. The HTTP spec doesn't require capitalization to match, and some libraries use all lower case forms. * Fix the return value for merit_publishBlock. * Also test getMeritHolderKey's WatchWallet behavior. * Misc tweaks in preparation for merging. * Delete the samples folder. The RPC Sample should be, and with this commit will be, deprecated for curl at this point. The DB dump sample is irrelevant when we can just share a data folder and go from there. Sure, it's not in a JSON format, but it's easy enough to spin up a node and run the relevant commands as needed. * Remove stray file. * Replace usage of hardcoded lengths with properly generated lengths. I have no idea why these made it in the first place. * Correct oversight in the Spendable test. It assumed an output's destination had outputs considered spendable despite not verifying all Transaction. Adds another safety check that may or may not be needed to ensure a lack of such crashes. * Restore the samples folder. * Correct the help stament for the RPC sample. We abstracted field names into the historical array structure via providing an ordering. --- Meros.nimble | 3 +- docs/Protocol/Transactions.md | 2 +- docs/RPC/Consensus.md | 35 ++ docs/RPC/Current/Consensus.md | 22 - docs/RPC/Current/Errors.md | 12 - docs/RPC/Current/Merit.md | 86 --- docs/RPC/Current/Network.md | 15 - docs/RPC/Current/Personal.md | 40 -- docs/RPC/Current/System.md | 5 - docs/RPC/Current/Transactions.md | 63 -- docs/RPC/Current/Usage.md | 12 - docs/RPC/Errors.md | 8 + docs/RPC/Eventual/Consensus.md | 69 --- docs/RPC/Eventual/Errors.md | 12 - docs/RPC/Eventual/Network.md | 23 - docs/RPC/Eventual/Personal.md | 119 ---- docs/RPC/Eventual/System.md | 5 - docs/RPC/Eventual/Transactions.md | 77 --- docs/RPC/Eventual/Usage.md | 12 - docs/RPC/{Eventual => }/Merit.md | 62 +- docs/RPC/Network.md | 28 + docs/RPC/Personal.md | 107 ++++ docs/RPC/System.md | 5 + docs/RPC/Transactions.md | 76 +++ docs/RPC/Usage.md | 14 + e2e/Classes/Transactions/Data.py | 3 +- e2e/Classes/Transactions/Mint.py | 4 +- e2e/Classes/Transactions/Send.py | 3 +- e2e/Libs/BIP32.py | 74 +++ e2e/Libs/BLS.py | 4 +- e2e/Libs/ed25519.py | 178 ++++++ e2e/Meros/Meros.py | 33 +- e2e/Meros/RPC.py | 75 +-- e2e/Pylint/MultilineExpansionChecker.py | 2 +- e2e/Stubs/bip_utils/__init__.pyi | 39 ++ e2e/Stubs/requests/__init__.pyi | 17 + e2e/Tests/Consensus/BeatenTest.py | 12 +- .../LockedMeritDifficultiesTest.py | 6 +- .../DescendantHighestUnverifiedParentTest.py | 4 +- .../DescendantHighestVerifiedParentTest.py | 4 +- .../Families/ImpossibleFamilyTest.py | 4 +- .../Families/LowerHashTieBreakTest.py | 2 +- .../UnionedFamiliesMultipleWinnersTest.py | 4 +- .../UnionedFamiliesSingleWinnerTest.py | 4 +- .../Families/UnmentionedBeatMentionedTest.py | 2 +- .../HundredSixSignedElementsTest.py | 23 +- e2e/Tests/Consensus/NodeThresholdTest.py | 32 +- .../Consensus/Verification/CompetingTest.py | 4 +- .../Verification/HundredFiftyFiveTest.py | 15 +- .../Verification/HundredFortyTwoTest.py | 4 +- .../Consensus/Verification/HundredTwoTest.py | 7 +- .../Verification/PartialArchiveTest.py | 14 +- .../Verification/UnknownSignedTest.py | 2 +- e2e/Tests/Consensus/Verify.py | 2 +- e2e/Tests/Merit/HundredSeventySevenTest.py | 12 +- .../Merit/LockedMerit/KeepUnlockedTest.py | 2 +- .../Merit/LockedMerit/LocksUnlocksTest.py | 8 +- .../Merit/LockedMerit/PendingDieRegainTest.py | 2 +- .../LockedMerit/TwoHundredThirtyFiveTest.py | 15 +- .../Reorganizations/TwoHundredSixtyOneTest.py | 13 +- .../TwoHundredThirtyTwoTest.py | 8 +- e2e/Tests/Merit/StateTest.py | 2 +- e2e/Tests/Merit/Templates/EightyEightTest.py | 16 +- e2e/Tests/Merit/Templates/TElementTest.py | 24 +- e2e/Tests/Merit/TwoHundredFortyTest.py | 19 +- e2e/Tests/Merit/Verify.py | 7 +- e2e/Tests/RPC/AddressTest.py | 51 +- e2e/Tests/RPC/BatchTest.py | 133 ++++ e2e/Tests/RPC/Consensus/GetDifficultyTest.py | 73 +++ e2e/Tests/RPC/HTTP/ChunkedEncodingTest.py | 48 ++ e2e/Tests/RPC/HTTP/HTTP100Test.py | 65 ++ e2e/Tests/RPC/HTTP/HTTP401Test.py | 28 + e2e/Tests/RPC/HTTP/NewLineTest.py | 42 ++ e2e/Tests/RPC/IntegerBoundTest.py | 42 ++ e2e/Tests/RPC/InvalidRequestTest.py | 40 ++ e2e/Tests/RPC/Merit/GetBlockTemplateTest.py | 307 +++++++++ e2e/Tests/RPC/Merit/GetBlockTest.py | 184 ++++++ e2e/Tests/RPC/Network/BroadcastTest.py | 86 +++ e2e/Tests/RPC/Personal/AddressRecoveryTest.py | 172 +++++ e2e/Tests/RPC/Personal/DerivationTest.py | 211 +++++++ e2e/Tests/RPC/Personal/GetAddressTest.py | 244 ++++++++ e2e/Tests/RPC/Personal/HundredSixtyTwoTest.py | 38 ++ e2e/Tests/RPC/Personal/Lib.py | 124 ++++ .../RPC/Personal/PersonalAuthorizationTest.py | 28 + e2e/Tests/RPC/Personal/PersonalDataTest.py | 105 ++++ e2e/Tests/RPC/Personal/PersonalSendTest.py | 321 ++++++++++ e2e/Tests/RPC/Personal/WatchWalletTest.py | 379 ++++++++++++ e2e/Tests/RPC/StringBasedTypesTest.py | 84 +++ .../RPC/Transactions/GetTransactionTest.py | 166 +++++ e2e/Tests/RPC/Transactions/GetUTXOs/Lib.py | 52 ++ .../RPC/Transactions/GetUTXOs/TGUBasicTest.py | 84 +++ .../Transactions/GetUTXOs/TGUFinalizesTest.py | 68 ++ .../GetUTXOs/TGUImmediatelySpentTest.py | 78 +++ .../RPC/Transactions/GetUTXOs/TGUReorgTest.py | 133 ++++ .../Transactions/GetUTXOs/TGUUnverifyTest.py | 86 +++ .../Transactions/PublishTransactionTest.py | 249 ++++++++ .../Transactions/Prune/PruneUnaddableTest.py | 8 +- e2e/Tests/Transactions/Verify.py | 2 +- e2e/Vectors/Generation/RPC/Merit/GetBlock.py | 82 +++ .../Generation/RPC/Personal/WatchWallet.py | 38 ++ .../Generation/RPC/Transactions/GetUTXOs.py | 119 ++++ e2e/Vectors/RPC/Merit/GetBlock.json | 1 + e2e/Vectors/RPC/Personal/WatchWallet.json | 1 + e2e/Vectors/RPC/Transactions/GetUTXOs.json | 1 + e2e/requirements.txt | 2 + samples/DBDumpSample.nim | 42 +- samples/RPCSample.nim | 129 ++-- samples/config.nims | 3 - .../Consensus/objects/ConsensusObj.nim | 2 +- src/Database/Filesystem/DB/TransactionsDB.nim | 102 ++- src/Database/Filesystem/Wallet/WalletDB.nim | 585 ++++++++++++++++-- src/Database/Merit/State.nim | 26 +- src/Database/Transactions/Transactions.nim | 5 +- .../Transactions/objects/TransactionsObj.nim | 17 +- src/Interfaces/RPC/HTTP.nim | 391 ++++++++++++ .../RPC/Modules/ConsensusModule.nim | 70 +-- src/Interfaces/RPC/Modules/MeritModule.nim | 344 +++++----- src/Interfaces/RPC/Modules/NetworkModule.nim | 106 ++-- src/Interfaces/RPC/Modules/PersonalModule.nim | 383 +++++++++--- src/Interfaces/RPC/Modules/SystemModule.nim | 32 + .../RPC/Modules/TransactionsModule.nim | 196 +++--- src/Interfaces/RPC/RPC.nim | 567 ++++++++--------- src/Interfaces/RPC/objects/RPCObj.nim | 398 +++++++++--- src/MainConsensus.nim | 25 +- src/MainImports.nim | 8 +- src/MainInterfaces.nim | 25 +- src/MainMerit.nim | 37 +- src/MainPersonal.nim | 221 ++++--- src/MainReorganization.nim | 2 +- src/MainTransactions.nim | 60 +- src/Meros.nim | 2 +- .../Serialize/Merit/ParseBlockBody.nim | 3 + .../Serialize/Merit/ParseBlockHeader.nim | 11 + .../Serialize/Transactions/ParseClaim.nim | 7 + .../Serialize/Transactions/ParseData.nim | 9 +- .../Serialize/Transactions/ParseSend.nim | 10 + src/Network/objects/SocketObj.nim | 39 ++ src/Wallet/Address.nim | 25 +- src/Wallet/Ed25519.nim | 84 ++- src/Wallet/HDWallet.nim | 165 +++-- src/Wallet/MinerWallet.nim | 2 +- src/Wallet/Mnemonic.nim | 18 +- src/Wallet/Wallet.nim | 77 +-- src/lib/Errors.nim | 19 +- src/lib/Hash.nim | 11 +- src/lib/Hash/Blake2.nim | 38 +- src/lib/Util.nim | 2 +- src/lib/objects/ErrorObjs.nim | 7 +- src/objects/ConfigObj.nim | 38 +- src/objects/GlobalFunctionBoxObj.nim | 66 +- .../Consensus/ConsensusRevertTest.nim | 4 +- .../Transactions/SerializeSendOutputTest.nim | 2 +- .../DB/TransactionsDB/SpendableTest.nim | 57 +- .../Filesystem/Wallet/WalletDBTest.nim | 17 +- .../Transactions/TransactionsTest.nim | 19 +- .../Merit/SerializeBlockHeaderTest.nim | 5 +- .../Transactions/SerializeClaimTest.nim | 4 +- .../Transactions/SerializeDataTest.nim | 4 +- .../Transactions/SerializeSendTest.nim | 10 +- tests/Wallet/AddressTest.nim | 2 +- tests/Wallet/Ed25519Test.nim | 34 +- tests/Wallet/HDWalletTest.nim | 20 +- tests/Wallet/WalletTest.nim | 4 +- tests/lib/UtilTest.nim | 1 + 164 files changed, 7964 insertions(+), 2220 deletions(-) create mode 100644 docs/RPC/Consensus.md delete mode 100644 docs/RPC/Current/Consensus.md delete mode 100644 docs/RPC/Current/Errors.md delete mode 100644 docs/RPC/Current/Merit.md delete mode 100644 docs/RPC/Current/Network.md delete mode 100644 docs/RPC/Current/Personal.md delete mode 100644 docs/RPC/Current/System.md delete mode 100644 docs/RPC/Current/Transactions.md delete mode 100644 docs/RPC/Current/Usage.md create mode 100644 docs/RPC/Errors.md delete mode 100644 docs/RPC/Eventual/Consensus.md delete mode 100644 docs/RPC/Eventual/Errors.md delete mode 100644 docs/RPC/Eventual/Network.md delete mode 100644 docs/RPC/Eventual/Personal.md delete mode 100644 docs/RPC/Eventual/System.md delete mode 100644 docs/RPC/Eventual/Transactions.md delete mode 100644 docs/RPC/Eventual/Usage.md rename docs/RPC/{Eventual => }/Merit.md (56%) create mode 100644 docs/RPC/Network.md create mode 100644 docs/RPC/Personal.md create mode 100644 docs/RPC/System.md create mode 100644 docs/RPC/Transactions.md create mode 100644 docs/RPC/Usage.md create mode 100644 e2e/Libs/BIP32.py create mode 100644 e2e/Libs/ed25519.py create mode 100644 e2e/Stubs/bip_utils/__init__.pyi create mode 100644 e2e/Stubs/requests/__init__.pyi create mode 100644 e2e/Tests/RPC/BatchTest.py create mode 100644 e2e/Tests/RPC/Consensus/GetDifficultyTest.py create mode 100644 e2e/Tests/RPC/HTTP/ChunkedEncodingTest.py create mode 100644 e2e/Tests/RPC/HTTP/HTTP100Test.py create mode 100644 e2e/Tests/RPC/HTTP/HTTP401Test.py create mode 100644 e2e/Tests/RPC/HTTP/NewLineTest.py create mode 100644 e2e/Tests/RPC/IntegerBoundTest.py create mode 100644 e2e/Tests/RPC/InvalidRequestTest.py create mode 100644 e2e/Tests/RPC/Merit/GetBlockTemplateTest.py create mode 100644 e2e/Tests/RPC/Merit/GetBlockTest.py create mode 100644 e2e/Tests/RPC/Network/BroadcastTest.py create mode 100644 e2e/Tests/RPC/Personal/AddressRecoveryTest.py create mode 100644 e2e/Tests/RPC/Personal/DerivationTest.py create mode 100644 e2e/Tests/RPC/Personal/GetAddressTest.py create mode 100644 e2e/Tests/RPC/Personal/HundredSixtyTwoTest.py create mode 100644 e2e/Tests/RPC/Personal/Lib.py create mode 100644 e2e/Tests/RPC/Personal/PersonalAuthorizationTest.py create mode 100644 e2e/Tests/RPC/Personal/PersonalDataTest.py create mode 100644 e2e/Tests/RPC/Personal/PersonalSendTest.py create mode 100644 e2e/Tests/RPC/Personal/WatchWalletTest.py create mode 100644 e2e/Tests/RPC/StringBasedTypesTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetTransactionTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/Lib.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/TGUBasicTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/TGUFinalizesTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/TGUImmediatelySpentTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/TGUReorgTest.py create mode 100644 e2e/Tests/RPC/Transactions/GetUTXOs/TGUUnverifyTest.py create mode 100644 e2e/Tests/RPC/Transactions/PublishTransactionTest.py create mode 100644 e2e/Vectors/Generation/RPC/Merit/GetBlock.py create mode 100644 e2e/Vectors/Generation/RPC/Personal/WatchWallet.py create mode 100644 e2e/Vectors/Generation/RPC/Transactions/GetUTXOs.py create mode 100644 e2e/Vectors/RPC/Merit/GetBlock.json create mode 100644 e2e/Vectors/RPC/Personal/WatchWallet.json create mode 100644 e2e/Vectors/RPC/Transactions/GetUTXOs.json mode change 100755 => 100644 samples/DBDumpSample.nim mode change 100755 => 100644 samples/RPCSample.nim mode change 100755 => 100644 samples/config.nims create mode 100644 src/Interfaces/RPC/HTTP.nim create mode 100644 src/Interfaces/RPC/Modules/SystemModule.nim diff --git a/Meros.nimble b/Meros.nimble index c3bf8d100..369393bef 100644 --- a/Meros.nimble +++ b/Meros.nimble @@ -17,11 +17,10 @@ requires "nim == 1.2.6" requires "https://github.com/MerosCrypto/Argon2 >= 1.1.2" requires "https://github.com/MerosCrypto/mc_randomx >= 0.9.4" requires "https://github.com/MerosCrypto/mc_bls >= 3.0.0" -requires "https://github.com/MerosCrypto/mc_ed25519 >= 1.0.1" +requires "https://github.com/MerosCrypto/mc_ed25519 >= 1.1.1" requires "https://github.com/MerosCrypto/mc_minisketch >= 0.8.5" requires "https://github.com/MerosCrypto/mc_lmdb >= 2.0.0" requires "https://github.com/MerosCrypto/mc_webview >= 0.1.1" -requires "https://github.com/MerosCrypto/Nim-MerosRPC >= 2.1.4" requires "https://github.com/kayabaNerve/ForceCheck >= 1.3.2" requires "nimcrypto >= 0.4.11" requires "normalize >= 0.7.1" diff --git a/docs/Protocol/Transactions.md b/docs/Protocol/Transactions.md index 6884d8cd5..ba1265e17 100644 --- a/docs/Protocol/Transactions.md +++ b/docs/Protocol/Transactions.md @@ -47,7 +47,7 @@ Send Transactions have the following additional field: - signature: Ed25519 Signature. - proof: Work that proves this isn't spam. -Every Send must have at least 1 input. Every Send input must be either a Claim or a Send, where the specified output is to the sender. If the specified outputs are to different keys, the sender is the MuSig Public Key created out of the unique keys. +Every Send must have at least 1 input. Every Send input must be either a Claim or a Send, where the specified output is to the sender. If the outputs used as inputs are to different keys, the sender is the MuSig Public Key created from them, where `H` is Blake2b-512, `L` is `H(keys)` instead of `keys`, and `Hagg` is `H` with a prefixed domain separation tag of "agg". Every output's key must be an Ed25519 Public Key. The specified key does not need to be a valid Ed25519 Public Key. The output's amount must be non-zero. diff --git a/docs/RPC/Consensus.md b/docs/RPC/Consensus.md new file mode 100644 index 000000000..057d22089 --- /dev/null +++ b/docs/RPC/Consensus.md @@ -0,0 +1,35 @@ +# Consensus Module + +### `getSendDifficulty` + +`getSendDifficulty` replies with the Send Difficulty. + +Arguments: +- `holder` (int; optional) + +The result is an int of the current difficulty if the Merit Holder isn't specified. If one is, the result is an int of what the specified Merit Holder voted. + +### `getDataDifficulty` + +`getDataDifficulty` replies with the Data Difficulty. + +Arguments: +- `holder` (int; optional) + +The result is an int of the current difficulty if the Merit Holder isn't specified. If one is, the result is an int of what the specified Merit Holder voted. + +### `getStatus` + +`getStatus` replies with the Status for the specified Transaction. + +Arguments: +- `hash` (string) + +The result is an object, as follows: +- `verifiers` (array of strings): The list of verifiers for this Transaction. +- `merit` (int): Sum of the Merit of the verifiers. Doesn't include any verifiers who have a pending Merit Removal. +- `threshold` (int): Merit needed to become verified. +- `verified` (bool): Whether or not the Transaction is verified. +- `finalized` (bool): Whether or not the Transaction has been finalized. +- `competing` (bool): Whether or not the Transaction has competitors. If it does, and isn't already verified, it can only be verified at the end of its Epoch. +- `beaten` (bool): Whether or not the Transaction was finalized with less Merit than a competitor. diff --git a/docs/RPC/Current/Consensus.md b/docs/RPC/Current/Consensus.md deleted file mode 100644 index 9b62dc339..000000000 --- a/docs/RPC/Current/Consensus.md +++ /dev/null @@ -1,22 +0,0 @@ -# Consensus Module - -### `getSendDifficulty` - -`getSendDifficulty` replies with a Send Difficulty. It takes in zero arguments and the result is an int of the current difficulty. - -### `getDataDifficulty` - -`getDataDifficulty` replies with a Data Difficulty. It takes in zero arguments and the result is an int of the current difficulty. - -### `getStatus` - -`getStatus` replies with the Status for the specified Transaction. It takes in one argument: -- hash (string) - -The result is an object, as follows: -- `verifiers` (array of strings): The list of verifiers for this Transaction. -- `merit` (int): Merit of all the Merit Holders who verified this Transaction. -- `threshold` (int): Merit needed to become verified. -- `verified` (bool): Whether or not the Transaction is verified. -- `competing` (bool): Whether or not the Transaction has competitors. If it does, and isn't already verified, it can only be verified at the end of its Epoch. -- `beaten` (bool): Whether or not the Transaction was finalized, with a different, competing, Transaction having more Merit. diff --git a/docs/RPC/Current/Errors.md b/docs/RPC/Current/Errors.md deleted file mode 100644 index 73715c88f..000000000 --- a/docs/RPC/Current/Errors.md +++ /dev/null @@ -1,12 +0,0 @@ -# Errors - -- NotEnoughMeros: 1 -- DataExists: 0 - -- GapError: -1 -- IndexError: -2 -- ValueError: -3 -- BLSError: -4 -- AddressError: -5 -- ClientError: -6 -- Spam: -7 diff --git a/docs/RPC/Current/Merit.md b/docs/RPC/Current/Merit.md deleted file mode 100644 index 553ba3fa9..000000000 --- a/docs/RPC/Current/Merit.md +++ /dev/null @@ -1,86 +0,0 @@ -# Merit Module - -### `getHeight` - -`getHeight` replies with the Blockchain's height. It takes in zero arguments and the result is an int of the height. - -### `getDifficulty` - -`getDifficulty` replies with the current difficulty. It takes in zero arguments and the result is an int of the difficulty. This should NOT be used by miners, as the difficulty is multiplied for new miners. `getBlockTemplate` is guaranteed to return the difficulty that a Block would be checked against. - -### `getBlock` - -`getBlock` replies with a Block. It takes in one argument. -- ID (int/string): Either the nonce as an int or hash as a string. - -The result is an object, as follows: -- `hash` (string) -- `header` (object) - - `version` (int) - - `last` (string) - - `contents` (string) - - `sketchSalt` (string) - - `sketchCheck` (string) - - `miner` (int/string): Either the miner's nick as an int or the key as a string if this is their first Block. - - `time` (int) - - `proof` (int) - - `signature` (string) - -- `transactions` (array of objects, each as follows) - - `hash` (string) - - `holders` (array of ints) - -- `elements` (array of objects, each as follows) - - `descendant` (string) - - `holder` (int) - - When `descendant` == "SendDifficulty": - - `nonce` (int) - - `difficulty` (int) - - When `descendant` == "DataDifficulty": - - `nonce` (int) - - `difficulty` (int) - -- `removals` (array of ints): Whoever got their Merit removed by this Block. - -- `aggregate` (string) - -### `getTotalMerit` - -`getTotalMerit` replies with the total amount of Merit in existence. It takes in zero arguments and the result is an int of the total amount of Merit. - -### `getUnlockedMerit` - -`getUnlockedMerit` replies with the amount of Unlocked Merit in existence. It takes in zero arguments and the result is an int of the amount of Unlocked Merit. - -### `getMerit` - -`getMerit` replies with a Merit Holder's Merit. It takes in one argument. -- Merit Holder Nickname (int) - -The result is an object, as follows: -- `status` (string): "Unlocked", "Locked", or "Pending". -- `malicious` (bool): Whether or not this holder has a Merit Removal against them pending. -- `merit` (int) - -### `getBlockTemplate` - -`getBlockTemplate` replies with a template for mining a Block. It takes in one argument. -- miner (string): BLS Public Key of the Miner. - -The result is an object, as follows: -- `id` (int): The template ID. -- `key` (string): The RandomX cache key. -- `header` (string) -- `difficulty` (int) - -Mining the Block occurs by hashing the header with a 4-byte proof appended. After the initial hash, the hash is signed by the miner, and the hash is hashed with the signature appended. If it beats the difficulty, it can be published by appending the 4-byte proof to the header, then appending the signature to the header, and then calling `merit_publishBlock` with the ID (see below). - -### `publishBlock` - -`publishBlock` adds the Block to the local Blockchain, and if it's valid, publishes it. It takes in two arguments. -- ID (int): The ID of a template. Only an ID from the last 5 templates is valid. -- Block (string): A serialized BlockHeader. - -The result is a bool of true. diff --git a/docs/RPC/Current/Network.md b/docs/RPC/Current/Network.md deleted file mode 100644 index 5bda54435..000000000 --- a/docs/RPC/Current/Network.md +++ /dev/null @@ -1,15 +0,0 @@ -# Network Module - -### `connect` - -`connect` attempts to connect to another Meros node. It takes in two arguments: -- IP/Domain (string) -- Port (int): Optional; defaults to 5132 if omitted. - -The result is a bool of true. - -### `getPeers` -`getPeers` replies with a list of every node we're connected it. It takes in zero arguments and replies with an array of objects, each as follows: -- `ip` (string) -- `server` (bool) -- `port` (int): Only present the peer has a Server Socket. diff --git a/docs/RPC/Current/Personal.md b/docs/RPC/Current/Personal.md deleted file mode 100644 index 6f60cd335..000000000 --- a/docs/RPC/Current/Personal.md +++ /dev/null @@ -1,40 +0,0 @@ -# Personal Module - -### `getMiner` - -`getMiner` replies with the BLS Private Key of the current Miner Wallet. It takes in zero arguments and the result is a string of the private key. - -### `setMnemonic` - -`setMnemonic` creates a new Wallet using the passed in Mnemonic and password and sets the Node's Wallet to it. It takes in two arguments: -- Mnemonic (string): Optional; creates a new Mnemonic if omitted. -- Password (string): Optional; defaults to "" if omitted, as according to the BIP 39 spec. - -The result is a bool of true. - -### `getMnemonic` - -`getMnemonic` replies with the Node's Wallet's Mnemonic, without the password. It takes in zero arguments and the result is a string of the mnemonic. - -### `getAddress` - -`getAddress` replies with an address. It takes in two arguments: -- Account (int): Optional; defaults to 0; used in hardened derivation. -- Change (bool): Optional; defaults to false. - -The result is a string of the Wallet's address. - -### `send` - -`send` creates and publishes a Send using the Wallet on the Node. It takes in an array, with a variable length, of objects, each as follows: -- Destination Address (string) -- Amount (string) - -The result is a string of the hash. - -### `data` - -`data` creates and publishes a Data using the Wallet on the Node. It takes in one argument: -- Data (string) - -The result is a string of the hash. diff --git a/docs/RPC/Current/System.md b/docs/RPC/Current/System.md deleted file mode 100644 index 7a2d5c0e4..000000000 --- a/docs/RPC/Current/System.md +++ /dev/null @@ -1,5 +0,0 @@ -# System Module - -### `quit` - -`quit` will finish up all of current operations and attempt to safely shutdown. It takes in zero arguments and the result is always true. diff --git a/docs/RPC/Current/Transactions.md b/docs/RPC/Current/Transactions.md deleted file mode 100644 index f7dc6484f..000000000 --- a/docs/RPC/Current/Transactions.md +++ /dev/null @@ -1,63 +0,0 @@ -# Transactions Module - -### `getTransaction` - -`getTransaction` replies with a Transaction. It takes in one argument: -- Hash (string) - -The result is an object, as follows: -- `descendant` (string) - -- `inputs` (array of objects, each as follows) - - `hash` (string) - - When (`descendant` == "Claim") or (`descendant` == "Send"): - - `nonce` (int) - -- `outputs` (array of objects, each as follows) - - `amount` (string) - - When `descendant` == "Mint": - - `key` (int): Miner nickname. - - When `descendant` == "Claim" or `descendant` == "Send": - - `key` (string): Ed25519 Public Key. - -- `hash` (string) - - When `descendant` == "Claim": - - `signature` (string) - - When `descendant` == "Send": - - `signature` (string) - - `proof` (int) - - `argon` (string) - - When `descendant` == "Data": - - `data` (string) - - `signature` (string) - - `proof` (int) - - `argon` (string) - -### `getUTXOs` - -`getUTXOs` replies with the addresses' UTXOs. It takes in one argument: -- Address (string) - -The result is an array of objects, each as follows: -- `hash` (string) -- `nonce` (int) - -### `getBalance` - -`getBalance` replies with the balance of the specified address. It takes in one argument: -- address (string) - -The result is a string of the balance. - -### `publishSend` - -`publishSend` parses the serialized Send, adds it to the local Transactions DAG, and if it's valid, publishes it. It takes in one argument. -- Send (string) - -The result is a bool of if the transaction was successfully added. This will return true if the transaction is valid but already exists, yet it will NOT be published in that case. diff --git a/docs/RPC/Current/Usage.md b/docs/RPC/Current/Usage.md deleted file mode 100644 index 0b805a550..000000000 --- a/docs/RPC/Current/Usage.md +++ /dev/null @@ -1,12 +0,0 @@ -# Usage - -Meros RPC's is fully compliant with the JSON-RPC 2.0 standard, and is available on port 5133 by default. Calls are sent to the node via TCP. - -Meros sorts calls into the following modules: -- `system` -- `personal` -- `merit` -- `transactions` -- `network` - -The JSON-RPC 2.0 `method` field is constructed via prefixing each RPC method with its module's name plus an underscore, as so: `module_method`. Every JSON-RPC 2.0 `params` is an array. In order to specify an optional argument after an argument you want to omit, supply the default value for that argument. Bytes are sent, and received, in hexadecimal notation. diff --git a/docs/RPC/Errors.md b/docs/RPC/Errors.md new file mode 100644 index 000000000..90e28bb29 --- /dev/null +++ b/docs/RPC/Errors.md @@ -0,0 +1,8 @@ +# Errors + +- Spam: 2 +- NotEnoughMeros: 1 + +- DataMissing: -1 +- IndexError: -2 +- ValueError: -3 diff --git a/docs/RPC/Eventual/Consensus.md b/docs/RPC/Eventual/Consensus.md deleted file mode 100644 index 9ffa77ee9..000000000 --- a/docs/RPC/Eventual/Consensus.md +++ /dev/null @@ -1,69 +0,0 @@ -# Consensus Module - -### `getSendDifficulty` - -`getSendDifficulty` replies with a Send Difficulty. It takes in one argument: -- Merit Holder (int): Optional; defaults to -1. - -The result is an int of the current difficulty if the Merit Holder is -1, or if it isn't, what the specified Merit Holder voted. - -### `getDataDifficulty` - -`getDataDifficulty` replies with a Data Difficulty. It takes in one argument: -- Merit Holder (int): Optional; defaults to -1. - -The result is an int of the current difficulty if the Merit Holder is -1, or if it isn't, what the specified Merit Holder voted. - -### `getGasDifficulty` - -`getGasDifficulty` replies with the current Gas Difficulty. It takes in one argument: -- Merit Holder (int): Optional; defaults to -1. - -The result is an int of the current difficulty if the Merit Holder is -1, or if it isn't, what the specified Merit Holder voted. - -### `getStatus` - -`getStatus` replies with the Status for the specified Transaction. It takes in one argument: -- hash (string) - -The result is an object, as follows: -- `verifiers` (array of strings): The list of verifiers for this Transaction. -- `merit` (int): Merit of all the Merit Holders who verified this Transaction. -- `threshold` (int): Merit needed to become verified. -- `verified` (bool): Whether or not the Transaction is verified. -- `competing` (bool): Whether or not the Transaction has competitors. If it does, and isn't already verified, it can only be verified at the end of its Epoch. - -### `publishSignedVerification` - -`publishSignedVerification` parses the serialized Signed Verification, adds it to the local Consensus DAG, and if it's valid, publishes it. It takes in one argument. -- Signed Verification (string) - -The result is a bool of true. - -### `publishSignedSendDifficulty` - -`publishSignedSendDifficulty` parses the serialized Signed Send Difficulty, adds it to the local Consensus DAG, and if it's valid, publishes it. It takes in one argument. -- Signed Send Difficulty (string) - -The result is a bool of true. - -### `publishSignedDataDifficulty` - -`publishSignedDataDifficulty` parses the serialized Signed Data Difficulty, adds it to the local Consensus DAG, and if it's valid, publishes it. It takes in one argument. -- Signed Data Difficulty (string) - -The result is a bool of true. - -### `publishSignedGasPrice` - -`publishSignedGasPrice` parses the serialized Signed Gas Price, adds it to the local Consensus DAG, and if it's valid, publishes it. It takes in one argument. -- Signed Gas Price (string) - -The result is a bool of true. - -### `publishSignedMeritRemoval` - -`publishMeritRemoval` parses the serialized Signed Merit Removal, adds it to the local Consensus DAG, and if it's valid, publishes it. It takes in one argument. -- Signed Merit Removal (string) - -The result is a bool of true. diff --git a/docs/RPC/Eventual/Errors.md b/docs/RPC/Eventual/Errors.md deleted file mode 100644 index 73715c88f..000000000 --- a/docs/RPC/Eventual/Errors.md +++ /dev/null @@ -1,12 +0,0 @@ -# Errors - -- NotEnoughMeros: 1 -- DataExists: 0 - -- GapError: -1 -- IndexError: -2 -- ValueError: -3 -- BLSError: -4 -- AddressError: -5 -- ClientError: -6 -- Spam: -7 diff --git a/docs/RPC/Eventual/Network.md b/docs/RPC/Eventual/Network.md deleted file mode 100644 index 0d5cec4d7..000000000 --- a/docs/RPC/Eventual/Network.md +++ /dev/null @@ -1,23 +0,0 @@ -# Network Module - -### `connect` - -`connect` attempts to connect to another Meros node. It takes in two arguments: -- IP/Domain (string) -- Port (int): Optional; defaults to 5132 if omitted. - -The result is a bool of true. - -### `getPeers` -`getPeers` replies with a list of every node we're connected it. It takes in zero arguments and replies with an array of objects, each as follows: -- `ip` (string) -- `server` (bool) -- `port` (int): Only present the peer has a Server Socket. - -### `rebroadcast` - -`rebroadcast` rebroadcasts existing local data. It takes in up to two arguments: -- ID 1 (string/int): Transaction hash/Merit Holder's BLS Public Key as a string or Block nonce as an int. -- ID 2 (int): Element nonce; only used when ID 1 is a string of a Merit Holder's BLS Public Key. - -The result is a bool of true. diff --git a/docs/RPC/Eventual/Personal.md b/docs/RPC/Eventual/Personal.md deleted file mode 100644 index 4465e3ca4..000000000 --- a/docs/RPC/Eventual/Personal.md +++ /dev/null @@ -1,119 +0,0 @@ -# Personal Module - -### `setMiner` - -`setMiner` creates a new Miner Wallet using the passed in BLS Private Key and sets the Node's Miner Wallet to it. It takes in one argument: -- Private Key (string): Optional; creates a new Private Key if omitted. - -The result is a bool of true. - -### `getMiner` - -`getMiner` replies with the BLS Private Key of the current Miner Wallet. It takes in zero arguments and the result is a string of the private key. - -### `setMnemonic` - -`setMnemonic` creates a new Wallet using the passed in Mnemonic and password and sets the Node's Wallet to it. It takes in two arguments: -- Mnemonic (string): Optional; creates a new Mnemonic if omitted. -- Password (string): Optional; defaults to "" if omitted, as according to the BIP 39 spec. - -The result is a bool of true. - -### `getMnemonic` - -`getMnemonic` replies with the Node's Wallet's Mnemonic, without the password. It takes in zero arguments and the result is a string of the mnemonic. - -### `getParentPublicKey` - -`getParentPublicKey` replies with the Parent Public Key for the Node's HD Wallet, after applying BIP 44 purpose/coin type/account derivation. It takes in one argument: -- Account (int): Optional; defaults to 0. - -The result is a string of the Parent Public Key. - -### `getAddress` - -`getAddress` replies with an address. It takes in three arguments: -- Account (int): Optional; defaults to 0; used in hardened derivation. -- Change (bool): Optional; defaults to false. -- Index (int): Optional; defaults to the next underived index. If an index above the hardened threshold is specified, hardened derivation is used. If the next unused index is used, and it's above the hardened threshold, this will error. - -The result is a string of the Wallet's address. - -### `getWatchedAddresses` - -`getWatchedAddresses` replies with every watched address. It takes in zero arguments and the result is an array of strings, each a watched address. - -### `watchAddress` - -`watchAddress` instructs Meros to use inputs from the specified address when creating Send Templates (see below). It takes one argument: -- `address` (string) - -The result is a bool of true. - -### `unwatchAddress` - -`watchAddress` instructs Meros to no longer watch an address. It takes one argument: -- `address` (string) - -The result is a bool of true. - -### `claim` - -`claim` creates and publishes a Claim using the Miner Wallet on the Node. It takes in two arguments: -- Mint Hashes (array of strings) -- Destination Address (string) - -The result is a string of the hash. - -### `send` - -`send` creates and publishes a Send using the Wallet on the Node. It takes in an array, with a variable length, of objects, each as follows: -- Destination Address (string) -- Amount (string) - -The result is a string of the hash. - -### `data` - -`data` creates and publishes a Data using the Wallet on the Node. It takes in one argument: -- Data (string) - -The result is a string of the hash. - -### `getClaimTemplate` - -`getClaimTemplate` replies with a template for remotely signing a Claim. It takes in two arguments: -- Mint Hashes (array of strings) -- Destination Address (string) - -The result is an object, as follows: -- `inputs` (array of strings) -- `claim` (string) - -There will be one input per mint hash, each to be signed by the BLS Private Key of the BLS Public Key the Mint was meant for. Aggregating the signatures and appending the result to `claim` will make it publishable via `transactions_publishClaim`. - -### `getSendTemplate` - -`getSendTemplate` replies with a template for remotely signing a Send. It takes in two arguments: -- Outputs (array of objects, each as follows): - - `address` (string) - - `amount` (string) -- Include Watch Only (bool): Optional, defaults to true. - -The result is an object, as follows: -- `send` (string) -- `prefixedHash` (string) - -The prefixed hash should be signed by the proper key. If every input was to the same address, the key is the normal Private Key. If the inputs were to multiple addresses, the key is a MuSig Private Key. Appending the signature, and then valid work, to `send` will make it publishable via `transactions_publishSend`. - -### `getDataTemplate` - -`getDataTemplate` replies with a template for remotely signing a Data. It takes in two arguments: -- Sender (string) -- Data (string) - -The result is an object, as follows: -- `data` (string) -- `prefixedHash` (string) - -The prefixed hash should be signed by the proper Private Key for the sender. Appending the signature, and then valid work, to `data` will make it publishable via `transactions_publishData`. diff --git a/docs/RPC/Eventual/System.md b/docs/RPC/Eventual/System.md deleted file mode 100644 index 7a2d5c0e4..000000000 --- a/docs/RPC/Eventual/System.md +++ /dev/null @@ -1,5 +0,0 @@ -# System Module - -### `quit` - -`quit` will finish up all of current operations and attempt to safely shutdown. It takes in zero arguments and the result is always true. diff --git a/docs/RPC/Eventual/Transactions.md b/docs/RPC/Eventual/Transactions.md deleted file mode 100644 index 808b7915e..000000000 --- a/docs/RPC/Eventual/Transactions.md +++ /dev/null @@ -1,77 +0,0 @@ -# Transactions Module - -### `getTransaction` - -`getTransaction` replies with a Transaction. It takes in one argument: -- Hash (string) - -The result is an object, as follows: -- `descendant` (string) - -- `inputs` (array of objects, each as follows) - - `hash` (string) - - When (`descendant` == "Claim") or (`descendant` == "Send"): - - `nonce` (int) - -- `outputs` (array of objects, each as follows) - - `amount` (string) - - When `descendant` == "Mint": - - `key` (int): Miner nickname. - - When `descendant` == "Claim" or `descendant` == "Send": - - `key` (string): Ed25519 Public Key. - -- `hash` (string) - - When `descendant` == "Claim": - - `signature` (string) - - When `descendant` == "Send": - - `signature` (string) - - `proof` (int) - - `argon` (string) - - When `descendant` == "Data": - - `data` (string) - - `signature` (string) - - `proof` (int) - - `argon` (string) - -### `getUTXOs` - -`getUTXOs` replies with the addresses' UTXOs. It takes in one argument: -- Address (string) - -The result is an array of objects, each as follows: -- `hash` (string) -- `nonce` (int) - -### `getBalance` - -`getBalance` replies with the balance of the specified address. It takes in one argument: -- address (string) - -The result is a string of the balance. - -### `publishClaim` - -`publishClaim` parses the serialized Claim, adds it to the local Transactions DAG, and if it's valid, publishes it. It takes in one argument. -- Claim (string) - -The result is a bool of if the transaction was successfully added. This will return true if the transaction is valid but already exists, yet it will NOT be published in that case. - -### `publishSend` - -`publishSend` parses the serialized Send, adds it to the local Transactions DAG, and if it's valid, publishes it. It takes in one argument. -- Send (string) - -The result is a bool of if the transaction was successfully added. This will return true if the transaction is valid but already exists, yet it will NOT be published in that case. - -### `publishData` - -`publishData` parses the serialized Data, adds it to the local Transactions DAG, and if it's valid, publishes it. It takes in one argument. -- Data (string) - -The result is a bool of if the transaction was successfully added. This will return true if the transaction is valid but already exists, yet it will NOT be published in that case. diff --git a/docs/RPC/Eventual/Usage.md b/docs/RPC/Eventual/Usage.md deleted file mode 100644 index 0b805a550..000000000 --- a/docs/RPC/Eventual/Usage.md +++ /dev/null @@ -1,12 +0,0 @@ -# Usage - -Meros RPC's is fully compliant with the JSON-RPC 2.0 standard, and is available on port 5133 by default. Calls are sent to the node via TCP. - -Meros sorts calls into the following modules: -- `system` -- `personal` -- `merit` -- `transactions` -- `network` - -The JSON-RPC 2.0 `method` field is constructed via prefixing each RPC method with its module's name plus an underscore, as so: `module_method`. Every JSON-RPC 2.0 `params` is an array. In order to specify an optional argument after an argument you want to omit, supply the default value for that argument. Bytes are sent, and received, in hexadecimal notation. diff --git a/docs/RPC/Eventual/Merit.md b/docs/RPC/Merit.md similarity index 56% rename from docs/RPC/Eventual/Merit.md rename to docs/RPC/Merit.md index bb33cdd25..821d2a013 100644 --- a/docs/RPC/Eventual/Merit.md +++ b/docs/RPC/Merit.md @@ -2,16 +2,18 @@ ### `getHeight` -`getHeight` replies with the Blockchain's height. It takes in zero arguments and the result is an int of the height. +`getHeight` replies with the Blockchain's height. The result is an int of the height. ### `getDifficulty` -`getDifficulty` replies with the current difficulty. It takes in zero arguments and the result is an int of the difficulty. This should NOT be used by miners, as the difficulty is multiplied for new miners. `getBlockTemplate` is guaranteed to return the difficulty that a Block would be checked against. +`getDifficulty` replies with the current difficulty. This should NOT be used by miners, as the difficulty is multiplied for new miners. `getBlockTemplate` is guaranteed to return the difficulty that a Block would be checked against. The result is an int of the difficulty. ### `getBlock` -`getBlock` replies with a Block. It takes in one argument. -- ID (int/string): Either the nonce as an int or hash as a string. +`getBlock` replies with the requested Block. + +Arguments: +- `block` (int/string): Either the nonce or hash. The result is an object, as follows: - `hash` (string) @@ -19,6 +21,7 @@ The result is an object, as follows: - `version` (int) - `last` (string) - `contents` (string) + - `packets` (int): Amount of packets in this Block. - `sketchSalt` (string) - `sketchCheck` (string) - `miner` (int/string): Either the miner's nick as an int or the key as a string if this is their first Block. @@ -33,58 +36,65 @@ The result is an object, as follows: - `elements` (array of objects, each as follows) - `descendant` (string) - `holder` (int) + - `nonce` (int) When `descendant` == "SendDifficulty": - - `nonce` (int) - `difficulty` (int) When `descendant` == "DataDifficulty": - - `nonce` (int) - `difficulty` (int) - `removals` (array of ints): Whoever got their Merit removed by this Block. - `aggregate` (string) -### `getNickname` +### `getPublicKey` -`getNickname` replies with the Merit Holder's nickname. It takes in one argument. -- Merit Holder (string) +`getPublicKey` replies with the specified Merit Holder's BLS Public Key. -The result is an int of the nickname. +Arguments: +- `nick` (int) -### `getPublicKey` +The result is an string of the BLS Public Key. + +### `getNickname` -`getPublicKey` replies with the specified Merit Holder's BLS Public Key. It takes in one argument. -- Nickname (int) +`getNickname` replies with a Merit Holder's nickname. -The result is an string of the BLS Public Key. +Arguments: +- `key` (string): Merit Holder's BLS Public Key. + +The result is an int of the nickname. ### `getTotalMerit` -`getTotalMerit` replies with the total amount of Merit in existence. It takes in zero arguments and the result is an int of the total amount of Merit. +`getTotalMerit` replies with the total amount of Merit in existence. The result is an int of the total amount of Merit. ### `getUnlockedMerit` -`getUnlockedMerit` replies with the amount of Unlocked Merit in existence. It takes in zero arguments and the result is an int of the amount of Unlocked Merit. +`getUnlockedMerit` replies with the amount of Unlocked Merit in existence. The result is an int of the amount of Unlocked Merit. ### `getMerit` -`getMerit` replies with a Merit Holder's Merit. It takes in one argument. -- Merit Holder Nickname (int) +`getMerit` replies with the specified Merit Holder's Merit. + +Arguments: +- `nick` (int) The result is an object, as follows: - `status` (string): "Unlocked", "Locked", or "Pending". -- `malicious` (bool): Whether or not this holder has a Merit Removal against them pending. +- `malicious` (bool): Whether or not this holder has a Merit Removal against them pending. - `merit` (int) ### `getBlockTemplate` -`getBlockTemplate` replies with a template for mining a Block. It takes in one argument. -- miner (string): BLS Public Key of the Miner. +`getBlockTemplate` replies with a template for mining a Block. + +Arguments: +- `miner` (string): BLS Public Key of the Miner. The result is an object, as follows: -- `id` (int): The template ID. +- `id` (int): The template ID. - `key` (string): The RandomX cache key. - `header` (string) - `difficulty` (int) @@ -93,8 +103,10 @@ Mining the Block occurs by hashing the header with a 4-byte proof appended. Afte ### `publishBlock` -`publishBlock` adds the Block to the local Blockchain, and if it's valid, publishes it. It takes in two arguments. -- ID (int): The ID of a template. Only an ID from the last 5 templates is valid. -- Block (string): A serialized BlockHeader. +`publishBlock` adds a Block to the local Blockchain, and if it's valid, publishes it. + +Arguments: +- `id` (int): ID of the template used. +- `header` (string): The serialized BlockHeader. The result is a bool of true. diff --git a/docs/RPC/Network.md b/docs/RPC/Network.md new file mode 100644 index 000000000..fb4287cf1 --- /dev/null +++ b/docs/RPC/Network.md @@ -0,0 +1,28 @@ +# Network Module + +### `connect` + +`connect` attempts to connect to another Meros node. This method requires authentication. + +Arguments: +- `address` (string): IPv4 address or domain. Cannot be IPv6. +- `port` (int; optional): Defaults to 5132 if omitted. + +The result is a bool of true, regardless of if the connection succeeded. This will never return an error. + +### `getPeers` + +`getPeers` replies with a list of every node Meros is currently connected it. The result is an array of objects, each as follows: +- `ip` (string) +- `server` (bool) +- `port` (int): Only present if the peer is a server. + +### `broadcast` + +`broadcast` broadcasts existing local data. Because it's already been processed locally, its presumably already been broadcasted around the network. This is meant to cover for any local networking issues/propagation shortcomings that may occur. + +Arguments: +- `transaction` (string; optional): Hash of the Transaction to broadcast. +- `block` (string; optional): Hash of the Block to broadcast. + +The result is a bool of true. diff --git a/docs/RPC/Personal.md b/docs/RPC/Personal.md new file mode 100644 index 000000000..543999afd --- /dev/null +++ b/docs/RPC/Personal.md @@ -0,0 +1,107 @@ +# Personal Module + +Every route in this module requires authentication. + +### `setWallet` + +`setWallet` creates a new Wallet using the passed in Mnemonic and password. This is irreversible and will delete the existing Wallet, having the node lose all access to the current Merit Holder and all funds. + +Arguments: +- `mnemonic` (string; optional): Creates a new Mnemonic if omitted. +- `password` (string; optional): Defaults to "" if omitted, as according to the BIP 39 spec. + +The result is a bool of true. + +### `setAccount` + +`setAccount` deletes the existing Wallet on the node, in an irreversible manner, losing all access to the current Merit Holder and all funds. It sets the Wallet to track the specified account, presumably one returned from `getAccount`. This will disable any operations requiring access to the private key, as well as all Merit Holder related operations. That said, this will preserve the functionality of address generation, `getTransactionTemplate`, and more, enabling Watch Wallet functionality. + +Arguments: +- `key` (string): Account public key to use in accordance with BIP 44. +- `chainCode` (string): Chain code to use in accordance with BIP 32. + +The result is a bool of true. + +### `getMnemonic` + +`getMnemonic` replies with the Wallet's Mnemonic, without any password needed to use it. The result is a string of the Mnemonic. + +### `getMeritHolderKey` + +`getMeritHolderKey` replies with the BLS Private Key of the node's Merit Holder. The result is a string of the Private Key. + +### `getMeritHolderNick` + +`getMeritHolderKey` replies with the nickname of the node's Merit Holder. The result is an int of the nickname. + +### `getAccount` + +`getAccount` replies with the public key and chain code for the account being used from the node's HD Wallet. + +The result is an object, as follows: +- `key` (string) +- `chainCode` (string) + +### `getAddress` + +`getAddress` replies with an address derived from the seed. + +Arguments: +- `index` (int; optional): Defaults to a sequential index for an address which has not received any funds. + +The result is a string of the generated address. + +### `send` + +`send` creates and publishes a Send using the Wallet on the node. + +Arguments: +- `outputs` (array of objects) + - `address` (string) + - `amount` (string) +- `password` (string; optional): Defaults to "". + +The result is a string of the hash. + +### `data` + +`data` creates and publishes a Data using the Wallet on the node. + +Arguments: +- `hex` (bool; optional): Defaults to false. When true, data is treated as bytes, instead of as text. +- `data` (string): Must be at least 1 byte and at most 256 bytes. +- `password` (string; optional): Defaults to "". + +The result is a string of the hash. + +### `getUTXOs` + +`getUTXOs` replies with every UTXO known to the node's Wallet. If you only want the UTXOs for a specific address, use `transactions_getUTXOs`. + +The result is an array of objects, each as follows: +- `address` (string) +- `hash` (string) +- `nonce` (int) + +### `getTransactionTemplate` + +`getTransactionTemplate` replies with a signable transaction template usable by a program with the relevant private keys. + +Arguments: +- `outputs` (array of objects, each as follows) + - `address` (string): Address to send to. + - `amount` (string): Amount to send. +- `from` (array of strings; optional): Addresses to use the UTXOs of; must be part of the current Wallet. +- `change` (string; optional): Address to use as change. + +The result is an object, as follows: +- `type` (string): Type of the Transaction. +- `inputs` (array of objects, each as follows) + - `hash` (string) + - `nonce` (int) + - `change` (bool): If this is a change address. + - `index` (int): Address index. +- `outputs` (array of objects, each as follows) + - `key` (string) + - `amount` (string) +- `publicKey` (string): The public key this transaction will be checked against. Used to verify the correct private key is being used. diff --git a/docs/RPC/System.md b/docs/RPC/System.md new file mode 100644 index 000000000..bba975cef --- /dev/null +++ b/docs/RPC/System.md @@ -0,0 +1,5 @@ +# System Module + +### `quit` + +`quit` will finish up all current operations and safely shutdown Meros. This will never error and always has a result of true, regardless of what happens. diff --git a/docs/RPC/Transactions.md b/docs/RPC/Transactions.md new file mode 100644 index 000000000..cd53a67a2 --- /dev/null +++ b/docs/RPC/Transactions.md @@ -0,0 +1,76 @@ +# Transactions Module + +### `getTransaction` + +`getTransaction` replies with the requested Transaction. + +Arguments: +- `hash` (string) + +The result is an object, as follows: +- `descendant` (string) + +- `inputs` (array of objects, each as follows) + - `hash` (string) + + When (`descendant` == "Claim") or (`descendant` == "Send"): + - `nonce` (int) + +- `outputs` (array of objects, each as follows) + - `amount` (string) + + When `descendant` == "Mint": + - `nick` (int): Miner nickname. + + When `descendant` == "Claim" or `descendant` == "Send": + - `key` (string): Ed25519 Public Key. + +- `hash` (string) + + When `descendant` == "Claim": + - `signature` (string) + + When `descendant` == "Send": + - `signature` (string) + - `proof` (int) + + When `descendant` == "Data": + - `data` (string) + - `signature` (string) + - `proof` (int) + +### `getUTXOs` + +`getUTXOs` replies with an address's UTXOs (which are marked confirmed by the node and have no existing spenders). + +Arguments: +- `address` (string) + +The result is an array of objects, each as follows: +- `hash` (string) +- `nonce` (int) + +### `getBalance` + +`getBalance` replies with the balance of the specified address, defined as the sum of the value of its UTXOs (using the same rules as above for which are considered). + +Arguments: +- `address` (string) + +The result is a string of the balance. + +### `publishTransaction` + +`publishTransaction` accepts a serialized Transaction, attempts to add it to the local Transactions DAG, and on success, broadcasts it to the network. + +Arguments: +- `type` (string): "Claim", "Send", or "Data". +- `transaction` (string): Serialized Transaction. + +The result is a bool of if the transaction was successfully added. This will return true if the transaction is valid yet already exists, though it will NOT be broadcasted again in that case. + +### `publishTransactionWithoutWork` + +`publishTransactionWithoutWork` accepts a serialized Transaction, without work, generates work for it against the current difficulty, and then attempts to add it to the local Transactions DAG. Upon success, it's broadcasted to the network. This method requires authentication. + +The inputs and outputs of this function match `publishTransaction` exactly, except the serialized Transaction must not have a proof included. diff --git a/docs/RPC/Usage.md b/docs/RPC/Usage.md new file mode 100644 index 000000000..97f104777 --- /dev/null +++ b/docs/RPC/Usage.md @@ -0,0 +1,14 @@ +# Usage + +Meros's RPC is fully compliant with the JSON-RPC 2.0 standard with HTTP POST being used for the transport. The default port for the server is 5133. + +Meros sorts calls into the following modules: +- `system` +- `personal` +- `merit` +- `transactions` +- `network` + +The JSON-RPC 2.0 `method` field is constructed via prefixing each RPC method with its module's name plus an underscore, such as `module_methodName`. `params` are always an object. Bytes are transmitted as hexadecimal strings, without any prefix. All Meros amounts are represented atomically, without decimal formatting, and transmitted as strings. + +`system` and `personal` (as well as select methods elsewhere) require authentication to be used, which is handled via HTTP Bearer Authentication. The token is available in a file named `.token`, under Meros's data directory. This file is generated every time Meros boots up, so repeated access to the filesystem is effectively required. This is to effectively lock certain methods to whoever actually has access to the system, enabling other methods such as `getPeers` to be called by anyone simply wishing to gather data. diff --git a/e2e/Classes/Transactions/Data.py b/e2e/Classes/Transactions/Data.py index 3e5eee789..0c8c8a1e4 100644 --- a/e2e/Classes/Transactions/Data.py +++ b/e2e/Classes/Transactions/Data.py @@ -69,8 +69,7 @@ def toJSON( "data": self.data.hex().upper(), "signature": self.signature.hex().upper(), - "proof": self.proof, - "argon": self.argon.hex().upper() + "proof": self.proof } @staticmethod diff --git a/e2e/Classes/Transactions/Mint.py b/e2e/Classes/Transactions/Mint.py index 9a688aa10..38b225e8a 100644 --- a/e2e/Classes/Transactions/Mint.py +++ b/e2e/Classes/Transactions/Mint.py @@ -36,7 +36,7 @@ def toJSON( } for output in self.outputs: result["outputs"].append({ - "key": output[0], + "nick": output[0], "amount": str(output[1]) }) return result @@ -47,5 +47,5 @@ def fromJSON( ) -> Any: outputs: List[Tuple[int, int]] = [] for output in json["outputs"]: - outputs.append((output["key"], int(output["amount"]))) + outputs.append((output["nick"], int(output["amount"]))) return Mint(bytes.fromhex(json["hash"]), outputs) diff --git a/e2e/Classes/Transactions/Send.py b/e2e/Classes/Transactions/Send.py index 430da8a38..7000e9b92 100644 --- a/e2e/Classes/Transactions/Send.py +++ b/e2e/Classes/Transactions/Send.py @@ -96,8 +96,7 @@ def toJSON( "hash": self.hash.hex().upper(), "signature": self.signature.hex().upper(), - "proof": self.proof, - "argon": self.argon.hex().upper() + "proof": self.proof } for txInput in self.inputs: result["inputs"].append({ diff --git a/e2e/Libs/BIP32.py b/e2e/Libs/BIP32.py new file mode 100644 index 000000000..c35c790bf --- /dev/null +++ b/e2e/Libs/BIP32.py @@ -0,0 +1,74 @@ +#pylint: disable=invalid-name + +from typing import List, Tuple +import hashlib +import hmac + +import e2e.Libs.ed25519 as ed + +HARDENED_THRESHOLD: int = 1 << 31 + +def hmac512( + key: bytes, + msg: bytes +) -> bytes: + return hmac.new(key, msg, hashlib.sha512).digest() + +def deriveKeyAndChainCode( + secret: bytes, + path: List[int] +) -> Tuple[bytes, bytes]: + #Clamp the secret. + k: bytes = ed.H(secret) + kL: bytes = k[:32] + kR: bytes = k[32:] + if kL[31] & 0b00100000 != 0: + raise Exception("Invalid secret to derive from.") + kLArr: bytearray = bytearray(kL) + kLArr[0] = (kL[0] >> 3) << 3 + kLArr[31] = ((kL[31] << 1) & 255) >> 1 + kLArr[31] = kLArr[31] | (1 << 6) + kL = bytes(kLArr) + k = kL + kR + + #Parent public key/chain code. + A: bytes = ed.encodepoint(ed.scalarmult(ed.B, ed.decodeint(kL))) + c: bytes = hashlib.sha256(bytes([1]) + secret).digest() + + #Derive each child. + for i in path: + iBytes: bytes = i.to_bytes(4, "little") + Z: bytes + if i < HARDENED_THRESHOLD: + Z = hmac512(c, bytes([2]) + A + iBytes) + c = hmac512(c, bytes([3]) + A + iBytes)[32:] + else: + Z = hmac512(c, bytes([0]) + k + iBytes) + c = hmac512(c, bytes([1]) + k + iBytes)[32:] + + zL: bytearray = bytearray(Z[:28]) + for _ in range(4): + zL.append(0) + zR: bytes = Z[32:] + #This should probably be mod l. That said, the paper isn't clear, and Meros defers to the existing impl. + #Said existing impl is probably wrong. + #While we could move to the proper form, it's unclear, and Meros is planning on moving to Ristretto anyways. + #That will void all these concerns. + kL = ed.encodeint((8 * ed.decodeint(bytes(zL))) + ed.decodeint(kL)) + if (ed.decodeint(kL) % ed.l) == 0: + raise Exception("Invalid child.") + #This modulus should be redundant given encodeint only uses the latter 32 bytes. + kR = ed.encodeint((ed.decodeint(zR) + ed.decodeint(kR)) % (1 << 256)) + k = kL + kR + + A = ed.encodepoint(ed.scalarmult(ed.B, ed.decodeint(kL))) + + return (k, c) + +def derive( + secret: bytes, + path: List[int] +) -> bytes: + key: bytes + key, _ = deriveKeyAndChainCode(secret, path) + return key diff --git a/e2e/Libs/BLS.py b/e2e/Libs/BLS.py index 59b0a6cf6..b41a03602 100644 --- a/e2e/Libs/BLS.py +++ b/e2e/Libs/BLS.py @@ -289,7 +289,7 @@ def __init__( if isinstance(key, int): key = blake2b(key.to_bytes(2 if key > 255 else 1, "little"), digest_size=32).digest() - key = key.rjust(48, b'\0') + key = bytes(48 - len(key)) + key self.value: Big384 = Big384() MilagroCurve.BIG_384_58_fromBytesLen(self.value, c_char_p(key), 48) MilagroCurve.BIG_384_58_mod(self.value, r) @@ -319,4 +319,4 @@ def serialize( ) -> bytes: result: Array[c_char] = create_string_buffer(48) MilagroCurve.BIG_384_58_toBytes(result, self.value) - return bytes(result) + return bytes(result)[16:] diff --git a/e2e/Libs/ed25519.py b/e2e/Libs/ed25519.py new file mode 100644 index 000000000..6cf6808ab --- /dev/null +++ b/e2e/Libs/ed25519.py @@ -0,0 +1,178 @@ +#pylint: disable=invalid-name + +from typing import List + +import hashlib + +b: int = 256 +q: int = 2**255 - 19 +l: int = 2**252 + 27742317777372353535851937790883648493 + +def H( + m: bytes +) -> bytes: + return hashlib.sha512(m).digest() + +def expmod( + bExp: int, + e: int, + m: int +) -> int: + if e == 0: + return 1 + t: int = (expmod(bExp, e // 2, m) ** 2) % m + if e & 1: + t = (t * bExp) % m + return t + +def inv( + x: int +) -> int: + return expmod(x, q - 2, q) + +d: int = -121665 * inv(121666) +I: int = expmod(2, (q - 1) // 4, q) + +def xrecover( + y: int +) -> int: + xx: int = ((y * y) - 1) * inv((d * y * y) + 1) + x: int = expmod(xx, (q + 3) // 8, q) + if (((x * x) - xx) % q) != 0: + x = (x * I) % q + if x % 2 != 0: + x = q - x + return x + +By: int = 4 * inv(5) +Bx: int = xrecover(By) +B: List[int] = [Bx % q, By % q] + +def edwards( + P: List[int], + Q: List[int] +): + x1: int = P[0] + y1: int = P[1] + x2: int = Q[0] + y2: int = Q[1] + x3: int = ((x1 * y2) + (x2 * y1)) * inv(1 + (d * x1 * x2 * y1 * y2)) + y3: int = ((y1 * y2) + (x1 * x2)) * inv(1 - (d * x1 * x2 * y1 * y2)) + return [x3 % q, y3 % q] + +def scalarmult( + P: List[int], + e: int +) -> List[int]: + if e == 0: + return [0, 1] + Q: List[int] = scalarmult(P, e // 2) + Q = edwards(Q, Q) + if e & 1: + #pylint: disable=arguments-out-of-order + Q = edwards(Q, P) + return Q + +def encodeint( + y: int +) -> bytes: + return y.to_bytes(32, "little") + +def encodepoint( + P: List[int] +) -> bytes: + res: bytearray = bytearray(P[1].to_bytes(32, "little")) + res[-1] = (((res[-1] << 1) & 255) >> 1) | ((P[0] & 1) << 7) + return bytes(res) + +def bit( + h: bytes, + i: int +): + return (h[i // 8] >> (i % 8)) & 1 + +def Hint( + m: bytes +) -> int: + return int.from_bytes(H(m), "little") % l + +def Bint( + m: bytes +) -> int: + return int.from_bytes(hashlib.blake2b(m).digest(), "little") % l + +def sign( + msg: bytes, + secret: bytes +) -> bytes: + s: int = int.from_bytes(secret[:32], "little") + s &= (1 << 254) - 8 + s |= (1 << 254) + prefix: bytes = secret[32:] + + A: bytes = encodepoint(scalarmult(B, s)) + r: int = Hint(prefix + msg) + R: bytes = encodepoint(scalarmult(B, r)) + k: int = Hint(R + A + msg) + s: int = (r + (k * s)) % l + return R + int.to_bytes(s, 32, "little") + +def isoncurve( + P: List[int] +) -> bool: + x: int = P[0] + y: int = P[1] + return ((-(x * x) + (y*y) - 1 - (d * x * x * y * y)) % q) == 0 + +def decodeint( + s: bytes +) -> int: + return sum(((2 ** i) * bit(s, i)) for i in range(0, b)) + +def decodepoint( + s: bytes +) -> List[int]: + y: int = sum(((2 ** i) * bit(s, i)) for i in range(0, b - 1)) + x: int = xrecover(y) + if x & 1 != bit(s, b - 1): + x = q - x + P: List[int] = [x, y] + if not isoncurve(P): + raise Exception("decoding point that is not on curve") + return P + +def verify( + s: bytes, + m: bytes, + pk: bytes +) -> bool: + if len(s) != b // 4: + return False + if len(pk) != b // 8: + return False + R: List[int] = decodepoint(s[:(b // 8)]) + A: List[int] = decodepoint(pk) + S: int = decodeint(s[(b // 8) : (b // 4)]) + h: int = Hint(encodepoint(R) + pk + m) + return scalarmult(B, S) == edwards(R, scalarmult(A, h)) + +#Aggregate Ed25519 public keys for usage with MuSig. +def aggregate( + keys: List[bytes] +) -> bytes: + #Single key/no different keys. + if len(set(keys)) == 1: + return keys[0] + + L: bytes = b"" + for key in keys: + L = L + key + L = hashlib.blake2b(L).digest() + + res: List[int] = [] + for key in keys: + if len(res) == 0: + res = scalarmult(decodepoint(key), Bint(b"agg" + L + key)) + else: + res = edwards(res, scalarmult(decodepoint(key), Bint(b"agg" + L + key))) + return encodepoint(res) diff --git a/e2e/Meros/Meros.py b/e2e/Meros/Meros.py index 87d4cf8b3..a061f09c6 100644 --- a/e2e/Meros/Meros.py +++ b/e2e/Meros/Meros.py @@ -3,7 +3,8 @@ from subprocess import Popen from time import sleep import socket -import json + +import requests from e2e.Classes.Transactions.Transaction import Transaction from e2e.Classes.Transactions.Claim import Claim @@ -133,9 +134,8 @@ def recv( header: MessageType = MessageType(result[0]) #Get the rest of the message. - length: int for l in range(len(lengths[header])): - length = lengths[header][l] + length: int = lengths[header][l] if length < 0: length = int.from_bytes( result[-lengths[header][l - 1]:], @@ -282,7 +282,7 @@ def __init__( self.rpc: int = rpc self.calledQuit: bool = False - self.process: Popen[Any] = Popen(["./build/Meros", "--data-dir", dataDir, "--log-file", self.log, "--db", self.db, "--network", "devnet", "--tcp-port", str(tcp), "--rpc-port", str(rpc), "--no-gui"]) + self.process: Popen[Any] = Popen(["./build/Meros", "--data-dir", dataDir, "--log-file", self.log, "--db", self.db, "--network", "devnet", "--token", "TEST_TOKEN", "--tcp-port", str(tcp), "--rpc-port", str(rpc), "--no-gui"]) while True: sleep(1) try: @@ -518,22 +518,17 @@ def quit( self ) -> None: if not self.calledQuit: - rpcConnection: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - rpcConnection.connect(("127.0.0.1", self.rpc)) - rpcConnection.send( - bytes( - json.dumps( - { - "jsonrpc": "2.0", - "id": 0, - "method": "system_quit", - "params": [] - } - ), - "utf-8" - ) + requests.post( + "http://127.0.0.1:" + str(self.rpc), + json={ + "jsonrpc": "2.0", + "id": 0, + "method": "system_quit" + }, + headers={ + "Authorization": "Bearer TEST_TOKEN" + } ) - rpcConnection.close() self.calledQuit = True while self.process.poll() is None: diff --git a/e2e/Meros/RPC.py b/e2e/Meros/RPC.py index 5f2e8454f..36482bec5 100644 --- a/e2e/Meros/RPC.py +++ b/e2e/Meros/RPC.py @@ -1,8 +1,8 @@ -from typing import Dict, List, Any +from typing import Dict, List, Union, Any from os import remove from time import sleep -import json -import socket + +import requests from e2e.Meros.Meros import Meros @@ -14,57 +14,46 @@ def __init__( meros: Meros ) -> None: self.meros: Meros = meros - self.socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.connect(("127.0.0.1", self.meros.rpc)) def call( self, module: str, method: str, - args: List[Any] = [] + args: Union[List[Dict[str, Any]], Dict[str, Any]] = {}, + auth: bool = True ) -> Any: try: - self.socket.send( - bytes( - json.dumps( - { - "jsonrpc": "2.0", - "id": 0, - "method": module + "_" + method, - "params": args - } - ), - "utf-8" - ) + request: requests.Response = requests.post( + "http://127.0.0.1:" + str(self.meros.rpc), + json={ + "jsonrpc": "2.0", + "id": 0, + "method": module + "_" + method, + "params": args + }, + headers={ + "Authorization": "Bearer TEST_TOKEN" + } if auth else {} ) - except BrokenPipeError: - raise NodeError() + except Exception as e: + raise NodeError(str(e)) - response: bytes = bytes() - nextChar: bytes = bytes() - counter: int = 0 - while True: - try: - nextChar = self.socket.recv(1) - except Exception: - raise NodeError() - if not nextChar: - raise NodeError() - response += nextChar + if request.status_code != 200: + raise TestError("HTTP status isn't 200: " + str(request.status_code)) + result: Dict[str, Any] = request.json() - if response[-1] == response[0]: - counter += 1 - elif (chr(response[-1]) == ']') and (chr(response[0]) == '['): - counter -= 1 - elif (chr(response[-1]) == '}') and (chr(response[0]) == '{'): - counter -= 1 - if counter == 0: - break + if result["jsonrpc"] != "2.0": + raise TestError("Meros didn't respond with the \"jsonrpc\" field.") - #Raise an exception on error. - result: Dict[str, Any] = json.loads(response) + checkID: bool = True if "error" in result: + #Don't check the ID if we had a parse error, as Meros uses an ID of null, as it should. + checkID = result["error"]["code"] != -32700 raise TestError(str(result["error"]["code"]) + " " + result["error"]["message"] + ".") + + if checkID and (result["id"] != 0): + raise TestError("Meros didn't respond with the correct ID.") + return result["result"] def quit( @@ -89,7 +78,3 @@ def reset( pass self.meros = Meros(self.meros.db, self.meros.tcp, self.meros.rpc) - sleep(5) - - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.connect(("127.0.0.1", self.meros.rpc)) diff --git a/e2e/Pylint/MultilineExpansionChecker.py b/e2e/Pylint/MultilineExpansionChecker.py index eedc899a1..e7af8162d 100644 --- a/e2e/Pylint/MultilineExpansionChecker.py +++ b/e2e/Pylint/MultilineExpansionChecker.py @@ -356,7 +356,7 @@ def process_line( return #Check if the line starts with a function def. - if (len(line) > 3) and (line[0 : 3] == "def"): + if (len(line) > 4) and (line[0 : 4] == "def "): self.checkFunction(lines, num) return diff --git a/e2e/Stubs/bip_utils/__init__.pyi b/e2e/Stubs/bip_utils/__init__.pyi new file mode 100644 index 000000000..73271ec3c --- /dev/null +++ b/e2e/Stubs/bip_utils/__init__.pyi @@ -0,0 +1,39 @@ +from typing import Any + +from enum import Enum + +class Bip39WordsNum(Enum): + WORDS_NUM_12: int = ... + WORDS_NUM_24: int = ... + +class Bip39MnemonicGenerator: + @staticmethod + def FromWordsNumber( + num: Bip39WordsNum + ) -> Any: + ... + +class Bip39MnemonicValidator: + def __init__( + self, + mnemonic: str + ) -> Any: + ... + + def Validate( + self + ) -> bool: + ... + +class Bip39SeedGenerator: + def __init__( + self, + mnemonic: str + ) -> Any: + ... + + def Generate( + self, + password: str + ) -> bytes: + ... diff --git a/e2e/Stubs/requests/__init__.pyi b/e2e/Stubs/requests/__init__.pyi new file mode 100644 index 000000000..8ff29beb8 --- /dev/null +++ b/e2e/Stubs/requests/__init__.pyi @@ -0,0 +1,17 @@ +from typing import Iterator, Dict, List, Optional, Union, Any + +class Response: + status_code: int + + def json( + self + ) -> Dict[str, Any]: + ... + +def post( + url: str, + data: Optional[Union[Iterator[bytes], str]] = ..., + json: Optional[Union[List[Dict[str, Any]], Dict[str, Any]]] = ..., + headers: Optional[Dict[str, str]] = ... +) -> Response: + ... diff --git a/e2e/Tests/Consensus/BeatenTest.py b/e2e/Tests/Consensus/BeatenTest.py index 8f91a96d8..1669515c8 100644 --- a/e2e/Tests/Consensus/BeatenTest.py +++ b/e2e/Tests/Consensus/BeatenTest.py @@ -51,7 +51,7 @@ def sendSends() -> None: #Sanity check to verify the Block Template contains the Verification. def verifyTemplate() -> None: if bytes.fromhex( - rpc.call("merit", "getBlockTemplate", [blsPubKey])["header"] + rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey})["header"] )[36:68] != BlockHeader.createContents([VerificationPacket(sends[2].hash, [1])]): raise TestError("Meros didn't add a SignedVerification to the Block Template.") @@ -59,15 +59,15 @@ def verifyBeaten() -> None: #Verify beaten was set. The fourth Transaction is also beaten, yet should be pruned. #That's why we don't check its status. for send in sends[1:3]: - if not rpc.call("consensus", "getStatus", [send.hash.hex()])["beaten"]: + if not rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["beaten"]: raise TestError("Meros didn't mark a child and its descendant as beaten.") #Check the pending Verification for the beaten descendant was deleted. if ( - (rpc.call("consensus", "getStatus", [sends[2].hash.hex()])["verifiers"] != [0]) or + (rpc.call("consensus", "getStatus", {"hash": sends[2].hash.hex()})["verifiers"] != [0]) or ( bytes.fromhex( - rpc.call("merit", "getBlockTemplate", [blsPubKey])["header"] + rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey})["header"] )[36:68] != bytes(32) ) ): @@ -75,7 +75,7 @@ def verifyBeaten() -> None: #Verify the fourth Transaction was pruned. with raises(TestError): - rpc.call("transactions", "getTransaction", [sends[3].hash.hex()]) + rpc.call("transactions", "getTransaction", {"hash": sends[3].hash.hex()}) #Verify neither the second or third Transaction tree can be appended to. #Publishes a never seen-before Send for the descendant. @@ -85,7 +85,7 @@ def verifyBeaten() -> None: #This has identical effects, returns an actual error instead of a disconnect, #and doesn't force us to wait a minute for our old socket to be cleared. with raises(TestError): - rpc.call("transactions", "publishSend", [send.serialize().hex()]) + rpc.call("transactions", "publishTransaction", {"type": "Send", "transaction": send.serialize().hex()}) #Not loaded above as it can only be loqaded after the chain starts, which is done by the Liver. #RandomX cache keys and all that. diff --git a/e2e/Tests/Consensus/Difficulties/LockedMeritDifficultiesTest.py b/e2e/Tests/Consensus/Difficulties/LockedMeritDifficultiesTest.py index 3545cd11f..3ad70ca48 100644 --- a/e2e/Tests/Consensus/Difficulties/LockedMeritDifficultiesTest.py +++ b/e2e/Tests/Consensus/Difficulties/LockedMeritDifficultiesTest.py @@ -14,19 +14,19 @@ def LockedMeritDifficultyTest( rpc: RPC ) -> None: def verifyVotedAndUnlocked() -> None: - if rpc.call("merit", "getMerit", [0])["status"] != "Unlocked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Unlocked": raise Exception(INVALID_TEST) verifySendDifficulty(rpc, 2) verifyDataDifficulty(rpc, 2) def verifyDiscountedAndLocked() -> None: - if rpc.call("merit", "getMerit", [0])["status"] != "Locked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Locked": raise Exception(INVALID_TEST) verifySendDifficulty(rpc, 3) verifyDataDifficulty(rpc, 5) def verifyCountedAndPending() -> None: - if rpc.call("merit", "getMerit", [0])["status"] != "Pending": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Pending": raise Exception(INVALID_TEST) verifySendDifficulty(rpc, 2) verifyDataDifficulty(rpc, 2) diff --git a/e2e/Tests/Consensus/Families/DescendantHighestUnverifiedParentTest.py b/e2e/Tests/Consensus/Families/DescendantHighestUnverifiedParentTest.py index 22dce0ea0..747bbcc6a 100644 --- a/e2e/Tests/Consensus/Families/DescendantHighestUnverifiedParentTest.py +++ b/e2e/Tests/Consensus/Families/DescendantHighestUnverifiedParentTest.py @@ -25,9 +25,9 @@ def sendSends() -> None: def verifyDescendantLost() -> None: for send in sends[1:]: - if rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros verified a beaten transaction or one of its children (one of which is impossible).") - if not rpc.call("consensus", "getStatus", [sends[0].hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": sends[0].hash.hex()})["verified"]: raise TestError("Meros either didn't verify the descendant or its parent.") Liver( diff --git a/e2e/Tests/Consensus/Families/DescendantHighestVerifiedParentTest.py b/e2e/Tests/Consensus/Families/DescendantHighestVerifiedParentTest.py index d3e962385..07afbd404 100644 --- a/e2e/Tests/Consensus/Families/DescendantHighestVerifiedParentTest.py +++ b/e2e/Tests/Consensus/Families/DescendantHighestVerifiedParentTest.py @@ -27,10 +27,10 @@ def sendSends() -> None: def verifyDescendantWon() -> None: for send in sends[::3]: - if rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros verified a beaten, or potentially impossible, transaction.") for send in sends[1:3]: - if not rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros either didn't verify the descendant or its parent.") Liver( diff --git a/e2e/Tests/Consensus/Families/ImpossibleFamilyTest.py b/e2e/Tests/Consensus/Families/ImpossibleFamilyTest.py index cc373fd46..6407f607c 100644 --- a/e2e/Tests/Consensus/Families/ImpossibleFamilyTest.py +++ b/e2e/Tests/Consensus/Families/ImpossibleFamilyTest.py @@ -28,9 +28,9 @@ def sendSends() -> None: raise TestError("Meros didn't broadcast a Send.") def verifyPossibleWon() -> None: - if rpc.call("consensus", "getStatus", [sends[1].hash.hex()])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": sends[1].hash.hex()})["verified"]: raise TestError("Meros verified an impossible Transaction.") - if not rpc.call("consensus", "getStatus", [sends[0].hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": sends[0].hash.hex()})["verified"]: raise TestError("Meros didn't verify the only possible Transaction.") Liver( diff --git a/e2e/Tests/Consensus/Families/LowerHashTieBreakTest.py b/e2e/Tests/Consensus/Families/LowerHashTieBreakTest.py index 5c781d286..f7387d69e 100644 --- a/e2e/Tests/Consensus/Families/LowerHashTieBreakTest.py +++ b/e2e/Tests/Consensus/Families/LowerHashTieBreakTest.py @@ -25,7 +25,7 @@ def verifyLowerHashWon() -> None: data: Data = datas[1] if int.from_bytes(data.hash, "little") > int.from_bytes(datas[2].hash, "little"): data = datas[2] - if not rpc.call("consensus", "getStatus", [data.hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": data.hash.hex()})["verified"]: raise TestError("Meros didn't verify the tied Transaction with a lower hash.") Liver( diff --git a/e2e/Tests/Consensus/Families/UnionedFamiliesMultipleWinnersTest.py b/e2e/Tests/Consensus/Families/UnionedFamiliesMultipleWinnersTest.py index a86b19d2c..d17356f76 100644 --- a/e2e/Tests/Consensus/Families/UnionedFamiliesMultipleWinnersTest.py +++ b/e2e/Tests/Consensus/Families/UnionedFamiliesMultipleWinnersTest.py @@ -23,10 +23,10 @@ def sendSends() -> None: def verifyMultipleWon() -> None: for send in [sends[1], *sends[3:]]: - if rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros verified a transaction which was beaten by another transaction.") for send in [sends[0], sends[2]]: - if not rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros didn't verify the verified transaction for each original family.") Liver( diff --git a/e2e/Tests/Consensus/Families/UnionedFamiliesSingleWinnerTest.py b/e2e/Tests/Consensus/Families/UnionedFamiliesSingleWinnerTest.py index 6ac4f8d30..3ff017e21 100644 --- a/e2e/Tests/Consensus/Families/UnionedFamiliesSingleWinnerTest.py +++ b/e2e/Tests/Consensus/Families/UnionedFamiliesSingleWinnerTest.py @@ -23,9 +23,9 @@ def sendSends() -> None: def verifyUnionizingWon() -> None: for send in sends[:-1]: - if rpc.call("consensus", "getStatus", [send.hash.hex()])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: raise TestError("Meros verified a transaction which was beaten by a unionizing transaction.") - if not rpc.call("consensus", "getStatus", [sends[-1].hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": sends[-1].hash.hex()})["verified"]: raise TestError("Meros didn't verify the verified unionizing transaction.") Liver( diff --git a/e2e/Tests/Consensus/Families/UnmentionedBeatMentionedTest.py b/e2e/Tests/Consensus/Families/UnmentionedBeatMentionedTest.py index 010e7fe2e..be7bbf882 100644 --- a/e2e/Tests/Consensus/Families/UnmentionedBeatMentionedTest.py +++ b/e2e/Tests/Consensus/Families/UnmentionedBeatMentionedTest.py @@ -29,7 +29,7 @@ def sendDatas() -> None: raise TestError("Meros didn't broadcast the Verification.") def verifyMentionedWon() -> None: - if not rpc.call("consensus", "getStatus", [datas[2].hash.hex()])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": datas[2].hash.hex()})["verified"]: raise TestError("Meros didn't verify the only Transaction on chain which has finalized.") Liver( diff --git a/e2e/Tests/Consensus/HundredSix/HundredSixSignedElementsTest.py b/e2e/Tests/Consensus/HundredSix/HundredSixSignedElementsTest.py index 64b5a5f52..e986243aa 100644 --- a/e2e/Tests/Consensus/HundredSix/HundredSixSignedElementsTest.py +++ b/e2e/Tests/Consensus/HundredSix/HundredSixSignedElementsTest.py @@ -3,15 +3,19 @@ from typing import List from time import sleep +import ed25519 from e2e.Libs.BLS import PrivateKey, Signature -from e2e.Classes.Merit.Blockchain import Blockchain +from e2e.Classes.Transactions.Data import Data +from e2e.Classes.Consensus.SpamFilter import SpamFilter from e2e.Classes.Consensus.Element import SignedElement from e2e.Classes.Consensus.Verification import SignedVerification from e2e.Classes.Consensus.SendDifficulty import SignedSendDifficulty from e2e.Classes.Consensus.DataDifficulty import SignedDataDifficulty +from e2e.Classes.Merit.Blockchain import Blockchain + from e2e.Meros.RPC import RPC from e2e.Tests.Errors import TestError @@ -22,24 +26,33 @@ def HundredSixSignedElementsTest( #Solely used to get the genesis Block hash. blockchain: Blockchain = Blockchain() + edPrivKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) blsPrivKey: PrivateKey = PrivateKey(0) sig: Signature = blsPrivKey.sign(bytes()) - #Create a Data. - #This is required so the Verification isn't terminated early for having an unknown hash. - data: bytes = bytes.fromhex(rpc.call("personal", "data", ["AA"])) + #Create a Data for the Verification. + data: Data = Data(bytes(32), edPrivKey.get_verifying_key().to_bytes()) + data.sign(edPrivKey) + data.beat(SpamFilter(5)) #Create a signed Verification, SendDifficulty, and DataDifficulty. elements: List[SignedElement] = [ - SignedVerification(data, 1, sig), + SignedVerification(data.hash, 1, sig), SignedSendDifficulty(0, 0, 1, sig), SignedDataDifficulty(0, 0, 1, sig) ] + dataSent: bool = False for elem in elements: #Handshake with the node. rpc.meros.liveConnect(blockchain.blocks[0].header.hash) + #Send the Data if we have yet to. + if not dataSent: + if rpc.meros.liveTransaction(data) != rpc.meros.live.recv(): + raise TestError("Data wasn't rebroadcasted.") + dataSent = True + #Send the Element. rpc.meros.signedElement(elem) diff --git a/e2e/Tests/Consensus/NodeThresholdTest.py b/e2e/Tests/Consensus/NodeThresholdTest.py index e301c8576..345bd977f 100644 --- a/e2e/Tests/Consensus/NodeThresholdTest.py +++ b/e2e/Tests/Consensus/NodeThresholdTest.py @@ -1,27 +1,47 @@ +from typing import List import json +import ed25519 + +from e2e.Classes.Transactions.Data import Data +from e2e.Classes.Consensus.SpamFilter import SpamFilter + from e2e.Meros.RPC import RPC from e2e.Meros.Liver import Liver from e2e.Tests.Errors import TestError -def NodeThresholdAdjustmentTest( +def NodeThresholdTest( rpc: RPC ) -> None: + edPrivKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + + dataFilter: SpamFilter = SpamFilter(5) + + datas: List[Data] = [Data(bytes(32), edPrivKey.get_verifying_key().to_bytes())] + datas[-1].sign(edPrivKey) + datas[-1].beat(dataFilter) + def verifyThreshold( b: int ) -> None: - data: str = rpc.call("personal", "data", ["aabb"]) + rpc.meros.liveTransaction(datas[-1]) + datas.append(Data(datas[-1].hash, b"a")) + datas[-1].sign(edPrivKey) + datas[-1].beat(dataFilter) + #Swallow the new Data(s). if b == 1: rpc.meros.live.recv() rpc.meros.live.recv() + + #Check the threshold. + threshold: int = rpc.call("consensus", "getStatus", {"hash": datas[-2].hash.hex()})["threshold"] if b < 9: - if rpc.call("consensus", "getStatus", [data])["threshold"] != (max(b + 6, 5) // 5 * 4) + 1: + if threshold != ((max(b + 6, 5) // 5 * 4) + 1): raise TestError("Meros didn't calculate the right node threshold. That said, this isn't defined by the protocol.") - else: - if rpc.call("consensus", "getStatus", [data])["threshold"] != 5: - raise TestError("Meros didn't lower the node threshold.") + elif threshold != 5: + raise TestError("Meros didn't lower the node threshold.") with open("e2e/Vectors/Merit/BlankBlocks.json", "r") as file: Liver(rpc, json.loads(file.read())[:9], everyBlock=verifyThreshold).live() diff --git a/e2e/Tests/Consensus/Verification/CompetingTest.py b/e2e/Tests/Consensus/Verification/CompetingTest.py index 86d81b255..a129a6947 100644 --- a/e2e/Tests/Consensus/Verification/CompetingTest.py +++ b/e2e/Tests/Consensus/Verification/CompetingTest.py @@ -22,10 +22,10 @@ def VCompetingTest( #Function to verify the right Transaction was confirmed. def verifyConfirmation() -> None: - if not rpc.call("consensus", "getStatus", [vectors["verified"]])["verified"]: + if not rpc.call("consensus", "getStatus", {"hash": vectors["verified"]})["verified"]: raise TestError("Didn't verify the Send which should have been verified.") - if rpc.call("consensus", "getStatus", [vectors["beaten"]])["verified"]: + if rpc.call("consensus", "getStatus", {"hash": vectors["beaten"]})["verified"]: raise TestError("Did verify the Send which should have been beaten.") Liver(rpc, vectors["blockchain"], transactions, callbacks={19: verifyConfirmation}).live() diff --git a/e2e/Tests/Consensus/Verification/HundredFiftyFiveTest.py b/e2e/Tests/Consensus/Verification/HundredFiftyFiveTest.py index e7b33dddf..b41ad81dd 100644 --- a/e2e/Tests/Consensus/Verification/HundredFiftyFiveTest.py +++ b/e2e/Tests/Consensus/Verification/HundredFiftyFiveTest.py @@ -33,7 +33,7 @@ def HundredFiftyFiveTest( edPrivKeys[1].get_verifying_key() ] - blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) blsPubKey: bytes = blsPrivKey.toPublicKey().serialize() blockchain: Blockchain = Blockchain() @@ -48,7 +48,9 @@ def HundredFiftyFiveTest( template: Dict[str, Any] = rpc.call( "merit", "getBlockTemplate", - [blsPubKey.hex()] + { + "miner": blsPubKey.hex() + } ) #Mine a Block. @@ -69,7 +71,14 @@ def HundredFiftyFiveTest( block.mine(blsPrivKey, blockchain.difficulty()) blockchain.add(block) - rpc.call("merit", "publishBlock", [template["id"], block.header.serialize().hex()]) + rpc.call( + "merit", + "publishBlock", + { + "id": template["id"], + "header": block.header.serialize().hex() + } + ) if MessageType(rpc.meros.live.recv()[0]) != MessageType.BlockHeader: raise TestError("Meros didn't broadcast the Block we just published.") diff --git a/e2e/Tests/Consensus/Verification/HundredFortyTwoTest.py b/e2e/Tests/Consensus/Verification/HundredFortyTwoTest.py index c9998fae6..84cc7acb5 100644 --- a/e2e/Tests/Consensus/Verification/HundredFortyTwoTest.py +++ b/e2e/Tests/Consensus/Verification/HundredFortyTwoTest.py @@ -28,7 +28,7 @@ def verifyUnarchivedMerit() -> None: if rpc.meros.signedElement(SignedVerification.fromSignedJSON(vectors["verification"])) != rpc.meros.live.recv(): raise TestError("Meros didn't send back the SignedVerification.") - status: Dict[str, Any] = rpc.call("consensus", "getStatus", [vectors["transaction"]]) + status: Dict[str, Any] = rpc.call("consensus", "getStatus", {"hash": vectors["transaction"]}) if sorted(status["verifiers"]) != [0, 1]: raise TestError("Status didn't include verifiers which have yet to be archived.") if status["merit"] != 7: @@ -36,7 +36,7 @@ def verifyUnarchivedMerit() -> None: #Function to verify the Transaction doesn't include unarchived Merit after finalization. def verifyArchivedMerit() -> None: - status: Dict[str, Any] = rpc.call("consensus", "getStatus", [vectors["transaction"]]) + status: Dict[str, Any] = rpc.call("consensus", "getStatus", {"hash": vectors["transaction"]}) if status["verifiers"] != [0]: raise TestError("Status included verifiers which were never archived.") if status["merit"] != 1: diff --git a/e2e/Tests/Consensus/Verification/HundredTwoTest.py b/e2e/Tests/Consensus/Verification/HundredTwoTest.py index b4c2b9e98..59e5a5904 100644 --- a/e2e/Tests/Consensus/Verification/HundredTwoTest.py +++ b/e2e/Tests/Consensus/Verification/HundredTwoTest.py @@ -22,14 +22,17 @@ def HundredTwoTest( #Verifies the Transaction is added, it has the right holders, the holders Merit surpasses the threshold, yet it isn't verified. def verify() -> None: for tx in transactions.txs: - status: Dict[str, Any] = rpc.call("consensus", "getStatus", [tx.hex()]) + status: Dict[str, Any] = rpc.call("consensus", "getStatus", {"hash": tx.hex()}) if set(status["verifiers"]) != set([0, 1]): raise TestError("Meros doesn't have the right list of verifiers for this Transaction.") if status["merit"] != 80: raise TestError("Meros doesn't have the right amount of Merit for this Transaction.") - if rpc.call("merit", "getMerit", [0])["merit"] + rpc.call("merit", "getMerit", [1])["merit"] < status["threshold"]: + if ( + rpc.call("merit", "getMerit", {"nick": 0})["merit"] + + rpc.call("merit", "getMerit", {"nick": 1})["merit"] + ) < status["threshold"]: raise TestError("Merit sum of holders is less than the threshold.") if status["verified"]: diff --git a/e2e/Tests/Consensus/Verification/PartialArchiveTest.py b/e2e/Tests/Consensus/Verification/PartialArchiveTest.py index 051d8c5ed..f66c58c8c 100644 --- a/e2e/Tests/Consensus/Verification/PartialArchiveTest.py +++ b/e2e/Tests/Consensus/Verification/PartialArchiveTest.py @@ -29,7 +29,7 @@ def PartialArchiveTest( SignedVerification.fromSignedJSON(vectors["verifs"][1]) ] - key: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + key: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) def sendDataAndVerifications() -> None: if rpc.meros.liveTransaction(data) != rpc.meros.live.recv(): @@ -40,12 +40,12 @@ def sendDataAndVerifications() -> None: #As we don't have a quality RPC route for this, we need to use getTemplate. if bytes.fromhex( - rpc.call("merit", "getBlockTemplate", [key.toPublicKey().serialize().hex()])["header"] + rpc.call("merit", "getBlockTemplate", {"miner": key.toPublicKey().serialize().hex()})["header"] )[36 : 68] != BlockHeader.createContents([VerificationPacket(data.hash, [0, 1])]): raise TestError("New Block template doesn't have a properly created packet.") def verifyRecreation() -> None: - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [key.toPublicKey().serialize().hex()]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": key.toPublicKey().serialize().hex()}) if bytes.fromhex(template["header"])[36 : 68] != BlockHeader.createContents([VerificationPacket(data.hash, [1])]): raise TestError("New Block template doesn't have a properly recreated packet.") @@ -67,10 +67,10 @@ def verifyRecreation() -> None: rpc.call( "merit", "publishBlock", - [ - template["id"], - (header + proof.to_bytes(4, byteorder="little") + sig).hex() - ] + { + "id": template["id"], + "header": (header + proof.to_bytes(4, byteorder="little") + sig).hex() + } ) raise SuccessError("Stop Liver from trying to verify the vector chain which doesn't have this Block.") diff --git a/e2e/Tests/Consensus/Verification/UnknownSignedTest.py b/e2e/Tests/Consensus/Verification/UnknownSignedTest.py index 65cc89cce..556d708b6 100644 --- a/e2e/Tests/Consensus/Verification/UnknownSignedTest.py +++ b/e2e/Tests/Consensus/Verification/UnknownSignedTest.py @@ -68,5 +68,5 @@ def VUnknownSignedTest( else: rpc.meros.syncTransaction(data) sleep(2) - if not rpc.call("consensus", "getStatus", [data.hash.hex()])["verifiers"]: + if not rpc.call("consensus", "getStatus", {"hash": data.hash.hex()})["verifiers"]: raise TestError("Meros didn't add the Verification.") diff --git a/e2e/Tests/Consensus/Verify.py b/e2e/Tests/Consensus/Verify.py index 59d501051..6b13269b8 100644 --- a/e2e/Tests/Consensus/Verify.py +++ b/e2e/Tests/Consensus/Verify.py @@ -34,7 +34,7 @@ def verifyMeritRemoval( if rpc.call("merit", "getTotalMerit") != total if pending else total - merit: raise TestError("Total Merit doesn't match.") - if rpc.call("merit", "getMerit", [holder]) != { + if rpc.call("merit", "getMerit", {"nick": holder}) != { "status": "Unlocked", "malicious": pending, "merit": merit if pending else 0 diff --git a/e2e/Tests/Merit/HundredSeventySevenTest.py b/e2e/Tests/Merit/HundredSeventySevenTest.py index 886c8887a..ee70c4430 100644 --- a/e2e/Tests/Merit/HundredSeventySevenTest.py +++ b/e2e/Tests/Merit/HundredSeventySevenTest.py @@ -26,7 +26,7 @@ def HundredSeventySevenTest( rpc: RPC ) -> None: #Grab the keys. - blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) blsPubKey: PublicKey = blsPrivKey.toPublicKey() #Faux Blockchain used to calculate the difficulty. @@ -37,7 +37,7 @@ def HundredSeventySevenTest( #The next 6 pop it from the Epochs. #One more is to verify the next is popped as well. for b in range(0, 8): - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey.serialize().hex()]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey.serialize().hex()}) template["header"] = bytes.fromhex(template["header"]) header: BlockHeader = BlockHeader( @@ -70,14 +70,14 @@ def HundredSeventySevenTest( rpc.call( "merit", "publishBlock", - [ - template["id"], - ( + { + "id": template["id"], + "header": ( template["header"] + header.proof.to_bytes(4, byteorder="little") + header.signature ).hex() - ] + } ) if rpc.meros.live.recv() != rpc.meros.liveBlockHeader(header): diff --git a/e2e/Tests/Merit/LockedMerit/KeepUnlockedTest.py b/e2e/Tests/Merit/LockedMerit/KeepUnlockedTest.py index d05ae11ea..b854af257 100644 --- a/e2e/Tests/Merit/LockedMerit/KeepUnlockedTest.py +++ b/e2e/Tests/Merit/LockedMerit/KeepUnlockedTest.py @@ -12,7 +12,7 @@ def KeepUnlockedTest( def verifyUnlocked( _: int ) -> None: - if rpc.call("merit", "getMerit", [0])["status"] != "Unlocked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Unlocked": raise TestError("Meros didn't keep Merit unlocked.") with open("e2e/Vectors/Merit/LockedMerit/KeepUnlocked.json", "r") as file: diff --git a/e2e/Tests/Merit/LockedMerit/LocksUnlocksTest.py b/e2e/Tests/Merit/LockedMerit/LocksUnlocksTest.py index 2b294306a..4c3b9595a 100644 --- a/e2e/Tests/Merit/LockedMerit/LocksUnlocksTest.py +++ b/e2e/Tests/Merit/LockedMerit/LocksUnlocksTest.py @@ -18,13 +18,13 @@ def verifyCorrectlyLocked( raise TestError("Meros didn't return the correct amount of Unlocked Merit.") if height < 9: - if rpc.call("merit", "getMerit", [0])["status"] != "Unlocked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Unlocked": raise TestError("Merit was locked early.") elif height == 9: - if rpc.call("merit", "getMerit", [0])["status"] != "Locked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Locked": raise TestError("Merit wasn't locked.") elif height < 19: - if rpc.call("merit", "getMerit", [0])["status"] != "Pending": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Pending": raise TestError("Merit was unlocked early.") elif height == 19: #This may be the first global used in this codebase. @@ -32,7 +32,7 @@ def verifyCorrectlyLocked( #pylint: disable=global-statement global correctVectorsHeight correctVectorsHeight = True - if rpc.call("merit", "getMerit", [0])["status"] != "Unlocked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Unlocked": raise TestError("Merit wasn't unlocked.") with open("e2e/Vectors/Merit/LockedMerit/LocksUnlocks.json", "r") as file: diff --git a/e2e/Tests/Merit/LockedMerit/PendingDieRegainTest.py b/e2e/Tests/Merit/LockedMerit/PendingDieRegainTest.py index b74064e36..0cda0d81f 100644 --- a/e2e/Tests/Merit/LockedMerit/PendingDieRegainTest.py +++ b/e2e/Tests/Merit/LockedMerit/PendingDieRegainTest.py @@ -13,7 +13,7 @@ def PendingDieRegainTest( def verifyCorrectlyLocked( height: int ) -> None: - merit: Dict[str, Any] = rpc.call("merit", "getMerit", [0]) + merit: Dict[str, Any] = rpc.call("merit", "getMerit", {"nick": 0}) merit = { "merit": merit["merit"], "status": merit["status"] diff --git a/e2e/Tests/Merit/LockedMerit/TwoHundredThirtyFiveTest.py b/e2e/Tests/Merit/LockedMerit/TwoHundredThirtyFiveTest.py index 5d77ab599..87975c96e 100644 --- a/e2e/Tests/Merit/LockedMerit/TwoHundredThirtyFiveTest.py +++ b/e2e/Tests/Merit/LockedMerit/TwoHundredThirtyFiveTest.py @@ -29,7 +29,7 @@ def TwoHundredThirtyFiveTest( edPubKey: ed25519.VerifyingKey = edPrivKey.get_verifying_key() #Mine one Block to the node. - blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) blsPubKey: bytes = blsPrivKey.toPublicKey().serialize() #Call getBlockTemplate just to get an ID. @@ -37,7 +37,7 @@ def TwoHundredThirtyFiveTest( template: Dict[str, Any] = rpc.call( "merit", "getBlockTemplate", - [blsPubKey.hex()] + {"miner": blsPubKey.hex()} ) #Mine a Block. @@ -58,7 +58,14 @@ def TwoHundredThirtyFiveTest( block.mine(blsPrivKey, blockchain.difficulty()) blockchain.add(block) - rpc.call("merit", "publishBlock", [template["id"], block.header.serialize().hex()]) + rpc.call( + "merit", + "publishBlock", + { + "id": template["id"], + "header": block.header.serialize().hex() + } + ) #Send Meros a Data and receive its Verification to make sure it's verifying Transactions in the first place. data: Data = Data(bytes(32), edPubKey.to_bytes()) @@ -112,7 +119,7 @@ def TwoHundredThirtyFiveTest( #Therefore, if the above last Block had its Data verified, this issue should be closed. #That said, the timing is a bit too tight for comfort. #Better safe than sorry. Hence why the code after this check exists. - if rpc.call("merit", "getMerit", [0])["status"] != "Locked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Locked": raise TestError("Merit wasn't locked when it was supposed to be.") #Send it a Transaction and make sure Meros verifies it, despite having its Merit locked. diff --git a/e2e/Tests/Merit/Reorganizations/TwoHundredSixtyOneTest.py b/e2e/Tests/Merit/Reorganizations/TwoHundredSixtyOneTest.py index 56a11acd7..8184d6f78 100644 --- a/e2e/Tests/Merit/Reorganizations/TwoHundredSixtyOneTest.py +++ b/e2e/Tests/Merit/Reorganizations/TwoHundredSixtyOneTest.py @@ -21,11 +21,11 @@ def TwoHundredSixtyOneTest( ) -> None: merit: Merit = Merit() - blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) blsPubKey: str = blsPrivKey.toPublicKey().serialize().hex() #Get a template. - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey}) template["header"] = bytes.fromhex(template["header"]) #Mine it. @@ -40,7 +40,14 @@ def TwoHundredSixtyOneTest( rpc.meros.syncConnect(merit.blockchain.blocks[0].header.hash) #Publish it. - rpc.call("merit", "publishBlock", [template["id"], block.header.serialize().hex()]) + rpc.call( + "merit", + "publishBlock", + { + "id": template["id"], + "header": block.header.serialize().hex() + } + ) if MessageType(rpc.meros.live.recv()[0]) != MessageType.BlockHeader: raise TestError("Meros didn't broadcast a published Block.") diff --git a/e2e/Tests/Merit/Reorganizations/TwoHundredThirtyTwoTest.py b/e2e/Tests/Merit/Reorganizations/TwoHundredThirtyTwoTest.py index 2359de96d..72a94b385 100644 --- a/e2e/Tests/Merit/Reorganizations/TwoHundredThirtyTwoTest.py +++ b/e2e/Tests/Merit/Reorganizations/TwoHundredThirtyTwoTest.py @@ -58,14 +58,14 @@ def sendBlock( #Cause the re-organization to fail. rpc.meros.live.connection.close() rpc.meros.sync.connection.close() - rpc.socket.close() sleep(35) #Reboot the node to reload the database. rpc.meros.quit() - rpc.meros.calledQuit = False - rpc.meros.process = Popen(["./build/Meros", "--data-dir", rpc.meros.dataDir, "--log-file", rpc.meros.log, "--db", rpc.meros.db, "--network", "devnet", "--tcp-port", str(rpc.meros.tcp), "--rpc-port", str(rpc.meros.rpc), "--no-gui"]) + #Reset the RPC's tracking variables. + rpc.meros.calledQuit = False + rpc.meros.process = Popen(["./build/Meros", "--data-dir", rpc.meros.dataDir, "--log-file", rpc.meros.log, "--db", rpc.meros.db, "--network", "devnet", "--token", "TEST_TOKEN", "--tcp-port", str(rpc.meros.tcp), "--rpc-port", str(rpc.meros.rpc), "--no-gui"]) while True: try: connection: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -80,6 +80,4 @@ def sendBlock( rpc.meros.syncConnect(main.blocks[0].header.hash) sendBlock(main.blocks[2]) - rpc.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - rpc.socket.connect(("127.0.0.1", rpc.meros.rpc)) verifyBlockchain(rpc, main) diff --git a/e2e/Tests/Merit/StateTest.py b/e2e/Tests/Merit/StateTest.py index 8f1177519..beaa524e3 100644 --- a/e2e/Tests/Merit/StateTest.py +++ b/e2e/Tests/Merit/StateTest.py @@ -31,7 +31,7 @@ def checkState( meritSum: int = 0 for miner in range(len(state.balances)): meritSum += state.balances[miner] - if rpc.call("merit", "getMerit", [miner]) != { + if rpc.call("merit", "getMerit", {"nick": miner}) != { "status": "Unlocked", "malicious": False, "merit": state.balances[miner] diff --git a/e2e/Tests/Merit/Templates/EightyEightTest.py b/e2e/Tests/Merit/Templates/EightyEightTest.py index 5f505eeb6..6eb33380d 100644 --- a/e2e/Tests/Merit/Templates/EightyEightTest.py +++ b/e2e/Tests/Merit/Templates/EightyEightTest.py @@ -73,20 +73,20 @@ def EightyEightTest( #Only transmit the second. rpc.meros.signedElement(verifs[1]) - sleep(0.5) + sleep(1.5) #Verify the block template has no verifications. if bytes.fromhex( - rpc.call("merit", "getBlockTemplate", [blsPubKey])["header"] + rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey})["header"] )[36 : 68] != bytes(32): raise TestError("Block template has Verification Packets.") #Transmit the first signed verification. rpc.meros.signedElement(verifs[0]) - sleep(0.5) + sleep(1.5) #Verify the block template has both verifications. - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey}) template["header"] = bytes.fromhex(template["header"]) packets: List[VerificationPacket] = [VerificationPacket(datas[0].hash, [0]), VerificationPacket(datas[1].hash, [0])] if template["header"][36 : 68] != BlockHeader.createContents(packets): @@ -119,14 +119,14 @@ def EightyEightTest( rpc.call( "merit", "publishBlock", - [ - template["id"], - ( + { + "id": template["id"], + "header": ( template["header"] + block.header.proof.to_bytes(4, byteorder="little") + block.header.signature ).hex() - ] + } ) verifyBlockchain(rpc, merit.blockchain) diff --git a/e2e/Tests/Merit/Templates/TElementTest.py b/e2e/Tests/Merit/Templates/TElementTest.py index 91a5f2b1e..537dc6318 100644 --- a/e2e/Tests/Merit/Templates/TElementTest.py +++ b/e2e/Tests/Merit/Templates/TElementTest.py @@ -44,10 +44,10 @@ def TElementTest( dataDiff: SignedDataDifficulty = SignedDataDifficulty(0, 0, 0) dataDiff.sign(0, blsPrivKey) rpc.meros.signedElement(dataDiff) - sleep(0.5) + sleep(1.5) #Verify the block template has the DataDifficulty. - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey}) template["header"] = bytes.fromhex(template["header"]) if template["header"][36 : 68] != BlockHeader.createContents([], [dataDiff]): raise TestError("Block template doesn't have the Data Difficulty.") @@ -76,25 +76,25 @@ def TElementTest( rpc.call( "merit", "publishBlock", - [ - template["id"], - ( + { + "id": template["id"], + "header": ( template["header"] + block.header.proof.to_bytes(4, byteorder="little") + block.header.signature ).hex() - ] + } ) #Create and transmit a new DataDifficulty. dataDiff = SignedDataDifficulty(3, 0, 0) dataDiff.sign(0, blsPrivKey) rpc.meros.signedElement(dataDiff) - sleep(0.5) + sleep(1.5) #Verify the block template has a MeritRemoval. #Thanks to implicit Merit Removals, this just means it has the new difficulty. - template = rpc.call("merit", "getBlockTemplate", [blsPubKey]) + template = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey}) template["header"] = bytes.fromhex(template["header"]) if template["header"][36 : 68] != BlockHeader.createContents([], [dataDiff]): raise TestError("Block template doesn't have the Merit Removal.") @@ -122,14 +122,14 @@ def TElementTest( rpc.call( "merit", "publishBlock", - [ - template["id"], - ( + { + "id": template["id"], + "header": ( template["header"] + block.header.proof.to_bytes(4, byteorder="little") + block.header.signature ).hex() - ] + } ) verifyBlockchain(rpc, merit.blockchain) diff --git a/e2e/Tests/Merit/TwoHundredFortyTest.py b/e2e/Tests/Merit/TwoHundredFortyTest.py index 13980805b..fd7b08a96 100644 --- a/e2e/Tests/Merit/TwoHundredFortyTest.py +++ b/e2e/Tests/Merit/TwoHundredFortyTest.py @@ -21,7 +21,7 @@ def TwoHundredFourtyTest( rpc: RPC ) -> None: #Grab the keys. - blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMiner"))) + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) blsPubKey: PublicKey = blsPrivKey.toPublicKey() #Blockchain used to calculate the difficulty. @@ -29,7 +29,7 @@ def TwoHundredFourtyTest( #Mine enough blocks to lose Merit. for b in range(9): - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey.serialize().hex()]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey.serialize().hex()}) template["header"] = bytes.fromhex(template["header"]) header: BlockHeader = BlockHeader( @@ -67,10 +67,10 @@ def TwoHundredFourtyTest( pass #Verify our Merit is locked. - if rpc.call("merit", "getMerit", [0])["status"] != "Locked": + if rpc.call("merit", "getMerit", {"nick": 0})["status"] != "Locked": raise Exception("Our Merit isn't locked so this test is invalid.") - template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", [blsPubKey.serialize().hex()]) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPubKey.serialize().hex()}) template["header"] = bytes.fromhex(template["header"]) header: BlockHeader = BlockHeader( @@ -91,19 +91,18 @@ def TwoHundredFourtyTest( rpc.call( "merit", "publishBlock", - [ - template["id"], - ( + { + "id": template["id"], + "header": ( template["header"] + header.proof.to_bytes(4, byteorder="little") + header.signature ).hex() - ] + } ) #To verify the entire chain, we just need to verify this last header. #This is essential as our chain isn't equivalent. ourHeader: Dict[str, Any] = header.toJSON() - del ourHeader["packets"] - if rpc.call("merit", "getBlock", [header.hash.hex()])["header"] != ourHeader: + if rpc.call("merit", "getBlock", {"block": header.hash.hex()})["header"] != ourHeader: raise TestError("Header wasn't added to the blockchain.") diff --git a/e2e/Tests/Merit/Verify.py b/e2e/Tests/Merit/Verify.py index 24816380f..3380f1183 100644 --- a/e2e/Tests/Merit/Verify.py +++ b/e2e/Tests/Merit/Verify.py @@ -21,10 +21,7 @@ def verifyBlockchain( for b in range(len(blockchain.blocks)): ourBlock: Dict[str, Any] = blockchain.blocks[b].toJSON() - #Info Python saves so it can properly load from the vectors yet the Meros RPC excludes. - del ourBlock["header"]["packets"] - - blockJSON: Dict[str, Any] = rpc.call("merit", "getBlock", [b]) + blockJSON: Dict[str, Any] = rpc.call("merit", "getBlock", {"block": b}) #Contextual info Python doesn't track. del blockJSON["removals"] if blockJSON != ourBlock: @@ -34,7 +31,7 @@ def verifyBlockchain( blockJSON = rpc.call( "merit", "getBlock", - [blockchain.blocks[b].header.hash.hex().upper()] + {"block": blockchain.blocks[b].header.hash.hex().upper()} ) del blockJSON["removals"] if blockJSON != ourBlock: diff --git a/e2e/Tests/RPC/AddressTest.py b/e2e/Tests/RPC/AddressTest.py index fa4dd0b78..e6f2a425e 100644 --- a/e2e/Tests/RPC/AddressTest.py +++ b/e2e/Tests/RPC/AddressTest.py @@ -1,3 +1,10 @@ +#Tests handling of Addresses by the RPC. +#Checks checksum mutability such as https://github.com/sipa/bech32/issues/51. +#Also checks: +# - Blatantly incorrect checksums. +# - Incorrect lengths. +# - Unsupported address types. + from typing import Union, List, Tuple import os @@ -5,7 +12,7 @@ from bech32 import CHARSET, convertbits, bech32_encode, bech32_decode from e2e.Meros.RPC import RPC -from e2e.Tests.Errors import TestError +from e2e.Tests.Errors import MessageException, TestError def encodeAddress( data: bytes @@ -18,47 +25,49 @@ def test( invalid: bool, msg: str ) -> None: + if isinstance(address, bytes): + address = encodeAddress(address) + try: - if isinstance(address, bytes): - address = encodeAddress(address) - rpc.call("personal", "send", [address, "1"]) - #Raise a TestError with a different code than expected to ensure the below check is run and fails. - raise TestError("0 ") + rpc.call("transactions", "getBalance", {"address": address}, False) + #If the call passed, and the address is invalid, raise. + if invalid: + raise MessageException(msg) except TestError as e: - if int(e.message.split(" ")[0]) != (-3 if invalid else 1): + if int(e.message.split(" ")[0]) != -32602: + raise Exception("Non-ParamError was raised by this RPC call, which shouldn't be able to raise anything else.") + if not invalid: raise TestError(msg) + except MessageException as e: + raise TestError(e.message) def AddressTest( rpc: RPC ) -> None: - #Sanity test. - test(rpc, bytes(33), False, "Meros didn't use the NotEnoughMeros error when trying to send while having 0 Meros.") - #Test a variety of valid addresses. for _ in range(50): - test(rpc, bytes([0]) + os.urandom(32), False, "Meros didn't use the NotEnoughMeros error when trying to send while having 0 Meros.") + test(rpc, bytes([0]) + os.urandom(32), False, "Meros rejected a valid address.") #Invalid checksum. - invalidChecksum: str = encodeAddress(bytes(33)) + invalidChecksum: str = encodeAddress(os.urandom(33)) if invalidChecksum[-1] != 'q': invalidChecksum = invalidChecksum[:-1] + 'q' else: invalidChecksum = invalidChecksum[:-1] + 't' - test(rpc, invalidChecksum, True, "Meros accepted an address with an invalid checksum") + test(rpc, invalidChecksum, True, "Meros accepted an address with an invalid checksum.") - #Invalid version byte. 255 was used as it's expected version bytes will become VarInts if ever needed. - #That said, even 127 is high enough we're likely to never come close. - test(rpc, bytes([255]) + bytes(32), True, "Meros accepted an address with an invalid version byte.") + #Invalid version byte. + test(rpc, bytes([255]) + os.urandom(32), True, "Meros accepted an address with an invalid version byte.") #Invalid length. - test(rpc, bytes(32), True, "Meros accepted an address with an invalid length.") - test(rpc, bytes(34), True, "Meros accepted an address with an invalid length.") + test(rpc, os.urandom(32), True, "Meros accepted an address with an invalid length.") + test(rpc, os.urandom(34), True, "Meros accepted an address with an invalid length.") - #Create a random address for us to mutate. + #Create a random address for us to mutate while preserving the checksum. randomKey: bytes = os.urandom(32) unchanged: str = encodeAddress(bytes([0]) + randomKey) #Sanity check against it. - test(rpc, unchanged, False, "Meros didn't use the NotEnoughMeros error when trying to send while having 0 Meros.") + test(rpc, unchanged, False, "Meros rejected a valid address.") #Mutate it as described in https://github.com/sipa/bech32/issues/51#issuecomment-496797984. #Since we can insert any amount of 'q's, run this ten times. @@ -75,6 +84,6 @@ def AddressTest( #Sanity check that our mutation worked. decoded: Union[Tuple[None, None], Tuple[str, List[int]]] = bech32_decode(mutated) if decoded is Tuple[None, None]: - raise TestError("Mutation stopped the checksum from passing.") + raise Exception("Mutation stopped the checksum from passing.") test(rpc, mutated, True, "Meros accepted an address which had been mutated yet still passed the checksum.") diff --git a/e2e/Tests/RPC/BatchTest.py b/e2e/Tests/RPC/BatchTest.py new file mode 100644 index 000000000..1354d23f4 --- /dev/null +++ b/e2e/Tests/RPC/BatchTest.py @@ -0,0 +1,133 @@ +from typing import Dict, List, Union, Any + +import requests + +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError + +def request( + rpc: RPC, + req: Union[List[Any], Dict[str, Any]], + headers: Dict[str, str] = {} +) -> Union[List[Any], Dict[str, Any]]: + res: requests.Response = requests.post( + "http://127.0.0.1:" + str(rpc.meros.rpc), + headers=headers, + json=req + ) + if res.status_code != 200: + raise TestError("HTTP status isn't 200: " + str(res.status_code)) + return res.json() + +def BatchTest( + rpc: RPC +) -> None: + #Most basic case; two valid requests. + if request( + rpc, + [ + {"jsonrpc": "2.0", "id": 1, "method": "merit_getHeight"}, + {"jsonrpc": "2.0", "id": 0, "method": "merit_getDifficulty"} + ] + ) != [ + {"jsonrpc": "2.0", "id": 1, "result": 1}, + {"jsonrpc": "2.0", "id": 0, "result": Blockchain().difficulty()} + ]: + raise TestError("Meros didn't respond to a batch request properly.") + + #Test handling of empty batches. + if request(rpc, []) != {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}: + raise TestError("Empty batch wasn't handled correctly.") + + #Batches with invalid individual requests. + if request(rpc, [1, 2, 3]) != [ + {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}, + {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}, + {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None} + ]: + raise TestError("Batch with invalid individual entries wasn't handled correctly.") + + if request( + rpc, + [1, {"jsonrpc": "2.0", "id": 1, "method": "merit_getHeight"}, 2] + ) != [ + {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}, + {"jsonrpc": "2.0", "id": 1, "result": 1}, + {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None} + ]: + raise TestError("Batch with some invalid individual entries wasn't handled correctly.") + + #Test authorization. + #If the token is passed, calling multiple methods requiring authorization should work. + #If not passing a token, calling multiple methods not requiring authorization should work. This is tested implicitly by the first test case here. + #If not passing a token, or passing an invalid token, calling any method requiring auth should cause the entire request to 401. + multipleAuthed: Union[List[Any], Dict[str, Any]] = request( + rpc, + [ + {"jsonrpc": "2.0", "id": 0, "method": "personal_setWallet"}, + {"jsonrpc": "2.0", "id": 1, "method": "personal_getMnemonic"}, + ], + {"Authorization": "Bearer TEST_TOKEN"} + ) + if isinstance(multipleAuthed, List): + if multipleAuthed != [ + {"jsonrpc": "2.0", "id": 0, "result": True}, + #The Mnemonic will be random, hence this. + {"jsonrpc": "2.0", "id": 1, "result": multipleAuthed[1]["result"]} + ]: + raise TestError("Batch request didn't work when it had multiple methods requiring authentication.") + else: + raise TestError("Response to a batch request wasn't a list.") + + #Not passing a token. + try: + request( + rpc, + [ + {"jsonrpc": "2.0", "id": 0, "method": "merit_getHeight"}, + {"jsonrpc": "2.0", "id": 1, "method": "personal_setWallet"}, + {"jsonrpc": "2.0", "id": 2, "method": "merit_getHeight"} + ] + ) + raise Exception() + except Exception as e: + if str(e) != "HTTP status isn't 200: 401": + raise TestError("Meros didn't respond to a batch request without authorization yet needing it as expected.") + + #Invalid token. + try: + request( + rpc, + [ + {"jsonrpc": "2.0", "id": 0, "method": "merit_getHeight"}, + {"jsonrpc": "2.0", "id": 1, "method": "personal_setWallet"}, + {"jsonrpc": "2.0", "id": 2, "method": "merit_getHeight"} + ], + {"Authorization": "Bearer INVALID_TOKEN"} + ) + raise Exception() + except Exception as e: + if str(e) != "HTTP status isn't 200: 401": + raise TestError("Meros didn't respond to a batch request without authorization yet needing it as expected.") + + #Test batch requests containing quit. + #Meros should return responses for all requests it handled before quit, yet still quit without further handling. + if request( + rpc, + [ + {"jsonrpc": "2.0", "id": 0, "method": "merit_getHeight"}, + {"jsonrpc": "2.0", "id": 1, "method": "system_quit"}, + {"jsonrpc": "2.0", "id": 2, "method": "merit_getDifficulty"}, + ], + {"Authorization": "Bearer TEST_TOKEN"} + ) != [ + {"jsonrpc": "2.0", "id": 0, "result": 1}, + {"jsonrpc": "2.0", "id": 1, "result": True} + ]: + raise TestError("Meros didn't respond to a batch request containing quit as expected.") + + #Mark Meros as having called quit so teardown works. + rpc.meros.calledQuit = True diff --git a/e2e/Tests/RPC/Consensus/GetDifficultyTest.py b/e2e/Tests/RPC/Consensus/GetDifficultyTest.py new file mode 100644 index 000000000..fbaafb08a --- /dev/null +++ b/e2e/Tests/RPC/Consensus/GetDifficultyTest.py @@ -0,0 +1,73 @@ +import json + +from e2e.Classes.Consensus.SendDifficulty import SignedSendDifficulty +from e2e.Classes.Consensus.DataDifficulty import SignedDataDifficulty + +from e2e.Libs.BLS import PrivateKey + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def GetDifficultyTest( + rpc: RPC +) -> None: + #Check the global difficulty. + if rpc.call("consensus", "getSendDifficulty", auth=False) != 3: + raise TestError("getSendDifficulty didn't reply properly.") + if rpc.call("consensus", "getDataDifficulty", auth=False) != 5: + raise TestError("getDataDifficulty didn't reply properly.") + + #Check the difficulties for a holder who doesn't exist. + try: + rpc.call("consensus", "getSendDifficulty", {"holder": 0}, False) + raise TestError("") + except TestError as e: + if str(e) != "-2 Holder doesn't have a SendDifficulty.": + raise TestError("getSendDifficulty didn't raise when asked about a non-existent holder.") + + try: + rpc.call("consensus", "getDataDifficulty", {"holder": 0}, False) + raise TestError("") + except TestError as e: + if str(e) != "-2 Holder doesn't have a DataDifficulty.": + raise TestError("getDataDifficulty didn't raise when asked about a non-existent holder.") + + def voteAndVerify() -> None: + #Check the difficulties for a holder who has yet to vote. + try: + rpc.call("consensus", "getSendDifficulty", {"holder": 0}, False) + raise TestError("") + except TestError as e: + if str(e) != "-2 Holder doesn't have a SendDifficulty.": + raise TestError("getSendDifficulty didn't raise when asked about a holder who has yet to vote.") + try: + rpc.call("consensus", "getDataDifficulty", {"holder": 0}, False) + raise TestError("") + except TestError as e: + if str(e) != "-2 Holder doesn't have a DataDifficulty.": + raise TestError("getDataDifficulty didn't raise when asked about a holder who has yet to vote.") + + #Create the votes. + sendDiff: SignedSendDifficulty = SignedSendDifficulty(6, 0) + sendDiff.sign(0, PrivateKey(0)) + + dataDiff: SignedDataDifficulty = SignedDataDifficulty(10, 1) + dataDiff.sign(0, PrivateKey(0)) + + #Send them. + rpc.meros.signedElement(sendDiff) + rpc.meros.signedElement(dataDiff) + rpc.meros.live.recv() + rpc.meros.live.recv() + + #Check them. + if rpc.call("consensus", "getSendDifficulty", {"holder": 0}, False) != 6: + raise TestError("getSendDifficulty didn't reply with the holder's current difficulty.") + if rpc.call("consensus", "getDataDifficulty", {"holder": 0}, False) != 10: + raise TestError("getDataDifficulty didn't reply with the holder's current difficulty.") + + with open("e2e/Vectors/Merit/BlankBlocks.json", "r") as file: + Liver(rpc, json.loads(file.read())[:1], callbacks={1: voteAndVerify}).live() diff --git a/e2e/Tests/RPC/HTTP/ChunkedEncodingTest.py b/e2e/Tests/RPC/HTTP/ChunkedEncodingTest.py new file mode 100644 index 000000000..9aac4b9bc --- /dev/null +++ b/e2e/Tests/RPC/HTTP/ChunkedEncodingTest.py @@ -0,0 +1,48 @@ +from typing import Iterator +import json + +import requests + +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError + +def reqIter() -> Iterator[bytes]: + yield b"[" + yield json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "merit_getHeight" + } + ).encode() + b"," + yield json.dumps( + { + "jsonrpc": "2.0", + "id": 0, + "method": "merit_getDifficulty" + } + ).encode() + yield b"]" + +def ChunkedEncodingTest( + rpc: RPC +) -> None: + request: requests.Response = requests.post("http://127.0.0.1:" + str(rpc.meros.rpc), data=reqIter()) + if request.status_code != 200: + raise TestError("HTTP status isn't 200: " + str(request.status_code)) + if request.json() != [ + { + "jsonrpc": "2.0", + "id": 1, + "result": 1 + }, + { + "jsonrpc": "2.0", + "id": 0, + "result": Blockchain().difficulty() + } + ]: + raise TestError("Meros didn't respond to a batch request (sent as chunks) properly.") diff --git a/e2e/Tests/RPC/HTTP/HTTP100Test.py b/e2e/Tests/RPC/HTTP/HTTP100Test.py new file mode 100644 index 000000000..5442139e1 --- /dev/null +++ b/e2e/Tests/RPC/HTTP/HTTP100Test.py @@ -0,0 +1,65 @@ +import socket +import json + +from e2e.Meros.Meros import Meros + +from e2e.Tests.Errors import TestError + +CURL_100_CONTINUE: str = """POST / HTTP/1.1 +Host: 127.0.0.1:5133 +User-Agent: curl/7.75.0 +Accept: */* +Content-Length: 59 +Content-Type: application/x-www-form-urlencoded +Expect: 100-continue\r\n\r\n""" + +#Doesn't support \r line endings. +def readLine( + conn: socket.socket +) -> str: + res: str = conn.recv(1).decode() + while res[-1] != "\n": + res += conn.recv(1).decode() + res = res[:-1] + + #Support \r\n. + if res[-1] == "\r": + res = res[:-1] + + return res + +def HTTP100Test( + meros: Meros +) -> None: + conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.connect(("127.0.0.1", meros.rpc)) + conn.send(CURL_100_CONTINUE.encode()) + + res: str = "" + last: str = " " + while last != "": + last = readLine(conn) + res += last + "\n" + + if res.split("\n")[0] != "HTTP/1.1 100 Continue": + raise TestError("Meros didn't respond with 100 Continue.") + + conn.send(b"""{"jsonrpc": "2.0", "id": null, "method": "merit_getHeight"}""") + + res = "" + last = " " + contentLength: int = -1 + while last != "": + last = readLine(conn) + if last.startswith("Content-Length"): + contentLength = int(last.split(" ")[1]) + res += last + "\n" + + if res.split("\n")[0] != "HTTP/1.1 200 OK": + raise TestError("Meros didn't respond with 200 OK.") + if json.loads(conn.recv(contentLength).decode()) != { + "jsonrpc": "2.0", + "id": None, + "result": 1 + }: + raise TestError("Meros didn't respond to our request.") diff --git a/e2e/Tests/RPC/HTTP/HTTP401Test.py b/e2e/Tests/RPC/HTTP/HTTP401Test.py new file mode 100644 index 000000000..e8b645958 --- /dev/null +++ b/e2e/Tests/RPC/HTTP/HTTP401Test.py @@ -0,0 +1,28 @@ +import requests + +from e2e.Meros.Meros import Meros +from e2e.Tests.Errors import TestError + +def HTTP401Test( + meros: Meros +) -> None: + for val in ["", "Bearer", "Bearer ", "Bearer X", "Bearer TEST", "NotBearer TEST_TOKEN"]: + request: requests.Response = requests.post( + "http://127.0.0.1:" + str(meros.rpc), + json={ + "jsonrpc": "2.0", + "id": 0, + #Route which requires auth. Any would work. + "method": "transactions_publishTransactionWithoutWork", + #Stub params to ensure they're a non-issue. + "params": { + "type": "Data", + "transaction": "" + } + }, + headers={ + "Authorization": val + } + ) + if request.status_code != 401: + raise TestError("Meros didn't return 401 Unauthorized to an invalid authorization.") diff --git a/e2e/Tests/RPC/HTTP/NewLineTest.py b/e2e/Tests/RPC/HTTP/NewLineTest.py new file mode 100644 index 000000000..411502e74 --- /dev/null +++ b/e2e/Tests/RPC/HTTP/NewLineTest.py @@ -0,0 +1,42 @@ +from typing import List +import socket + +from e2e.Meros.Meros import Meros + +from e2e.Tests.Errors import TestError + +REQUEST: List[str] = [ + "POST / HTTP/1.1", + "Accept: */*", + "Content-Length: 59", + "Content-Type: application/x-www-form-urlencoded", + "", + """{"jsonrpc": "2.0", "id": null, "method": "merit_getHeight"}""" +] + +#Doesn't support \r line endings. +def readLine( + conn: socket.socket +) -> str: + res: str = conn.recv(1).decode() + while res[-1] != "\n": + res += conn.recv(1).decode() + res = res[:-1] + + #Support \r\n. + if res[-1] == "\r": + res = res[:-1] + + return res + +def NewLineTest( + meros: Meros +) -> None: + for ending in ["\r", "\n", "\r\n"]: + conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.connect(("127.0.0.1", meros.rpc)) + conn.send(ending.join(REQUEST).encode()) + + if readLine(conn) != "HTTP/1.1 200 OK": + raise TestError("Meros didn't respond with 200 OK.") + conn.close() diff --git a/e2e/Tests/RPC/IntegerBoundTest.py b/e2e/Tests/RPC/IntegerBoundTest.py new file mode 100644 index 000000000..ae715d4e8 --- /dev/null +++ b/e2e/Tests/RPC/IntegerBoundTest.py @@ -0,0 +1,42 @@ +from e2e.Meros.RPC import RPC +from e2e.Tests.Errors import TestError + +def IntegerBoundTest( + rpc: RPC +) -> None: + #uint16. + try: + rpc.call("merit", "getPublicKey", {"nick": -1}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a negative integer for an unsigned integer.") + + try: + rpc.call("merit", "getPublicKey", {"nick": 65536}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a too large unsigned integer.") + + #uint. + try: + rpc.call("merit", "getBlock", {"block": -1}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a negative integer for an unsigned integer.") + + try: + rpc.call("merit", "getBlock", {"block": (2 ** 63)}) + raise TestError() + except Exception as e: + if str(e) != "-32700 Parse error.": + raise TestError("Meros parsed an integer outside of the int64 bounds.") + + try: + rpc.call("merit", "getBlock", {"block": (2 ** 64)}) + raise TestError() + except Exception as e: + if str(e) != "-32700 Parse error.": + raise TestError("Meros parsed an integer outside of the uint64 bounds.") diff --git a/e2e/Tests/RPC/InvalidRequestTest.py b/e2e/Tests/RPC/InvalidRequestTest.py new file mode 100644 index 000000000..e40e563b8 --- /dev/null +++ b/e2e/Tests/RPC/InvalidRequestTest.py @@ -0,0 +1,40 @@ +from typing import Any +import json + +import requests + +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError + +def request( + rpc: RPC, + req: Any +): + res: requests.Response = requests.post( + "http://127.0.0.1:" + str(rpc.meros.rpc), + data=json.dumps(req) + ) + if res.status_code != 200: + raise TestError("HTTP status isn't 200: " + str(res.status_code)) + if res.json() != {"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}}: + raise TestError("Invalid request wasn't considered invalid.") + +def InvalidRequestTest( + rpc: RPC +) -> None: + #Try a bool as a request object. + request(rpc, True) + #Try a string. + request(rpc, "") + #Try an int. + request(rpc, 5) + #Try a float. + request(rpc, 1.0) + #Try null. + request(rpc, None) + + #Empty object. + request(rpc, {}) + #Valid request except the ID is an object. + request(rpc, {"jsonrpc": "2.0", "id": {}, "method": "merit_getHeight"}) diff --git a/e2e/Tests/RPC/Merit/GetBlockTemplateTest.py b/e2e/Tests/RPC/Merit/GetBlockTemplateTest.py new file mode 100644 index 000000000..5666d2476 --- /dev/null +++ b/e2e/Tests/RPC/Merit/GetBlockTemplateTest.py @@ -0,0 +1,307 @@ +from typing import Dict, List, Any +import time +import json + +import ed25519 + +from e2e.Libs.RandomX import RandomX +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Data import Data + +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Consensus.Verification import SignedVerification +from e2e.Classes.Consensus.VerificationPacket import VerificationPacket +from e2e.Classes.Consensus.SendDifficulty import SignedSendDifficulty +from e2e.Classes.Consensus.DataDifficulty import SignedDataDifficulty + +from e2e.Classes.Merit.BlockHeader import BlockHeader +from e2e.Classes.Merit.BlockBody import BlockBody +from e2e.Classes.Merit.Block import Block +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError + +#Sleep to the next second, giving almost an entire second for clock-based operations. +def nextSecond() -> float: + startTime: float = time.time() + time.sleep(1 - (startTime - int(startTime))) + return time.time() + +def getMiner( + k: int +) -> str: + return PrivateKey(k).toPublicKey().serialize().hex() + +#pylint: disable=too-many-statements,too-many-locals +def getBlockTemplateTest( + rpc: RPC +) -> None: + edPrivKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + edPubKey: ed25519.VerifyingKey = edPrivKey.get_verifying_key() + blockchain: Blockchain = Blockchain() + + #Get multiple templates to verify they share an ID if they're requested within the same second. + templates: List[Dict[str, Any]] = [] + startTime: float = nextSecond() + for k in range(5): + templates.append(rpc.call("merit", "getBlockTemplate", {"miner": getMiner(k)}, False)) + if int(startTime) != int(time.time()): + #Testing https://github.com/MerosCrypto/Meros/issues/278 has a much more forgiving timer of < 1 second each. + #That said, this test was written on the fair assumption of all the above taking place in a single second. + raise Exception("getBlockTemplate is incredibly slow, to the point an empty Block template takes > 0.2 seconds to grab, invalidating this test.") + + for k, template in zip(range(5), templates): + if template["id"] != int(startTime): + raise TestError("Template ID isn't the time.") + + #Also check general accuracy. + if bytes.fromhex(template["key"]) != blockchain.genesis: + raise TestError("Template has the wrong RandomX key.") + + bytesHeader: bytes = bytes.fromhex(template["header"]) + serializedHeader: bytes = BlockHeader( + 0, + blockchain.blocks[0].header.hash, + bytes(32), + 0, + bytes(4), + bytes(32), + PrivateKey(k).toPublicKey().serialize(), + int(startTime) + ).serialize()[:-52] + #Skip over the randomized sketch salt. + if (bytesHeader[:72] + bytesHeader[76:]) != (serializedHeader[:72] + serializedHeader[76:]): + raise TestError("Template has an invalid header.") + #Difficulty modified as this is a new miner. + if template["difficulty"] != (blockchain.difficulty() * 11 // 10): + raise TestError("Template's difficulty is wrong.") + + currTime: int = int(nextSecond()) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": getMiner(0)}, False) + if template["id"] != currTime: + raise TestError("Template ID wasn't advanced with the time.") + + #Override the ID to enable easy comparison against a historical template. + template["id"] = int(startTime) + + if int.from_bytes(bytes.fromhex(template["header"])[-4:], "little") != currTime: + raise TestError("The header has the wrong time.") + template["header"] = ( + bytes.fromhex(template["header"])[:72] + + #Use the header we'll compare to's salt. + bytes.fromhex(templates[0]["header"])[72 : 76] + + bytes.fromhex(template["header"])[76 : -4] + + #Also use its time. + int(startTime).to_bytes(4, "little") + ).hex().upper() + + if template != templates[0]: + raise TestError("Template, minus the time difference, doesn't match the originally provided template.") + + #Test that the templates are deleted whenever a new Block appears. + #This is done by checking the error given when we use an old template. + with open("e2e/Vectors/Merit/BlankBlocks.json", "r") as file: + block: Block = Block.fromJSON(json.loads(file.read())[0]) + blockchain.add(block) + rpc.meros.liveConnect(blockchain.blocks[0].header.hash) + rpc.meros.syncConnect(blockchain.blocks[0].header.hash) + rpc.meros.liveBlockHeader(block.header) + rpc.meros.rawBlockBody(block, 0) + time.sleep(1) + #Sanity check. + if rpc.call("merit", "getHeight", auth=False) != 2: + raise Exception("Didn't successfully send Meros the Block.") + + #Get a new template so Meros realizes the template situation has changed. + rpc.call("merit", "getBlockTemplate", {"miner": getMiner(0)}, False) + + try: + rpc.call("merit", "publishBlock", {"id": int(startTime), "header": ""}, False) + raise Exception("") + except Exception as e: + if str(e) != "-2 Invalid ID.": + raise TestError("Meros didn't delete old template IDs.") + + #Test VerificationPacket inclusion. + data: Data = Data(bytes(32), edPubKey.to_bytes()) + data.sign(edPrivKey) + data.beat(SpamFilter(5)) + verif: SignedVerification = SignedVerification(data.hash) + verif.sign(0, PrivateKey(0)) + packet = VerificationPacket(data.hash, [0]) + + rpc.meros.liveTransaction(data) + rpc.meros.signedElement(verif) + time.sleep(1) + if bytes.fromhex( + rpc.call( + "merit", + "getBlockTemplate", + {"miner": getMiner(0)}, + False + )["header"] + )[36:68] != BlockHeader.createContents([packet]): + raise TestError("Meros didn't include the Verification in its new template.") + + #Test Element inclusion. + sendDiff: SignedSendDifficulty = SignedSendDifficulty(0, 0) + sendDiff.sign(0, PrivateKey(0)) + rpc.meros.signedElement(sendDiff) + time.sleep(1) + if bytes.fromhex( + rpc.call( + "merit", + "getBlockTemplate", + {"miner": getMiner(0)}, + False + )["header"] + )[36:68] != BlockHeader.createContents([packet], [sendDiff]): + raise TestError("Meros didn't include the Element in its new template.") + + #The 88 test checks for the non-inclusion of Verifications with unmentioned predecessors. + #Test for non-inclusion of Elements with unmentioned predecessors. + sendDiffChild: SignedSendDifficulty = SignedSendDifficulty(0, 2) + sendDiffChild.sign(0, PrivateKey(0)) + rpc.meros.signedElement(sendDiffChild) + time.sleep(1) + if bytes.fromhex( + rpc.call( + "merit", + "getBlockTemplate", + {"miner": getMiner(0)}, + False + )["header"] + )[36:68] != BlockHeader.createContents([packet], [sendDiff]): + raise TestError("Meros did include an Element with an unmentioned parent in its new template.") + + #If we send a block with a time in the future, yet within FTL (of course), make sure Meros can still generate a template. + #Naively using the current time will create a negative clock, which isn't allowed. + #Start by closing the sockets to give us time to work. + rpc.meros.live.connection.close() + rpc.meros.sync.connection.close() + #Sleep to reset the connection state. + time.sleep(35) + + #Create and mine the Block. + header: BlockHeader = BlockHeader( + 0, + blockchain.blocks[-1].header.hash, + bytes(32), + 0, + bytes(4), + bytes(32), + PrivateKey(0).toPublicKey().serialize(), + 0, + ) + miningStart: int = 0 + #If this block takes longer than 10 seconds to mine, try another. + #Low future time (20 seconds) is chosen due to feasibility + supporting lowering the FTL in the future. + while time.time() > miningStart + 10: + miningStart = int(time.time()) + header = BlockHeader( + 0, + blockchain.blocks[-1].header.hash, + bytes(32), + 0, + bytes(4), + bytes(32), + #Mod by something is due to a 2-byte limit (IIRC -- Kayaba). + #100 is just clean. +11 ensures an offset from the above, which really shouldn't be necessary. + #If we did need one, +1 should work, as we only ever work with PrivateKey(0) on the blockchain. + PrivateKey((miningStart % 100) + 10).toPublicKey().serialize(), + int(time.time()) + 20, + ) + header.mine(PrivateKey((miningStart % 100) + 10), blockchain.difficulty() * 11 // 10) + blockchain.add(Block(header, BlockBody())) + + #Send it and verify it. + rpc.meros.liveConnect(blockchain.blocks[0].header.hash) + rpc.meros.syncConnect(blockchain.blocks[0].header.hash) + rpc.meros.liveBlockHeader(header) + rpc.meros.rawBlockBody(Block(header, BlockBody()), 0) + rpc.meros.live.connection.close() + rpc.meros.sync.connection.close() + time.sleep(1) + + #Ensure a stable template ID. + currTime = int(nextSecond()) + template = rpc.call( + "merit", + "getBlockTemplate", + {"miner": getMiner(0)}, + False + ) + if template["id"] != currTime: + raise TestError("Template ID isn't the time when the previous Block is in the future.") + if int.from_bytes(bytes.fromhex(template["header"])[-4:], "little") != (header.time + 1): + raise TestError("Meros didn't handle generating a template off a Block in the future properly.") + + #Verify a Block with three Elements from a holder, where two form a Merit Removal. + #Only the two which cause a MeritRemoval should be included. + #Mine a Block to a new miner and clear the current template with it (might as well). + #Also allows us to test template clearing. + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": getMiner(1)}, False) + #Mine the Block. + proof: int = -1 + tempHash: bytes = bytes() + tempSignature: bytes = bytes() + while ( + (proof == -1) or + ((int.from_bytes(tempHash, "little") * (blockchain.difficulty() * 11 // 10)) > int.from_bytes(bytes.fromhex("FF" * 32), "little")) + ): + proof += 1 + tempHash = RandomX(bytes.fromhex(template["header"]) + proof.to_bytes(4, "little")) + tempSignature = PrivateKey(1).sign(tempHash).serialize() + tempHash = RandomX(tempHash + tempSignature) + rpc.call("merit", "publishBlock", {"id": template["id"], "header": template["header"] + proof.to_bytes(4, "little").hex() + tempSignature.hex()}) + time.sleep(1) + + #Verify the template was cleared. + currTime = int(nextSecond()) + bytesHeader: bytes = bytes.fromhex(rpc.call("merit", "getBlockTemplate", {"miner": getMiner(0)}, False)["header"]) + serializedHeader: bytes = BlockHeader( + 0, + tempHash, + bytes(32), + 0, + bytes(4), + bytes(32), + 0, + #Ensures that the previous time manipulation doesn't come back to haunt us. + max(currTime, blockchain.blocks[-1].header.time + 1) + ).serialize()[:-52] + #Skip over the randomized sketch salt and time (which we don't currently have easy access to). + if (bytesHeader[:72] + bytesHeader[76:-4]) != (serializedHeader[:72] + serializedHeader[76:-4]): + raise TestError("Template wasn't cleared.") + + #Sleep so we can reconnect. + time.sleep(35) + rpc.meros.liveConnect(blockchain.blocks[0].header.hash) + + #Finally create the Elements. + dataDiff: SignedDataDifficulty = SignedDataDifficulty(1, 0) + dataDiff.sign(2, PrivateKey(1)) + rpc.meros.signedElement(dataDiff) + sendDiffs: List[SignedSendDifficulty] = [ + SignedSendDifficulty(1, 1), + SignedSendDifficulty(2, 1) + ] + for sd in sendDiffs: + sd.sign(2, PrivateKey(1)) + rpc.meros.signedElement(sd) + time.sleep(1) + + #`elem for elem` is used below due to Pyright not handling inheritance properly when nested. + #pylint: disable=unnecessary-comprehension + if bytes.fromhex( + rpc.call( + "merit", + "getBlockTemplate", + {"miner": getMiner(0)}, + False + )["header"] + )[36:68] != BlockHeader.createContents([], [elem for elem in sendDiffs[::-1]]): + raise TestError("Meros didn't include just the malicious Elements in its new template.") diff --git a/e2e/Tests/RPC/Merit/GetBlockTest.py b/e2e/Tests/RPC/Merit/GetBlockTest.py new file mode 100644 index 000000000..4ec213570 --- /dev/null +++ b/e2e/Tests/RPC/Merit/GetBlockTest.py @@ -0,0 +1,184 @@ +from typing import Callable, Dict, List, Any +import json + +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Send, Data, Transactions + +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def GetBlockTest( + rpc: RPC +) -> None: + blockchain: Blockchain + claim: Claim + send: Send + datas: List[Data] + + txKey: Callable[[Dict[str, Any]], str] = lambda tx: tx["hash"] + + def verify() -> None: + for b in range(len(blockchain.blocks)): + block: Dict[str, Any] = rpc.call("merit", "getBlock", {"block": blockchain.blocks[b].header.hash.hex().upper()}, False) + if rpc.call("merit", "getBlock", {"block": b}, False) != block: + raise TestError("Meros reported different Blocks depending on if nonce/hash indexing.") + + #Python doesn't keep track of the removals. + #That said, they should all be empty except for the last one. + if b != (len(blockchain.blocks) - 1): + if block["removals"] != []: + raise TestError("Meros reported the Block had removals.") + del block["removals"] + + if blockchain.blocks[b].toJSON() != block: + raise TestError("Meros's JSON serialization of Blocks differs from Python's.") + + #Test the key serialization of the first Block. + #The final Block uses a nick, hence the value in this. + if rpc.call("merit", "getBlock", {"block": 1}, False)["header"]["miner"] != PrivateKey(0).toPublicKey().serialize().hex().upper(): + raise TestError("Meros didn't serialize a miner's key properly.") + + #Manually test the final, and most complex, block. + final: Dict[str, Any] = rpc.call("merit", "getBlock", {"block": len(blockchain.blocks) - 1}, False) + final["transactions"].sort(key=txKey) + final["removals"].sort() + if final != { + "hash": blockchain.blocks[-1].header.hash.hex().upper(), + + "header": { + "version": blockchain.blocks[-1].header.version, + "last": blockchain.blocks[-1].header.last.hex().upper(), + "contents": blockchain.blocks[-1].header.contents.hex().upper(), + "packets": blockchain.blocks[-1].header.packetsQuantity, + "sketchSalt": blockchain.blocks[-1].header.sketchSalt.hex().upper(), + "sketchCheck": blockchain.blocks[-1].header.sketchCheck.hex().upper(), + "miner": blockchain.blocks[-1].header.minerKey.hex().upper() if blockchain.blocks[-1].header.newMiner else blockchain.blocks[-1].header.minerNick, + "time": blockchain.blocks[-1].header.time, + "proof": blockchain.blocks[-1].header.proof, + "signature": blockchain.blocks[-1].header.signature.hex().upper() + }, + + "transactions": sorted( + [ + { + "hash": claim.hash.hex().upper(), + "holders": [0] + }, + { + "hash": send.hash.hex().upper(), + "holders": [0, 1, 2] + }, + { + "hash": datas[0].hash.hex().upper(), + "holders": [0, 2] + }, + { + "hash": datas[1].hash.hex().upper(), + "holders": [0, 1, 3] + }, + { + "hash": datas[2].hash.hex().upper(), + "holders": [0, 1, 2, 3, 4] + }, + { + "hash": datas[3].hash.hex().upper(), + "holders": [0, 1, 2, 3] + } + ], + key=txKey + ), + + "elements": [ + { + "descendant": "DataDifficulty", + "holder": 3, + "nonce": 0, + "difficulty": 8 + }, + { + "descendant": "SendDifficulty", + "holder": 0, + "nonce": 0, + "difficulty": 1 + }, + { + "descendant": "DataDifficulty", + "holder": 3, + "nonce": 0, + "difficulty": 4 + }, + { + "descendant": "DataDifficulty", + "holder": 4, + "nonce": 2, + "difficulty": 1 + }, + { + "descendant": "SendDifficulty", + "holder": 4, + "nonce": 1, + "difficulty": 3 + }, + { + "descendant": "SendDifficulty", + "holder": 2, + "nonce": 1, + "difficulty": 2 + }, + { + "descendant": "DataDifficulty", + "holder": 0, + "nonce": 0, + "difficulty": 7 + }, + ], + + "removals": [0, 3], + + "aggregate": blockchain.blocks[-1].body.aggregate.serialize().hex().upper() + }: + raise TestError("Final Block wasn't correct.") + + #Test invalid calls. + try: + rpc.call("merit", "getBlock", {"block": 100}, False) + raise Exception("") + except Exception as e: + if str(e) != "-2 Block not found.": + raise TestError("getBlock didn't error when we used a non-existent nonce.") + + try: + rpc.call("merit", "getBlock", {"block": -5}, False) + raise Exception("") + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("getBlock didn't error when we used a negative (signed) integer for a nonce.") + + try: + rpc.call("merit", "getBlock", {"block": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, False) + raise Exception("") + except Exception as e: + if str(e) != "-2 Block not found.": + raise TestError("getBlock didn't error when we used a non-existent hash.") + + try: + rpc.call("merit", "getBlock", {"block": ""}, False) + raise Exception("") + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("getBlock didn't error when we used an invalid hash.") + + with open("e2e/Vectors/RPC/Merit/GetBlock.json", "r") as file: + vectors: Dict[str, Any] = json.loads(file.read()) + blockchain = Blockchain.fromJSON(vectors["blockchain"]) + claim = Claim.fromJSON(vectors["claim"]) + send = Send.fromJSON(vectors["send"]) + datas = [Data.fromJSON(data) for data in vectors["datas"]] + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + Liver(rpc, vectors["blockchain"], transactions, {(len(blockchain.blocks) - 1): verify}).live() diff --git a/e2e/Tests/RPC/Network/BroadcastTest.py b/e2e/Tests/RPC/Network/BroadcastTest.py new file mode 100644 index 000000000..737246df8 --- /dev/null +++ b/e2e/Tests/RPC/Network/BroadcastTest.py @@ -0,0 +1,86 @@ +from typing import Dict, Any +import json + +from e2e.Classes.Transactions.Transactions import Claim, Data, Transactions +from e2e.Classes.Merit.Blockchain import BlockHeader, Block, Blockchain + +from e2e.Meros.Meros import MessageType +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def BroadcastTest( + rpc: RPC +) -> None: + vectors: Dict[str, Any] + with open("e2e/Vectors/Transactions/ClaimedMint.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + claim: Claim = Claim.fromTransaction(iter(transactions.txs.values()).__next__()) + mint: bytes = claim.inputs[0][0] + + def test() -> None: + #Data. + dataHash: str = rpc.call("personal", "data", {"data": "abc"}) + #Read the initial Data. + if MessageType(rpc.meros.live.recv()[0]) != MessageType.Data: + raise TestError("Meros didn't broadcast the initial Data.") + #Read the actual Data. + serializedData: bytes = rpc.meros.live.recv() + if serializedData[1:] != Data.fromJSON(rpc.call("transactions", "getTransaction", {"hash": dataHash})).serialize(): + raise TestError("Meros didn't broadcast the created Data.") + res: Any = rpc.call("network", "broadcast", {"transaction": dataHash}) + if not (isinstance(res, bool) and res): + raise TestError("Broadcast didn't return true.") + if rpc.meros.live.recv() != serializedData: + raise TestError("Meros didn't broadcast a Transaction when told to.") + + #Block. + header: BlockHeader = Block.fromJSON(vectors["blockchain"][0]).header + rpc.call("network", "broadcast", {"block": header.hash.hex()}) + if rpc.meros.live.recv() != (MessageType.BlockHeader.toByte() + header.serialize()): + raise TestError("Meros didn't broadcast the Blockheader.") + + #Data and Block. + rpc.call("network", "broadcast", {"block": header.hash.hex(), "transaction": dataHash}) + if rpc.meros.live.recv() != serializedData: + raise TestError("Meros didn't broadcast a Transaction when told to.") + if rpc.meros.live.recv() != (MessageType.BlockHeader.toByte() + header.serialize()): + raise TestError("Meros didn't broadcast the Blockheader.") + + #Non-existent Transaction. + try: + rpc.call("network", "broadcast", {"transaction": bytes(32).hex()}) + raise TestError() + except TestError as e: + if str(e) != "-2 Transaction not found.": + raise TestError("Meros didn't error when told to broadcast a non-existent Transaction.") + + #Non-existent Block. + try: + rpc.call("network", "broadcast", {"block": bytes(32).hex()}) + raise TestError() + except TestError as e: + if str(e) != "-2 Block not found.": + raise TestError("Meros didn't error when told to broadcast a non-existent Block.") + + #Mint. + try: + rpc.call("network", "broadcast", {"transaction": mint.hex()}) + raise TestError() + except TestError as e: + if str(e) != "-3 Transaction is a Mint.": + raise TestError("Meros didn't error when told to broadcast a Mint.") + + #Genesis Block. + try: + rpc.call("network", "broadcast", {"block": Blockchain().blocks[0].header.hash.hex()}) + raise TestError() + except TestError as e: + if str(e) != "-3 Block is the genesis Block.": + raise TestError("Meros didn't error when told to broadcast the genesis Block.") + + #Create and execute a Liver. + Liver(rpc, vectors["blockchain"], transactions, callbacks={8: test}).live() diff --git a/e2e/Tests/RPC/Personal/AddressRecoveryTest.py b/e2e/Tests/RPC/Personal/AddressRecoveryTest.py new file mode 100644 index 000000000..eff52c329 --- /dev/null +++ b/e2e/Tests/RPC/Personal/AddressRecoveryTest.py @@ -0,0 +1,172 @@ +#Tests address recovery when reloading seeds, and does so via personal_getUTXOs. +#Therefore also tests personal_getUTXOs, which just iterates over transactions_getUTXOs which is thoroughly tested. +#Hence why it is not more thoroughly tested, though this is comprehensive enough. +#Arguably, this should be the personal_getUTXOs test with address recovery also tested. +#As one other note, this test excessively uses sleep. For some reason, things that shouldn't take 20+ seconds do. +#Instead of debugging this for a faster runtime, the quick and easy solution was taken. +#This should be improved. + +from typing import Dict, List, Union, Any + +from time import sleep +import json + +import ed25519 +from pytest import raises + +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.Verification import SignedVerification +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.Meros import Meros +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import mineBlock +from e2e.Tests.RPC.Personal.Lib import getIndex, getAddress, decodeAddress +from e2e.Tests.Errors import TestError, SuccessError + +def createSend( + rpc: RPC, + last: Union[Claim, Send], + toAddress: str +) -> Send: + funded: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + if isinstance(last, Claim): + send: Send = Send( + [(last.hash, 0)], + [ + (decodeAddress(toAddress), 1), + (funded.get_verifying_key().to_bytes(), last.amount - 1) + ] + ) + else: + send: Send = Send( + [(last.hash, 1)], + [ + (decodeAddress(toAddress), 1), + (funded.get_verifying_key().to_bytes(), last.outputs[1][1] - 1) + ] + ) + + send.sign(funded) + send.beat(SpamFilter(3)) + sleep(65) + rpc.meros.liveConnect(Blockchain().blocks[0].header.hash) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + + sv: SignedVerification = SignedVerification(send.hash) + sv.sign(0, PrivateKey(0)) + if rpc.meros.signedElement(sv) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Verification.") + + return send + +def sortUTXOs( + utxos: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + return sorted(utxos, key=lambda utxo: utxo["hash"]) + +#pylint: disable=too-many-statements +def AddressRecoveryTest( + rpc: RPC +) -> None: + mnemonic: str = rpc.call("personal", "getMnemonic") + + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + def test() -> None: + #Send to the new address and get the next address. + dest: str = rpc.call("personal", "getAddress") + last: Send = createSend(rpc, Claim.fromJSON(vectors["newerMintClaim"]), dest) + + utxos: List[Dict[str, Any]] = rpc.call("personal", "getUTXOs") + if utxos != [{"address": dest, "hash": last.hash.hex().upper(), "nonce": 0}]: + raise TestError("personal_getUTXOs didn't return the correct UTXOs.") + + #Set a different mnemonic to verify the tracked addresses is cleared. + rpc.call("personal", "setWallet") + if rpc.call("personal", "getUTXOs") != []: + raise TestError("Setting a new Mnemonic didn't clear the tracked addresses.") + + #Reload the Mnemonic and verify the UTXOs are correct. + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + #This error message points out how no addresses are really being discovered yet; this is account zero's address. + #That said, if the test started at the next address, there'd be a gap. + #As that's an extra factor, this is tested before gaps are. + raise TestError("Meros didn't recover the very first address.") + + #Now send to the next address and check accuracy. + dest = rpc.call("personal", "getAddress") + last = createSend(rpc, last, dest) + utxos.append({"address": dest, "hash": last.hash.hex().upper(), "nonce": 0}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("Meros didn't track an implicitly gotten address.") + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("Meros didn't recover the first address after the initial address.") + + #Send funds to the address after the next address; tests a gap when discovering addresses. + last = createSend(rpc, last, getAddress(mnemonic, "", 3)) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("Meros magically recognized UTXOs as belonging to this Wallet or someone implemented an address cache.") + utxos.append({"address": getAddress(mnemonic, "", 3), "hash": last.hash.hex().upper(), "nonce": 0}) + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("Meros didn't discover a used address in the Wallet when there was a gap.") + + #Finally, anything 10+ unused addresses out shouldn't be recovered. + last = createSend(rpc, last, getAddress(mnemonic, "", 14)) + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("Meros recovered an address's UTXOs despite it being 10 unused addresses out.") + + #Explicitly generating this address should start tracking it though. + rpc.call("personal", "getAddress", {"index": getIndex(mnemonic, "", 14)}) + utxos.append({"address": getAddress(mnemonic, "", 14), "hash": last.hash.hex().upper(), "nonce": 0}) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("personal_getUTXOs didn't track an address explicitly indexed.") + + #If asked for an address, Meros should return the 5th (skip 4). + #It's the first unused address AFTER used addresss EXCEPT ones explicitly requested. + #This can, in the future, be juwst the first unused address/include ones explicitly requested (see DerivationTest for commentary on that). + #This is really meant to ensure consistent behavior until we properly decide otherwise. + if rpc.call("personal", "getAddress") != getAddress(mnemonic, "", 4): + raise TestError("Meros didn't return the next unused address (with conditions; see comment).") + + #Mine a Block to flush the Transactions and Verifications to disk. + sleep(65) + rpc.meros.liveConnect(Blockchain().blocks[0].header.hash) + mineBlock(rpc) + + #Existing values used to test getAddress/getUTXOs consistency. + #The former is thoroughly tested elsewhere, making it quite redundant. + existing: Dict[str, Any] = { + "getAddress": rpc.call("personal", "getAddress"), + "getUTXOs": rpc.call("personal", "getUTXOs") + } + + #Reboot the node and verify consistency. + rpc.quit() + sleep(3) + rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc) + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(existing["getUTXOs"]): + raise TestError("Rebooting the node caused the WalletDB to improperly reload UTXOs.") + if rpc.call("personal", "getAddress") != existing["getAddress"]: + raise TestError("Rebooting the node caused the WalletDB to improperly reload the next address.") + + #Used so Liver doesn't run its own post-test checks. + #Since we added our own Blocks, those will fail. + raise SuccessError() + + #Used so we don't have to write a sync loop. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/Personal/DerivationTest.py b/e2e/Tests/RPC/Personal/DerivationTest.py new file mode 100644 index 000000000..ac4189664 --- /dev/null +++ b/e2e/Tests/RPC/Personal/DerivationTest.py @@ -0,0 +1,211 @@ +#Tests setWallet, getMnemonic, getMeritHolderKey, getMeritHolderNick's non-existent case, getAccount, and getAddress calls with specified indexes. +#Used to be part of one larger test with GetAddressTest. + +import os +from hashlib import sha256 + +from bip_utils import Bip39WordsNum, Bip39MnemonicGenerator, Bip39MnemonicValidator, Bip39SeedGenerator +from bech32 import convertbits, bech32_encode + +from e2e.Libs.BLS import PrivateKey +import e2e.Libs.ed25519 as ed +import e2e.Libs.BIP32 as BIP32 + +from e2e.Classes.Transactions.Transactions import Data + +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError +from e2e.Tests.RPC.Personal.Lib import getMnemonic, getPrivateKey + +def verifyMnemonicAndAccount( + rpc: RPC, + mnemonic: str = "", + password: str = "" +) -> None: + #If a Mnemonic wasn't specified, grab the node's. + if mnemonic == "": + mnemonic = rpc.call("personal", "getMnemonic") + + #Verify Mnemonic equivalence. + if mnemonic != rpc.call("personal", "getMnemonic"): + raise TestError("Node had a different Mnemonic.") + + #Validate it. + if not Bip39MnemonicValidator(mnemonic).Validate(): + raise TestError("Mnemonic checksum was incorrect.") + + #Verify derivation from seed to wallet. + seed: bytes = Bip39SeedGenerator(mnemonic).Generate(password) + #Check the Merit Holder key. + if rpc.call("personal", "getMeritHolderKey") != PrivateKey(seed[:32]).serialize().hex().upper(): + raise TestError("Meros generated a different Merit Holder Key.") + #Verify getting the Merit Holder nick errors. + try: + rpc.call("personal", "getMeritHolderNick") + except TestError as e: + if e.message != "-2 Wallet doesn't have a Merit Holder nickname assigned.": + raise TestError("getMeritHolderNick didn't error.") + + #Hash the seed again for the wallet seed (first is the Merit Holder seed). + seed = sha256(seed).digest() + + #Derive the first account. + extendedKey: bytes + chainCode: bytes + try: + extendedKey, chainCode = BIP32.deriveKeyAndChainCode( + seed, + [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31)] + ) + except Exception: + raise TestError("Meros gave us an invalid Mnemonic to derive (or the test generated an unusable one).") + + #For some reason, pylint decided to add in detection of stdlib members. + #It doesn't do it properly, and thinks encodepoint returns a string. + #It returns bytes, which does have hex as a method. + #pylint: disable=no-member + if rpc.call("personal", "getAccount") != { + "key": ed.encodepoint( + ed.scalarmult(ed.B, ed.decodeint(extendedKey[:32]) % ed.l) + ).hex().upper(), + "chainCode": chainCode.hex().upper() + }: + #The Nim tests ensure accurate BIP 32 derivation thanks to vectors. + #That leaves BIP 39/44 in the air. + #This isn't technically true due to an ambiguity/the implementation we used the vectors of, yet it's true enough for this comment. + raise TestError("Meros generated a different account public key.") + + #Also test that the correct public key is used when creating Datas. + #It should be the first public key of the external chain for account 0. + data: str = rpc.call("personal", "data", {"data": "a", "password": password}) + initial: Data = Data( + bytes(32), + ed.encodepoint( + ed.scalarmult(ed.B, ed.decodeint(getPrivateKey(mnemonic, password, 0)[:32]) % ed.l) + ) + ) + #Checks via the initial Data. + if bytes.fromhex(rpc.call("transactions", "getTransaction", {"hash": data})["inputs"][0]["hash"]) != initial.hash: + raise TestError("Meros used the wrong key to create the Data Transactions.") + +#pylint: disable=too-many-statements +def DerivationTest( + rpc: RPC +) -> None: + #Start by testing BIP 32, 39, and 44 functionality in general. + for _ in range(10): + rpc.call("personal", "setWallet") + verifyMnemonicAndAccount(rpc) + + #Set specific Mnemonics and ensure they're handled properly. + for _ in range(10): + mnemonic: str = getMnemonic() + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + verifyMnemonicAndAccount(rpc, mnemonic) + + #Create Mnemonics with passwords and ensure they're handled properly. + for _ in range(10): + password: str = os.urandom(32).hex() + rpc.call("personal", "setWallet", {"password": password}) + verifyMnemonicAndAccount(rpc, password=password) + + #Set specific Mnemonics with passwords and ensure they're handled properly. + for i in range(10): + password: str = os.urandom(32).hex() + #Non-hex string. + if i == 0: + password = "xyz" + mnemonic: str = getMnemonic(password) + rpc.call("personal", "setWallet", {"mnemonic": mnemonic, "password": password}) + verifyMnemonicAndAccount(rpc, mnemonic, password) + + #setWallet, getMnemonic, getMeritHolderKey, getMeritHolderNick's non-existent case, and getAccount have now been tested. + #This leaves getAddress with specific indexes. + + #Clear the Wallet. + rpc.call("personal", "setWallet") + + #Start by testing specific derivation. + password: str = "password since it shouldn't be relevant" + for _ in range(10): + mnemonic: str = getMnemonic(password) + index: int = 100 + key: bytes + while True: + try: + key = BIP32.derive( + sha256(Bip39SeedGenerator(mnemonic).Generate(password)).digest(), + [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 0, index] + ) + break + except Exception: + index += 1 + + rpc.call("personal", "setWallet", {"mnemonic": mnemonic, "password": password}) + addr: str = bech32_encode( + "mr", + convertbits( + ( + bytes([0]) + + ed.encodepoint(ed.scalarmult(ed.B, ed.decodeint(key[:32]) % ed.l)) + ), + 8, + 5 + ) + ) + if rpc.call("personal", "getAddress", {"index": index}) != addr: + raise TestError("Didn't get the correct address for this index.") + + #Test if a specific address is requested, it won't come up naturally. + #This isn't explicitly required by the RPC spec, which has been worded carefully to leave this open ended. + #The only requirement is the address was never funded and the index is sequential (no moving backwards). + #The node offers this feature to try to make mixing implicit/explicit addresses safer, along with some internal benefits. + #That said, said internal benefits are minimal or questionable, hence why the RPC docs are open ended. + #This way we can decide differently in the future. + rpc.call("personal", "setWallet") + firstAddr: str = rpc.call("personal", "getAddress") + #Explicitly get the first address. + for i in range(256): + try: + rpc.call("personal", "getAddress", {"index": i}) + break + except TestError: + if i == 255: + raise Exception("The first 256 address were invalid; this should be practically impossible.") + if firstAddr == rpc.call("personal", "getAddress"): + raise TestError("Explicitly grabbed address was naturally returned.") + + #Test error cases. + + #Mnemonic with an improper amount of entropy. + #Runs multiple times in case the below error pops up for the sole reason the Mnemonic didn't have viable keys. + #This should error earlier than that though. + for _ in range(16): + try: + rpc.call( + "personal", + "setWallet", + { + "mnemonic": Bip39MnemonicGenerator.FromWordsNumber(Bip39WordsNum.WORDS_NUM_12) + } + ) + raise Exception() + except Exception as e: + if str(e) != "-3 Invalid mnemonic or password.": + raise TestError("Could set a Mnemonic with too little entropy.") + + #Mnemonic with additional spaces. + rpc.call("personal", "setWallet") + mnemonic: str = rpc.call("personal", "getMnemonic") + rpc.call("personal", "setWallet", {"mnemonic": " " + (" " * 2).join(mnemonic.split(" ")) + " "}) + if rpc.call("personal", "getMnemonic") != mnemonic: + raise TestError("Meros didn't handle a mnemonic with extra whitespace.") + + #Negative index to getAddress. + try: + rpc.call("personal", "getAddress", {"index": -1}) + raise Exception() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Could call getAddress with a negative index.") diff --git a/e2e/Tests/RPC/Personal/GetAddressTest.py b/e2e/Tests/RPC/Personal/GetAddressTest.py new file mode 100644 index 000000000..7acab23a5 --- /dev/null +++ b/e2e/Tests/RPC/Personal/GetAddressTest.py @@ -0,0 +1,244 @@ +#Tests getAddress primarily, yet takes the opportunity to also test getMeritHolderNick and consistency. +#Consistency is defined as consistency when rebooting the node, as well as when reloading wallets. +#Used to be part of one larger test with DerivationTest. + +from typing import Dict, List, Union, Any + +from time import sleep +import json + +import ed25519 +from pytest import raises + +from e2e.Libs.BLS import PrivateKey +import e2e.Libs.ed25519 as ed +from e2e.Libs.RandomX import RandomX + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.Verification import SignedVerification +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Merit.Blockchain import Block, Blockchain + +from e2e.Meros.Meros import MessageType, Meros +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Errors import TestError, SuccessError +from e2e.Tests.RPC.Personal.Lib import getPrivateKey, getAddress, decodeAddress + +def createSend( + rpc: RPC, + last: Union[Claim, Send], + toAddress: str +) -> Send: + funded: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + if isinstance(last, Claim): + send: Send = Send( + [(last.hash, 0)], + [ + (decodeAddress(toAddress), 1), + (funded.get_verifying_key().to_bytes(), last.amount - 1) + ] + ) + else: + send: Send = Send( + [(last.hash, 1)], + [ + (decodeAddress(toAddress), 1), + (funded.get_verifying_key().to_bytes(), last.outputs[1][1] - 1) + ] + ) + send.sign(funded) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + return send + +#pylint: disable=too-many-statements,too-many-locals +def GetAddressTest( + rpc: RPC +) -> None: + password: str = "password since it shouldn't be relevant" + rpc.call("personal", "setWallet", {"password": password}) + mnemonic: str = rpc.call("personal", "getMnemonic") + + #Test getAddress. Doesn't test specific indexing, as that's handled by DerivationTest. + #Not only does getAddress need to correctly derive addresses along the external chain, it needs to return new addresses. + #That said, new is defined by use; use on the network. If it has a TX sent to it, it's used. + #This is different than checking for UTXOs because that means any address no longer having UTXOs would be considered new again. + + expected: str = getAddress(mnemonic, password, 0) + if rpc.call("personal", "getAddress") != expected: + raise TestError("getAddress didn't return the next unused address (the first one).") + #It should be returned again given it's still unused. + if rpc.call("personal", "getAddress") != expected: + raise TestError("getAddress didn't return the same address when there was a lack of usage.") + + #Reboot the node and verify consistency around the initial address. + #Added due to an edge case that appeared. + rpc.quit() + sleep(3) + rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc) + if rpc.call("personal", "getAddress") != expected: + raise TestError("getAddress didn't return the initial address after a reboot.") + + #Send enough Blocks to have funds available to continue testing. + vectors: Dict[str, Any] + with open("e2e/Vectors/Transactions/ClaimedMint.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + def restOfTest() -> None: + #Move expected into scope. + expected: str = getAddress(mnemonic, password, 0) + + #Send to the new address, then call getAddress again. Verify a new address appears. + last: Send = createSend( + rpc, + Claim.fromTransaction(iter(transactions.txs.values()).__next__()), + expected + ) + hashes: List[bytes] = [last.hash] + + expected = getAddress(mnemonic, password, 1) + if rpc.call("personal", "getAddress") != expected: + raise TestError("Meros didn't move to the next address once the existing one was used.") + + #Send to the new unused address, spending the funds before calling getAddress again. + #Checks address usage isn't defined as having an UTXO, yet rather any TXO. + #Also confirm the spending TX with full finalization before checking. + #Ensures the TXO isn't unspent by any definition. + last = createSend(rpc, last, expected) + hashes.append(last.hash) + + #Spending TX. + send: Send = Send([(hashes[-1], 0)], [(bytes(32), 1)]) + send.signature = ed.sign( + b"MEROS" + send.hash, + getPrivateKey(mnemonic, password, 1) + ) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back the spending Send.") + hashes.append(send.hash) + + #In order to finalize, we need to mine 6 Blocks once this Transaction and its parent have Verifications. + for txHash in hashes: + sv: SignedVerification = SignedVerification(txHash) + sv.sign(0, PrivateKey(0)) + if rpc.meros.signedElement(sv) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Verification.") + + #Close the sockets while we mine. + rpc.meros.live.connection.close() + rpc.meros.sync.connection.close() + + #Mine these to the Wallet on the node so we can test getMeritHolderNick. + privKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) + blockchain: Blockchain = Blockchain.fromJSON(vectors["blockchain"]) + for _ in range(6): + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": privKey.toPublicKey().serialize().hex()}) + proof: int = -1 + tempHash: bytes = bytes() + tempSignature: bytes = bytes() + while ( + (proof == -1) or + ((int.from_bytes(tempHash, "little") * (blockchain.difficulty() * 11 // 10)) > int.from_bytes(bytes.fromhex("FF" * 32), "little")) + ): + proof += 1 + tempHash = RandomX(bytes.fromhex(template["header"]) + proof.to_bytes(4, "little")) + tempSignature = privKey.sign(tempHash).serialize() + tempHash = RandomX(tempHash + tempSignature) + + rpc.call("merit", "publishBlock", {"id": template["id"], "header": template["header"] + proof.to_bytes(4, "little").hex() + tempSignature.hex()}) + blockchain.add(Block.fromJSON(rpc.call("merit", "getBlock", {"block": len(blockchain.blocks)}))) + + #Verify a new address is returned. + expected = getAddress(mnemonic, password, 2) + if rpc.call("personal", "getAddress") != expected: + raise TestError("Meros didn't move to the next address once the existing one was used.") + + #Reopen the sockets. + sleep(65) + rpc.meros.liveConnect(Blockchain().blocks[0].header.hash) + rpc.meros.syncConnect(Blockchain().blocks[0].header.hash) + + #Get a new address after sending to the address after it. + #Use both, and then call getAddress. + #getAddress should detect X is used, move to Y, detect Y is used, and move to Z. + #It shouldn't assume the next address after an used address is unused. + #Actually has two Ys as one iteration of the code only ran for the next address; not all future addresses. + + #Send to the next next addresses. + for i in range(2): + last = createSend(rpc, last, getAddress(mnemonic, password, 3 + i)) + if MessageType(rpc.meros.live.recv()[0]) != MessageType.SignedVerification: + raise TestError("Meros didn't create and broadcast a SignedVerification for this Send.") + + #Verify getAddress returns the existing next address. + if rpc.call("personal", "getAddress") != expected: + raise TestError("Sending to the address after this address caused Meros to consider this address used.") + + #Send to the next address. + last = createSend(rpc, last, expected) + if MessageType(rpc.meros.live.recv()[0]) != MessageType.SignedVerification: + raise TestError("Meros didn't create and broadcast a SignedVerification for this Send.") + + #Verify getAddress returns the address after the next next addresses. + expected = getAddress(mnemonic, password, 5) + if rpc.call("personal", "getAddress") != expected: + raise TestError("Meros didn't return the correct next address after using multiple addresses in a row.") + + #Now that we have mined a Block as part of this test, ensure the Merit Holder nick is set. + if rpc.call("personal", "getMeritHolderNick") != 1: + raise TestError("Merit Holder nick wasn't made available despite having one.") + + #Sanity check off Mnemonic. + if rpc.call("personal", "getMnemonic") != mnemonic: + raise TestError("getMnemonic didn't return the correct Mnemonic.") + + #Existing values used to test getMnemonic/getMeritHolderKey/getMeritHolderNick/getAccount/getAddress consistency. + existing: Dict[str, Any] = { + #Should be equal to the mnemonic variable, which is verified in a check above. + "getMnemonic": rpc.call("personal", "getMnemonic"), + "getMeritHolderKey": rpc.call("personal", "getMeritHolderKey"), + "getMeritHolderNick": rpc.call("personal", "getMeritHolderNick"), + "getAccount": rpc.call("personal", "getAccount"), + #Should be equal to expected, which is also verified in a check above. + "getAddress": rpc.call("personal", "getAddress") + } + + #Set a new seed and verify the Merit Holder nick is cleared. + rpc.call("personal", "setWallet") + try: + rpc.call("personal", "getMeritHolderNick") + raise TestError("") + except TestError as e: + if str(e) != "-2 Wallet doesn't have a Merit Holder nickname assigned.": + raise TestError("getMeritHolderNick returned something or an unexpected error when a new Mnemonic was set.") + + #Set back the old seed and verify consistency. + rpc.call("personal", "setWallet", {"mnemonic": mnemonic, "password": password}) + for method in existing: + if rpc.call("personal", method) != existing[method]: + raise TestError("Setting an old seed caused the WalletDB to improperly reload.") + + #Verify calling getAddress returns the expected address. + if rpc.call("personal", "getAddress") != expected: + raise TestError("Meros returned an address that wasn't next after reloading the seed.") + + #Reboot the node and verify consistency. + rpc.quit() + sleep(3) + rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc) + for method in existing: + if rpc.call("personal", method) != existing[method]: + raise TestError("Rebooting the node caused the WalletDB to improperly reload.") + + #Used so Liver doesn't run its own post-test checks. + #Since we added our own Blocks, those will fail. + raise SuccessError() + + #Used so we don't have to write a sync loop. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {8: restOfTest}).live() diff --git a/e2e/Tests/RPC/Personal/HundredSixtyTwoTest.py b/e2e/Tests/RPC/Personal/HundredSixtyTwoTest.py new file mode 100644 index 000000000..b77ed0c72 --- /dev/null +++ b/e2e/Tests/RPC/Personal/HundredSixtyTwoTest.py @@ -0,0 +1,38 @@ +from typing import Dict, Any + +from e2e.Meros.RPC import RPC +from e2e.Tests.Errors import TestError + +def HundredSixtyTwoTest( + rpc: RPC +) -> None: + #Create the first Datas. + mnemonic: str = rpc.call("personal", "getMnemonic") + abcData: str = rpc.call("personal", "data", {"data": "abc"}) + + #Create a Data on a different account. + rpc.call("personal", "setWallet") + defData: str = rpc.call("personal", "data", {"data": "def"}) + + #Verify the def Data was created. + if rpc.call("transactions", "getTransaction", {"hash": defData})["descendant"] != "Data": + raise TestError("Meros didn't create a Data for an imported account when the existing account had Datas.") + + #Switch back to the old Mnemonic. + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + + #Ensure we can create new Datas on it as well, meaning switching to a Mnemonic ports the chain. + ghiDataHash: str = rpc.call("personal", "data", {"data": "ghi"}) + ghiData: Dict[str, Any] = rpc.call("transactions", "getTransaction", {"hash": ghiDataHash}) + del ghiData["signature"] + del ghiData["proof"] + if ghiData != { + "descendant": "Data", + "inputs": [{ + "hash": abcData + }], + "outputs": [], + "hash": ghiDataHash, + "data": b"ghi".hex() + }: + raise TestError("Data created for an imported account with Datas isn't correct.") diff --git a/e2e/Tests/RPC/Personal/Lib.py b/e2e/Tests/RPC/Personal/Lib.py new file mode 100644 index 000000000..df6491943 --- /dev/null +++ b/e2e/Tests/RPC/Personal/Lib.py @@ -0,0 +1,124 @@ +#Helpers for working with Mnemonics/keys/addresses. Used by a few personal tests. + +from typing import List, Tuple, Union + +from hashlib import sha256 + +from bip_utils import Bip39WordsNum, Bip39MnemonicGenerator, Bip39SeedGenerator +from bech32 import convertbits, bech32_encode, bech32_decode + +import e2e.Libs.ed25519 as ed +import e2e.Libs.BIP32 as BIP32 + +from e2e.Tests.Errors import TestError + +def getMnemonic( + password: str = "" +) -> str: + while True: + res: str = Bip39MnemonicGenerator.FromWordsNumber(Bip39WordsNum.WORDS_NUM_24) + seed: bytes = sha256(Bip39SeedGenerator(res).Generate(password)).digest() + try: + BIP32.derive(seed, [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 0]) + BIP32.derive(seed, [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 1]) + except Exception: + continue + return res + +def getIndex( + mnemonic: str, + password: str, + skip: int +) -> int: + seed: bytes = sha256(Bip39SeedGenerator(mnemonic).Generate(password)).digest() + + c: int = -1 + failures: int = 0 + while skip != -1: + c += 1 + try: + BIP32.derive( + seed, + [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 0, c] + ) + + #Since we derived a valid address, decrement skip. + skip -= 1 + failures = 0 + except Exception: + #Safety check to prevent infinite execution. + failures += 1 + if failures == 100: + raise Exception("Invalid mnemonic passed to getPrivateKey.") + continue + + return c + +def getPrivateKey( + mnemonic: str, + password: str, + skip: int +) -> bytes: + seed: bytes = sha256(Bip39SeedGenerator(mnemonic).Generate(password)).digest() + return BIP32.derive( + seed, + [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 0, getIndex(mnemonic, password, skip)] + ) + +def getPublicKey( + mnemonic: str, + password: str, + skip: int +) -> bytes: + return ed.encodepoint( + ed.scalarmult(ed.B, ed.decodeint(getPrivateKey(mnemonic, password, skip)[:32]) % ed.l) + ) + +def getChangePublicKey( + mnemonic: str, + password: str, + skip: int +) -> bytes: + seed: bytes = sha256(Bip39SeedGenerator(mnemonic).Generate(password)).digest() + extendedKey: bytes = bytes() + + #Above's getIndex, yet utilizing the return value of derive + c: int = -1 + failures: int = 0 + while skip != -1: + c += 1 + try: + extendedKey = BIP32.derive( + seed, + [44 + (1 << 31), 5132 + (1 << 31), 0 + (1 << 31), 1, c] + ) + + #Since we derived a valid address, decrement skip. + skip -= 1 + failures = 0 + except Exception: + #Safety check to prevent infinite execution. + failures += 1 + if failures == 100: + raise Exception("Invalid mnemonic passed to getPrivateKey.") + continue + + return ed.encodepoint(ed.scalarmult(ed.B, ed.decodeint(extendedKey[:32]) % ed.l)) + +def getAddress( + mnemonic: str, + password: str, + skip: int +) -> str: + return bech32_encode("mr", convertbits(bytes([0]) + getPublicKey(mnemonic, password, skip), 8, 5)) + +def decodeAddress( + address: str +) -> bytes: + decoded: Union[Tuple[None, None], Tuple[str, List[int]]] = bech32_decode(address) + if decoded[1] is None: + raise TestError("Decoding an invalid address.") + res: List[int] = convertbits(decoded[1], 5, 8) + if res[0] != 0: + raise TestError("Decoding an address which isn't a Public Key.") + return bytes(res[1:33]) diff --git a/e2e/Tests/RPC/Personal/PersonalAuthorizationTest.py b/e2e/Tests/RPC/Personal/PersonalAuthorizationTest.py new file mode 100644 index 000000000..a6a12277d --- /dev/null +++ b/e2e/Tests/RPC/Personal/PersonalAuthorizationTest.py @@ -0,0 +1,28 @@ +from e2e.Meros.RPC import RPC +from e2e.Tests.Errors import TestError + +def PersonalAuthorizationTest( + rpc: RPC +) -> None: + #Test all these methods require authorization. + #Doesn't test personal_data as that's not officially part of this test; just in it as a side note on key usage. + #The actual personal_data test should handle that check. + for method in [ + "setWallet", + "setAccount", + "getMnemonic", + "getMeritHolderKey", + "getMeritHolderNick", + "getAccount", + "getAddress", + "send", + "data", + "getUTXOs", + "getTransactionTemplate" + ]: + try: + rpc.call("personal", method, auth=False) + raise Exception() + except Exception as e: + if str(e) != "HTTP status isn't 200: 401": + raise TestError("Could call personal_" + method + " without authorization.") diff --git a/e2e/Tests/RPC/Personal/PersonalDataTest.py b/e2e/Tests/RPC/Personal/PersonalDataTest.py new file mode 100644 index 000000000..162d9e96e --- /dev/null +++ b/e2e/Tests/RPC/Personal/PersonalDataTest.py @@ -0,0 +1,105 @@ +from time import sleep + +from typing import Dict, Any + +from e2e.Meros.Meros import Meros +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError +from e2e.Tests.RPC.Personal.Lib import decodeAddress + +def checkData( + rpc: RPC, + dataHash: str, + expected: bytes +) -> str: + data: Dict[str, Any] = rpc.call("transactions", "getTransaction", {"hash": dataHash}) + + if len(data["inputs"]) != 1: + raise TestError("Data had multiple inputs.") + res: str = data["inputs"][0]["hash"] + del data["inputs"] + del data["signature"] + del data["proof"] + + if data != { + "descendant": "Data", + "outputs": [], + "hash": dataHash, + "data": expected.hex().upper() + }: + raise TestError("Data wasn't as expected.") + + return res + +def PersonalDataTest( + rpc: RPC +) -> None: + #Create a Data. + firstData: str = rpc.call("personal", "data", {"data": "a"}) + initial: str = checkData(rpc, firstData, b"a") + + #Meros should've also created an initial Data. + if checkData(rpc, initial, decodeAddress(rpc.call("personal", "getAddress"))) != bytes(32).hex(): + raise TestError("Initial Data didn't have a 0-hash input.") + + #Create a Data using hex data. Also tests upper case hex. + if checkData(rpc, rpc.call("personal", "data", {"data": "AABBCC", "hex": True}), b"\xAA\xBB\xCC") != firstData: + raise TestError("Newly created Data wasn't a descendant of the existing Data.") + + #Should support using 256 bytes of Data. Also tests lower case hex. + checkData(rpc, rpc.call("personal", "data", {"data": bytes([0xaa] * 256).hex(), "hex": True}), bytes([0xaa] * 256)) + + #Should properly error when we input no data. All Datas must have at least 1 byte of Data. + try: + rpc.call("personal", "data", {"data": ""}) + raise Exception() + except Exception as e: + if str(e) != "-3 Data is too small or too large.": + raise TestError("Meros didn't handle Data that was too small.") + + #Should properly error when we supply more than 256 bytes of data. + try: + rpc.call("personal", "data", {"data": "a" * 257}) + raise Exception() + except Exception as e: + if str(e) != "-3 Data is too small or too large.": + raise TestError("Meros didn't handle Data that was too large.") + + #Should properly error when we supply non-hex data with the hex flag. + try: + rpc.call("personal", "data", {"data": "zz", "hex": True}) + raise Exception() + except Exception as e: + if str(e) != "-3 Invalid hex char `z` (ord 122).": + raise TestError("Meros didn't properly handle invalid hex.") + + #Should properly error when we supply non-even hex data. + try: + rpc.call("personal", "data", {"data": "a", "hex": True}) + raise Exception() + except Exception as e: + if str(e) != "-3 Incorrect hex string len.": + raise TestError("Meros didn't properly handle non-even hex.") + + #Test Datas when the Wallet has a password. + rpc.call("personal", "setWallet", {"password": "password"}) + + #Shouldn't work due to the lack of a password. + try: + rpc.call("personal", "data", {"data": "abc"}) + raise Exception() + except Exception as e: + if str(e) != "-3 Invalid password.": + raise TestError("Meros didn't properly handle creating a Data without a password.") + + #Should work due to the existence of a password. + lastData: str = rpc.call("personal", "data", {"data": "abc", "password": "password"}) + checkData(rpc, lastData, b"abc") + + #Reboot the node and verify we can create a new Data without issue. + rpc.quit() + sleep(3) + rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc) + if checkData(rpc, rpc.call("personal", "data", {"data": "def", "password": "password"}), b"def") != lastData: + raise TestError("Couldn't create a new Data after rebooting.") diff --git a/e2e/Tests/RPC/Personal/PersonalSendTest.py b/e2e/Tests/RPC/Personal/PersonalSendTest.py new file mode 100644 index 000000000..7e254a621 --- /dev/null +++ b/e2e/Tests/RPC/Personal/PersonalSendTest.py @@ -0,0 +1,321 @@ +#Since personal_send internally called personal_getTransactionTemplate, this has heavy overlap with the WatchWallet test. +#That test is what explicitly handles personal_getTransactionTemplate. +#That said, that test also uses personal_send, and thanks to it, personal_send is a much simpler/more concise test. +#So while WatchWalletTest was heavily developed before this was started, this will finish first as concside and clean. + +from typing import Dict, List, Any +from time import sleep +import json + +import ed25519 +from bech32 import convertbits, bech32_encode +from pytest import raises + +from e2e.Libs.BLS import PrivateKey +from e2e.Libs.RandomX import RandomX + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Consensus.Verification import SignedVerification + +from e2e.Meros.Meros import MessageType, Meros +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Personal.Lib import getChangePublicKey, decodeAddress +from e2e.Tests.Errors import TestError, SuccessError + +def verify( + rpc: RPC, + tx: bytes +) -> None: + sv: SignedVerification = SignedVerification(tx) + sv.sign(0, PrivateKey(0)) + if rpc.meros.signedElement(sv) != rpc.meros.live.recv(): + raise TestError("Meros didn't send back a Verification.") + +def createSend( + rpc: RPC, + claim: Claim, + to: bytes +) -> bytes: + send: Send = Send([(claim.hash, 0)], [(to, claim.amount)]) + send.sign(ed25519.SigningKey(b'\0' * 32)) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't send back a Send.") + verify(rpc, send.hash) + return send.hash + +def sortUTXOs( + utxos: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + return sorted(utxos, key=lambda utxo: utxo["hash"] + str(utxo["nonce"])) + +def checkSend( + rpc: RPC, + sendHash: str, + expected: Dict[str, Any] +) -> None: + send: Dict[str, Any] = rpc.call("transactions", "getTransaction", {"hash": sendHash}) + serialized: bytes = Send.fromJSON(send).serialize() + del send["signature"] + del send["proof"] + + expected["descendant"] = "Send" + expected["hash"] = sendHash + if sortUTXOs(send["inputs"]) != sortUTXOs(expected["inputs"]): + raise TestError("Send inputs weren't as expected.") + del send["inputs"] + del expected["inputs"] + if send != expected: + raise TestError("Send wasn't as expected.") + + if rpc.meros.live.recv() != (MessageType.Send.toByte() + serialized): + raise TestError("Meros didn't broadcast a Send it created.") + +#pylint: disable=too-many-statements,too-many-locals +def PersonalSendTest( + rpc: RPC +) -> None: + #Load the vectors. + #Uses the WatchWallet test's vectors for the reasons noted above. + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Personal/WatchWallet.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + #The order of the Claims isn't relevant to this test. + claims: List[Claim] = [] + for tx in transactions.txs.values(): + claims.append(Claim.fromTransaction(tx)) + + def test() -> None: + #Send to the first address from outside the Wallet. First address is now funded. + sendHash: bytes = createSend(rpc, claims[0], decodeAddress(rpc.call("personal", "getAddress"))) + + #Send to the second address with all of the funds. Second address is now funded. + #Tests send's minimal case (single input, no change). + nextAddr: str = rpc.call("personal", "getAddress") + sends: List[str] = [ + rpc.call( + "personal", + "send", + {"outputs": [{"address": nextAddr, "amount": str(claims[0].amount)}]} + ) + ] + checkSend( + rpc, + sends[-1], + { + "inputs": [{"hash": sendHash.hex().upper(), "nonce": 0}], + "outputs": [{ + "key": decodeAddress(nextAddr).hex().upper(), + "amount": str(claims[0].amount) + }] + } + ) + verify(rpc, bytes.fromhex(sends[-1])) + + #Send to the third address with some of the funds. Third and change addresses are now funded. + #Tests send's capability to create a change output. + mnemonic: str = rpc.call("personal", "getMnemonic") + nextAddr = rpc.call("personal", "getAddress") + sends.append( + rpc.call( + "personal", + "send", + {"outputs": [{"address": nextAddr, "amount": str(claims[0].amount - 1)}]} + ) + ) + checkSend( + rpc, + sends[-1], + { + "inputs": [{"hash": sends[-2], "nonce": 0}], + "outputs": [ + { + "key": decodeAddress(nextAddr).hex().upper(), + "amount": str(claims[0].amount - 1) + }, + { + "key": getChangePublicKey(mnemonic, "", 0).hex().upper(), + "amount": "1" + } + ] + } + ) + verify(rpc, bytes.fromhex(sends[-1])) + + #Send all funds out of Wallet. + #Tests MuSig signing and change UTXO detection. + privKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + pubKey: bytes = privKey.get_verifying_key().to_bytes() + sends.append( + rpc.call( + "personal", + "send", + {"outputs": [{"address": bech32_encode("mr", convertbits(bytes([0]) + pubKey, 8, 5)), "amount": str(claims[0].amount)}]} + ) + ) + checkSend( + rpc, + sends[-1], + { + "inputs": [{"hash": sends[-2], "nonce": 0}, {"hash": sends[-2], "nonce": 1}], + "outputs": [{ + "key": pubKey.hex().upper(), + "amount": str(claims[0].amount) + }] + } + ) + verify(rpc, bytes.fromhex(sends[-1])) + + #Clear Wallet. Set a password this time around to make sure the password is properly carried. + #Send two instances of funds to the first address. + rpc.call("personal", "setWallet", {"password": "test"}) + mnemonic = rpc.call("personal", "getMnemonic") + nodeKey: bytes = decodeAddress(rpc.call("personal", "getAddress")) + send: Send = Send([(bytes.fromhex(sends[-1]), 0)], [(nodeKey, claims[0].amount // 2), (nodeKey, claims[0].amount // 2)]) + send.sign(ed25519.SigningKey(b'\0' * 32)) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't send back a Send.") + verify(rpc, send.hash) + sends = [send.hash.hex().upper()] + + #Send to self. + #Tests send's capability to handle multiple UTXOs per key/lack of aggregation when all keys are the same/multiple output Sends. + nextAddr = rpc.call("personal", "getAddress") + changeKey: bytes = getChangePublicKey(mnemonic, "test", 0) + sends.append( + rpc.call( + "personal", + "send", + {"outputs": [{"address": nextAddr, "amount": str(claims[0].amount - 1)}], "password": "test"} + ) + ) + checkSend( + rpc, + sends[-1], + { + "inputs": [{"hash": sends[-2], "nonce": 0}, {"hash": sends[-2], "nonce": 1}], + "outputs": [ + { + "key": decodeAddress(nextAddr).hex().upper(), + "amount": str(claims[0].amount - 1) + }, + { + "key": changeKey.hex().upper(), + "amount": "1" + } + ] + } + ) + verify(rpc, bytes.fromhex(sends[-1])) + + #Externally send to the second/change address. + #Enables entering multiple instances of each key into MuSig, which is significant as we originally only used the unique keys. + sends.append(createSend(rpc, claims[1], decodeAddress(nextAddr)).hex().upper()) + sends.append(createSend(rpc, claims[2], changeKey).hex().upper()) + + #Check personal_getUTXOs. + utxos: List[Dict[str, Any]] = [ + { + "hash": sends[-3], + "nonce": 0, + "address": nextAddr + }, + { + "hash": sends[-3], + "nonce": 1, + "address": bech32_encode("mr", convertbits(bytes([0]) + changeKey, 8, 5)) + }, + { + "hash": sends[-2], + "nonce": 0, + "address": nextAddr + }, + { + "hash": sends[-1], + "nonce": 0, + "address": bech32_encode("mr", convertbits(bytes([0]) + changeKey, 8, 5)) + } + ] + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos): + raise TestError("personal_getUTXOs was incorrect.") + for utxo in utxos: + del utxo["address"] + + #Send to any address with all funds minus one. + #Test MuSig signing, multiple inputs per key on account chains, change output creation to the next change key... + sends.append( + rpc.call( + "personal", + "send", + {"outputs": [{"address": nextAddr, "amount": str(claims[0].amount + claims[1].amount + claims[2].amount - 1)}], "password": "test"} + ) + ) + checkSend( + rpc, + sends[-1], + { + "inputs": utxos, + "outputs": [ + { + "key": decodeAddress(nextAddr).hex().upper(), + "amount": str(claims[0].amount + claims[1].amount + claims[2].amount - 1) + }, + { + "key": getChangePublicKey(mnemonic, "test", 1).hex().upper(), + "amount": "1" + } + ] + } + ) + verify(rpc, bytes.fromhex(sends[-1])) + + #Mine a Block so we can reboot the node without losing data. + blsPrivKey: PrivateKey = PrivateKey(bytes.fromhex(rpc.call("personal", "getMeritHolderKey"))) + for _ in range(6): + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": blsPrivKey.toPublicKey().serialize().hex()}) + proof: int = -1 + tempHash: bytes = bytes() + tempSignature: bytes = bytes() + while ( + (proof == -1) or + ((int.from_bytes(tempHash, "little") * template["difficulty"]) > int.from_bytes(bytes.fromhex("FF" * 32), "little")) + ): + proof += 1 + tempHash = RandomX(bytes.fromhex(template["header"]) + proof.to_bytes(4, "little")) + tempSignature = blsPrivKey.sign(tempHash).serialize() + tempHash = RandomX(tempHash + tempSignature) + + rpc.call("merit", "publishBlock", {"id": template["id"], "header": template["header"] + proof.to_bytes(4, "little").hex() + tempSignature.hex()}) + + #Reboot the node and verify it still tracks the same change address. + #Also reload the Wallet and verify it still tracks the same change address. + #Really should be part of address discovery; we just have the opportunity right here. + #Due to the timing of how the codebase was developed, and a personal frustration for how long this has taken... + rpc.quit() + sleep(3) + rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc) + if rpc.call( + "personal", + "getTransactionTemplate", + {"outputs": [{"address": nextAddr, "amount": "1"}]} + )["outputs"][1]["key"] != getChangePublicKey(mnemonic, "test", 2).hex().upper(): + raise TestError("Rebooting the node caused the WalletDB to stop tracking the next change address.") + rpc.call("personal", "setAccount", rpc.call("personal", "getAccount")) + if rpc.call( + "personal", + "getTransactionTemplate", + {"outputs": [{"address": nextAddr, "amount": "1"}]} + )["outputs"][1]["key"] != getChangePublicKey(mnemonic, "test", 2).hex().upper(): + raise TestError("Reloading the Wallet caused the WalletDB to stop tracking the next change address.") + + raise SuccessError() + + #Use a late enough block we can instantly verify transactions. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/Personal/WatchWalletTest.py b/e2e/Tests/RPC/Personal/WatchWalletTest.py new file mode 100644 index 000000000..4dbb3b8ff --- /dev/null +++ b/e2e/Tests/RPC/Personal/WatchWalletTest.py @@ -0,0 +1,379 @@ +#Developed before, yet finished after, personal_send's test. +#PersonalSendTest is much cleaner and wraps getTransactionTemplate, meaning this really only needs to test from/change. +#Some old, overlapping test cases have been left in. + +from typing import Dict, List, Any +from time import sleep +import json + +import ed25519 +from bech32 import convertbits, bech32_encode + +import e2e.Libs.ed25519 as ed +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Consensus.Verification import SignedVerification +from e2e.Classes.Merit.Blockchain import Blockchain + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Personal.Lib import getPublicKey, getChangePublicKey, getAddress +from e2e.Tests.Errors import TestError + +def createSend( + rpc: RPC, + claim: Claim, + to: bytes +) -> bytes: + send: Send = Send([(claim.hash, 0)], [(to, claim.amount)]) + send.sign(ed25519.SigningKey(b'\0' * 32)) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't send back a Send.") + return send.hash + +def verify( + rpc: RPC, + tx: bytes +) -> None: + sv: SignedVerification = SignedVerification(tx) + sv.sign(0, PrivateKey(0)) + if rpc.meros.signedElement(sv) != rpc.meros.live.recv(): + raise TestError("Meros didn't send back a Verification.") + +def sortUTXOs( + utxos: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + return sorted(utxos, key=lambda utxo: utxo["hash"]) + +def checkTemplate( + rpc: RPC, + mnemonic: str, + req: Dict[str, Any], + inputs: List[Dict[str, Any]], + outputs: List[Dict[str, Any]] +) -> None: + template: Dict[str, Any] = rpc.call("personal", "getTransactionTemplate", req) + if template["type"] != "Send": + raise TestError("Template isn't of type Send.") + if sortUTXOs(template["inputs"]) != sortUTXOs(inputs): + raise TestError("Template inputs aren't as expected.") + if template["outputs"] != outputs: + raise TestError("Template outputs are incorrect.") + + keys: List[bytes] = [] + for inputJSON in template["inputs"]: + key: bytes + if inputJSON["change"]: + key = getChangePublicKey(mnemonic, "", inputJSON["index"]) + else: + key = getPublicKey(mnemonic, "", inputJSON["index"]) + if key not in keys: + keys.append(key) + + if template["publicKey"] != ed.aggregate(keys).hex().upper(): + if len(keys) == 1: + raise TestError("Template public key isn't correct when only a single key is present.") + raise TestError("Public key aggregation isn't correct.") + +#pylint: disable=too-many-statements +def WatchWalletTest( + rpc: RPC +) -> None: + #Keys to send funds to later. + keys: List[bytes] = [ + ed25519.SigningKey(i.to_bytes(1, "little") * 32).get_verifying_key().to_bytes() for i in range(5) + ] + + #Backup the Mnemonic so we can independently derive this data and verify it. + mnemonic: str = rpc.call("personal", "getMnemonic") + + #Node's Wallet's keys. + nodeKeys: List[bytes] = [getPublicKey(mnemonic, "", i) for i in range(4)] + nodeAddresses: List[str] = [getAddress(mnemonic, "", i) for i in range(4)] + + #Convert this to a WatchWallet node. + account: Dict[str, Any] = rpc.call("personal", "getAccount") + rpc.call("personal", "setAccount", account) + if rpc.call("personal", "getAccount") != account: + raise TestError("Meros set a different account.") + + #Verify it has the correct initial address. + if rpc.call("personal", "getAddress") != nodeAddresses[0]: + raise TestError("WatchWallet has an incorrect initial address.") + + #Load the vectors. + #This test requires 3 Claims be available. + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Personal/WatchWallet.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + #The order of the Claims isn't relevant to this test. + claims: List[Claim] = [] + for tx in transactions.txs.values(): + claims.append(Claim.fromTransaction(tx)) + + def test() -> None: + #Send to it. + sends: List[bytes] = [createSend(rpc, claims[0], nodeKeys[0])] + verify(rpc, sends[-1]) + + #Test the most basic template possible. + checkTemplate( + rpc, + mnemonic, + { + "outputs": [{ + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[0], 8, 5)), + "amount": "1" + }] + }, + [{ + "hash": sends[-1].hex().upper(), + "nonce": 0, + "change": False, + "index": 0 + }], + [ + {"key": keys[0].hex().upper(), "amount": "1"}, + { + "key": getChangePublicKey(mnemonic, "", 0).hex().upper(), + "amount": str(claims[0].amount - 1) + } + ] + ) + + #Verify it has the correct next address. + if rpc.call("personal", "getAddress") != nodeAddresses[1]: + raise TestError("WatchWallet has an incorrect next address.") + + #Send to it. + sends.append(createSend(rpc, claims[1], nodeKeys[1])) + verify(rpc, sends[-1]) + + #Get and send to one more, yet don't verify it yet. + if rpc.call("personal", "getAddress") != nodeAddresses[2]: + raise TestError("WatchWallet has an incorrect next next address.") + sends.append(createSend(rpc, claims[2], nodeKeys[2])) + + #Verify it can get UTXOs properly. + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs( + [ + { + "address": getAddress(mnemonic, "", i), + "hash": sends[i].hex().upper(), + "nonce": 0 + } for i in range(2) + ] + ): + raise TestError("WatchWallet Meros couldn't get its UTXOs.") + + #Verify the third Send. + verify(rpc, sends[-1]) + + #Close the sockets for now. + rpc.meros.live.connection.close() + rpc.meros.sync.connection.close() + + #Verify getUTXOs again. Redundant thanks to the extensive getUTXO testing elsewhere, yet valuable. + if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs( + [ + { + "address": getAddress(mnemonic, "", i), + "hash": sends[i].hex().upper(), + "nonce": 0 + } for i in range(3) + ] + ): + raise TestError("WatchWallet Meros couldn't get its UTXOs.") + + #Verify it can craft a Transaction Template properly. + claimsAmount: int = sum(claim.amount for claim in claims) + amounts: List[int] = [claimsAmount // 4, claimsAmount // 4, claimsAmount // 5] + amounts.append(claimsAmount - sum(amounts)) + req: Dict[str, Any] = { + "outputs": [ + { + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[0], 8, 5)), + "amount": str(amounts[0]) + }, + { + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[1], 8, 5)), + "amount": str(amounts[1]) + }, + { + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[2], 8, 5)), + "amount": str(amounts[2]) + } + ] + } + inputs: List[Dict[str, Any]] = [ + { + "hash": sends[i].hex().upper(), + "nonce": 0, + "change": False, + "index": i + } for i in range(3) + ] + outputs: List[Dict[str, Any]] = [ + { + "key": keys[i].hex().upper(), + "amount": str(amounts[i]) + } for i in range(3) + ] + [ + { + "key": getChangePublicKey(mnemonic, "", 0).hex().upper(), + "amount": str(amounts[-1]) + } + ] + checkTemplate(rpc, mnemonic, req, inputs, outputs) + + #Specify only to use specific addresses and verify Meros does so. + req["from"] = [nodeAddresses[1], nodeAddresses[2]] + + #Correct the amounts so this is feasible. + del req["outputs"][-1] + del outputs[-2] + #Remove the change output amount and actual output amount. + for _ in range(2): + del amounts[-1] + claimsAmount -= claims[-1].amount + #Correct the change output. + outputs[-1]["amount"] = str(claimsAmount - sum(amounts)) + + del inputs[0] + checkTemplate(rpc, mnemonic, req, inputs, outputs) + del req["from"] + + #Use the change address in question and verify the next template uses the next change address. + #This is done via creating a Send which doesn't spend all of inputs value. + #Also tests Meros handles Sends, and therefore templates, which don't use all funds. + change: bytes = getChangePublicKey(mnemonic, "", 0) + + #Convert to a Wallet in order to do so. + rpc.call("personal", "setWallet", {"mnemonic": mnemonic}) + send: Dict[str, Any] = rpc.call( + "transactions", + "getTransaction", + { + "hash": rpc.call( + "personal", + "send", + { + "outputs": [ + { + "address": bech32_encode("mr", convertbits(bytes([0]) + change, 8, 5)), + "amount": "1" + } + ] + } + ) + } + ) + #Convert back. + rpc.call("personal", "setAccount", account) + + #Reconnect. + sleep(65) + rpc.meros.liveConnect(Blockchain().blocks[0].header.hash) + rpc.meros.syncConnect(Blockchain().blocks[0].header.hash) + + #Verify the Send so the Wallet doesn't lose Meros from consideration. + verify(rpc, bytes.fromhex(send["hash"])) + + #Verify the Send's accuracy. + if len(send["inputs"]) != 1: + raise TestError("Meros used more inputs than neccessary.") + if send["outputs"] != [ + {"key": change.hex().upper(), "amount": "1"}, + #Uses the existing, unused, change address as change. + #While this Transaction will make it used, that isn't detected. + #This isn't worth programming around due to the lack of implications except potentially minor metadata. + {"key": change.hex().upper(), "amount": str(claims[0].amount - 1)} + ]: + raise TestError("Send outputs weren't as expected.") + + if rpc.call("personal", "getTransactionTemplate", req)["outputs"][-1]["key"] != getChangePublicKey(mnemonic, "", 1).hex().upper(): + raise TestError("Meros didn't move to the next change address.") + + #Specify an explicit change address. + req["change"] = nodeAddresses[3] + if rpc.call("personal", "getTransactionTemplate", req)["outputs"][-1]["key"] != nodeKeys[3].hex().upper(): + raise TestError("Meros didn't handle an explicitly set change address.") + + #Verify RPC methods which require the private key error properly. + #Tests via getMnemonic and data. + try: + rpc.call("personal", "getMnemonic") + raise TestError() + except Exception as e: + if str(e) != "-3 This is a WatchWallet node; no Mnemonic is set.": + raise TestError("getMnemonic didn't error as expected when Meros didn't have a Wallet.") + + try: + rpc.call("personal", "data", {"data": "abc"}) + raise TestError() + except Exception as e: + if str(e) != "-3 This is a WatchWallet node; no Mnemonic is set.": + raise TestError("data didn't error as expected when Meros didn't have a Wallet.") + + #Also test getMeritHolderKey, as no Merit Holder key should exist. + try: + rpc.call("personal", "getMeritHolderKey") + raise TestError() + except Exception as e: + if str(e) != "-3 Node is running as a WatchWallet and has no Merit Holder.": + raise TestError("data didn't error as expected when Meros didn't have a Wallet.") + + #Try calling getTransactionTemplate spending way too much Meros. + try: + rpc.call( + "personal", + "getTransactionTemplate", + { + "outputs": [ + { + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[0], 8, 5)), + "amount": str(claimsAmount * 100) + } + ] + } + ) + raise TestError() + except Exception as e: + if str(e) != "1 Wallet doesn't have enough Meros.": + raise TestError("Meros didn't error as expected when told to spend more Meros than it has.") + + #Try calling getTransactionTemplate with no outputs. + try: + rpc.call("personal", "getTransactionTemplate", {"outputs": []}) + raise TestError() + except Exception as e: + if str(e) != "-3 No outputs were provided.": + raise TestError("Meros didn't error as expected when told to create a template with no outputs.") + + #Try calling getTransactionTemplate with a 0 value output. + try: + rpc.call( + "personal", + "getTransactionTemplate", + { + "outputs": [ + { + "address": bech32_encode("mr", convertbits(bytes([0]) + keys[0], 8, 5)), + "amount": "0" + } + ] + } + ) + raise TestError() + except Exception as e: + if str(e) != "-3 0 value output was provided.": + raise TestError("Meros didn't error as expected when told to create a template with a 0 value output.") + + #Use a late enough block we can instantly verify transactions. + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/StringBasedTypesTest.py b/e2e/Tests/RPC/StringBasedTypesTest.py new file mode 100644 index 000000000..2cead5d03 --- /dev/null +++ b/e2e/Tests/RPC/StringBasedTypesTest.py @@ -0,0 +1,84 @@ +#Tests handling of erroneous values for hex/Hash/BLSPublicKey/EdPublicKey. +#Address is tested in the AddressTest. + +import ed25519 + +from e2e.Libs.BLS import PrivateKey, PublicKey + +from e2e.Classes.Transactions.Data import Data + +from e2e.Meros.RPC import RPC +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def StringBasedTypesTest( + rpc: RPC +) -> None: + edPrivKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + edPubKey: bytes = edPrivKey.get_verifying_key().to_bytes() + blsPubKey: PublicKey = PrivateKey(0).toPublicKey() + + #hex. + #Test 0x-prefixed and no-0x both work without issue. + data: Data = Data(bytes(32), edPubKey) + data.sign(edPrivKey) + rpc.call("transactions", "publishTransactionWithoutWork", {"type": "Data", "transaction": "0x" + data.serialize()[:-4].hex()}) + + data = Data(data.hash, b"abc") + data.sign(edPrivKey) + rpc.call("transactions", "publishTransactionWithoutWork", {"type": "Data", "transaction": data.serialize()[:-4].hex()}) + + #Test non-hex data is properly handled. + try: + rpc.call("transactions", "publishTransactionWithoutWork", {"type": "Data", "transaction": "az"}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted non-hex data for a hex argument.") + + #Hash. + rpc.call("transactions", "getTransaction", {"hash": data.hash.hex()}) + rpc.call("transactions", "getTransaction", {"hash": "0x" + data.hash.hex()}) + + #Also call the upper form, supplementing the above hex tests (as Hash routes through hex). + rpc.call("transactions", "getTransaction", {"hash": data.hash.hex().upper()}) + rpc.call("transactions", "getTransaction", {"hash": "0x" + data.hash.hex().upper()}) + + #Improper length. + try: + rpc.call("transactions", "getTransaction", {"hash": data.hash.hex().upper()[:-2]}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a hex string with an improper length as a Hash.") + + #BLSPublicKey. + try: + rpc.call("merit", "getNickname", {"key": blsPubKey.serialize().hex()}) + raise TestError() + except Exception as e: + if str(e) != "-2 Key doesn't have a nickname assigned.": + raise TestError("Meros didn't accept a valid BLSPublicKey.") + try: + rpc.call("merit", "getNickname", {"key": blsPubKey.serialize().hex()[:-2]}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a hex string with an improper length as a BLSPublicKey.") + + #Missing flags. + try: + rpc.call("merit", "getNickname", {"key": "0" + blsPubKey.serialize().hex()[1]}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted an invalid BLSPublicKey as a BLSPublicKey.") + + #EdPublicKey. + rpc.call("personal", "setAccount", {"key": edPubKey.hex(), "chainCode": bytes(32).hex()}) + try: + rpc.call("personal", "setAccount", {"key": edPubKey[:-2].hex(), "chainCode": bytes(32).hex()}) + raise TestError() + except Exception as e: + if str(e) != "-32602 Invalid params.": + raise TestError("Meros accepted a hex string with an improper length as an EdPublicKey.") diff --git a/e2e/Tests/RPC/Transactions/GetTransactionTest.py b/e2e/Tests/RPC/Transactions/GetTransactionTest.py new file mode 100644 index 000000000..e71d4587b --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetTransactionTest.py @@ -0,0 +1,166 @@ +from typing import Dict, Any +import json + +import ed25519 + +from e2e.Classes.Transactions.Mint import Mint +from e2e.Classes.Transactions.Transactions import Claim, Send, Data, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Merit.Merit import Merit + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def GetTransactionTest( + rpc: RPC +) -> None: + privKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + pubKey: ed25519.VerifyingKey = privKey.get_verifying_key() + + sendFilter: SpamFilter = SpamFilter(3) + dataFilter: SpamFilter = SpamFilter(5) + + vectors: Dict[str, Any] + with open("e2e/Vectors/Transactions/ClaimedMint.json", "r") as file: + vectors = json.loads(file.read()) + + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + if len(transactions.txs) != 1: + raise Exception("Transactions DAG doesn't have just the Claim.") + claim: Claim = Claim.fromTransaction(next(iter(transactions.txs.values()))) + + send: Send = Send( + [(claim.hash, 0)], + [( + ed25519.SigningKey(b'\1' * 32).get_verifying_key().to_bytes(), + claim.amount + )] + ) + send.sign(privKey) + send.beat(sendFilter) + + data: Data = Data(bytes(32), pubKey.to_bytes()) + data.sign(privKey) + data.beat(dataFilter) + + def sendAndVerify() -> None: + rpc.meros.liveTransaction(send) + rpc.meros.liveTransaction(data) + rpc.meros.live.recv() + rpc.meros.live.recv() + + #We now have a Mint, a Claim, a Send, a Data, a lion, a witch, and a wardrobe. + + #Check the Mint. + mint: Mint = Merit.fromJSON(vectors["blockchain"]).mints[0] + #pylint: disable=invalid-name + EXPECTED_MINT: Dict[str, Any] = { + "descendant": "Mint", + "inputs": [], + "outputs": [ + { + "amount": str(txOutput[1]), + "nick": txOutput[0] + } for txOutput in mint.outputs + ], + "hash": mint.hash.hex().upper() + } + #Also sanity check against the in-house JSON. + if mint.toJSON() != EXPECTED_MINT: + raise TestError("Python's Mint toJSON doesn't match the spec.") + if rpc.call("transactions", "getTransaction", {"hash": mint.hash.hex()}, False) != EXPECTED_MINT: + raise TestError("getTransaction didn't report the Mint properly.") + + #Check the Claim. + #pylint: disable=invalid-name + EXPECTED_CLAIM: Dict[str, Any] = { + "descendant": "Claim", + "inputs": [ + { + "hash": txInput[0].hex().upper(), + "nonce": txInput[1] + } for txInput in claim.inputs + ], + "outputs": [{ + "amount": str(claim.amount), + "key": claim.output.hex().upper() + }], + "hash": claim.hash.hex().upper(), + "signature": claim.signature.hex().upper() + } + if claim.amount == 0: + raise Exception("Python didn't instantiate the Claim with an amount, leading to invalid testing methodology.") + if claim.toJSON() != EXPECTED_CLAIM: + raise TestError("Python's Claim toJSON doesn't match the spec.") + if rpc.call("transactions", "getTransaction", {"hash": claim.hash.hex()}, False) != EXPECTED_CLAIM: + raise TestError("getTransaction didn't report the Claim properly.") + + #Check the Send. + #pylint: disable=invalid-name + EXPECTED_SEND: Dict[str, Any] = { + "descendant": "Send", + "inputs": [ + { + "hash": txInput[0].hex().upper(), + "nonce": txInput[1] + } for txInput in send.inputs + ], + "outputs": [ + { + "amount": str(txOutput[1]), + "key": txOutput[0].hex().upper() + } for txOutput in send.outputs + ], + "hash": send.hash.hex().upper(), + "signature": send.signature.hex().upper(), + "proof": send.proof + } + if send.toJSON() != EXPECTED_SEND: + raise TestError("Python's Send toJSON doesn't match the spec.") + if rpc.call("transactions", "getTransaction", {"hash": send.hash.hex()}, False) != EXPECTED_SEND: + raise TestError("getTransaction didn't report the Send properly.") + + #Check the Data. + #pylint: disable=invalid-name + EXPECTED_DATA: Dict[str, Any] = { + "descendant": "Data", + "inputs": [{ + "hash": data.txInput.hex().upper() + }], + "outputs": [], + "hash": data.hash.hex().upper(), + "data": data.data.hex().upper(), + "signature": data.signature.hex().upper(), + "proof": data.proof + } + if data.toJSON() != EXPECTED_DATA: + raise TestError("Python's Data toJSON doesn't match the spec.") + if rpc.call("transactions", "getTransaction", {"hash": data.hash.hex()}, False) != EXPECTED_DATA: + raise TestError("getTransaction didn't report the Data properly.") + + #Non-existent hash; should cause an IndexError + nonExistentHash: str = data.hash.hex() + if data.hash[0] == "0": + nonExistentHash = "1" + nonExistentHash[1:] + else: + nonExistentHash = "0" + nonExistentHash[1:] + try: + rpc.call("transactions", "getTransaction", {"hash": nonExistentHash}, False) + except TestError as e: + if str(e) != "-2 Transaction not found.": + raise TestError("getTransaction didn't raise IndexError on a non-existent hash.") + + #Invalid argument; should cause a ParamError + #This is still a hex value + try: + rpc.call("transactions", "getTransaction", {"hash": "00" + data.hash.hex()}, False) + raise TestError("Meros didn't error when we asked for a 33-byte hex value.") + except TestError as e: + if str(e) != "-32602 Invalid params.": + raise TestError("getTransaction didn't raise on invalid parameters.") + + Liver(rpc, vectors["blockchain"], transactions, {8: sendAndVerify}).live() diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/Lib.py b/e2e/Tests/RPC/Transactions/GetUTXOs/Lib.py new file mode 100644 index 000000000..ad2f4bf06 --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/Lib.py @@ -0,0 +1,52 @@ +from typing import Dict, Any + +from e2e.Libs.BLS import PrivateKey +from e2e.Libs.RandomX import RandomX + +from e2e.Classes.Consensus.Verification import SignedVerification + +from e2e.Meros.Meros import MessageType +from e2e.Meros.RPC import RPC + +from e2e.Tests.Errors import TestError + +def verify( + rpc: RPC, + txHash: bytes, + nick: int = 0, + mr: bool = False +) -> None: + sv: SignedVerification = SignedVerification(txHash) + sv.sign(nick, PrivateKey(nick)) + temp: bytes = rpc.meros.signedElement(sv) + if mr: + if MessageType(rpc.meros.live.recv()[0]) != MessageType.SignedMeritRemoval: + raise TestError("Meros didn't create a MeritRemoval.") + elif temp != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Verification.") + +#This really should've been vectored out. +def mineBlock( + rpc: RPC, + nick: int = 0 +) -> None: + privKey: PrivateKey = PrivateKey(nick) + template: Dict[str, Any] = rpc.call("merit", "getBlockTemplate", {"miner": privKey.toPublicKey().serialize().hex()}) + header: bytes = bytes.fromhex(template["header"])[:-4] + header += (rpc.call("merit", "getBlock", {"block": rpc.call("merit", "getHeight") - 1})["header"]["time"] + 1200).to_bytes(4, "little") + + proof: int = -1 + tempHash: bytes = bytes() + signature: bytes = bytes() + while ( + (proof == -1) or + ((int.from_bytes(tempHash, "little") * template["difficulty"]) > int.from_bytes(bytes.fromhex("FF" * 32), "little")) + ): + proof += 1 + tempHash = RandomX(header + proof.to_bytes(4, "little")) + signature = privKey.sign(tempHash).serialize() + tempHash = RandomX(tempHash + signature) + + rpc.call("merit", "publishBlock", {"id": template["id"], "header": header.hex() + proof.to_bytes(4, "little").hex() + signature.hex()}) + if rpc.meros.live.recv() != (MessageType.BlockHeader.toByte() + header + proof.to_bytes(4, "little") + signature): + raise TestError("Meros didn't broadcast back the BlockHeader.") diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/TGUBasicTest.py b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUBasicTest.py new file mode 100644 index 000000000..76a53887c --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUBasicTest.py @@ -0,0 +1,84 @@ +#Also tests transactions_getBalance. + +from typing import Dict, Any +import json + +import ed25519 +from bech32 import convertbits, bech32_encode + +from e2e.Classes.Transactions.Transactions import Send, Transactions + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import verify +from e2e.Tests.Errors import TestError + +def TGUBasicTest( + rpc: RPC +) -> None: + recipient: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + recipientPub: bytes = recipient.get_verifying_key().to_bytes() + address: str = bech32_encode("mr", convertbits(bytes([0]) + recipientPub, 8, 5)) + + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + send: Send = Send.fromJSON(vectors["send"]) + spendingSend: Send = Send.fromJSON(vectors["spendingSend"]) + + def start() -> None: + #Send the Send. + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros considered an unconfirmed Transaction's outputs as UTXOs.") + + #Verify the Send and make sure it's considered as a valid UTXO. + verify(rpc, send.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != [{"hash": send.hash.hex().upper(), "nonce": 0}]: + raise TestError("Meros didn't consider a confirmed Transaction's outputs as UTXOs.") + if rpc.call("transactions", "getBalance", {"address": address}) != str(send.outputs[0][1]): + raise TestError("transactions_getBalance didn't count an active UTXO.") + + def verified() -> None: + #Spend it. + if rpc.meros.liveTransaction(spendingSend) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a Transaction's inputs as spent.") + if rpc.call("transactions", "getBalance", {"address": address}) != "0": + raise TestError("transactions_getBalance counted a spent TXO.") + + #Verify the spender and verify the state is unchanged. + verify(rpc, spendingSend.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a verified Transaction's inputs as spent.") + + def finalizedSend() -> None: + #Sanity check the spending TX has yet to also finalize. + if rpc.call("consensus", "getStatus", {"hash": spendingSend.hash.hex()})["finalized"]: + raise Exception("Test meant to only finalize the first Send, not both.") + + #Verify the state is unchanged. + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a verified Transaction's inputs as spent after the input finalized.") + + def finalizedSpendingSend() -> None: + #Verify the state is unchanged. + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a finalized Transaction's inputs as spent.") + + Liver( + rpc, + vectors["blockchain"], + transactions, + { + 50: start, + 51: verified, + 56: finalizedSend, + 57: finalizedSpendingSend + } + ).live() diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/TGUFinalizesTest.py b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUFinalizesTest.py new file mode 100644 index 000000000..b9ae4c945 --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUFinalizesTest.py @@ -0,0 +1,68 @@ +#Tests a Transaction which is never verified, yet does finalize as the winner, creates UTXOs. + +from typing import Dict, Any +import json + +import ed25519 +from bech32 import convertbits, bech32_encode +from pytest import raises + +from e2e.Classes.Transactions.Transactions import Send, Transactions + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import verify, mineBlock +from e2e.Tests.Errors import TestError, SuccessError + +def TGUFinalizesTest( + rpc: RPC +) -> None: + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + def test() -> None: + recipient: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + recipientPub: bytes = recipient.get_verifying_key().to_bytes() + address: str = bech32_encode("mr", convertbits(bytes([0]) + recipientPub, 8, 5)) + + otherRecipient: bytes = ed25519.SigningKey(b'\2' * 32).get_verifying_key().to_bytes() + otherAddress: str = bech32_encode("mr", convertbits(bytes([0]) + otherRecipient, 8, 5)) + + #Create a Send. + send: Send = Send.fromJSON(vectors["send"]) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros considered an unconfirmed Transaction's outputs as UTXOs.") + verify(rpc, send.hash) + + #Spend it. + spendingSend: Send = Send.fromJSON(vectors["spendingSend"]) + if rpc.meros.liveTransaction(spendingSend) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a Transaction's inputs as spent.") + + #Verify with another party, so it won't be majority verified, yet will still have a Verification. + mineBlock(rpc, 1) + verify(rpc, spendingSend.hash, 1) + #Verify it didn't create a UTXO. + if rpc.call("transactions", "getUTXOs", {"address": otherAddress}) != []: + raise TestError("Unverified Transaction created a UTXO.") + + #Finalize. + for _ in range(6): + mineBlock(rpc) + + #Check the UTXOs were created. + if rpc.call("transactions", "getUTXOs", {"address": otherAddress}) != [{"hash": spendingSend.hash.hex().upper(), "nonce": 0}]: + raise TestError("Meros didn't consider a finalized Transaction's outputs as UTXOs.") + + raise SuccessError() + + #Send Blocks so we have a Merit Holder who can instantly verify Transactions, not to mention Mints. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/TGUImmediatelySpentTest.py b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUImmediatelySpentTest.py new file mode 100644 index 000000000..5494fb9b6 --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUImmediatelySpentTest.py @@ -0,0 +1,78 @@ +from typing import Dict, Any +import json + +import ed25519 +from bech32 import convertbits, bech32_encode + +from e2e.Classes.Transactions.Transactions import Send, Transactions + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import verify +from e2e.Tests.Errors import TestError + +def TGUImmediatelyTest( + rpc: RPC +) -> None: + recipient: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + recipientPub: bytes = recipient.get_verifying_key().to_bytes() + address: str = bech32_encode("mr", convertbits(bytes([0]) + recipientPub, 8, 5)) + + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + send: Send = Send.fromJSON(vectors["send"]) + spendingSend: Send = Send.fromJSON(vectors["spendingSend"]) + + def start() -> None: + #Send the Send. + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros considered an unconfirmed Transaction's outputs as UTXOs.") + + #Immediately spend it. + if rpc.meros.liveTransaction(spendingSend) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a Transaction's inputs as spent.") + + #Verify the Send and make sure it's not considered as a valid UTXO. + verify(rpc, send.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros considered a just confirmed Transaction with a spender's outputs as UTXOs.") + + def verified() -> None: + #Verify the spender and verify the state is unchanged. + verify(rpc, spendingSend.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a verified Transaction's inputs as spent.") + + def finalizedSend() -> None: + #Sanity check the spending TX has yet to also finalize. + if rpc.call("consensus", "getStatus", {"hash": spendingSend.hash.hex()})["finalized"]: + raise Exception("Test meant to only finalize the first Send, not both.") + + #Verify the state is unchanged. + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a verified Transaction's inputs as spent after the input finalized.") + + def finalizedSpendingSend() -> None: + #Verify the state is unchanged. + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a finalized Transaction's inputs as spent.") + + Liver( + rpc, + vectors["blockchain"], + transactions, + { + 50: start, + 51: verified, + 56: finalizedSend, + 57: finalizedSpendingSend + } + ).live() diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/TGUReorgTest.py b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUReorgTest.py new file mode 100644 index 000000000..c66df217b --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUReorgTest.py @@ -0,0 +1,133 @@ +from typing import Dict, List, Tuple, Union, Any +import json + +import ed25519 +from bech32 import convertbits, bech32_encode +from pytest import raises + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Merit.Blockchain import Block, Blockchain + +from e2e.Meros.Meros import MessageType +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import verify +from e2e.Tests.Errors import TestError, SuccessError + +def createSend( + rpc: RPC, + inputs: List[Union[Claim, Send]], + to: bytes, + key: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) +) -> Send: + pub: bytes = key.get_verifying_key().to_bytes() + actualInputs: List[Tuple[bytes, int]] = [] + outputs: List[Tuple[bytes, int]] = [(to, 1)] + toSpend: int = 0 + for txInput in inputs: + if isinstance(txInput, Claim): + actualInputs.append((txInput.hash, 0)) + toSpend += txInput.amount + else: + for n in range(len(txInput.outputs)): + if txInput.outputs[n][0] == key.get_verifying_key().to_bytes(): + actualInputs.append((txInput.hash, n)) + toSpend += txInput.outputs[n][1] + if toSpend > 1: + outputs.append((pub, toSpend - 1)) + + send: Send = Send(actualInputs, outputs) + send.sign(key) + send.beat(SpamFilter(3)) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + return send + +def reorg( + rpc: RPC, + alt: Blockchain +) -> None: + #Sync up the new Blockchain. + header: bytes = rpc.meros.liveBlockHeader(alt.blocks[-1].header) + #Use the genesis as the default value for this outer defined variable. + lastBlock: Block = alt.blocks[0] + while True: + req: bytes = rpc.meros.sync.recv() + if MessageType(req[0]) == MessageType.BlockListRequest: + blockList: List[bytes] = [] + for block in alt.blocks: + if block.header.hash == req[2:]: + break + blockList.append(block.header.hash) + blockList = blockList[-req[1]:] + blockList.reverse() + rpc.meros.blockList(blockList) + + elif MessageType(req[0]) == MessageType.BlockHeaderRequest: + reqHash: bytes = req[1:] + for block in alt.blocks: + if reqHash == block.header.hash: + rpc.meros.syncBlockHeader(block.header) + break + + elif MessageType(req[0]) == MessageType.BlockBodyRequest: + reqHash: bytes = req[1:-4] + for block in alt.blocks: + if reqHash == block.header.hash: + lastBlock = block + rpc.meros.rawBlockBody(block, 5) + break + + elif MessageType(req[0]) == MessageType.SketchHashRequests: + rpc.meros.packet(lastBlock.body.packets[0]) + + #If we've sent the last BlockBody, and its packets, we've synced the chain. + if lastBlock.header.hash == alt.blocks[-1].header.hash: + break + + if header != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back the alt chain's header.") + +def TGUReorgTest( + rpc: RPC +) -> None: + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + def test() -> None: + recipient: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + recipientPub: bytes = recipient.get_verifying_key().to_bytes() + address: str = bech32_encode("mr", convertbits(bytes([0]) + recipientPub, 8, 5)) + + #Create a Send. + send: Send = Send.fromJSON(vectors["send"]) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + verify(rpc, send.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != [{"hash": send.hash.hex().upper(), "nonce": 0}]: + raise TestError("Meros didn't consider a confirmed Transaction's outputs as UTXOs.") + #Spend it, with a newer Mint as an input as well so we can prune it without pruning the original. + newerSend: Send = createSend(rpc, [Claim.fromJSON(vectors["newerMintClaim"])], recipientPub) + _: Send = createSend(rpc, [send, newerSend], bytes(32), recipient) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros thinks the recipient has UTXOs.") + + #Remove the spending Send by pruning its ancestor (a Mint). + reorg(rpc, Blockchain.fromJSON(vectors["blocksWithoutNewerMint"])) + #Meros should add back its parent as an UTXO. + if rpc.call("transactions", "getUTXOs", {"address": address}) != [{"hash": send.hash.hex().upper(), "nonce": 0}]: + raise TestError("Meros didn't consider a Transaction without spenders as an UTXO.") + #Remove the original Send and verify its outputs are no longer considered UTXOs. + reorg(rpc, Blockchain.fromJSON(vectors["blocksWithoutOlderMint"])) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't remove the outputs of a pruned Transaction as UTXOs.") + + raise SuccessError() + + #Send Blocks so we have a Merit Holder who can instantly verify Transactions, not to mention Mints. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/Transactions/GetUTXOs/TGUUnverifyTest.py b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUUnverifyTest.py new file mode 100644 index 000000000..edf690910 --- /dev/null +++ b/e2e/Tests/RPC/Transactions/GetUTXOs/TGUUnverifyTest.py @@ -0,0 +1,86 @@ +from typing import Dict, List, Any +import json + +import ed25519 +from bech32 import convertbits, bech32_encode +from pytest import raises + +from e2e.Classes.Transactions.Transactions import Send, Data, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter + +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.RPC.Transactions.GetUTXOs.Lib import verify, mineBlock +from e2e.Tests.Errors import TestError, SuccessError + +def TGUUnverifyTest( + rpc: RPC +) -> None: + vectors: Dict[str, Any] + with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "r") as file: + vectors = json.loads(file.read()) + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + def test() -> None: + recipient: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + recipientPub: bytes = recipient.get_verifying_key().to_bytes() + address: str = bech32_encode("mr", convertbits(bytes([0]) + recipientPub, 8, 5)) + + otherRecipient: bytes = ed25519.SigningKey(b'\2' * 32).get_verifying_key().to_bytes() + otherAddress: str = bech32_encode("mr", convertbits(bytes([0]) + otherRecipient, 8, 5)) + + #Create a Send. + send: Send = Send.fromJSON(vectors["send"]) + if rpc.meros.liveTransaction(send) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros considered an unconfirmed Transaction's outputs as UTXOs.") + verify(rpc, send.hash) + + #Finalize the parent. + for _ in range(6): + mineBlock(rpc) + + #Spend it. + spendingSend: Send = Send.fromJSON(vectors["spendingSend"]) + if rpc.meros.liveTransaction(spendingSend) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Send.") + verify(rpc, spendingSend.hash) + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a verified Transaction's inputs as spent.") + if rpc.call("transactions", "getUTXOs", {"address": otherAddress}) != [{"hash": spendingSend.hash.hex().upper(), "nonce": 0}]: + raise TestError("Meros didn't consider a verified Transaction's outputs as UTXOs.") + + #Unverify the spending Send. This would also unverify the parent if it wasn't finalized. + #This is done via causing a Merit Removal. + #Uses two competing Datas to not change the Send's status to competing. + datas: List[Data] = [Data(bytes(32), recipientPub)] + for _ in range(2): + datas.append(Data(datas[0].hash, datas[-1].hash)) + for data in datas: + data.sign(recipient) + data.beat(SpamFilter(5)) + if rpc.meros.liveTransaction(data) != rpc.meros.live.recv(): + raise TestError("Meros didn't broadcast back a Data.") + verify(rpc, data.hash, mr=(datas[-1].hash == data.hash)) + #Verify the MeritRemoval happened and the spending Send is no longer verified. + #These first two checks are more likely to symbolize a failure in testing methodology than Meros. + if not rpc.call("merit", "getMerit", {"nick": 0})["malicious"]: + raise TestError("Meros didn't create a Merit Removal.") + if not rpc.call("consensus", "getStatus", {"hash": send.hash.hex()})["verified"]: + raise TestError("Finalized Transaction became unverified.") + if rpc.call("consensus", "getStatus", {"hash": spendingSend.hash.hex()})["verified"]: + raise TestError("Meros didn't unverify a Transaction which is currently below the required threshold.") + #Even after unverification, since the Transaction still exists, the input shouldn't be considered a UTXO. + if rpc.call("transactions", "getUTXOs", {"address": address}) != []: + raise TestError("Meros didn't consider a unverified yet existing Transaction's inputs as spent.") + #That said, its outputs should no longer be considered a UTXO. + if rpc.call("transactions", "getUTXOs", {"address": otherAddress}) != []: + raise TestError("Meros considered a unverified Transaction's outputs as UTXOs.") + + raise SuccessError() + + #Send Blocks so we have a Merit Holder who can instantly verify Transactions, not to mention Mints. + with raises(SuccessError): + Liver(rpc, vectors["blockchain"], transactions, {50: test}).live() diff --git a/e2e/Tests/RPC/Transactions/PublishTransactionTest.py b/e2e/Tests/RPC/Transactions/PublishTransactionTest.py new file mode 100644 index 000000000..85d0430c8 --- /dev/null +++ b/e2e/Tests/RPC/Transactions/PublishTransactionTest.py @@ -0,0 +1,249 @@ +from typing import Dict, Any +import json + +import ed25519 + +from e2e.Classes.Transactions.Transactions import Claim, Send, Data, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter + +from e2e.Meros.Meros import MessageType +from e2e.Meros.RPC import RPC +from e2e.Meros.Liver import Liver + +from e2e.Tests.Transactions.Verify import verifyTransaction + +from e2e.Tests.Errors import TestError + +#pylint: disable=too-many-statements +def PublishTransactionTest( + rpc: RPC +) -> None: + privKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) + pubKey: ed25519.VerifyingKey = privKey.get_verifying_key() + + sentToKey: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) + + sendFilter: SpamFilter = SpamFilter(3) + dataFilter: SpamFilter = SpamFilter(5) + + vectors: Dict[str, Any] + with open("e2e/Vectors/Transactions/ClaimedMint.json", "r") as file: + vectors = json.loads(file.read()) + + transactions: Transactions = Transactions.fromJSON(vectors["transactions"]) + + if len(transactions.txs) != 1: + raise Exception("Transactions DAG doesn't have just the Claim.") + claim: Claim = Claim.fromTransaction(next(iter(transactions.txs.values()))) + + send: Send = Send( + [(claim.hash, 0)], + [(sentToKey.get_verifying_key().to_bytes(), claim.amount)] + ) + send.sign(privKey) + send.beat(sendFilter) + + data: Data = Data(bytes(32), pubKey.to_bytes()) + data.sign(privKey) + data.beat(dataFilter) + + def publishAndVerify() -> None: + if not rpc.call( + "transactions", + "publishTransaction", + { + "type": "Claim", + "transaction": claim.serialize().hex() + }, + False + ): + raise TestError("Publishing a valid Transaction didn't return true.") + if rpc.meros.live.recv()[1:] != claim.serialize(): + raise TestError("publishTransaction didn't broadcast the Transaction.") + + if not rpc.call( + "transactions", + "publishTransaction", + { + "type": "Send", + "transaction": send.serialize().hex() + }, + False + ): + raise TestError("Publishing a valid Transaction didn't return true.") + if rpc.meros.live.recv()[1:] != send.serialize(): + raise TestError("publishTransaction didn't broadcast the Transaction.") + + if not rpc.call( + "transactions", + "publishTransaction", + { + "type": "Data", + "transaction": data.serialize().hex() + }, + False + ): + raise TestError("Publishing a valid Transaction didn't return true.") + if rpc.meros.live.recv()[1:] != data.serialize(): + raise TestError("publishTransaction didn't broadcast the Transaction.") + + #Verify all three were entered properly. + verifyTransaction(rpc, claim) + verifyTransaction(rpc, send) + verifyTransaction(rpc, data) + + #Create a new Send/Data and publish them without work. + sendSentWithoutWork: Send = Send([(send.hash, 0)], [(pubKey.to_bytes(), claim.amount)]) + sendSentWithoutWork.sign(sentToKey) + sendSentWithoutWork.beat(sendFilter) + + dataSentWithoutWork: Data = Data(bytes(32), sentToKey.get_verifying_key().to_bytes()) + dataSentWithoutWork.sign(sentToKey) + dataSentWithoutWork.beat(dataFilter) + + if not rpc.call( + "transactions", + "publishTransactionWithoutWork", + { + "type": "Send", + "transaction": sendSentWithoutWork.serialize()[:-4].hex() + }, + True + ): + raise TestError("Publishing a valid Transaction without work didn't return true.") + if rpc.meros.live.recv()[1:] != sendSentWithoutWork.serialize(): + raise TestError("publishTransaction didn't broadcast the Transaction.") + + if not rpc.call( + "transactions", + "publishTransactionWithoutWork", + { + "type": "Data", + "transaction": dataSentWithoutWork.serialize()[:-4].hex() + }, + True + ): + raise TestError("Publishing a valid Transaction without work didn't return true.") + if rpc.meros.live.recv()[1:] != dataSentWithoutWork.serialize(): + raise TestError("publishTransaction didn't broadcast the Transaction.") + + #Call verify now, which will test ours with work against Meros's with generated work. + #Both should terminate on the earliest valid proof, making them identical. + verifyTransaction(rpc, sendSentWithoutWork) + verifyTransaction(rpc, dataSentWithoutWork) + + #Re-publishing a Transaction should still return true. + if not rpc.call( + "transactions", + "publishTransaction", + { + "type": "Data", + "transaction": data.serialize().hex() + }, + False + ): + raise TestError("Publishing an existing Transaction didn't return true.") + if MessageType(rpc.meros.live.recv()[0]) == MessageType.Data: + raise TestError("publishTransaction broadcasted an existing Transaction.") + + #No arguments. + try: + rpc.call("transactions", "publishTransaction") + except TestError as e: + if str(e) != "-32602 Invalid params.": + raise TestError("publishTransaction didn't error when passed no arguments.") + + #Invalid type. + try: + rpc.call( + "transactions", + "publishTransaction", + { + "type": "", + "transaction": data.serialize().hex() + }, + False + ) + raise TestError("") + except TestError as e: + if str(e) != "-3 Invalid Transaction type specified.": + raise TestError("publishTransaction didn't error when passed an invalid type.") + + #Data sent with Send as a type. + try: + rpc.call( + "transactions", + "publishTransaction", + { + "type": "Send", + "transaction": data.serialize().hex() + }, + False + ) + raise TestError("") + except TestError as e: + if str(e) != "-3 Transaction is invalid: parseSend handed the wrong amount of data.": + raise TestError("publishTransaction didn't error when passed a non-parsable Send (a Data).") + + #Invalid Data (signature). + invalidData: Data = Data(bytes(32), sentToKey.get_verifying_key().to_bytes()) + newData: bytearray = bytearray(invalidData.data) + newData[-1] = newData[-1] ^ 1 + invalidData.data = bytes(newData) + invalidData.sign(sentToKey) + invalidData.beat(dataFilter) + try: + rpc.call( + "transactions", + "publishTransaction", + { + "type": "Data", + "transaction": invalidData.serialize().hex() + }, + False + ) + raise TestError("") + except TestError as e: + if str(e) != "-3 Transaction is invalid: Data has an invalid Signature.": + raise TestError("publishTransaction didn't error when passed an invalid Transaction.") + + #Spam. + spamData: Data = data + if spamData.proof == 0: + spamData = dataSentWithoutWork + if spamData.proof == 0: + raise Exception("Neither Data is considered as Spam.") + spamData.proof = 0 + + try: + rpc.call( + "transactions", + "publishTransaction", + { + "type": "Data", + "transaction": spamData.serialize().hex() + }, + False + ) + raise TestError("") + except TestError as e: + if str(e) != "2 Transaction didn't beat the spam filter.": + raise TestError("publishTransaction didn't error when passed a Transaction which didn't beat its difficulty.") + + #Test publishTransactionWithoutWork requires authorization. + try: + rpc.call( + "transactions", + "publishTransactionWithoutWork", + { + "type": "Data", + "transaction": dataSentWithoutWork.serialize()[:-4].hex() + }, + False + ) + raise TestError("") + except Exception as e: + if str(e) != "HTTP status isn't 200: 401": + raise TestError("Called publishTransactionWithoutWork despite not being authed.") + + Liver(rpc, vectors["blockchain"][:-1], transactions, {7: publishAndVerify}).live() diff --git a/e2e/Tests/Transactions/Prune/PruneUnaddableTest.py b/e2e/Tests/Transactions/Prune/PruneUnaddableTest.py index acdd008e4..48e426cef 100644 --- a/e2e/Tests/Transactions/Prune/PruneUnaddableTest.py +++ b/e2e/Tests/Transactions/Prune/PruneUnaddableTest.py @@ -39,15 +39,15 @@ def sendDatas() -> None: def verifyAdded() -> None: for data in datas: - rpc.call("transactions", "getTransaction", [data.hash.hex()]) - rpc.call("consensus", "getStatus", [data.hash.hex()]) + rpc.call("transactions", "getTransaction", {"hash": data.hash.hex()}) + rpc.call("consensus", "getStatus", {"hash": data.hash.hex()}) def verifyPruned() -> None: for data in datas[2:]: with raises(TestError): - rpc.call("transactions", "getTransaction", [data.hash.hex()]) + rpc.call("transactions", "getTransaction", {"hash": data.hash.hex()}) with raises(TestError): - rpc.call("consensus", "getStatus", [data.hash.hex()]) + rpc.call("consensus", "getStatus", {"hash": data.hash.hex()}) Liver( rpc, diff --git a/e2e/Tests/Transactions/Verify.py b/e2e/Tests/Transactions/Verify.py index 8802f5d30..9085a0365 100644 --- a/e2e/Tests/Transactions/Verify.py +++ b/e2e/Tests/Transactions/Verify.py @@ -11,7 +11,7 @@ def verifyTransaction( tx: Transaction ) -> None: sleep(1) - if rpc.call("transactions", "getTransaction", [tx.hash.hex()]) != tx.toJSON(): + if rpc.call("transactions", "getTransaction", {"hash": tx.hash.hex()}) != tx.toJSON(): raise TestError("Transaction doesn't match.") def verifyTransactions( diff --git a/e2e/Vectors/Generation/RPC/Merit/GetBlock.py b/e2e/Vectors/Generation/RPC/Merit/GetBlock.py new file mode 100644 index 000000000..e8b2552ee --- /dev/null +++ b/e2e/Vectors/Generation/RPC/Merit/GetBlock.py @@ -0,0 +1,82 @@ +from typing import List +import json + +import ed25519 +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Send, Data, Transactions + +from e2e.Classes.Consensus.VerificationPacket import VerificationPacket +from e2e.Classes.Consensus.SendDifficulty import SendDifficulty +from e2e.Classes.Consensus.DataDifficulty import DataDifficulty +from e2e.Classes.Consensus.SpamFilter import SpamFilter + +from e2e.Classes.Merit.Merit import Merit + +from e2e.Vectors.Generation.PrototypeChain import PrototypeBlock, PrototypeChain + +proto: PrototypeChain = PrototypeChain(7) +proto.add(1) +proto.add(2) +proto.add(3) +proto.add(4) +proto.add(elements=[SendDifficulty(1, 0, 2), SendDifficulty(1, 0, 4)]) +merit: Merit = Merit.fromJSON(proto.toJSON()) +transactions: Transactions = Transactions() + +claim: Claim = Claim( + [(merit.mints[-1], 0)], + ed25519.SigningKey(b'\0' * 32).get_verifying_key().to_bytes() +) +claim.sign(PrivateKey(0)) +transactions.add(claim) + +send: Send = Send( + [(claim.hash, 0)], + [(ed25519.SigningKey(b'\1' * 32).get_verifying_key().to_bytes(), claim.amount)] +) +send.sign(ed25519.SigningKey(b'\0' * 32)) +send.beat(SpamFilter(3)) +transactions.add(send) + +datas: List[Data] = [ + Data(bytes(32), ed25519.SigningKey(b'\0' * 32).get_verifying_key().to_bytes()) +] +for _ in range(4): + datas[-1].sign(ed25519.SigningKey(b'\0' * 32)) + datas[-1].beat(SpamFilter(5)) + transactions.add(datas[-1]) + datas.append(Data(datas[-1].hash, b'\0')) +del datas[-1] + +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[ + VerificationPacket(claim.hash, [0]), + VerificationPacket(send.hash, [0, 1, 2]), + VerificationPacket(datas[0].hash, [0, 2]), + VerificationPacket(datas[1].hash, [0, 1, 3]), + VerificationPacket(datas[2].hash, [0, 1, 2, 3, 4]), + VerificationPacket(datas[3].hash, [0, 1, 2, 3]) + ], + elements=[ + DataDifficulty(8, 0, 3), + SendDifficulty(1, 0, 0), + DataDifficulty(4, 0, 3), + DataDifficulty(1, 2, 4), + SendDifficulty(3, 1, 4), + SendDifficulty(2, 1, 2), + DataDifficulty(7, 0, 0), + ] + ).finish(0, merit) +) + +with open("e2e/Vectors/RPC/Merit/GetBlock.json", "w") as vectors: + vectors.write(json.dumps({ + "blockchain": merit.toJSON(), + "transactions": transactions.toJSON(), + "claim": claim.toJSON(), + "send": send.toJSON(), + "datas": [data.toJSON() for data in datas] + })) diff --git a/e2e/Vectors/Generation/RPC/Personal/WatchWallet.py b/e2e/Vectors/Generation/RPC/Personal/WatchWallet.py new file mode 100644 index 000000000..7b41dc533 --- /dev/null +++ b/e2e/Vectors/Generation/RPC/Personal/WatchWallet.py @@ -0,0 +1,38 @@ +import json + +import ed25519 +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Transactions + +from e2e.Classes.Consensus.VerificationPacket import VerificationPacket + +from e2e.Classes.Merit.Merit import Merit + +from e2e.Vectors.Generation.PrototypeChain import PrototypeBlock, PrototypeChain + +edPubKey: bytes = ed25519.SigningKey(b'\0' * 32).get_verifying_key().to_bytes() + +proto: PrototypeChain = PrototypeChain(49, keepUnlocked=True) +merit: Merit = Merit.fromJSON(proto.toJSON()) + +transactions: Transactions = Transactions() + +#Create the Claims. +for m in range(3): + claim: Claim = Claim([(merit.mints[m], 0)], edPubKey) + claim.sign(PrivateKey(0)) + transactions.add(claim) + +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[VerificationPacket(tx.hash, [0]) for tx in transactions.txs.values()] + ).finish(0, merit) +) + +with open("e2e/Vectors/RPC/Personal/WatchWallet.json", "w") as vectors: + vectors.write(json.dumps({ + "blockchain": merit.toJSON(), + "transactions": transactions.toJSON() + })) diff --git a/e2e/Vectors/Generation/RPC/Transactions/GetUTXOs.py b/e2e/Vectors/Generation/RPC/Transactions/GetUTXOs.py new file mode 100644 index 000000000..845218915 --- /dev/null +++ b/e2e/Vectors/Generation/RPC/Transactions/GetUTXOs.py @@ -0,0 +1,119 @@ +from typing import Dict, List, Any +import json + +import ed25519 +from e2e.Libs.BLS import PrivateKey + +from e2e.Classes.Transactions.Transactions import Claim, Send, Transactions +from e2e.Classes.Consensus.SpamFilter import SpamFilter +from e2e.Classes.Consensus.VerificationPacket import VerificationPacket +from e2e.Classes.Merit.Merit import Merit + +from e2e.Vectors.Generation.PrototypeChain import PrototypeBlock, PrototypeChain + +merit: Merit = Merit.fromJSON(PrototypeChain(47).toJSON()) +transactions: Transactions = Transactions() + +privKey: ed25519.SigningKey = ed25519.SigningKey(b'\0' * 32) +pubKey: bytes = privKey.get_verifying_key().to_bytes() + +recipientPriv: ed25519.SigningKey = ed25519.SigningKey(b'\1' * 32) +recipientPub: bytes = recipientPriv.get_verifying_key().to_bytes() + +olderClaim: Claim = Claim([(merit.mints[-1], 0)], pubKey) +olderClaim.sign(PrivateKey(0)) +transactions.add(olderClaim) + +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[VerificationPacket(olderClaim.hash, [0])] + ).finish(0, merit) +) +merit.add(PrototypeBlock(merit.blockchain.blocks[-1].header.time + 1200).finish(0, merit)) + +newerClaim: Claim = Claim([(merit.mints[-1], 0)], pubKey) +newerClaim.sign(PrivateKey(0)) +transactions.add(newerClaim) + +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[VerificationPacket(newerClaim.hash, [0])] + ).finish(0, merit) +) + +coreMerit: List[Dict[str, Any]] = merit.toJSON() + +send: Send = Send( + [(olderClaim.hash, 0)], + [(recipientPub, 1), (pubKey, olderClaim.amount - 1)] +) +send.sign(privKey) +send.beat(SpamFilter(3)) +transactions.add(send) + +otherRecipient: bytes = ed25519.SigningKey(b'\2' * 32).get_verifying_key().to_bytes() + +spendingSend: Send = Send([(send.hash, 0)], [(otherRecipient, 1)]) +spendingSend.sign(recipientPriv) +spendingSend.beat(SpamFilter(3)) +transactions.add(spendingSend) + +#Used by the basic and immediately spent tests. +#Staggered finalization. +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[VerificationPacket(send.hash, [0])] + ).finish(0, merit) +) + +merit.add( + PrototypeBlock( + merit.blockchain.blocks[-1].header.time + 1200, + packets=[VerificationPacket(spendingSend.hash, [0])] + ).finish(0, merit) +) + +for _ in range(5): + merit.add( + PrototypeBlock(merit.blockchain.blocks[-1].header.time + 1200).finish(0, merit) + ) + +heightToBeat: int = len(merit.blockchain.blocks) +def reorgPast( + mint: bytes +) -> Merit: + #Safe due to performing the shallower reorg first. + while coreMerit[-1]["hash"] != mint.hex().upper(): + del coreMerit[-1] + del coreMerit[-1] + + newMerit: Merit = Merit.fromJSON(coreMerit) + #pylint: disable=global-statement + global heightToBeat + while len(newMerit.blockchain.blocks) <= heightToBeat: + newMerit.add( + PrototypeBlock( + #Use a slightly faster time differential. + newMerit.blockchain.blocks[-1].header.time + + ((newMerit.blockchain.blocks[-1].header.time - newMerit.blockchain.blocks[-2].header.time) - 1) + ).finish(1, newMerit) + ) + heightToBeat = len(newMerit.blockchain.blocks) + return newMerit + +#blocksWithout... are solely used by the reorg test. +#newerMintClaim is used by the reorg test and Personal's AddressRecovery test. +#It may be best to split this out with a common parent generator. +with open("e2e/Vectors/RPC/Transactions/GetUTXOs.json", "w") as vectors: + vectors.write(json.dumps({ + "blockchain": merit.toJSON(), + "transactions": transactions.toJSON(), + "send": send.toJSON(), + "spendingSend": spendingSend.toJSON(), + "newerMintClaim": newerClaim.toJSON(), + "blocksWithoutNewerMint": reorgPast(newerClaim.inputs[0][0]).toJSON(), + "blocksWithoutOlderMint": reorgPast(olderClaim.inputs[0][0]).toJSON() + })) diff --git a/e2e/Vectors/RPC/Merit/GetBlock.json b/e2e/Vectors/RPC/Merit/GetBlock.json new file mode 100644 index 000000000..8bdf06182 --- /dev/null +++ b/e2e/Vectors/RPC/Merit/GetBlock.json @@ -0,0 +1 @@ +{"blockchain": [{"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "360E204A3E870E34FC69B65536A6C8354966F95950D4C55A5C10FC276149FB2E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "B73B38B8D005DF431DB1CBF3462348FB8DCD213B3980A6C0E26D7B95A79F4E88324832A5ECB684436852ADCC8511C0BC0306EEB6D9E5E581AD279B633C0291157EB8ADC56D24750C95C8B676BA66C1FF5AAC4FA60EB5C8EEF0768067426602EC", "time": 1200, "proof": 162, "signature": "82021D8D5B5EF89C40548BF31C2890177B7CE874E306A62A0C1E6FD46F1583BD5E9E18299EC5D64A2D90E0B2CEC47B44"}, "hash": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700"}, {"transactions": [{"hash": "72EFE1EEF8D6372BBBA749E7CA910F210A2BC80C46A688D966F84C36AAFA3732", "holders": [0]}], "elements": [], "aggregate": "8B04E9F4BD7CC9A83D324ADCAC4FB2246A2A1A7AB87885A7F6B0A7A119CAB298D24ECE2A47239AB296E0A0505E2C478D", "header": {"version": 0, "last": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700", "contents": "C9521F92299A0B088B8141B012B42100E5FE6E72189F58DE714C22B1E910358B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7953C6E651693D49AEDDBB5FCDE84E3264648828330B8E8261C2EEBEDE4ED175", "miner": 0, "time": 2400, "proof": 218, "signature": "8CA439D808746224FAEB94D9E2980DE1B92E0947FB401CC458530A5E2D9E227C69FABC83E050EA5981F2AAFBF5F3A209"}, "hash": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602"}, {"transactions": [{"hash": "6281D2C3868D973A5EF0505A8AC35DCEB4C02671CF32195CDE3399D064AD3573", "holders": [0]}], "elements": [], "aggregate": "A070246256DB4536DEF33503DD7E63D4EE6B528BD445F9F671DA43C09945B3196AE4E65BE683EEAEAE41A0A26B3AB197", "header": {"version": 0, "last": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602", "contents": "A8D4C85179A8C6D6A7538F1A182D12A22C3200F4F97C49E85CD3B2185D22F8DA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A9A9C38D542DFA0477E4A01DD1DCF9EC3390986E1F944C9A4B204412E133B288", "miner": 0, "time": 3600, "proof": 144, "signature": "96B09D483F134D88486DC1079ECEE623620A5186E54B6AD2802FAE542E2BA8F60A4DB8A5A95D6E853FAA0C52CC40B8E4"}, "hash": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02"}, {"transactions": [{"hash": "278C104FEB0EB60FDD554EA0157704706F5587B165DC6356749B4AB20781BC56", "holders": [0]}], "elements": [], "aggregate": "B6F88AB1861E8739DC9CFFFC5B91F8ED221E65C2E1CAE55C58B59A6D0F9B93123DE2C680F83FB629F87E49621E3385B0", "header": {"version": 0, "last": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02", "contents": "A809E61ABF343724E17003F6EDC3FC31604BFC8CAC89E733167D422CB355B429", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "791F73693CA57D52320A0227EC5359BD325CDDC300B9B94091F53A77233883C2", "miner": 0, "time": 4800, "proof": 30, "signature": "A2D8A2CAB1ECBBA004769A57E782F308E1308DF6D64D64C0D2A7122BF023A586CA7F3B2D60FE3CFD91B80621D6E42A0A"}, "hash": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700"}, {"transactions": [{"hash": "86E44D62E8D27DA57458A2DE80698A6EC88054C5ABA9083FBE54EE0141E93A93", "holders": [0]}], "elements": [], "aggregate": "8B129406C90F252DBA681AB51D48B0B4B3C99E681B477411DF137B7657103EF92C5FF48ACCD05B8E87807F1906D2BF96", "header": {"version": 0, "last": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700", "contents": "0DD37CA5DDBFC97B394561381460ECA3212039FD592CBD232795C865E7C5CB6B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DB055B18061A1ED67925CC442B187CD56837434CB813865E70FDCBCF4480E426", "miner": 0, "time": 6000, "proof": 21, "signature": "B9685F27C2A79D71FCF6C14CBA94CD55F3E615F1BE5E27F23FB4F2DCC65A047928FC5C87B0115AAD791E40667C241286"}, "hash": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900"}, {"transactions": [{"hash": "018412CB2C17E3ADFEF52A98DD73EF145A46AAA76AA35A887246A83ABBFBEB89", "holders": [0]}], "elements": [], "aggregate": "8A6B7756526BDF6304907773176182EC8873536431E9F1B6001CEE8E6FC217B3AC34DB6FA3E8F8B46924685A14E5EDC5", "header": {"version": 0, "last": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900", "contents": "E27C9370E5749F06BDEC855865682DFFF18767D1A5AFBAD3007A8FC62820F3E5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DE6F5EE882121224255A2D752B891B43FBEA7BF74D42E7CBF251C7C2F28DA123", "miner": 0, "time": 7200, "proof": 0, "signature": "AC47691383371FC7A5EBEA5961BD060CED69FB56BCBE68A83221FE23AAB8BD0C4290AD38736B551C33FD918D7C6BFB7D"}, "hash": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623"}, {"transactions": [{"hash": "19C4B0914783A2CE9846044FFE48AD35E1E173023A64D7BD340734184EEF04B4", "holders": [0]}], "elements": [], "aggregate": "81EA7597EE88E818FA43F480FE43CA24269AC9BA4B26CB6A93B0DADAF06B679EE76B3371E9F2335488960CE7FB85D17B", "header": {"version": 0, "last": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623", "contents": "7E6C29898A4764EA8A42B39D3DC51472834FE78A67778A379DB6345EC81D4CF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "729018FFA42AEF0122ADA8FEC04B4271CADB3E9175472A7B4C0447ED720B4BE2", "miner": 0, "time": 8400, "proof": 0, "signature": "9638880A8D68E4B770802307E4CB24BACE6AEC6CAE02F623AF423F2C53D027B7F31DC8EEE8E9BA910B600B0A9D48318D"}, "hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "95DFF9D506F80F7E175840C52991F0FE7257906CE1F8C0822C489C0E1C7DA1B26F65CAADAE3B7393AD126812BBA47F1F034C06D4A1AB4A83008EB7B06D2117E0D903BDF5706783E9EDA25F3BA77C6A45A15A44EB8968CB315B28E029E9FD6B75", "time": 9600, "proof": 0, "signature": "A9FD4E87EFDDBA9746A627B75761476BF07E33F522D2F392147B213D9D92274377CD09341618E45636D33BCC9F493D81"}, "hash": "E3F90BDE63C64D64097A420F2E2DDE1960A689825CC0224B1C662829B7490B13"}, {"transactions": [{"hash": "EDEF8C88F9FA830170A4E398339883440C5ACD78E0034C58B14624F04E9D2DA6", "holders": [0]}], "elements": [], "aggregate": "8903EE4CAFB24C4910F65D05711A446F137FDDD2D7009991AF465E5FC480BDA99B2022CC00A9F4867D35616DB75EBA10", "header": {"version": 0, "last": "E3F90BDE63C64D64097A420F2E2DDE1960A689825CC0224B1C662829B7490B13", "contents": "F90D5B9907A6C07C2F0B6D8E770E465C5EDDD025DB81EFD784545F47FDAD15B3", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7666D332A3DBD69C7216F403ABFF4C0DE4865F49033D5E80B2C13054257878D1", "miner": "AEB40F2CA99B88444FEA94CA92D0307E8C6E1F8D53D8FE3196AA6B0563F7B3AE6CCC62FC5187CCE0AE4CD251F8975A2C18CB65F14287296220D8E11E3F6F30E6C7585B52D99D00B76B58ECB44C42AE3733B60D259F51FE49FCA9F5B9562A5AC2", "time": 10800, "proof": 0, "signature": "B33A352572D8CC633E317D15BB06244C8A5EA11CCF1A406BBEEB6EE84EF8C5A321AA014A0E4980F4CCA3CAB9676CABAF"}, "hash": "4FDC07B42A7E28710B21002E81D7F89F67AF0B97D551E30A539056B5DA0F2C57"}, {"transactions": [{"hash": "6DE70CFCECC0AF20D4764C20F500DC675D455C47410A6F5E84AA08E136B4F07E", "holders": [0, 1]}], "elements": [], "aggregate": "B07B3A0309BDAE3B74691514C2F0BEAF9A7D3C0719B4B9E1AC7D9E1AA4B2A3AABD1B914750AB12BB3397A9CA1B3D5336", "header": {"version": 0, "last": "4FDC07B42A7E28710B21002E81D7F89F67AF0B97D551E30A539056B5DA0F2C57", "contents": "220F22E17A7F7823E366B5BEAF3AD5927DFCD2684239DE0E04F13B4A20E9C9D4", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FE1F593EC798640F63430273837F4CF7DD22BBD5431C2D2431272C46CED9C768", "miner": "B6F25B23B524269DC6FA2B521DBC55BB6A239465859F07B8FB60B42956FD38242A9B69B1EE5304908F0C366E7230F60C01F645E6D28D4DC54C9A717FFC0813F6208F0AD531AEDBE53F66AB4FD15959C00B9213F529D380D304D1270C3999C3AC", "time": 12000, "proof": 0, "signature": "87E89B90020AAECF3E9013A3083FCC779B157E37A2FF8563FC6507749ABC36382CA0645635ABDA2B80D62E262EA9E8E4"}, "hash": "F9473C069958682410166E1340A714E2991A62F88A045C5E43EF905C3C980ACA"}, {"transactions": [{"hash": "A5BE93B3A874355C822B6C408DBEA4E8615F7ACAE98B75ABEB41744963F45B65", "holders": [0, 1, 2]}], "elements": [], "aggregate": "96701E8EE838CE34086D6E59E6BE452A0FECB6520FBBF6FA8EE03E258535F929F8CFCF0125F1C9D92DB09AA2B851FEBB", "header": {"version": 0, "last": "F9473C069958682410166E1340A714E2991A62F88A045C5E43EF905C3C980ACA", "contents": "B78F1421E16E3994324C03613AA8ABC477B7546B5648ACC00AC03A45958099F3", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F274B00CDE9B451D6C7F04148745F1F3EDF77FC47463F0DE7763B913E1D789BA", "miner": "A80235A389FF9CB92715D387E32567EA5221BC33776A0D51E100AA7D5E1AE0DA6675F6306CC0FBFEE8939F02174EED9A15C993DD87F7398C632D4452146168C40058E850F4FA1C19CA28FAA1786207A6A1574E5B135C52CABDDCB8539C1D8D0C", "time": 13200, "proof": 0, "signature": "B3604B4194CBCA7AD2384004FFB73DF1947282D10F45EF7520F2FD8E27AC24B5C98E609F96BD808087617F0CD4872CF0"}, "hash": "E744BAC542A1E934C314B7E0D82A565B732C4609566A1C423EED984630917C42"}, {"transactions": [{"hash": "8A6137996074299FA4DA0E1F8C754C6689F8D1703B2365D228E0F99ED5D16EF7", "holders": [0, 1, 2, 3, 4]}], "elements": [{"descendant": "SendDifficulty", "difficulty": 1, "nonce": 0, "holder": 2}, {"descendant": "SendDifficulty", "difficulty": 1, "nonce": 0, "holder": 4}], "aggregate": "8BAE063F50786972DA13DC7C60D44E7BE00ABECC30BF71062F73662E95D493A98E7C905CBE714FB7D759806BDAE41F95", "header": {"version": 0, "last": "E744BAC542A1E934C314B7E0D82A565B732C4609566A1C423EED984630917C42", "contents": "CAABBCDB7C4F10275A783A76C200523855333BA8D2199BA96199B066A03761D3", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "541866B42B5E7D878C075F514E37B26FF2C54672E26EDB5FA3FF52D49742744A", "miner": 0, "time": 14400, "proof": 0, "signature": "82FF68153ACE3729DEA133A15FE287956FE0C41B641B95AB6F7F67BD43F83C07324718AEAF6988FC5F1A327C896DB180"}, "hash": "CFCE96E126D68652BD1D9D1B140F5B2D7A25299D45A082E33BC2AAA4AEDABBFD"}, {"transactions": [{"hash": "D687D3EE416A9E8E2B5BD8786BE5ECD8038AAF634BAFF4E6616EEBCECD917B26", "holders": [0, 1, 2]}, {"hash": "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A", "holders": [0, 1, 3]}, {"hash": "99BF0E79745D12F76487E39717DB9B2B1EEF78514A3C788CB02DFF00D8B7DB87", "holders": [0, 1, 2, 3]}, {"hash": "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2", "holders": [0, 1, 2, 3, 4]}, {"hash": "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC", "holders": [0, 2]}, {"hash": "39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA", "holders": [0]}], "elements": [{"descendant": "DataDifficulty", "difficulty": 8, "nonce": 0, "holder": 3}, {"descendant": "SendDifficulty", "difficulty": 1, "nonce": 0, "holder": 0}, {"descendant": "DataDifficulty", "difficulty": 4, "nonce": 0, "holder": 3}, {"descendant": "DataDifficulty", "difficulty": 1, "nonce": 2, "holder": 4}, {"descendant": "SendDifficulty", "difficulty": 3, "nonce": 1, "holder": 4}, {"descendant": "SendDifficulty", "difficulty": 2, "nonce": 1, "holder": 2}, {"descendant": "DataDifficulty", "difficulty": 7, "nonce": 0, "holder": 0}], "aggregate": "86DEE472C94E295AB3D6DBC84BECA2C6EF6851E3C7BBBF361BF0E4B6454BB48DA715E0AFB5BD6F030EFB4799A89259B1", "header": {"version": 0, "last": "CFCE96E126D68652BD1D9D1B140F5B2D7A25299D45A082E33BC2AAA4AEDABBFD", "contents": "26124FD2879B610CE449E95621F597FDC84AE6123463122FD3BFAA85897457E2", "packets": 6, "sketchSalt": "00000000", "sketchCheck": "335802F3A7EE32E91C166388878FE1D9F22E289778656917BBA31B5B068B74A5", "miner": 0, "time": 15600, "proof": 0, "signature": "976471B47004C3D91B474E4BCC3B13B2C6F9769658498BA49D7EE7767F3C3706440DBFA1601E909B147C47EB32E9AD40"}, "hash": "65CE9A8C36196F944F54AFFA4AD34B777D5F7B96E775AC8D87D98A5973FA69A3"}], "transactions": {"39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA": {"descendant": "Claim", "inputs": [{"hash": "CFCE96E126D68652BD1D9D1B140F5B2D7A25299D45A082E33BC2AAA4AEDABBFD", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "A89C6762C69FEB773BAF6C9613C33091934F3F9276DD25C87A97142D2D0C5FD3649D07A2DF1DDA88951AE699387EC0CA", "hash": "39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA"}, "D687D3EE416A9E8E2B5BD8786BE5ECD8038AAF634BAFF4E6616EEBCECD917B26": {"descendant": "Send", "inputs": [{"hash": "39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA", "nonce": 0}], "outputs": [{"key": "8A88E3DD7409F195FD52DB2D3CBA5D72CA6709BF1D94121BF3748801B40F6F5C", "amount": "50000"}], "hash": "D687D3EE416A9E8E2B5BD8786BE5ECD8038AAF634BAFF4E6616EEBCECD917B26", "signature": "08F229077A8377BE398D679A89F2BF449F6303A0D90223D4123C0858AB34913FD47A63D91DE6061919AC1982CA5D08D2B71AB4A33A0DD0F3FB20B606C488120C", "proof": 3}, "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC": {"descendant": "Data", "inputs": [{"hash": "0000000000000000000000000000000000000000000000000000000000000000"}], "outputs": [], "hash": "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC", "data": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "signature": "F07D01CA558A1DC58633B38F47D7E5A7A5F71B21CE1FB170A4D88987DED5E44EA9261492F978EC170E48EC46D91D74DECF0C0D811332C1F8D880AD976927FB0D", "proof": 0}, "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A": {"descendant": "Data", "inputs": [{"hash": "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC"}], "outputs": [], "hash": "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A", "data": "00", "signature": "81A142F9C0D6FA3A989E21487C311058B6EE39DCAF30EDD2069B80B45966BA5ACFB2B4B130C1B9E067BAA46CB3A746210A23108C607AFA831609929E414EBC0F", "proof": 3}, "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2": {"descendant": "Data", "inputs": [{"hash": "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A"}], "outputs": [], "hash": "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2", "data": "00", "signature": "538813C2E54322DBC39055AD3E1C8B6CE8C78A587C2707F0068DF4656CB98E0F573234E037D5A9EB143EEAAD44195C2840C1B5A2096A39ED2FCBF66BB86FB20E", "proof": 8}, "99BF0E79745D12F76487E39717DB9B2B1EEF78514A3C788CB02DFF00D8B7DB87": {"descendant": "Data", "inputs": [{"hash": "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2"}], "outputs": [], "hash": "99BF0E79745D12F76487E39717DB9B2B1EEF78514A3C788CB02DFF00D8B7DB87", "data": "00", "signature": "4C392B77C83E8318A1AA0784CFDDFBC3F0F5C59437ABFE034C76C0CBAFEBD94EA1F8C5363F0A252793644D25A083CAE54FB16C8CF78549F8146ACD0FD1B7FD0C", "proof": 0}}, "claim": {"descendant": "Claim", "inputs": [{"hash": "CFCE96E126D68652BD1D9D1B140F5B2D7A25299D45A082E33BC2AAA4AEDABBFD", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "A89C6762C69FEB773BAF6C9613C33091934F3F9276DD25C87A97142D2D0C5FD3649D07A2DF1DDA88951AE699387EC0CA", "hash": "39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA"}, "send": {"descendant": "Send", "inputs": [{"hash": "39ED79A534AE65DDFC7DE8C2B654FFC5F6C564B9A3FFABC01D91B590C4D111FA", "nonce": 0}], "outputs": [{"key": "8A88E3DD7409F195FD52DB2D3CBA5D72CA6709BF1D94121BF3748801B40F6F5C", "amount": "50000"}], "hash": "D687D3EE416A9E8E2B5BD8786BE5ECD8038AAF634BAFF4E6616EEBCECD917B26", "signature": "08F229077A8377BE398D679A89F2BF449F6303A0D90223D4123C0858AB34913FD47A63D91DE6061919AC1982CA5D08D2B71AB4A33A0DD0F3FB20B606C488120C", "proof": 3}, "datas": [{"descendant": "Data", "inputs": [{"hash": "0000000000000000000000000000000000000000000000000000000000000000"}], "outputs": [], "hash": "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC", "data": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "signature": "F07D01CA558A1DC58633B38F47D7E5A7A5F71B21CE1FB170A4D88987DED5E44EA9261492F978EC170E48EC46D91D74DECF0C0D811332C1F8D880AD976927FB0D", "proof": 0}, {"descendant": "Data", "inputs": [{"hash": "DD47B887B1CF6C03DA5AF82D360F3FC14BADA198608BED9A8E0DDB4FE80756EC"}], "outputs": [], "hash": "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A", "data": "00", "signature": "81A142F9C0D6FA3A989E21487C311058B6EE39DCAF30EDD2069B80B45966BA5ACFB2B4B130C1B9E067BAA46CB3A746210A23108C607AFA831609929E414EBC0F", "proof": 3}, {"descendant": "Data", "inputs": [{"hash": "E1146D100A85C10746E58B1FAC7C6C3859369C0FA65E2D02B021E7E38F13214A"}], "outputs": [], "hash": "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2", "data": "00", "signature": "538813C2E54322DBC39055AD3E1C8B6CE8C78A587C2707F0068DF4656CB98E0F573234E037D5A9EB143EEAAD44195C2840C1B5A2096A39ED2FCBF66BB86FB20E", "proof": 8}, {"descendant": "Data", "inputs": [{"hash": "B3E5B8227043C72F7AE0FFB2D676DD4FAE12B1731DA93F882EA9A44B47A597C2"}], "outputs": [], "hash": "99BF0E79745D12F76487E39717DB9B2B1EEF78514A3C788CB02DFF00D8B7DB87", "data": "00", "signature": "4C392B77C83E8318A1AA0784CFDDFBC3F0F5C59437ABFE034C76C0CBAFEBD94EA1F8C5363F0A252793644D25A083CAE54FB16C8CF78549F8146ACD0FD1B7FD0C", "proof": 0}]} \ No newline at end of file diff --git a/e2e/Vectors/RPC/Personal/WatchWallet.json b/e2e/Vectors/RPC/Personal/WatchWallet.json new file mode 100644 index 000000000..c1be8532b --- /dev/null +++ b/e2e/Vectors/RPC/Personal/WatchWallet.json @@ -0,0 +1 @@ +{"blockchain": [{"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "360E204A3E870E34FC69B65536A6C8354966F95950D4C55A5C10FC276149FB2E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "B73B38B8D005DF431DB1CBF3462348FB8DCD213B3980A6C0E26D7B95A79F4E88324832A5ECB684436852ADCC8511C0BC0306EEB6D9E5E581AD279B633C0291157EB8ADC56D24750C95C8B676BA66C1FF5AAC4FA60EB5C8EEF0768067426602EC", "time": 1200, "proof": 162, "signature": "82021D8D5B5EF89C40548BF31C2890177B7CE874E306A62A0C1E6FD46F1583BD5E9E18299EC5D64A2D90E0B2CEC47B44"}, "hash": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700"}, {"transactions": [{"hash": "72EFE1EEF8D6372BBBA749E7CA910F210A2BC80C46A688D966F84C36AAFA3732", "holders": [0]}], "elements": [], "aggregate": "8B04E9F4BD7CC9A83D324ADCAC4FB2246A2A1A7AB87885A7F6B0A7A119CAB298D24ECE2A47239AB296E0A0505E2C478D", "header": {"version": 0, "last": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700", "contents": "C9521F92299A0B088B8141B012B42100E5FE6E72189F58DE714C22B1E910358B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7953C6E651693D49AEDDBB5FCDE84E3264648828330B8E8261C2EEBEDE4ED175", "miner": 0, "time": 2400, "proof": 218, "signature": "8CA439D808746224FAEB94D9E2980DE1B92E0947FB401CC458530A5E2D9E227C69FABC83E050EA5981F2AAFBF5F3A209"}, "hash": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602"}, {"transactions": [{"hash": "6281D2C3868D973A5EF0505A8AC35DCEB4C02671CF32195CDE3399D064AD3573", "holders": [0]}], "elements": [], "aggregate": "A070246256DB4536DEF33503DD7E63D4EE6B528BD445F9F671DA43C09945B3196AE4E65BE683EEAEAE41A0A26B3AB197", "header": {"version": 0, "last": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602", "contents": "A8D4C85179A8C6D6A7538F1A182D12A22C3200F4F97C49E85CD3B2185D22F8DA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A9A9C38D542DFA0477E4A01DD1DCF9EC3390986E1F944C9A4B204412E133B288", "miner": 0, "time": 3600, "proof": 144, "signature": "96B09D483F134D88486DC1079ECEE623620A5186E54B6AD2802FAE542E2BA8F60A4DB8A5A95D6E853FAA0C52CC40B8E4"}, "hash": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02"}, {"transactions": [{"hash": "278C104FEB0EB60FDD554EA0157704706F5587B165DC6356749B4AB20781BC56", "holders": [0]}], "elements": [], "aggregate": "B6F88AB1861E8739DC9CFFFC5B91F8ED221E65C2E1CAE55C58B59A6D0F9B93123DE2C680F83FB629F87E49621E3385B0", "header": {"version": 0, "last": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02", "contents": "A809E61ABF343724E17003F6EDC3FC31604BFC8CAC89E733167D422CB355B429", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "791F73693CA57D52320A0227EC5359BD325CDDC300B9B94091F53A77233883C2", "miner": 0, "time": 4800, "proof": 30, "signature": "A2D8A2CAB1ECBBA004769A57E782F308E1308DF6D64D64C0D2A7122BF023A586CA7F3B2D60FE3CFD91B80621D6E42A0A"}, "hash": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700"}, {"transactions": [{"hash": "86E44D62E8D27DA57458A2DE80698A6EC88054C5ABA9083FBE54EE0141E93A93", "holders": [0]}], "elements": [], "aggregate": "8B129406C90F252DBA681AB51D48B0B4B3C99E681B477411DF137B7657103EF92C5FF48ACCD05B8E87807F1906D2BF96", "header": {"version": 0, "last": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700", "contents": "0DD37CA5DDBFC97B394561381460ECA3212039FD592CBD232795C865E7C5CB6B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DB055B18061A1ED67925CC442B187CD56837434CB813865E70FDCBCF4480E426", "miner": 0, "time": 6000, "proof": 21, "signature": "B9685F27C2A79D71FCF6C14CBA94CD55F3E615F1BE5E27F23FB4F2DCC65A047928FC5C87B0115AAD791E40667C241286"}, "hash": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900"}, {"transactions": [{"hash": "018412CB2C17E3ADFEF52A98DD73EF145A46AAA76AA35A887246A83ABBFBEB89", "holders": [0]}], "elements": [], "aggregate": "8A6B7756526BDF6304907773176182EC8873536431E9F1B6001CEE8E6FC217B3AC34DB6FA3E8F8B46924685A14E5EDC5", "header": {"version": 0, "last": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900", "contents": "E27C9370E5749F06BDEC855865682DFFF18767D1A5AFBAD3007A8FC62820F3E5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DE6F5EE882121224255A2D752B891B43FBEA7BF74D42E7CBF251C7C2F28DA123", "miner": 0, "time": 7200, "proof": 0, "signature": "AC47691383371FC7A5EBEA5961BD060CED69FB56BCBE68A83221FE23AAB8BD0C4290AD38736B551C33FD918D7C6BFB7D"}, "hash": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623"}, {"transactions": [{"hash": "19C4B0914783A2CE9846044FFE48AD35E1E173023A64D7BD340734184EEF04B4", "holders": [0]}], "elements": [], "aggregate": "81EA7597EE88E818FA43F480FE43CA24269AC9BA4B26CB6A93B0DADAF06B679EE76B3371E9F2335488960CE7FB85D17B", "header": {"version": 0, "last": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623", "contents": "7E6C29898A4764EA8A42B39D3DC51472834FE78A67778A379DB6345EC81D4CF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "729018FFA42AEF0122ADA8FEC04B4271CADB3E9175472A7B4C0447ED720B4BE2", "miner": 0, "time": 8400, "proof": 0, "signature": "9638880A8D68E4B770802307E4CB24BACE6AEC6CAE02F623AF423F2C53D027B7F31DC8EEE8E9BA910B600B0A9D48318D"}, "hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042"}, {"transactions": [{"hash": "A8963D7A7144FDE3F7B5B64C1B3E6D0B1B5DA983BE0486B2FC6A746585BEC0C7", "holders": [0]}], "elements": [], "aggregate": "AEB80694FFDCCFE47F38CF3C5AEEC3F9B435AF5B3C5CDE5931EA4D15BF5A5E617AA414CE2B3305F85BD493292E879BBD", "header": {"version": 0, "last": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "contents": "E1BC8CE92B23858A2FA0093D944DEE8A08C66FFF8B799FC94595D2C18B4F4F2F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "560BB2F0F92A8DAF15FE89394733AA845880957823FC081A64233DA40F54582D", "miner": 0, "time": 9600, "proof": 0, "signature": "95E3DD10E00C916B59E9FE95894AF3EB7C9889B1FA67B6D10D549C5AF7258EE5A9E23E928B62F54EB708059DDF09D9A8"}, "hash": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D"}, {"transactions": [{"hash": "C8727936A3F41AB82E5564D94C5AAAD205F0B349470240C10752370BC8AB5591", "holders": [0]}], "elements": [], "aggregate": "B8A43C71E3F1F66E9E2D918EFF60927E85CE2A13440F3E8CAB0509DAB8A955E37ED81A3DB02F3E81F4568085B0D1CEAE", "header": {"version": 0, "last": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D", "contents": "9A09BA0879AF73C8F6762D6E17B6D1CD71DF45E882D26492671D89ABB17B4C3A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "9185C45438F953385D49F221070A1FD8DA98C3A8838B6DBC5DFFB3DEE4CF2B12", "miner": 0, "time": 10800, "proof": 0, "signature": "B736988A8A41D7EE74434414B3B3FD8DAD2725D2F45E6F28D8EBD19534DA37683869BA6CCBFAA3E6E076C2FFB0712107"}, "hash": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D"}, {"transactions": [{"hash": "BBF99A9FA8B5E3EDA2A9A01E9EF0216CA25BF1944E3C7BFE07E8AB76D79BF6E8", "holders": [0]}], "elements": [], "aggregate": "B7E6334C31D2C6ABA93103690CE1B97C807241F595E3BE37C14E6404A9DA9F18041B045EDE05EA8724ECE3DBE9DC3018", "header": {"version": 0, "last": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D", "contents": "3EB66D63EC0762271C12F3BE3F9F17B99A27BEEE28DEE121B43CE38E52EE9410", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "2E4AB694CFF8FAD0742A3A24E821848DA2B8E2586A9090DC5B950A18C600D554", "miner": 0, "time": 12000, "proof": 0, "signature": "83A746E816FA1390BD30698A4811A6C1D6808A58C6FA97B4FE24D53D579F94AC6B90A149C2CB81612266C8452BB65742"}, "hash": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18"}, {"transactions": [{"hash": "60F3797E55CCE4C9EDAB148470A9127AAF368647F2757A9E52312239466C8D71", "holders": [0]}], "elements": [], "aggregate": "A9BDEAE67E27D4B4CD97E39EC93207A585675C29AB60BCA0F60A0624225BD1486B2326CF8C74D21569D5DE928F0340A5", "header": {"version": 0, "last": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18", "contents": "8775AF24BD4FA0DCCB52F15283F214FCF4C32A76F2E9A03C3485D471D9DFAAE6", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AF47884FAA2EA0351B28450560297631E457194F63DE3E2C531ACF628575D97D", "miner": 0, "time": 13200, "proof": 0, "signature": "A84775CC6E97C7A86AB3EC7F2F0F6CA8368569A5ACAB5697EB0035B5C6E5A1F58FCF7C9EC34178617D5815942084A99F"}, "hash": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78"}, {"transactions": [{"hash": "351A83B6BD0129252F7064B983F5C6CDFD7D854B941E911BE9874B727CD67B95", "holders": [0]}], "elements": [], "aggregate": "8391B75C4ACE226798B29B4404C035CCE5AB32CDBE61E794E9699AF6B715AD237F8252DBFBCFFB68EEF59F887AA03A5A", "header": {"version": 0, "last": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78", "contents": "B3EB7A99EA5421D49D6D00998EE360B366F3C48E5A1D5D13D80B013B1800FCB2", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "513271EF4B1EE9E718BDD93AC973B8F35BAF74011AA76898F266F3EBCF179621", "miner": 0, "time": 14400, "proof": 0, "signature": "90D95A9689683749BF754CAD5F25D7CD556292A552AA98CE163FB16291BDC19F11BB262C3E4B42455FFDB4EAD900E010"}, "hash": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10"}, {"transactions": [{"hash": "775A77F43C5C8613C6CA27C5A200822FE7F7865EC19F05B1BDE8676A25D3761E", "holders": [0]}], "elements": [], "aggregate": "85558989C1C0061C57F816F668D1856293F350376E7D2A8DC2C2A65BB793A4B0391CE885C19F318E13E38DD91C424F83", "header": {"version": 0, "last": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10", "contents": "9D9AB4830989D1F9C40A92C0E69ED5BD8EF610C530C0391C180E663581534D0D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FDEEA3491C7B261560E7D340180B11DB522865602670483A0D3591F4FCB19FE8", "miner": 0, "time": 15600, "proof": 0, "signature": "ACB2C40A0BF40BC25EF6930ECD4C4A5927EF5C252DC0E2E0DEE2F6B9784032F3B3936A05BEE3396057259B765ED77391"}, "hash": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A"}, {"transactions": [{"hash": "CD083ED5396FB5CC7DD6FDC217A91579255A97099E70D94591ABA9E4CF544A4B", "holders": [0]}], "elements": [], "aggregate": "A2719780937C21736440CE95C7792C5A689F1EC60488ABBE62450BEC33BA727B9E8D0C88323BC5E730CF38AD4105AB0D", "header": {"version": 0, "last": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A", "contents": "811FB083989D6585F335B0AFFDD0F166428B28603A780DD966B6CE1D54B2F6E1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A795C21BC025C44937FDEE5BDDE19DFE543B7997675D26A4C7BA034FED5419F4", "miner": 0, "time": 16800, "proof": 0, "signature": "97D2DC9E1B5454886C2C7A6AC13C06D0ADE94F33CF0B127407642A4019E9ADEBD0028BF7FF46A77AACC5736B0F2BA4E4"}, "hash": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD"}, {"transactions": [{"hash": "CA17D0EE821EA30021EA4637FE0E07F205710142CDE20F7DA9E5E8E6B40C2AEB", "holders": [0]}], "elements": [], "aggregate": "AA3FF79A19A97594CD2BEC4231E7EF8B445C3BED12AF224FA6F46F49234AF116046700B8C51AC62182C86C567C232AF8", "header": {"version": 0, "last": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD", "contents": "A031FAA34DB6E84F0B327B9ACFCDA17725CE6E0CA7C17B4F7111B89069D6E39B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "32E686539EB9152C9F37A1E1786C5493B08385491CF00DA633100F4A63C62901", "miner": 0, "time": 18000, "proof": 0, "signature": "AEFB5604981AA60CDB8F7A0472A0F4CBF21B957A403A5F34B8AD1292363D697A7475D9D6574E996F0B71C37B95C93F8D"}, "hash": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615"}, {"transactions": [{"hash": "0C7486E9CEB3030F18DBD9945929CF92BE3E60C8A31C997FCDEB80898A40E082", "holders": [0]}], "elements": [], "aggregate": "8A9141DA89C315F921F241ECC9346B541BC619436CBD0775C8C900A7B65AA39EA20BA65673DD27FBD1D3012650964D4F", "header": {"version": 0, "last": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615", "contents": "5C93322261C6FF291512BEC2F9A544BC9B8CB0169A4B1A3D2B62C4995D12072C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "519724F8B3CFEA672C1B1D683FB72975563CD47F113791B8576BE16BA4B524B5", "miner": 0, "time": 19200, "proof": 0, "signature": "991C269C809FBF0ED4AB45D9758E376C71D2BFFE6D24B625CBEA88FB68A443A8E62A4A27E579827D6CB882E8D419DCF6"}, "hash": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40"}, {"transactions": [{"hash": "387FC354C6A3D12E40466B080A0CD2C3C97EF9913D892919E08B17CDEB6CFFFA", "holders": [0]}], "elements": [], "aggregate": "94516A29CB0FC365B65879E5C9F34B101C2B2BEBB228EAA2FCE79058A24C93D56C674931184F56C0FF14D5068D76B0DD", "header": {"version": 0, "last": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40", "contents": "DB352DFE3A5DDE7F6E578088377C2D94B39E231A2EA7FF8DBD88FD6DBC9CA8D7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "089B9D00365E3EAD207BEF01F083490CF1F3B1BF195CB7B1DDF5EC3E263F239A", "miner": 0, "time": 20400, "proof": 0, "signature": "8042C2B741D709FD403A830BA298FD2DF611C2CD085BE63E5E0B924D162E182A4AF287069750297CE45F3B7DBEB1E90D"}, "hash": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525"}, {"transactions": [{"hash": "5C3F6AF8E5D45304AD7F387FADF9E3AF131C56E653D65DA0510DCB4088610CAE", "holders": [0]}], "elements": [], "aggregate": "8CB2DE75C0EB4213BA9E6C68CBB839838DDCCB2F845CEEFF505F9C00704D256FE774339C1712B5106BD27D4E2E2E3B59", "header": {"version": 0, "last": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525", "contents": "DB4F7545C4235D98A55E018A9C607A0F3006E51F0ACFB50FFD0954C5BC509A05", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "5F86DF5E696115C6B5F5B3DD88A95F0E1FBEF991C3508B2C2FF8444E58F68E22", "miner": 0, "time": 21600, "proof": 0, "signature": "AEED882FAB73397288E5CE76B611AC6DD589CE7863500ED1D95C6D4CF3EF0528BDE1CDB729D6803CDB33DE57EFD08C37"}, "hash": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C"}, {"transactions": [{"hash": "D1FC6A19E245897AB2F37E33DDA544C5C702D75CAE323C0B9576E0AA49204C17", "holders": [0]}], "elements": [], "aggregate": "B8E70BA7DD0EB28B36A147309B71F29E290D95EFC8F1A02120EDCA434ADB29AF85083E5C9994B7D79A78AF36124D2D03", "header": {"version": 0, "last": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C", "contents": "6AC0A02AD6E722A8B68E9535D876F7DE9DD666EFA0C3F7965145CB588393563F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EA827E1705E6B10688301DB3BB0AFDD25E09313A96EE093616B2D2C4AD5A649C", "miner": 0, "time": 22800, "proof": 0, "signature": "A7E7BB684B26DAB5619093EACFB7D01D145C75DD4C9C2687F6E57C40D50BFCAB2910521BF09EBC56D5AF70729B940E46"}, "hash": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9"}, {"transactions": [{"hash": "4663CF896951DA74019FBB40ACA05D193FA36947DD00C6381A174EB7ED42459F", "holders": [0]}], "elements": [], "aggregate": "85FE98831A681C0052B0239CD186BA4197C0CF8680C46F3719DDB9A7F2DB016F791260DBFEA99AD55A15D8EBDE562C85", "header": {"version": 0, "last": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9", "contents": "D47F54489B54352B703153B00552D3D2BFBACF08977756F475119087F9C211A7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AEEA9B2AB377ACF84D2D25C1CC9A1E611CA13ECB337F8A8C3892C32905275F04", "miner": 0, "time": 24000, "proof": 0, "signature": "895C44B71029343EF6D92D320ED563F3F2E96F95742EFA14A794BB3E8A036C8EDEB6027F52B97C24EC68AE8D89E3B141"}, "hash": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361"}, {"transactions": [{"hash": "2CD1A225E20AAACE9A34B0CFC2F07C9FCF80B9D4A47812CEFCB23012962BFF5D", "holders": [0]}], "elements": [], "aggregate": "AD6E8902BF0C69248E65C34683E015831102B7753E661ED26364F67B7E53C756DCCBD683C1442BD4D9664520385739F8", "header": {"version": 0, "last": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361", "contents": "DD85BC873116FF0253FDE52723C7F0F55EBDEE71E5990CABA412440206CC7662", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1AC6CE760921B1B70AA1575CC7A625E0FFE8073D72711E8B976322275968A9CF", "miner": 0, "time": 25200, "proof": 0, "signature": "B1DBCCBBFDB8A9F41176A91AC7FB3105FDF05246A757B7C018F0A12EDE54896F1CEA1378CD83493D31BA1FB367EFBF2C"}, "hash": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25"}, {"transactions": [{"hash": "1914829F4A76D1ED019BF9D2E3198BE0B6A1B55E0170D4EEF8D3772937799B70", "holders": [0]}], "elements": [], "aggregate": "A579D7772E589647623C357F1FA8CB2AADAA8A348389B4F7345507F29D3DD5D9D6B74D52D8504E0AA802203315B57C65", "header": {"version": 0, "last": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25", "contents": "A1E5B19F192EA5BA11FB02EFC47C19731C4A61996A0DD8141E9250AFC702F42B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E895BBBAFA1388A73917ABA96B3D276468F54CAFA23D4E1C92604C780E4FA72E", "miner": 0, "time": 26400, "proof": 0, "signature": "B541F894A8B0F540F5FDF447747BEDCF5473026EFD38775CF022A6E6E71B5621807DF1C64520F1A74261CAD9F72B0404"}, "hash": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B"}, {"transactions": [{"hash": "886F3192EF28DB8E9476A85AC3E2BA505743198602E89F72339C96A6BC6CAEC0", "holders": [0]}], "elements": [], "aggregate": "AFB23913A0589BAD19804AB1984CF2AE40067483571B598043F188482F94E4218594F963632B5401CE9165C74A564A9F", "header": {"version": 0, "last": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B", "contents": "24E54640F6DC403551054EDA610E734D6A00332BD3847DC7A7DE17EA61C58718", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E52203148F778F29C26C4A5EED60316866F0C36F6735291D9E38DB897F83109B", "miner": 0, "time": 27600, "proof": 0, "signature": "B8A51E0BA8649D6EFF075F70E76C2879FF57F944ED6A05C424AB6861C57B7489CAB77DA3FD5FE640FBA9A8AE3D1C9E69"}, "hash": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B"}, {"transactions": [{"hash": "AA383287CBD1280772D520162020F4B89A44683FE12BFCBFB2203EA7A25D7D17", "holders": [0]}], "elements": [], "aggregate": "83275ECE29DA906B17730B1D83E30182D7A54B58FAEEE123F4C7C49F863C1A5AD0C46F5D61A80198B06054EE647AAEF1", "header": {"version": 0, "last": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B", "contents": "1D4FCE21C0FE6A24C386D40C92D26F0E8B4AB1CA1D52E281F552B6E8B31E6803", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "ADEEF2E532DFBF8B41384A61173FD3FF6BE2542080B106FA57CE6C546E4E176B", "miner": 0, "time": 28800, "proof": 0, "signature": "925B65382C0A8B26393EBE08F8B72E4BBD45E275CFC749A163858A582D9DD474EA60A527671D1280D7B76B4B9232BFF5"}, "hash": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF"}, {"transactions": [{"hash": "C9A65D759E9D37FF276B2733376F82140763F6FE90D2E2C6040A0BB62EF51152", "holders": [0]}], "elements": [], "aggregate": "AD3406EACE13826010E20C65D38AD688FCC28B24B3A7B78B9459AFDE89E5E831EE789B971B5175B0ED83E4C70AA42710", "header": {"version": 0, "last": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF", "contents": "B69147F0EEF64CEE66828986C8C9F06364FFEBC61C56680FF1659CCAF985CA9D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8C9811D818346EE6C40848C9F8B15D1E61227639EA2F0F1DD896AA3DA2C44441", "miner": 0, "time": 30000, "proof": 0, "signature": "A2AD4BE21A833707E3F42DF55E4300089C4FADCA6010008501BB4133F90793653B6C7E8D45DE7A320428A75F69DFD980"}, "hash": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583"}, {"transactions": [{"hash": "7ECE5339A3ADF8C9A93E348BE975C2161BB2CA99B0A95D5E7367E94534E1EF89", "holders": [0]}], "elements": [], "aggregate": "89E22645E0F5509B5AB620C71D146E3863209BB1FEAA1A98A2AA44685A788AC81B72F0791CFEB627E1BB33EB6767F198", "header": {"version": 0, "last": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583", "contents": "B684AEBF86A1F120E5A44A8C14A159973288246CCC77B44F7E554A5D0D8D67AB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "BE619866422407EDECCCF01D40CFEA4CD97B108AD4AC8A859565954F80355E4B", "miner": 0, "time": 31200, "proof": 0, "signature": "83693BA208148B3BBB92BA670BD6320EB343B3998064F3C65D1D3FFFDF34625B0B63875C37808597BD1B151B35CE64BB"}, "hash": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB"}, {"transactions": [{"hash": "7B0498874B886C771502AE3A40F848F706C8B412517C33DFCDE0A3EE24896C3D", "holders": [0]}], "elements": [], "aggregate": "9777F1ECC189743CF8495F1593A4B9ECBA8E24F0E377F238AFF267B00451FA24A2F55BA867A5A3C5D2A7724C500A5CA4", "header": {"version": 0, "last": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB", "contents": "380410891EE3A5845B0438A8C2281B92E902D94B4FE4F7A9F383849718D21168", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FCFD4349689F789D33570A5B0A42CA821F6FF8F320D1A700ABD3B538FEB25466", "miner": 0, "time": 32400, "proof": 0, "signature": "8A858B58B9C089077FE125A55B2EE89F9629E460FF628545677881F14AB623AB673D8E50A70924EE2359307DE4462A6E"}, "hash": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C"}, {"transactions": [{"hash": "CA1CE7CDF5B2F3128DFC1050B240E48E13D313D951FD20BD60F1C89C71227EEB", "holders": [0]}], "elements": [], "aggregate": "9789EFEAEAD52C813157D9E7ED237FA6217BFD92C98C6AF10E75040A090CD184B97F0BA67E1B561971DA0C3128D3DADE", "header": {"version": 0, "last": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C", "contents": "628BD47A0591ADBDC780A7ECC8C86977DB29493E072761E00C64B51AC92E0732", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B87CF0B569E2F08B381753FA8F6359AD314319056E14BD28CB129D457949A14", "miner": 0, "time": 33600, "proof": 0, "signature": "AC71514D519AE5B9CA1E5B9C90940FBFAB7AF802B4928CB661040D82B5DA6CBDC96515140EC07297EE7292F4F40091EF"}, "hash": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A"}, {"transactions": [{"hash": "17F10521957248191BFEDAF36E6543FC8FFE61D10644E037E93F3A555443A9FB", "holders": [0]}], "elements": [], "aggregate": "B97955DE18257B2B74B54F2F56C4E0F34A11AE75F9AF7F1E9BD8EC66017F15962264DC654D23C69BE97931976A4C35B2", "header": {"version": 0, "last": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A", "contents": "1375723AD673ED9974F0B5CAE015F691581759434D076DA469E2AACADFF37F1A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "17B2C89050F788BFD1134009C1A396C2EDC2F8919F11497A7F6F9BB9FC88B780", "miner": 0, "time": 34800, "proof": 0, "signature": "B711DDFDF6717189817C83AF1D760045F6BD9A928EF1F3C8B514766781138741C6301B5FC430BC7832A7AF4530CA3A97"}, "hash": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42"}, {"transactions": [{"hash": "503D0FE35CFDC7F3A15DFCBEFB1AE55495EB1FEC723591E023B6C854D55E494B", "holders": [0]}], "elements": [], "aggregate": "A08D9E3C46934EAF57AB1D31EDFCCD0C031A9D8E35638631E76963CD8420B1B44F60FFCEEE88EFFAE22EAD3D435FFC4B", "header": {"version": 0, "last": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42", "contents": "BDCD84DC8A3C6E41FB8D432F2F69E2565BDB20CDEA60048355C0572F19FE2C69", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EDAE53D97A0A81B7D39FD161435E0BE196D5DC8E860DA6F1A6D3E9DA87956CE9", "miner": 0, "time": 36000, "proof": 0, "signature": "B912FBA94F16BB9AF34DE53A7A65532E3AD5841862EE34C85172F85C3ED55BF0F8C136BF4114298B86E19A5BA7105910"}, "hash": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096"}, {"transactions": [{"hash": "CF82B3A47ACB0B38E9FA9A8A8FD1C89E3D0023F016A2F207D629D6FB3AC0FC2B", "holders": [0]}], "elements": [], "aggregate": "93996B99767DEBFB239E3FA3ECA1A901D1D07401ED3BB751878D8C9127ED58EE981A0A2E9A25BFE577E03707089ED000", "header": {"version": 0, "last": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096", "contents": "6B58318212B14ACDD98D0EE097D5614FD5CD21AF968A0AB538D6AC3A00AA3C67", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F98042571452CA386BD6BDF2638A6BD9ABE56627AFEFB49DBA2A41FCD9474745", "miner": 0, "time": 37200, "proof": 0, "signature": "ADE7BBD187AD8E9625230E92F813E5BFD19FF8FE000FF96E29129CF7498C6066A4E992EA82D21979D20CFA6B8A4A0F90"}, "hash": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F"}, {"transactions": [{"hash": "EFD9B2A90A15401AB3D1AAC9262E70C9B9AE448AC7560E96A7766449AE5446D2", "holders": [0]}], "elements": [], "aggregate": "93926EAD07F7A93C23990C6A41F23FA3288232F741C97D29182E0AFE90C3E6DFFF1E54EEF666F78FDE513945407E285B", "header": {"version": 0, "last": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F", "contents": "37CD8023C91A9ADF1512D69A23B77A418478C6F6EF66D966182C1E5C95F092AC", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7D2362B3488EC1A041755C7A14B20463502292442C4E2FE96222B264E43985E3", "miner": 0, "time": 38400, "proof": 0, "signature": "A92DABCDB4078E26DA81109E38AD82397555543264028383E94DDE5DB9BD5079B8385329759F28D3D81A21E620E51DDF"}, "hash": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92"}, {"transactions": [{"hash": "768D69AFADCC7A29E23663E8A66EB8D7CC1563079A216EAA29B9783DD528DB8A", "holders": [0]}], "elements": [], "aggregate": "AF48FB26E57F2D619487811009F1E43A50E073A439DE7DBF899F9E158F8C6F0B6432B2754347C1CE6510E538C5D47291", "header": {"version": 0, "last": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92", "contents": "44571AF7F71017BC02764E5416B9E8CCFF1597E3480143808E1BF8E5F9B8023F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F0FD8CC4454CF3F1E0B3636CD046FABC7291F16D2D0A7054C8C3100E10D8BEB1", "miner": 0, "time": 39600, "proof": 0, "signature": "A84C6D840E538E70EA4AC4831853F3EE70A384DEEA885EBC0684248DD1905C7FD6465E37E6B187A10C1E084249BA1D17"}, "hash": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436"}, {"transactions": [{"hash": "5028DDEEEE12CC6FB156E713F8446325DBE6DAF87C4926EC9E12E8B028DE5433", "holders": [0]}], "elements": [], "aggregate": "B2C2CD38A31D99177B263A311BD14718B1012F75987B8A653A1C2C20FC239A86F7597F2BD9CF50784B8BF4559740F126", "header": {"version": 0, "last": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436", "contents": "A20B36DA35E654AC8C2DE99EE1915EE1ADF583F8F0EBEDBAF6DD4BF4EF2F5382", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "B5802C1E8CE29BD6FBCA50DCB4506A2563CC030A453D971594DBB27E72D8E6FE", "miner": 0, "time": 40800, "proof": 0, "signature": "AC4C3BE796F9B6978079119F47E4A2616787EA843A54015A85D4F357A1A62DDE1380105BC8C86389A892A8B934349D30"}, "hash": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E"}, {"transactions": [{"hash": "01FA1B676C326343DF4B03F3840DE9383EAF0EAA13898A278029EF821E459089", "holders": [0]}], "elements": [], "aggregate": "91B51AFF6A3F9FB9BADBDCCECCE5093F80F83067D51E9CE660368B89F0C6B200A6ECF0D2E38BB0B9D90ACE4E3FBC4F41", "header": {"version": 0, "last": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E", "contents": "4E18ECCBF34A734136250BB94C7EC609FD424D1443860FE53A082FF59A8B87F0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "239668D5D0544F01BE9E0C39363AF70E78335F388495EC1BAF779425A09E3B71", "miner": 0, "time": 42000, "proof": 0, "signature": "8EB2D10893C3DF1B5331E4ED390AA2C44AED0B48FA0DC8A1329C797ECD561A10A6D0548B77A03942D8356C43B392FB94"}, "hash": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65"}, {"transactions": [{"hash": "1D932D69B193AD9246D4B61F946270940083FA5ED35459F4ABF41DB8F1C888BB", "holders": [0]}], "elements": [], "aggregate": "B2D9926E7F67112737ABB78DC0526F3F3FD02005AA984C9C33F9B53FEE2BBB70E6370570C511A0966C21AEAB46592D35", "header": {"version": 0, "last": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65", "contents": "0F6821A9C7D8A2005B6BA5FAE7B2A60EE0809E53AEA2540A269B4EAA01EBB12E", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "655CDDB0EAB8FCC0A36AFB7D970980E30CD65830CB7DD3C4A43D6F19A8669D05", "miner": 0, "time": 43200, "proof": 0, "signature": "8EC7D88E37A2490D7954DB86D6A7A67E887E0D6D5186CACCB3F189E4520CCA2623F72C234C1D0162C48709EDDE583CE2"}, "hash": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B"}, {"transactions": [{"hash": "52A09B2A03602D5536DEA58861D24E61D50F9756D9F402D9BC80D092194C8914", "holders": [0]}], "elements": [], "aggregate": "980FA53F64DF42DE83F30EEF35F1ECFABDD1E650C96BC7E9BCC485C1A0190F48D08C2FD98D8C2D8A13FA3CB9CE598D5E", "header": {"version": 0, "last": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B", "contents": "63DF40FF9CBAD4F329AF28C1E9D32D6AD817AB330962FC2A22A0E0747B5E70BA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C05C3FCAC02C04C52FF67D092421BDCB1087040F479181B4CABA1D7CC971E77E", "miner": 0, "time": 44400, "proof": 0, "signature": "98F92E503B9FDBDF51AC6022913D5ADB6EEE9A46475E19F6EA3D14B89AA7E3A4E0410646360C852B86E94A6BD94DDA59"}, "hash": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785"}, {"transactions": [{"hash": "715C4EB143D757F3D9578085A9A493B62A12E63979F981FF71D7D0E5797F963D", "holders": [0]}], "elements": [], "aggregate": "838886F6A7C95D91E0046704A5829BFE80953CA62CAFA25F4966B912C188B30FB29565C3DC959A371C27D1A67D802411", "header": {"version": 0, "last": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785", "contents": "76CB3B34A2ABBE67A018D9329412A4A76D67838D06308E4029B3A8FAA78F6692", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "75E79993E688F0BABEBA4E8AF7EAB2D23BA0B20E35B53C4352FF25464FBC2305", "miner": 0, "time": 45600, "proof": 0, "signature": "82E559E958A3630F5B240B0F82DD0D7C8B786ED0889DFEA7AFF0074DF3A2342E77D9400CE8BA83F1A8101EAE45F9C0BB"}, "hash": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3"}, {"transactions": [{"hash": "76CF7E02283072268B70821E34C60372B84B3CFBE8FB315B3540CD7FA48FF2D9", "holders": [0]}], "elements": [], "aggregate": "A5C043E438254B6D82445D3D5858F7E37873B1178A2FE8E0B7ABCF5903EAA1F7955711A64F16C4F9B2A5D5AB7AA91DFB", "header": {"version": 0, "last": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3", "contents": "05ECD46C6FFDB6CE064B1A7BDE217199694C779906269E29B686AAC2ECD4AFBB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EB0E425AFDDEC9A6511202015E98197A4B5FDC8F8E0EA83B5F6E9F091486F308", "miner": 0, "time": 46800, "proof": 0, "signature": "8148B9722127E142AF9014C182B3924EC4D2187B6B56A50D9B679C856864DE5A210F372B410DFCAE53D237F7F5B3B50E"}, "hash": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34"}, {"transactions": [{"hash": "9B8A47FC44420DB73CF46F0168B53CF93D495CBBAA9B36A9913298B8648574FD", "holders": [0]}], "elements": [], "aggregate": "84A52DB751EEB72E1D9F9DA54509134F7F5F54C409C77343E78925583E5FD1209E66DDDEAACDD60ADFE3E133C266307F", "header": {"version": 0, "last": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34", "contents": "0843B460D5841D73032DCCA971D096930EABEB343420D5CE790A0D79BC4577F1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7228382536A5A5B26CD248EA461978371776D786A4574023AED45FA7EC07CFBE", "miner": 0, "time": 48000, "proof": 0, "signature": "925EACECFEC61C713E1B5E07DD7B21A5D9BD75177E6AD97ADB5D93FEAF358975D334E738609C390220EEA72B514EF93C"}, "hash": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF"}, {"transactions": [{"hash": "A8B87D0CE244374CCC914F0086D2A5ED6C7D7947F76AF07518BC17DADFE61B99", "holders": [0]}], "elements": [], "aggregate": "9197DFD18F76C8574169D64E947A07C61AF53BF4334972DFFDC81765D7B0B80E11A7421755BABF67CC78B51F645F20C7", "header": {"version": 0, "last": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF", "contents": "F33F7C3B8383B04FB3458B3F09135AFB25821E0988D835E9E02065EA10613DFF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1B4CBCDBD72F25DFDEEE604A1402555B911270C58AD3EEE05B5339FB47709AB0", "miner": 0, "time": 49200, "proof": 0, "signature": "978D37E5B2262D811FFA5D2551C4F6812E448E8652942414981F161EC81B30C97C55F55782088F0798FCFFE6718A67E2"}, "hash": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553"}, {"transactions": [{"hash": "0DEC8740B4E9BC79F54827B7F179BB804866B62DD8FB89D5A3AAE91127179B0C", "holders": [0]}], "elements": [], "aggregate": "A1927B616F7FCBD04954B26122881383B90A18153EA39FF2B85C5006243E8F1EABE29BD9640044B2B157FE130D5710FD", "header": {"version": 0, "last": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553", "contents": "FD65AB96924037AE17DE36A779438D3E0F1D5B6AB86EB13294B4B74B0B3EBCFA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "81F9805AC3B6E2A7E2334F535FA802F144B11C2FCF36D7225354E6889B4B8362", "miner": 0, "time": 50400, "proof": 0, "signature": "A15685AFE8A8ABF35FE4E8DBC290284AB2941066A73AA8BE763E5A489F1FCF9EC6B425F892720726A2A3B7514A93FDAF"}, "hash": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D"}, {"transactions": [{"hash": "93851C8F4DD5348A50EB02FFA01D6948CB6E16819C0D7D13EB144281C17A0751", "holders": [0]}], "elements": [], "aggregate": "840A00EC75C90EAB1F50EA4718C87F189E21551BDA4C507058EBD3BAFFD916051688EC9E8DFD92A3F240BA27B474ADCE", "header": {"version": 0, "last": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D", "contents": "6EC6E7C25E62DD005D6FB65250FAB452453D9D9AD4CA93121C1275FFA8E4B207", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "79929DBA45BB3E810CB2847915609D2D9E3CEB998F2B09F6EB0B5E90C9D497F9", "miner": 0, "time": 51600, "proof": 0, "signature": "808F073633DF55ADBCD93035466E9254300AF1518E923477B6FC5D483CC8A2C75D87E6FF24CEE061FED050BE3CCE244E"}, "hash": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D"}, {"transactions": [{"hash": "0B85FF7C665D0229B14C2FCEB168D27E30A0E955B6BF5176EA9AE549042A2011", "holders": [0]}], "elements": [], "aggregate": "A03EED165302DE3DCE12645AF9FF5D2618B23454AD8D2429CFC582C5F9E8EDB6CB63B60DD0F0FC5A2AB1F44C7CAF4340", "header": {"version": 0, "last": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D", "contents": "D81C83FD51D107CD3F0020796868A59557628C9B6D62693ADCB6E3A4328159FD", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8415D8191D3B91B5B7ED68975872BE5EC17F030C2CA2EB056F84294E0382FA6A", "miner": 0, "time": 52800, "proof": 0, "signature": "ABAEBE124340D09ED8D202418FC060174C432D383F36FC5D25F3C0E81DD694CF74DA28AF1458A55693E3FFCD916684A1"}, "hash": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3"}, {"transactions": [{"hash": "F4001DCD38B0E23768108C1F0CEC8FAA1777AE0FD4EEB38E6DD3E0CDC9735696", "holders": [0]}], "elements": [], "aggregate": "A1DED17B59BFE2F34FB6011D9FAA2F60B5D557895BCB1B2D927B021E21EB22F7DEB115A78B350D486014ADDFF69EF391", "header": {"version": 0, "last": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3", "contents": "BBF99503895A2E37FE5599ABDB7C1C1672D003666B29E9F28BDEC380E35EF040", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B7E79ABB5483F92115182B9B5CDC30709D8192DE966E8B831E4A6314BD3D5D8", "miner": 0, "time": 54000, "proof": 0, "signature": "8DE8C11F66CDD2B75998A8936778DC624C382ACBEBFDFE322BEC0904F730F033AFE70748AE98F49308C3CE3D3E2981A7"}, "hash": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8"}, {"transactions": [{"hash": "602BA0D211013BDE8EB9619F8431EBDAE3BD4B5FD09F4266B19432A6AC6DDBC8", "holders": [0]}], "elements": [], "aggregate": "A3737B6AD6E12FB918672E22099A7C113A9590787723EBD5D387B7871EB60D424A8450B271DFFAAEEAF2C375E4D403FB", "header": {"version": 0, "last": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8", "contents": "478094914295E4311699033104C5BAB93F471B4EA137209F7DC3B10E97678068", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8D97A09A1AD26F8A100AB558630D930F60A42500F5AB309609ADC50B7F9EF07C", "miner": 0, "time": 55200, "proof": 0, "signature": "8C36F0B44D5120FBB910D3CE312F9BA135CF19ABD39D84EC5560642EF1FD4BAED46957CE94DBCC1B4FA7C2BC736CD0E0"}, "hash": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3"}, {"transactions": [{"hash": "2FD1583B1B31B0FE60632F556903257FC3734E39AEC237EDB5CCE784777A1A6B", "holders": [0]}], "elements": [], "aggregate": "B60B0A0084D20E0CCACCF0797884A1DBFB10EAA1D634584D0BE0299343285D137EF5CCE91DFB9B4D1D52791BD98FC40E", "header": {"version": 0, "last": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3", "contents": "6EF462AF646189E7E659FB67F082F67442B1ACB3D1439EF97B410C8D94975815", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8337618DDD6041028BE2E2F00A9D6592E0C80F6900F1DD5772856A59ED589736", "miner": 0, "time": 56400, "proof": 0, "signature": "A20D6B1BF11A779FCFC017DCCE8334DB9C7E5406FA8B718C8820BC5A07B3246ECA22D6455524D0F746872575AC19C979"}, "hash": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368"}, {"transactions": [{"hash": "F003464578174C8D663D441EBF43AA2CE9409A6784FE8E29D31143F7E23925D8", "holders": [0]}], "elements": [], "aggregate": "B6C4AA689DF5DEFDA7A6A5FBD55EA9D943A31F3D093074952A423D9283A5E12FCB85C2DDF9C0FEC2B699C7FDA7820EE5", "header": {"version": 0, "last": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368", "contents": "AD92B26343DDFCED5C846B36720F406D9841B5FB4420C7AFAF3FC7CE94C1A00B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A0356AD99D6FF0B01DE64830881AB19B7DBDEE10D98E9E68D7F3233CC0EF1B0E", "miner": 0, "time": 57600, "proof": 0, "signature": "B6895FCA06DA3B2E7D680A9DF29870DF8B7C1C5698A11474640E6D6CB73ACEB481799F98B3EC9652720261C44E51B6F0"}, "hash": "1E75F8DA3FCD70981EE43D32AEF584F05DCD0D61ABB3EA5ADAEAE2CA38437E86"}, {"transactions": [{"hash": "D723A340D0C84D77F1D7446073D6D31F8CC22001A042BC0D2ADA92A34B50C6A9", "holders": [0]}], "elements": [], "aggregate": "97DD907C8DAA5445CFD102C872BE6AE3A542D33E056EEBAA154A80569809AAE8800C191DE46C4DDE64FBB433971920E5", "header": {"version": 0, "last": "1E75F8DA3FCD70981EE43D32AEF584F05DCD0D61ABB3EA5ADAEAE2CA38437E86", "contents": "EDF8294035A34F768BA6025B646EFD5A3E1BE54905F5CB4E8A5CA5AB61B50A19", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "48D199C5500DF3970E7BC51EDD313FE40447AC78A1F7DFAB816F30480EC4B073", "miner": 0, "time": 58800, "proof": 0, "signature": "AC5888331D8C91A842FD342D6F754CD79F0D1D2002E6AA2EC1977CADA9B1987170B9F0821D803B33CFE4B00782E0FE37"}, "hash": "81F9042604E366781D5AAEE8D264D032B42745ECBCBEC31C7D14A8CC76CCB057"}, {"transactions": [{"hash": "44D63D8D29040379268ABCC7559F80644E84001510DA3408368C99E7C13CC450", "holders": [0]}, {"hash": "AF1B4DB98D77082CB87ACADE24A80658A9F8DB9DC5283AD9FB91DEEDADE25A98", "holders": [0]}, {"hash": "EE74317F517514B154FC7DCB835B997F82FBA126D616A2A3922DB56D9E2567AD", "holders": [0]}], "elements": [], "aggregate": "A14288D988FBE7F9E60A992A54D5DBD178E6A5BE8A6A6BBE828C51F4B60115FD25E2284543C3C6DF91DD64266903DDFA", "header": {"version": 0, "last": "81F9042604E366781D5AAEE8D264D032B42745ECBCBEC31C7D14A8CC76CCB057", "contents": "082BEB3AC01F42A562DB4A437D8362AF3117B6412B694E03B914CA77EEE9CC2D", "packets": 3, "sketchSalt": "00000000", "sketchCheck": "31EBAE47FE274CB9BD00CEA44F56416D7A9D4E0BA302A5A0B0FA7BED23E9FB78", "miner": 0, "time": 60000, "proof": 0, "signature": "96D3C433F6B3944214080FA573D4DCCE42E2824F4F8159E735C9769A29766F8D0F801F4EB776E5AAD407F2A71A149DE5"}, "hash": "3D67FED16389C51F8E9397789A64A80290C2683008930F53EEEB2AD6B5980077"}], "transactions": {"AF1B4DB98D77082CB87ACADE24A80658A9F8DB9DC5283AD9FB91DEEDADE25A98": {"descendant": "Claim", "inputs": [{"hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "8FD8E0AD24823D28E95C0431EE208221BD085CA410CD763467D87ED6B3FC85B9CD9DBB78E6BED065F3C297AC9A948AF9", "hash": "AF1B4DB98D77082CB87ACADE24A80658A9F8DB9DC5283AD9FB91DEEDADE25A98"}, "44D63D8D29040379268ABCC7559F80644E84001510DA3408368C99E7C13CC450": {"descendant": "Claim", "inputs": [{"hash": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "889A4FB8207DB9BDFADAD4FB113C3B55FBE8ABC7FA7232A6A603F2F5281D209D49F6E27C2AD55C86F820E8497422DF62", "hash": "44D63D8D29040379268ABCC7559F80644E84001510DA3408368C99E7C13CC450"}, "EE74317F517514B154FC7DCB835B997F82FBA126D616A2A3922DB56D9E2567AD": {"descendant": "Claim", "inputs": [{"hash": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "A85E7979C541F88F6106F463054CF8FF96F55BE21FE052985484B0F6FA3959AC146BC013F6D6229E8D5070DDF9BF9E5F", "hash": "EE74317F517514B154FC7DCB835B997F82FBA126D616A2A3922DB56D9E2567AD"}}} \ No newline at end of file diff --git a/e2e/Vectors/RPC/Transactions/GetUTXOs.json b/e2e/Vectors/RPC/Transactions/GetUTXOs.json new file mode 100644 index 000000000..b6e8ecc2f --- /dev/null +++ b/e2e/Vectors/RPC/Transactions/GetUTXOs.json @@ -0,0 +1 @@ +{"blockchain": [{"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "360E204A3E870E34FC69B65536A6C8354966F95950D4C55A5C10FC276149FB2E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "B73B38B8D005DF431DB1CBF3462348FB8DCD213B3980A6C0E26D7B95A79F4E88324832A5ECB684436852ADCC8511C0BC0306EEB6D9E5E581AD279B633C0291157EB8ADC56D24750C95C8B676BA66C1FF5AAC4FA60EB5C8EEF0768067426602EC", "time": 1200, "proof": 162, "signature": "82021D8D5B5EF89C40548BF31C2890177B7CE874E306A62A0C1E6FD46F1583BD5E9E18299EC5D64A2D90E0B2CEC47B44"}, "hash": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700"}, {"transactions": [{"hash": "72EFE1EEF8D6372BBBA749E7CA910F210A2BC80C46A688D966F84C36AAFA3732", "holders": [0]}], "elements": [], "aggregate": "8B04E9F4BD7CC9A83D324ADCAC4FB2246A2A1A7AB87885A7F6B0A7A119CAB298D24ECE2A47239AB296E0A0505E2C478D", "header": {"version": 0, "last": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700", "contents": "C9521F92299A0B088B8141B012B42100E5FE6E72189F58DE714C22B1E910358B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7953C6E651693D49AEDDBB5FCDE84E3264648828330B8E8261C2EEBEDE4ED175", "miner": 0, "time": 2400, "proof": 218, "signature": "8CA439D808746224FAEB94D9E2980DE1B92E0947FB401CC458530A5E2D9E227C69FABC83E050EA5981F2AAFBF5F3A209"}, "hash": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602"}, {"transactions": [{"hash": "6281D2C3868D973A5EF0505A8AC35DCEB4C02671CF32195CDE3399D064AD3573", "holders": [0]}], "elements": [], "aggregate": "A070246256DB4536DEF33503DD7E63D4EE6B528BD445F9F671DA43C09945B3196AE4E65BE683EEAEAE41A0A26B3AB197", "header": {"version": 0, "last": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602", "contents": "A8D4C85179A8C6D6A7538F1A182D12A22C3200F4F97C49E85CD3B2185D22F8DA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A9A9C38D542DFA0477E4A01DD1DCF9EC3390986E1F944C9A4B204412E133B288", "miner": 0, "time": 3600, "proof": 144, "signature": "96B09D483F134D88486DC1079ECEE623620A5186E54B6AD2802FAE542E2BA8F60A4DB8A5A95D6E853FAA0C52CC40B8E4"}, "hash": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02"}, {"transactions": [{"hash": "278C104FEB0EB60FDD554EA0157704706F5587B165DC6356749B4AB20781BC56", "holders": [0]}], "elements": [], "aggregate": "B6F88AB1861E8739DC9CFFFC5B91F8ED221E65C2E1CAE55C58B59A6D0F9B93123DE2C680F83FB629F87E49621E3385B0", "header": {"version": 0, "last": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02", "contents": "A809E61ABF343724E17003F6EDC3FC31604BFC8CAC89E733167D422CB355B429", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "791F73693CA57D52320A0227EC5359BD325CDDC300B9B94091F53A77233883C2", "miner": 0, "time": 4800, "proof": 30, "signature": "A2D8A2CAB1ECBBA004769A57E782F308E1308DF6D64D64C0D2A7122BF023A586CA7F3B2D60FE3CFD91B80621D6E42A0A"}, "hash": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700"}, {"transactions": [{"hash": "86E44D62E8D27DA57458A2DE80698A6EC88054C5ABA9083FBE54EE0141E93A93", "holders": [0]}], "elements": [], "aggregate": "8B129406C90F252DBA681AB51D48B0B4B3C99E681B477411DF137B7657103EF92C5FF48ACCD05B8E87807F1906D2BF96", "header": {"version": 0, "last": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700", "contents": "0DD37CA5DDBFC97B394561381460ECA3212039FD592CBD232795C865E7C5CB6B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DB055B18061A1ED67925CC442B187CD56837434CB813865E70FDCBCF4480E426", "miner": 0, "time": 6000, "proof": 21, "signature": "B9685F27C2A79D71FCF6C14CBA94CD55F3E615F1BE5E27F23FB4F2DCC65A047928FC5C87B0115AAD791E40667C241286"}, "hash": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900"}, {"transactions": [{"hash": "018412CB2C17E3ADFEF52A98DD73EF145A46AAA76AA35A887246A83ABBFBEB89", "holders": [0]}], "elements": [], "aggregate": "8A6B7756526BDF6304907773176182EC8873536431E9F1B6001CEE8E6FC217B3AC34DB6FA3E8F8B46924685A14E5EDC5", "header": {"version": 0, "last": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900", "contents": "E27C9370E5749F06BDEC855865682DFFF18767D1A5AFBAD3007A8FC62820F3E5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DE6F5EE882121224255A2D752B891B43FBEA7BF74D42E7CBF251C7C2F28DA123", "miner": 0, "time": 7200, "proof": 0, "signature": "AC47691383371FC7A5EBEA5961BD060CED69FB56BCBE68A83221FE23AAB8BD0C4290AD38736B551C33FD918D7C6BFB7D"}, "hash": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623"}, {"transactions": [{"hash": "19C4B0914783A2CE9846044FFE48AD35E1E173023A64D7BD340734184EEF04B4", "holders": [0]}], "elements": [], "aggregate": "81EA7597EE88E818FA43F480FE43CA24269AC9BA4B26CB6A93B0DADAF06B679EE76B3371E9F2335488960CE7FB85D17B", "header": {"version": 0, "last": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623", "contents": "7E6C29898A4764EA8A42B39D3DC51472834FE78A67778A379DB6345EC81D4CF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "729018FFA42AEF0122ADA8FEC04B4271CADB3E9175472A7B4C0447ED720B4BE2", "miner": 0, "time": 8400, "proof": 0, "signature": "9638880A8D68E4B770802307E4CB24BACE6AEC6CAE02F623AF423F2C53D027B7F31DC8EEE8E9BA910B600B0A9D48318D"}, "hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042"}, {"transactions": [{"hash": "A8963D7A7144FDE3F7B5B64C1B3E6D0B1B5DA983BE0486B2FC6A746585BEC0C7", "holders": [0]}], "elements": [], "aggregate": "AEB80694FFDCCFE47F38CF3C5AEEC3F9B435AF5B3C5CDE5931EA4D15BF5A5E617AA414CE2B3305F85BD493292E879BBD", "header": {"version": 0, "last": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "contents": "E1BC8CE92B23858A2FA0093D944DEE8A08C66FFF8B799FC94595D2C18B4F4F2F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "560BB2F0F92A8DAF15FE89394733AA845880957823FC081A64233DA40F54582D", "miner": 0, "time": 9600, "proof": 0, "signature": "95E3DD10E00C916B59E9FE95894AF3EB7C9889B1FA67B6D10D549C5AF7258EE5A9E23E928B62F54EB708059DDF09D9A8"}, "hash": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D"}, {"transactions": [{"hash": "C8727936A3F41AB82E5564D94C5AAAD205F0B349470240C10752370BC8AB5591", "holders": [0]}], "elements": [], "aggregate": "B8A43C71E3F1F66E9E2D918EFF60927E85CE2A13440F3E8CAB0509DAB8A955E37ED81A3DB02F3E81F4568085B0D1CEAE", "header": {"version": 0, "last": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D", "contents": "9A09BA0879AF73C8F6762D6E17B6D1CD71DF45E882D26492671D89ABB17B4C3A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "9185C45438F953385D49F221070A1FD8DA98C3A8838B6DBC5DFFB3DEE4CF2B12", "miner": 0, "time": 10800, "proof": 0, "signature": "B736988A8A41D7EE74434414B3B3FD8DAD2725D2F45E6F28D8EBD19534DA37683869BA6CCBFAA3E6E076C2FFB0712107"}, "hash": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D"}, {"transactions": [{"hash": "BBF99A9FA8B5E3EDA2A9A01E9EF0216CA25BF1944E3C7BFE07E8AB76D79BF6E8", "holders": [0]}], "elements": [], "aggregate": "B7E6334C31D2C6ABA93103690CE1B97C807241F595E3BE37C14E6404A9DA9F18041B045EDE05EA8724ECE3DBE9DC3018", "header": {"version": 0, "last": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D", "contents": "3EB66D63EC0762271C12F3BE3F9F17B99A27BEEE28DEE121B43CE38E52EE9410", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "2E4AB694CFF8FAD0742A3A24E821848DA2B8E2586A9090DC5B950A18C600D554", "miner": 0, "time": 12000, "proof": 0, "signature": "83A746E816FA1390BD30698A4811A6C1D6808A58C6FA97B4FE24D53D579F94AC6B90A149C2CB81612266C8452BB65742"}, "hash": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18"}, {"transactions": [{"hash": "60F3797E55CCE4C9EDAB148470A9127AAF368647F2757A9E52312239466C8D71", "holders": [0]}], "elements": [], "aggregate": "A9BDEAE67E27D4B4CD97E39EC93207A585675C29AB60BCA0F60A0624225BD1486B2326CF8C74D21569D5DE928F0340A5", "header": {"version": 0, "last": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18", "contents": "8775AF24BD4FA0DCCB52F15283F214FCF4C32A76F2E9A03C3485D471D9DFAAE6", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AF47884FAA2EA0351B28450560297631E457194F63DE3E2C531ACF628575D97D", "miner": 0, "time": 13200, "proof": 0, "signature": "A84775CC6E97C7A86AB3EC7F2F0F6CA8368569A5ACAB5697EB0035B5C6E5A1F58FCF7C9EC34178617D5815942084A99F"}, "hash": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78"}, {"transactions": [{"hash": "351A83B6BD0129252F7064B983F5C6CDFD7D854B941E911BE9874B727CD67B95", "holders": [0]}], "elements": [], "aggregate": "8391B75C4ACE226798B29B4404C035CCE5AB32CDBE61E794E9699AF6B715AD237F8252DBFBCFFB68EEF59F887AA03A5A", "header": {"version": 0, "last": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78", "contents": "B3EB7A99EA5421D49D6D00998EE360B366F3C48E5A1D5D13D80B013B1800FCB2", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "513271EF4B1EE9E718BDD93AC973B8F35BAF74011AA76898F266F3EBCF179621", "miner": 0, "time": 14400, "proof": 0, "signature": "90D95A9689683749BF754CAD5F25D7CD556292A552AA98CE163FB16291BDC19F11BB262C3E4B42455FFDB4EAD900E010"}, "hash": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10"}, {"transactions": [{"hash": "775A77F43C5C8613C6CA27C5A200822FE7F7865EC19F05B1BDE8676A25D3761E", "holders": [0]}], "elements": [], "aggregate": "85558989C1C0061C57F816F668D1856293F350376E7D2A8DC2C2A65BB793A4B0391CE885C19F318E13E38DD91C424F83", "header": {"version": 0, "last": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10", "contents": "9D9AB4830989D1F9C40A92C0E69ED5BD8EF610C530C0391C180E663581534D0D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FDEEA3491C7B261560E7D340180B11DB522865602670483A0D3591F4FCB19FE8", "miner": 0, "time": 15600, "proof": 0, "signature": "ACB2C40A0BF40BC25EF6930ECD4C4A5927EF5C252DC0E2E0DEE2F6B9784032F3B3936A05BEE3396057259B765ED77391"}, "hash": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A"}, {"transactions": [{"hash": "CD083ED5396FB5CC7DD6FDC217A91579255A97099E70D94591ABA9E4CF544A4B", "holders": [0]}], "elements": [], "aggregate": "A2719780937C21736440CE95C7792C5A689F1EC60488ABBE62450BEC33BA727B9E8D0C88323BC5E730CF38AD4105AB0D", "header": {"version": 0, "last": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A", "contents": "811FB083989D6585F335B0AFFDD0F166428B28603A780DD966B6CE1D54B2F6E1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A795C21BC025C44937FDEE5BDDE19DFE543B7997675D26A4C7BA034FED5419F4", "miner": 0, "time": 16800, "proof": 0, "signature": "97D2DC9E1B5454886C2C7A6AC13C06D0ADE94F33CF0B127407642A4019E9ADEBD0028BF7FF46A77AACC5736B0F2BA4E4"}, "hash": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD"}, {"transactions": [{"hash": "CA17D0EE821EA30021EA4637FE0E07F205710142CDE20F7DA9E5E8E6B40C2AEB", "holders": [0]}], "elements": [], "aggregate": "AA3FF79A19A97594CD2BEC4231E7EF8B445C3BED12AF224FA6F46F49234AF116046700B8C51AC62182C86C567C232AF8", "header": {"version": 0, "last": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD", "contents": "A031FAA34DB6E84F0B327B9ACFCDA17725CE6E0CA7C17B4F7111B89069D6E39B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "32E686539EB9152C9F37A1E1786C5493B08385491CF00DA633100F4A63C62901", "miner": 0, "time": 18000, "proof": 0, "signature": "AEFB5604981AA60CDB8F7A0472A0F4CBF21B957A403A5F34B8AD1292363D697A7475D9D6574E996F0B71C37B95C93F8D"}, "hash": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615"}, {"transactions": [{"hash": "0C7486E9CEB3030F18DBD9945929CF92BE3E60C8A31C997FCDEB80898A40E082", "holders": [0]}], "elements": [], "aggregate": "8A9141DA89C315F921F241ECC9346B541BC619436CBD0775C8C900A7B65AA39EA20BA65673DD27FBD1D3012650964D4F", "header": {"version": 0, "last": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615", "contents": "5C93322261C6FF291512BEC2F9A544BC9B8CB0169A4B1A3D2B62C4995D12072C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "519724F8B3CFEA672C1B1D683FB72975563CD47F113791B8576BE16BA4B524B5", "miner": 0, "time": 19200, "proof": 0, "signature": "991C269C809FBF0ED4AB45D9758E376C71D2BFFE6D24B625CBEA88FB68A443A8E62A4A27E579827D6CB882E8D419DCF6"}, "hash": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40"}, {"transactions": [{"hash": "387FC354C6A3D12E40466B080A0CD2C3C97EF9913D892919E08B17CDEB6CFFFA", "holders": [0]}], "elements": [], "aggregate": "94516A29CB0FC365B65879E5C9F34B101C2B2BEBB228EAA2FCE79058A24C93D56C674931184F56C0FF14D5068D76B0DD", "header": {"version": 0, "last": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40", "contents": "DB352DFE3A5DDE7F6E578088377C2D94B39E231A2EA7FF8DBD88FD6DBC9CA8D7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "089B9D00365E3EAD207BEF01F083490CF1F3B1BF195CB7B1DDF5EC3E263F239A", "miner": 0, "time": 20400, "proof": 0, "signature": "8042C2B741D709FD403A830BA298FD2DF611C2CD085BE63E5E0B924D162E182A4AF287069750297CE45F3B7DBEB1E90D"}, "hash": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525"}, {"transactions": [{"hash": "5C3F6AF8E5D45304AD7F387FADF9E3AF131C56E653D65DA0510DCB4088610CAE", "holders": [0]}], "elements": [], "aggregate": "8CB2DE75C0EB4213BA9E6C68CBB839838DDCCB2F845CEEFF505F9C00704D256FE774339C1712B5106BD27D4E2E2E3B59", "header": {"version": 0, "last": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525", "contents": "DB4F7545C4235D98A55E018A9C607A0F3006E51F0ACFB50FFD0954C5BC509A05", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "5F86DF5E696115C6B5F5B3DD88A95F0E1FBEF991C3508B2C2FF8444E58F68E22", "miner": 0, "time": 21600, "proof": 0, "signature": "AEED882FAB73397288E5CE76B611AC6DD589CE7863500ED1D95C6D4CF3EF0528BDE1CDB729D6803CDB33DE57EFD08C37"}, "hash": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C"}, {"transactions": [{"hash": "D1FC6A19E245897AB2F37E33DDA544C5C702D75CAE323C0B9576E0AA49204C17", "holders": [0]}], "elements": [], "aggregate": "B8E70BA7DD0EB28B36A147309B71F29E290D95EFC8F1A02120EDCA434ADB29AF85083E5C9994B7D79A78AF36124D2D03", "header": {"version": 0, "last": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C", "contents": "6AC0A02AD6E722A8B68E9535D876F7DE9DD666EFA0C3F7965145CB588393563F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EA827E1705E6B10688301DB3BB0AFDD25E09313A96EE093616B2D2C4AD5A649C", "miner": 0, "time": 22800, "proof": 0, "signature": "A7E7BB684B26DAB5619093EACFB7D01D145C75DD4C9C2687F6E57C40D50BFCAB2910521BF09EBC56D5AF70729B940E46"}, "hash": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9"}, {"transactions": [{"hash": "4663CF896951DA74019FBB40ACA05D193FA36947DD00C6381A174EB7ED42459F", "holders": [0]}], "elements": [], "aggregate": "85FE98831A681C0052B0239CD186BA4197C0CF8680C46F3719DDB9A7F2DB016F791260DBFEA99AD55A15D8EBDE562C85", "header": {"version": 0, "last": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9", "contents": "D47F54489B54352B703153B00552D3D2BFBACF08977756F475119087F9C211A7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AEEA9B2AB377ACF84D2D25C1CC9A1E611CA13ECB337F8A8C3892C32905275F04", "miner": 0, "time": 24000, "proof": 0, "signature": "895C44B71029343EF6D92D320ED563F3F2E96F95742EFA14A794BB3E8A036C8EDEB6027F52B97C24EC68AE8D89E3B141"}, "hash": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361"}, {"transactions": [{"hash": "2CD1A225E20AAACE9A34B0CFC2F07C9FCF80B9D4A47812CEFCB23012962BFF5D", "holders": [0]}], "elements": [], "aggregate": "AD6E8902BF0C69248E65C34683E015831102B7753E661ED26364F67B7E53C756DCCBD683C1442BD4D9664520385739F8", "header": {"version": 0, "last": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361", "contents": "DD85BC873116FF0253FDE52723C7F0F55EBDEE71E5990CABA412440206CC7662", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1AC6CE760921B1B70AA1575CC7A625E0FFE8073D72711E8B976322275968A9CF", "miner": 0, "time": 25200, "proof": 0, "signature": "B1DBCCBBFDB8A9F41176A91AC7FB3105FDF05246A757B7C018F0A12EDE54896F1CEA1378CD83493D31BA1FB367EFBF2C"}, "hash": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25"}, {"transactions": [{"hash": "1914829F4A76D1ED019BF9D2E3198BE0B6A1B55E0170D4EEF8D3772937799B70", "holders": [0]}], "elements": [], "aggregate": "A579D7772E589647623C357F1FA8CB2AADAA8A348389B4F7345507F29D3DD5D9D6B74D52D8504E0AA802203315B57C65", "header": {"version": 0, "last": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25", "contents": "A1E5B19F192EA5BA11FB02EFC47C19731C4A61996A0DD8141E9250AFC702F42B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E895BBBAFA1388A73917ABA96B3D276468F54CAFA23D4E1C92604C780E4FA72E", "miner": 0, "time": 26400, "proof": 0, "signature": "B541F894A8B0F540F5FDF447747BEDCF5473026EFD38775CF022A6E6E71B5621807DF1C64520F1A74261CAD9F72B0404"}, "hash": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B"}, {"transactions": [{"hash": "886F3192EF28DB8E9476A85AC3E2BA505743198602E89F72339C96A6BC6CAEC0", "holders": [0]}], "elements": [], "aggregate": "AFB23913A0589BAD19804AB1984CF2AE40067483571B598043F188482F94E4218594F963632B5401CE9165C74A564A9F", "header": {"version": 0, "last": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B", "contents": "24E54640F6DC403551054EDA610E734D6A00332BD3847DC7A7DE17EA61C58718", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E52203148F778F29C26C4A5EED60316866F0C36F6735291D9E38DB897F83109B", "miner": 0, "time": 27600, "proof": 0, "signature": "B8A51E0BA8649D6EFF075F70E76C2879FF57F944ED6A05C424AB6861C57B7489CAB77DA3FD5FE640FBA9A8AE3D1C9E69"}, "hash": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B"}, {"transactions": [{"hash": "AA383287CBD1280772D520162020F4B89A44683FE12BFCBFB2203EA7A25D7D17", "holders": [0]}], "elements": [], "aggregate": "83275ECE29DA906B17730B1D83E30182D7A54B58FAEEE123F4C7C49F863C1A5AD0C46F5D61A80198B06054EE647AAEF1", "header": {"version": 0, "last": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B", "contents": "1D4FCE21C0FE6A24C386D40C92D26F0E8B4AB1CA1D52E281F552B6E8B31E6803", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "ADEEF2E532DFBF8B41384A61173FD3FF6BE2542080B106FA57CE6C546E4E176B", "miner": 0, "time": 28800, "proof": 0, "signature": "925B65382C0A8B26393EBE08F8B72E4BBD45E275CFC749A163858A582D9DD474EA60A527671D1280D7B76B4B9232BFF5"}, "hash": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF"}, {"transactions": [{"hash": "C9A65D759E9D37FF276B2733376F82140763F6FE90D2E2C6040A0BB62EF51152", "holders": [0]}], "elements": [], "aggregate": "AD3406EACE13826010E20C65D38AD688FCC28B24B3A7B78B9459AFDE89E5E831EE789B971B5175B0ED83E4C70AA42710", "header": {"version": 0, "last": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF", "contents": "B69147F0EEF64CEE66828986C8C9F06364FFEBC61C56680FF1659CCAF985CA9D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8C9811D818346EE6C40848C9F8B15D1E61227639EA2F0F1DD896AA3DA2C44441", "miner": 0, "time": 30000, "proof": 0, "signature": "A2AD4BE21A833707E3F42DF55E4300089C4FADCA6010008501BB4133F90793653B6C7E8D45DE7A320428A75F69DFD980"}, "hash": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583"}, {"transactions": [{"hash": "7ECE5339A3ADF8C9A93E348BE975C2161BB2CA99B0A95D5E7367E94534E1EF89", "holders": [0]}], "elements": [], "aggregate": "89E22645E0F5509B5AB620C71D146E3863209BB1FEAA1A98A2AA44685A788AC81B72F0791CFEB627E1BB33EB6767F198", "header": {"version": 0, "last": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583", "contents": "B684AEBF86A1F120E5A44A8C14A159973288246CCC77B44F7E554A5D0D8D67AB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "BE619866422407EDECCCF01D40CFEA4CD97B108AD4AC8A859565954F80355E4B", "miner": 0, "time": 31200, "proof": 0, "signature": "83693BA208148B3BBB92BA670BD6320EB343B3998064F3C65D1D3FFFDF34625B0B63875C37808597BD1B151B35CE64BB"}, "hash": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB"}, {"transactions": [{"hash": "7B0498874B886C771502AE3A40F848F706C8B412517C33DFCDE0A3EE24896C3D", "holders": [0]}], "elements": [], "aggregate": "9777F1ECC189743CF8495F1593A4B9ECBA8E24F0E377F238AFF267B00451FA24A2F55BA867A5A3C5D2A7724C500A5CA4", "header": {"version": 0, "last": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB", "contents": "380410891EE3A5845B0438A8C2281B92E902D94B4FE4F7A9F383849718D21168", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FCFD4349689F789D33570A5B0A42CA821F6FF8F320D1A700ABD3B538FEB25466", "miner": 0, "time": 32400, "proof": 0, "signature": "8A858B58B9C089077FE125A55B2EE89F9629E460FF628545677881F14AB623AB673D8E50A70924EE2359307DE4462A6E"}, "hash": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C"}, {"transactions": [{"hash": "CA1CE7CDF5B2F3128DFC1050B240E48E13D313D951FD20BD60F1C89C71227EEB", "holders": [0]}], "elements": [], "aggregate": "9789EFEAEAD52C813157D9E7ED237FA6217BFD92C98C6AF10E75040A090CD184B97F0BA67E1B561971DA0C3128D3DADE", "header": {"version": 0, "last": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C", "contents": "628BD47A0591ADBDC780A7ECC8C86977DB29493E072761E00C64B51AC92E0732", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B87CF0B569E2F08B381753FA8F6359AD314319056E14BD28CB129D457949A14", "miner": 0, "time": 33600, "proof": 0, "signature": "AC71514D519AE5B9CA1E5B9C90940FBFAB7AF802B4928CB661040D82B5DA6CBDC96515140EC07297EE7292F4F40091EF"}, "hash": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A"}, {"transactions": [{"hash": "17F10521957248191BFEDAF36E6543FC8FFE61D10644E037E93F3A555443A9FB", "holders": [0]}], "elements": [], "aggregate": "B97955DE18257B2B74B54F2F56C4E0F34A11AE75F9AF7F1E9BD8EC66017F15962264DC654D23C69BE97931976A4C35B2", "header": {"version": 0, "last": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A", "contents": "1375723AD673ED9974F0B5CAE015F691581759434D076DA469E2AACADFF37F1A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "17B2C89050F788BFD1134009C1A396C2EDC2F8919F11497A7F6F9BB9FC88B780", "miner": 0, "time": 34800, "proof": 0, "signature": "B711DDFDF6717189817C83AF1D760045F6BD9A928EF1F3C8B514766781138741C6301B5FC430BC7832A7AF4530CA3A97"}, "hash": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42"}, {"transactions": [{"hash": "503D0FE35CFDC7F3A15DFCBEFB1AE55495EB1FEC723591E023B6C854D55E494B", "holders": [0]}], "elements": [], "aggregate": "A08D9E3C46934EAF57AB1D31EDFCCD0C031A9D8E35638631E76963CD8420B1B44F60FFCEEE88EFFAE22EAD3D435FFC4B", "header": {"version": 0, "last": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42", "contents": "BDCD84DC8A3C6E41FB8D432F2F69E2565BDB20CDEA60048355C0572F19FE2C69", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EDAE53D97A0A81B7D39FD161435E0BE196D5DC8E860DA6F1A6D3E9DA87956CE9", "miner": 0, "time": 36000, "proof": 0, "signature": "B912FBA94F16BB9AF34DE53A7A65532E3AD5841862EE34C85172F85C3ED55BF0F8C136BF4114298B86E19A5BA7105910"}, "hash": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096"}, {"transactions": [{"hash": "CF82B3A47ACB0B38E9FA9A8A8FD1C89E3D0023F016A2F207D629D6FB3AC0FC2B", "holders": [0]}], "elements": [], "aggregate": "93996B99767DEBFB239E3FA3ECA1A901D1D07401ED3BB751878D8C9127ED58EE981A0A2E9A25BFE577E03707089ED000", "header": {"version": 0, "last": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096", "contents": "6B58318212B14ACDD98D0EE097D5614FD5CD21AF968A0AB538D6AC3A00AA3C67", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F98042571452CA386BD6BDF2638A6BD9ABE56627AFEFB49DBA2A41FCD9474745", "miner": 0, "time": 37200, "proof": 0, "signature": "ADE7BBD187AD8E9625230E92F813E5BFD19FF8FE000FF96E29129CF7498C6066A4E992EA82D21979D20CFA6B8A4A0F90"}, "hash": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F"}, {"transactions": [{"hash": "EFD9B2A90A15401AB3D1AAC9262E70C9B9AE448AC7560E96A7766449AE5446D2", "holders": [0]}], "elements": [], "aggregate": "93926EAD07F7A93C23990C6A41F23FA3288232F741C97D29182E0AFE90C3E6DFFF1E54EEF666F78FDE513945407E285B", "header": {"version": 0, "last": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F", "contents": "37CD8023C91A9ADF1512D69A23B77A418478C6F6EF66D966182C1E5C95F092AC", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7D2362B3488EC1A041755C7A14B20463502292442C4E2FE96222B264E43985E3", "miner": 0, "time": 38400, "proof": 0, "signature": "A92DABCDB4078E26DA81109E38AD82397555543264028383E94DDE5DB9BD5079B8385329759F28D3D81A21E620E51DDF"}, "hash": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92"}, {"transactions": [{"hash": "768D69AFADCC7A29E23663E8A66EB8D7CC1563079A216EAA29B9783DD528DB8A", "holders": [0]}], "elements": [], "aggregate": "AF48FB26E57F2D619487811009F1E43A50E073A439DE7DBF899F9E158F8C6F0B6432B2754347C1CE6510E538C5D47291", "header": {"version": 0, "last": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92", "contents": "44571AF7F71017BC02764E5416B9E8CCFF1597E3480143808E1BF8E5F9B8023F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F0FD8CC4454CF3F1E0B3636CD046FABC7291F16D2D0A7054C8C3100E10D8BEB1", "miner": 0, "time": 39600, "proof": 0, "signature": "A84C6D840E538E70EA4AC4831853F3EE70A384DEEA885EBC0684248DD1905C7FD6465E37E6B187A10C1E084249BA1D17"}, "hash": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436"}, {"transactions": [{"hash": "5028DDEEEE12CC6FB156E713F8446325DBE6DAF87C4926EC9E12E8B028DE5433", "holders": [0]}], "elements": [], "aggregate": "B2C2CD38A31D99177B263A311BD14718B1012F75987B8A653A1C2C20FC239A86F7597F2BD9CF50784B8BF4559740F126", "header": {"version": 0, "last": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436", "contents": "A20B36DA35E654AC8C2DE99EE1915EE1ADF583F8F0EBEDBAF6DD4BF4EF2F5382", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "B5802C1E8CE29BD6FBCA50DCB4506A2563CC030A453D971594DBB27E72D8E6FE", "miner": 0, "time": 40800, "proof": 0, "signature": "AC4C3BE796F9B6978079119F47E4A2616787EA843A54015A85D4F357A1A62DDE1380105BC8C86389A892A8B934349D30"}, "hash": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E"}, {"transactions": [{"hash": "01FA1B676C326343DF4B03F3840DE9383EAF0EAA13898A278029EF821E459089", "holders": [0]}], "elements": [], "aggregate": "91B51AFF6A3F9FB9BADBDCCECCE5093F80F83067D51E9CE660368B89F0C6B200A6ECF0D2E38BB0B9D90ACE4E3FBC4F41", "header": {"version": 0, "last": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E", "contents": "4E18ECCBF34A734136250BB94C7EC609FD424D1443860FE53A082FF59A8B87F0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "239668D5D0544F01BE9E0C39363AF70E78335F388495EC1BAF779425A09E3B71", "miner": 0, "time": 42000, "proof": 0, "signature": "8EB2D10893C3DF1B5331E4ED390AA2C44AED0B48FA0DC8A1329C797ECD561A10A6D0548B77A03942D8356C43B392FB94"}, "hash": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65"}, {"transactions": [{"hash": "1D932D69B193AD9246D4B61F946270940083FA5ED35459F4ABF41DB8F1C888BB", "holders": [0]}], "elements": [], "aggregate": "B2D9926E7F67112737ABB78DC0526F3F3FD02005AA984C9C33F9B53FEE2BBB70E6370570C511A0966C21AEAB46592D35", "header": {"version": 0, "last": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65", "contents": "0F6821A9C7D8A2005B6BA5FAE7B2A60EE0809E53AEA2540A269B4EAA01EBB12E", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "655CDDB0EAB8FCC0A36AFB7D970980E30CD65830CB7DD3C4A43D6F19A8669D05", "miner": 0, "time": 43200, "proof": 0, "signature": "8EC7D88E37A2490D7954DB86D6A7A67E887E0D6D5186CACCB3F189E4520CCA2623F72C234C1D0162C48709EDDE583CE2"}, "hash": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B"}, {"transactions": [{"hash": "52A09B2A03602D5536DEA58861D24E61D50F9756D9F402D9BC80D092194C8914", "holders": [0]}], "elements": [], "aggregate": "980FA53F64DF42DE83F30EEF35F1ECFABDD1E650C96BC7E9BCC485C1A0190F48D08C2FD98D8C2D8A13FA3CB9CE598D5E", "header": {"version": 0, "last": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B", "contents": "63DF40FF9CBAD4F329AF28C1E9D32D6AD817AB330962FC2A22A0E0747B5E70BA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C05C3FCAC02C04C52FF67D092421BDCB1087040F479181B4CABA1D7CC971E77E", "miner": 0, "time": 44400, "proof": 0, "signature": "98F92E503B9FDBDF51AC6022913D5ADB6EEE9A46475E19F6EA3D14B89AA7E3A4E0410646360C852B86E94A6BD94DDA59"}, "hash": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785"}, {"transactions": [{"hash": "715C4EB143D757F3D9578085A9A493B62A12E63979F981FF71D7D0E5797F963D", "holders": [0]}], "elements": [], "aggregate": "838886F6A7C95D91E0046704A5829BFE80953CA62CAFA25F4966B912C188B30FB29565C3DC959A371C27D1A67D802411", "header": {"version": 0, "last": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785", "contents": "76CB3B34A2ABBE67A018D9329412A4A76D67838D06308E4029B3A8FAA78F6692", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "75E79993E688F0BABEBA4E8AF7EAB2D23BA0B20E35B53C4352FF25464FBC2305", "miner": 0, "time": 45600, "proof": 0, "signature": "82E559E958A3630F5B240B0F82DD0D7C8B786ED0889DFEA7AFF0074DF3A2342E77D9400CE8BA83F1A8101EAE45F9C0BB"}, "hash": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3"}, {"transactions": [{"hash": "76CF7E02283072268B70821E34C60372B84B3CFBE8FB315B3540CD7FA48FF2D9", "holders": [0]}], "elements": [], "aggregate": "A5C043E438254B6D82445D3D5858F7E37873B1178A2FE8E0B7ABCF5903EAA1F7955711A64F16C4F9B2A5D5AB7AA91DFB", "header": {"version": 0, "last": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3", "contents": "05ECD46C6FFDB6CE064B1A7BDE217199694C779906269E29B686AAC2ECD4AFBB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EB0E425AFDDEC9A6511202015E98197A4B5FDC8F8E0EA83B5F6E9F091486F308", "miner": 0, "time": 46800, "proof": 0, "signature": "8148B9722127E142AF9014C182B3924EC4D2187B6B56A50D9B679C856864DE5A210F372B410DFCAE53D237F7F5B3B50E"}, "hash": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34"}, {"transactions": [{"hash": "9B8A47FC44420DB73CF46F0168B53CF93D495CBBAA9B36A9913298B8648574FD", "holders": [0]}], "elements": [], "aggregate": "84A52DB751EEB72E1D9F9DA54509134F7F5F54C409C77343E78925583E5FD1209E66DDDEAACDD60ADFE3E133C266307F", "header": {"version": 0, "last": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34", "contents": "0843B460D5841D73032DCCA971D096930EABEB343420D5CE790A0D79BC4577F1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7228382536A5A5B26CD248EA461978371776D786A4574023AED45FA7EC07CFBE", "miner": 0, "time": 48000, "proof": 0, "signature": "925EACECFEC61C713E1B5E07DD7B21A5D9BD75177E6AD97ADB5D93FEAF358975D334E738609C390220EEA72B514EF93C"}, "hash": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF"}, {"transactions": [{"hash": "A8B87D0CE244374CCC914F0086D2A5ED6C7D7947F76AF07518BC17DADFE61B99", "holders": [0]}], "elements": [], "aggregate": "9197DFD18F76C8574169D64E947A07C61AF53BF4334972DFFDC81765D7B0B80E11A7421755BABF67CC78B51F645F20C7", "header": {"version": 0, "last": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF", "contents": "F33F7C3B8383B04FB3458B3F09135AFB25821E0988D835E9E02065EA10613DFF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1B4CBCDBD72F25DFDEEE604A1402555B911270C58AD3EEE05B5339FB47709AB0", "miner": 0, "time": 49200, "proof": 0, "signature": "978D37E5B2262D811FFA5D2551C4F6812E448E8652942414981F161EC81B30C97C55F55782088F0798FCFFE6718A67E2"}, "hash": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553"}, {"transactions": [{"hash": "0DEC8740B4E9BC79F54827B7F179BB804866B62DD8FB89D5A3AAE91127179B0C", "holders": [0]}], "elements": [], "aggregate": "A1927B616F7FCBD04954B26122881383B90A18153EA39FF2B85C5006243E8F1EABE29BD9640044B2B157FE130D5710FD", "header": {"version": 0, "last": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553", "contents": "FD65AB96924037AE17DE36A779438D3E0F1D5B6AB86EB13294B4B74B0B3EBCFA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "81F9805AC3B6E2A7E2334F535FA802F144B11C2FCF36D7225354E6889B4B8362", "miner": 0, "time": 50400, "proof": 0, "signature": "A15685AFE8A8ABF35FE4E8DBC290284AB2941066A73AA8BE763E5A489F1FCF9EC6B425F892720726A2A3B7514A93FDAF"}, "hash": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D"}, {"transactions": [{"hash": "93851C8F4DD5348A50EB02FFA01D6948CB6E16819C0D7D13EB144281C17A0751", "holders": [0]}], "elements": [], "aggregate": "840A00EC75C90EAB1F50EA4718C87F189E21551BDA4C507058EBD3BAFFD916051688EC9E8DFD92A3F240BA27B474ADCE", "header": {"version": 0, "last": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D", "contents": "6EC6E7C25E62DD005D6FB65250FAB452453D9D9AD4CA93121C1275FFA8E4B207", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "79929DBA45BB3E810CB2847915609D2D9E3CEB998F2B09F6EB0B5E90C9D497F9", "miner": 0, "time": 51600, "proof": 0, "signature": "808F073633DF55ADBCD93035466E9254300AF1518E923477B6FC5D483CC8A2C75D87E6FF24CEE061FED050BE3CCE244E"}, "hash": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D"}, {"transactions": [{"hash": "0B85FF7C665D0229B14C2FCEB168D27E30A0E955B6BF5176EA9AE549042A2011", "holders": [0]}], "elements": [], "aggregate": "A03EED165302DE3DCE12645AF9FF5D2618B23454AD8D2429CFC582C5F9E8EDB6CB63B60DD0F0FC5A2AB1F44C7CAF4340", "header": {"version": 0, "last": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D", "contents": "D81C83FD51D107CD3F0020796868A59557628C9B6D62693ADCB6E3A4328159FD", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8415D8191D3B91B5B7ED68975872BE5EC17F030C2CA2EB056F84294E0382FA6A", "miner": 0, "time": 52800, "proof": 0, "signature": "ABAEBE124340D09ED8D202418FC060174C432D383F36FC5D25F3C0E81DD694CF74DA28AF1458A55693E3FFCD916684A1"}, "hash": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3"}, {"transactions": [{"hash": "F4001DCD38B0E23768108C1F0CEC8FAA1777AE0FD4EEB38E6DD3E0CDC9735696", "holders": [0]}], "elements": [], "aggregate": "A1DED17B59BFE2F34FB6011D9FAA2F60B5D557895BCB1B2D927B021E21EB22F7DEB115A78B350D486014ADDFF69EF391", "header": {"version": 0, "last": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3", "contents": "BBF99503895A2E37FE5599ABDB7C1C1672D003666B29E9F28BDEC380E35EF040", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B7E79ABB5483F92115182B9B5CDC30709D8192DE966E8B831E4A6314BD3D5D8", "miner": 0, "time": 54000, "proof": 0, "signature": "8DE8C11F66CDD2B75998A8936778DC624C382ACBEBFDFE322BEC0904F730F033AFE70748AE98F49308C3CE3D3E2981A7"}, "hash": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8"}, {"transactions": [{"hash": "602BA0D211013BDE8EB9619F8431EBDAE3BD4B5FD09F4266B19432A6AC6DDBC8", "holders": [0]}], "elements": [], "aggregate": "A3737B6AD6E12FB918672E22099A7C113A9590787723EBD5D387B7871EB60D424A8450B271DFFAAEEAF2C375E4D403FB", "header": {"version": 0, "last": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8", "contents": "478094914295E4311699033104C5BAB93F471B4EA137209F7DC3B10E97678068", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8D97A09A1AD26F8A100AB558630D930F60A42500F5AB309609ADC50B7F9EF07C", "miner": 0, "time": 55200, "proof": 0, "signature": "8C36F0B44D5120FBB910D3CE312F9BA135CF19ABD39D84EC5560642EF1FD4BAED46957CE94DBCC1B4FA7C2BC736CD0E0"}, "hash": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3"}, {"transactions": [{"hash": "2FD1583B1B31B0FE60632F556903257FC3734E39AEC237EDB5CCE784777A1A6B", "holders": [0]}], "elements": [], "aggregate": "B60B0A0084D20E0CCACCF0797884A1DBFB10EAA1D634584D0BE0299343285D137EF5CCE91DFB9B4D1D52791BD98FC40E", "header": {"version": 0, "last": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3", "contents": "6EF462AF646189E7E659FB67F082F67442B1ACB3D1439EF97B410C8D94975815", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8337618DDD6041028BE2E2F00A9D6592E0C80F6900F1DD5772856A59ED589736", "miner": 0, "time": 56400, "proof": 0, "signature": "A20D6B1BF11A779FCFC017DCCE8334DB9C7E5406FA8B718C8820BC5A07B3246ECA22D6455524D0F746872575AC19C979"}, "hash": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368"}, {"transactions": [{"hash": "E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397", "holders": [0]}], "elements": [], "aggregate": "84F89FB08A88F3F3197AEA45CBD2C095DBFAD15E1537E19A7159BBC0B285C5A7F5BC2CF164C60B8308FD5B0188B4E762", "header": {"version": 0, "last": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368", "contents": "73FDB8E7FEA18F47DB89ABEB7F7BF375B5F994459F7855C40605AB07BDB17337", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1DBF1565853B1EA6467129426BE65B3A541BBBEEDF24E258559CA64F95967643", "miner": 0, "time": 57600, "proof": 0, "signature": "B76E1B0BD4277C4C3C708A9E28208123813C22C48CEC5A6C591A3C6CF4A2081C5FE6D5212990DC266FBA264B69C90795"}, "hash": "A7EFEF96DA3E639FF8871B6EF3E2462619CC8BE367B782FAAD821EC8CA71F6C2"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "A7EFEF96DA3E639FF8871B6EF3E2462619CC8BE367B782FAAD821EC8CA71F6C2", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 58800, "proof": 0, "signature": "8E0BE28119B5788B77BD3E6B6E15D20DFC453C61FCDC090E9147513AC170229A796867BF3F4760E6C5A980879273ED0D"}, "hash": "91FCA78D6FDE7757B3ED2667644005A5A2FBDC7118C4CD4C0063234ADFCEF7C1"}, {"transactions": [{"hash": "CEFCD616B16E9029A78C42D62712667001A177D3DB31246EB6B08E720A3D2DA9", "holders": [0]}], "elements": [], "aggregate": "95FD07BC0CBCADFA1E692748E0F4612339BA38E1707724C734EAD3F39B64F2BA4AF645FAD10701C9980AE4A8AA50E03F", "header": {"version": 0, "last": "91FCA78D6FDE7757B3ED2667644005A5A2FBDC7118C4CD4C0063234ADFCEF7C1", "contents": "71F8E159F71F3BC3D0A52BA8709E086C9056D5A494DA40F5461F6B12224B27F8", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EC54EBD64AA4C844EC34855C30C91523973211F68353AD2343879A5D715DDFB6", "miner": 0, "time": 60000, "proof": 0, "signature": "974323A45C4C50F49142CE05689FE44BAA706F5B3DC69341AD521F017E8EAC486A0A886E6BBBDC15FFA3EB578D2BCB13"}, "hash": "D530AFEA3586B387B97A8BA7CB05307AB7D46E62F53818C706D4A480FADEC6F7"}, {"transactions": [{"hash": "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E", "holders": [0]}], "elements": [], "aggregate": "A7DFD2DF5A8801A3D8B41C2307D12B9633332489454462D660D7A2C9C95F975470CA6BEB78C04877B09719D60BB5DE7B", "header": {"version": 0, "last": "D530AFEA3586B387B97A8BA7CB05307AB7D46E62F53818C706D4A480FADEC6F7", "contents": "75D9260A1F3CC7B43B9F7E41F330E429DF51245656184071718A75589DBF0DCF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "3E4D5D8F549C400496376EABCED5F00780EDECFAE33EF984563F5D10BB1232DB", "miner": 0, "time": 61200, "proof": 0, "signature": "950FF683F22F70C198D3048739387B22CCD2A4D85F818A839D3995FC33D1D702FE7BDF1C5312712149A267F15E8C0B20"}, "hash": "09C651D37FA6DF17B3F8B7A67E262645402E5029BC98D66A34EB0E9BBABC2AC2"}, {"transactions": [{"hash": "FF0EA66ED3545AB2B787D487105A1152A4E26CE30562EE94E5035DF806C9B88B", "holders": [0]}], "elements": [], "aggregate": "9033031FE9BD07FAC1D0C778BC32FE5DF92868CF3C674C62EF2184D298CD7F9E4A06B40F097A20532FE6B13D91C1E1C1", "header": {"version": 0, "last": "09C651D37FA6DF17B3F8B7A67E262645402E5029BC98D66A34EB0E9BBABC2AC2", "contents": "3096993C7D6DD73FE5C797F07354AC22241B7510DE96E414A99178099BC1BA5B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "60EA763D4E7E3522E8EF12A04BCD4993EAA7C7C07A628AB7E9EBFBA3E2C0FCE3", "miner": 0, "time": 62400, "proof": 0, "signature": "B9905E10A3B324775747B8292A43BF9C4176F9423BDAA09E5AF6EB07DA066CF85822D60F19308812977B8BA754FCCE65"}, "hash": "C2540B9EAFC060CC61E77DF4904F2FAB60AD336330D55778A23B0E2284A9B93E"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "C2540B9EAFC060CC61E77DF4904F2FAB60AD336330D55778A23B0E2284A9B93E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 63600, "proof": 0, "signature": "A3B74E149984B499B2138CAE02BA70179D755CD83085AF6C826A2B81EC7CAF60926613D137446CC322B33B334993FB31"}, "hash": "49BFDF2E66C8C2EFADDD6943DB684A34B6850FAE0D5BDF133C159AF45E7A9DD2"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "49BFDF2E66C8C2EFADDD6943DB684A34B6850FAE0D5BDF133C159AF45E7A9DD2", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 64800, "proof": 0, "signature": "AB3D0A5DF892591F432405A5ED784106D44EE54849B2A39EE1A01BE41A3113A701B5EA53FF78E2969EACE70D7853326A"}, "hash": "9947C730BD57ED6819424A50BC6BCE24019DD7938180685BA5807CEFD37C2EB4"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "9947C730BD57ED6819424A50BC6BCE24019DD7938180685BA5807CEFD37C2EB4", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 66000, "proof": 0, "signature": "86A5BC637D8C82BC39874CAB809A31F57DE80BB10140772E1CBBB74C9AE2CD1A2C563654F600669DAF868157175507EF"}, "hash": "ADA9CCFC5071B1B2AFB0471931260819B1C01A2AD39B6CA1BBCC9C2917DF37CC"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "ADA9CCFC5071B1B2AFB0471931260819B1C01A2AD39B6CA1BBCC9C2917DF37CC", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 67200, "proof": 0, "signature": "AD9DB6ED9E94A3A22F91026CC6DB12BF34055863C7D125363C1DC9599C62497796368CD585C1D6CE871EC47BD5411A9D"}, "hash": "2E24D33662D1660EB17AEE8312444A1F857A8B0E38E6B95A11C2E6C6EA7379DB"}, {"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "2E24D33662D1660EB17AEE8312444A1F857A8B0E38E6B95A11C2E6C6EA7379DB", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": 0, "time": 68400, "proof": 0, "signature": "90B6AB1AB9A7BCC7E5AE23EFD5339C4B40E669C6963470DB7394988371F36CCFE4C98CD06DA806F80CC27F5192EB3F4D"}, "hash": "7E023404D04D01A093428F5CEBAFFF821AE02A6D55791FF7E987B8147F4DFDB0"}], "transactions": {"E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397": {"descendant": "Claim", "inputs": [{"hash": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "822D22AF5230383E62FDB4F36D71AED3FA31F5BB821DA4EE176FD6051FA211E187247ADAE7CFA8155B9407D7EF49B3FD", "hash": "E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397"}, "CEFCD616B16E9029A78C42D62712667001A177D3DB31246EB6B08E720A3D2DA9": {"descendant": "Claim", "inputs": [{"hash": "91FCA78D6FDE7757B3ED2667644005A5A2FBDC7118C4CD4C0063234ADFCEF7C1", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "998BF8E6F832BB0B96B379B8D28D703558D52C434C9FDEA3B8B810C9D2C92A5669BD75F271F7C10E3ECB83694287AF04", "hash": "CEFCD616B16E9029A78C42D62712667001A177D3DB31246EB6B08E720A3D2DA9"}, "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E": {"descendant": "Send", "inputs": [{"hash": "E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397", "nonce": 0}], "outputs": [{"key": "8A88E3DD7409F195FD52DB2D3CBA5D72CA6709BF1D94121BF3748801B40F6F5C", "amount": "1"}, {"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "49999"}], "hash": "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E", "signature": "622A68DDB594EB003FA847FC6F7562B26AA2325B0496C8E292CC34A275226BD65946C1683F4FCBAA692B7B50AFE2AF62A1EE66FF36D7291BCCBCCE596BB2A405", "proof": 3}, "FF0EA66ED3545AB2B787D487105A1152A4E26CE30562EE94E5035DF806C9B88B": {"descendant": "Send", "inputs": [{"hash": "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E", "nonce": 0}], "outputs": [{"key": "8139770EA87D175F56A35466C34C7ECCCB8D8A91B4EE37A25DF60F5B8FC9B394", "amount": "1"}], "hash": "FF0EA66ED3545AB2B787D487105A1152A4E26CE30562EE94E5035DF806C9B88B", "signature": "D95A6D35780FB1B05D3A3B3F567B1126820B494374A1885565097DC0E3BF521ACEE375BE8CF0CAC0944C31E275AD8B4361A0E2677B5498864AF0A60C19C1A30B", "proof": 5}}, "send": {"descendant": "Send", "inputs": [{"hash": "E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397", "nonce": 0}], "outputs": [{"key": "8A88E3DD7409F195FD52DB2D3CBA5D72CA6709BF1D94121BF3748801B40F6F5C", "amount": "1"}, {"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "49999"}], "hash": "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E", "signature": "622A68DDB594EB003FA847FC6F7562B26AA2325B0496C8E292CC34A275226BD65946C1683F4FCBAA692B7B50AFE2AF62A1EE66FF36D7291BCCBCCE596BB2A405", "proof": 3}, "spendingSend": {"descendant": "Send", "inputs": [{"hash": "28251FDC9B5E643B8B930DC5147B2B64F84D3304996E944C8003909F79B8A09E", "nonce": 0}], "outputs": [{"key": "8139770EA87D175F56A35466C34C7ECCCB8D8A91B4EE37A25DF60F5B8FC9B394", "amount": "1"}], "hash": "FF0EA66ED3545AB2B787D487105A1152A4E26CE30562EE94E5035DF806C9B88B", "signature": "D95A6D35780FB1B05D3A3B3F567B1126820B494374A1885565097DC0E3BF521ACEE375BE8CF0CAC0944C31E275AD8B4361A0E2677B5498864AF0A60C19C1A30B", "proof": 5}, "newerMintClaim": {"descendant": "Claim", "inputs": [{"hash": "91FCA78D6FDE7757B3ED2667644005A5A2FBDC7118C4CD4C0063234ADFCEF7C1", "nonce": 0}], "outputs": [{"key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29", "amount": "50000"}], "signature": "998BF8E6F832BB0B96B379B8D28D703558D52C434C9FDEA3B8B810C9D2C92A5669BD75F271F7C10E3ECB83694287AF04", "hash": "CEFCD616B16E9029A78C42D62712667001A177D3DB31246EB6B08E720A3D2DA9"}, "blocksWithoutNewerMint": [{"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "360E204A3E870E34FC69B65536A6C8354966F95950D4C55A5C10FC276149FB2E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "B73B38B8D005DF431DB1CBF3462348FB8DCD213B3980A6C0E26D7B95A79F4E88324832A5ECB684436852ADCC8511C0BC0306EEB6D9E5E581AD279B633C0291157EB8ADC56D24750C95C8B676BA66C1FF5AAC4FA60EB5C8EEF0768067426602EC", "time": 1200, "proof": 162, "signature": "82021D8D5B5EF89C40548BF31C2890177B7CE874E306A62A0C1E6FD46F1583BD5E9E18299EC5D64A2D90E0B2CEC47B44"}, "hash": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700"}, {"transactions": [{"hash": "72EFE1EEF8D6372BBBA749E7CA910F210A2BC80C46A688D966F84C36AAFA3732", "holders": [0]}], "elements": [], "aggregate": "8B04E9F4BD7CC9A83D324ADCAC4FB2246A2A1A7AB87885A7F6B0A7A119CAB298D24ECE2A47239AB296E0A0505E2C478D", "header": {"version": 0, "last": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700", "contents": "C9521F92299A0B088B8141B012B42100E5FE6E72189F58DE714C22B1E910358B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7953C6E651693D49AEDDBB5FCDE84E3264648828330B8E8261C2EEBEDE4ED175", "miner": 0, "time": 2400, "proof": 218, "signature": "8CA439D808746224FAEB94D9E2980DE1B92E0947FB401CC458530A5E2D9E227C69FABC83E050EA5981F2AAFBF5F3A209"}, "hash": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602"}, {"transactions": [{"hash": "6281D2C3868D973A5EF0505A8AC35DCEB4C02671CF32195CDE3399D064AD3573", "holders": [0]}], "elements": [], "aggregate": "A070246256DB4536DEF33503DD7E63D4EE6B528BD445F9F671DA43C09945B3196AE4E65BE683EEAEAE41A0A26B3AB197", "header": {"version": 0, "last": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602", "contents": "A8D4C85179A8C6D6A7538F1A182D12A22C3200F4F97C49E85CD3B2185D22F8DA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A9A9C38D542DFA0477E4A01DD1DCF9EC3390986E1F944C9A4B204412E133B288", "miner": 0, "time": 3600, "proof": 144, "signature": "96B09D483F134D88486DC1079ECEE623620A5186E54B6AD2802FAE542E2BA8F60A4DB8A5A95D6E853FAA0C52CC40B8E4"}, "hash": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02"}, {"transactions": [{"hash": "278C104FEB0EB60FDD554EA0157704706F5587B165DC6356749B4AB20781BC56", "holders": [0]}], "elements": [], "aggregate": "B6F88AB1861E8739DC9CFFFC5B91F8ED221E65C2E1CAE55C58B59A6D0F9B93123DE2C680F83FB629F87E49621E3385B0", "header": {"version": 0, "last": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02", "contents": "A809E61ABF343724E17003F6EDC3FC31604BFC8CAC89E733167D422CB355B429", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "791F73693CA57D52320A0227EC5359BD325CDDC300B9B94091F53A77233883C2", "miner": 0, "time": 4800, "proof": 30, "signature": "A2D8A2CAB1ECBBA004769A57E782F308E1308DF6D64D64C0D2A7122BF023A586CA7F3B2D60FE3CFD91B80621D6E42A0A"}, "hash": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700"}, {"transactions": [{"hash": "86E44D62E8D27DA57458A2DE80698A6EC88054C5ABA9083FBE54EE0141E93A93", "holders": [0]}], "elements": [], "aggregate": "8B129406C90F252DBA681AB51D48B0B4B3C99E681B477411DF137B7657103EF92C5FF48ACCD05B8E87807F1906D2BF96", "header": {"version": 0, "last": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700", "contents": "0DD37CA5DDBFC97B394561381460ECA3212039FD592CBD232795C865E7C5CB6B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DB055B18061A1ED67925CC442B187CD56837434CB813865E70FDCBCF4480E426", "miner": 0, "time": 6000, "proof": 21, "signature": "B9685F27C2A79D71FCF6C14CBA94CD55F3E615F1BE5E27F23FB4F2DCC65A047928FC5C87B0115AAD791E40667C241286"}, "hash": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900"}, {"transactions": [{"hash": "018412CB2C17E3ADFEF52A98DD73EF145A46AAA76AA35A887246A83ABBFBEB89", "holders": [0]}], "elements": [], "aggregate": "8A6B7756526BDF6304907773176182EC8873536431E9F1B6001CEE8E6FC217B3AC34DB6FA3E8F8B46924685A14E5EDC5", "header": {"version": 0, "last": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900", "contents": "E27C9370E5749F06BDEC855865682DFFF18767D1A5AFBAD3007A8FC62820F3E5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DE6F5EE882121224255A2D752B891B43FBEA7BF74D42E7CBF251C7C2F28DA123", "miner": 0, "time": 7200, "proof": 0, "signature": "AC47691383371FC7A5EBEA5961BD060CED69FB56BCBE68A83221FE23AAB8BD0C4290AD38736B551C33FD918D7C6BFB7D"}, "hash": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623"}, {"transactions": [{"hash": "19C4B0914783A2CE9846044FFE48AD35E1E173023A64D7BD340734184EEF04B4", "holders": [0]}], "elements": [], "aggregate": "81EA7597EE88E818FA43F480FE43CA24269AC9BA4B26CB6A93B0DADAF06B679EE76B3371E9F2335488960CE7FB85D17B", "header": {"version": 0, "last": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623", "contents": "7E6C29898A4764EA8A42B39D3DC51472834FE78A67778A379DB6345EC81D4CF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "729018FFA42AEF0122ADA8FEC04B4271CADB3E9175472A7B4C0447ED720B4BE2", "miner": 0, "time": 8400, "proof": 0, "signature": "9638880A8D68E4B770802307E4CB24BACE6AEC6CAE02F623AF423F2C53D027B7F31DC8EEE8E9BA910B600B0A9D48318D"}, "hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042"}, {"transactions": [{"hash": "A8963D7A7144FDE3F7B5B64C1B3E6D0B1B5DA983BE0486B2FC6A746585BEC0C7", "holders": [0]}], "elements": [], "aggregate": "AEB80694FFDCCFE47F38CF3C5AEEC3F9B435AF5B3C5CDE5931EA4D15BF5A5E617AA414CE2B3305F85BD493292E879BBD", "header": {"version": 0, "last": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "contents": "E1BC8CE92B23858A2FA0093D944DEE8A08C66FFF8B799FC94595D2C18B4F4F2F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "560BB2F0F92A8DAF15FE89394733AA845880957823FC081A64233DA40F54582D", "miner": 0, "time": 9600, "proof": 0, "signature": "95E3DD10E00C916B59E9FE95894AF3EB7C9889B1FA67B6D10D549C5AF7258EE5A9E23E928B62F54EB708059DDF09D9A8"}, "hash": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D"}, {"transactions": [{"hash": "C8727936A3F41AB82E5564D94C5AAAD205F0B349470240C10752370BC8AB5591", "holders": [0]}], "elements": [], "aggregate": "B8A43C71E3F1F66E9E2D918EFF60927E85CE2A13440F3E8CAB0509DAB8A955E37ED81A3DB02F3E81F4568085B0D1CEAE", "header": {"version": 0, "last": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D", "contents": "9A09BA0879AF73C8F6762D6E17B6D1CD71DF45E882D26492671D89ABB17B4C3A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "9185C45438F953385D49F221070A1FD8DA98C3A8838B6DBC5DFFB3DEE4CF2B12", "miner": 0, "time": 10800, "proof": 0, "signature": "B736988A8A41D7EE74434414B3B3FD8DAD2725D2F45E6F28D8EBD19534DA37683869BA6CCBFAA3E6E076C2FFB0712107"}, "hash": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D"}, {"transactions": [{"hash": "BBF99A9FA8B5E3EDA2A9A01E9EF0216CA25BF1944E3C7BFE07E8AB76D79BF6E8", "holders": [0]}], "elements": [], "aggregate": "B7E6334C31D2C6ABA93103690CE1B97C807241F595E3BE37C14E6404A9DA9F18041B045EDE05EA8724ECE3DBE9DC3018", "header": {"version": 0, "last": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D", "contents": "3EB66D63EC0762271C12F3BE3F9F17B99A27BEEE28DEE121B43CE38E52EE9410", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "2E4AB694CFF8FAD0742A3A24E821848DA2B8E2586A9090DC5B950A18C600D554", "miner": 0, "time": 12000, "proof": 0, "signature": "83A746E816FA1390BD30698A4811A6C1D6808A58C6FA97B4FE24D53D579F94AC6B90A149C2CB81612266C8452BB65742"}, "hash": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18"}, {"transactions": [{"hash": "60F3797E55CCE4C9EDAB148470A9127AAF368647F2757A9E52312239466C8D71", "holders": [0]}], "elements": [], "aggregate": "A9BDEAE67E27D4B4CD97E39EC93207A585675C29AB60BCA0F60A0624225BD1486B2326CF8C74D21569D5DE928F0340A5", "header": {"version": 0, "last": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18", "contents": "8775AF24BD4FA0DCCB52F15283F214FCF4C32A76F2E9A03C3485D471D9DFAAE6", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AF47884FAA2EA0351B28450560297631E457194F63DE3E2C531ACF628575D97D", "miner": 0, "time": 13200, "proof": 0, "signature": "A84775CC6E97C7A86AB3EC7F2F0F6CA8368569A5ACAB5697EB0035B5C6E5A1F58FCF7C9EC34178617D5815942084A99F"}, "hash": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78"}, {"transactions": [{"hash": "351A83B6BD0129252F7064B983F5C6CDFD7D854B941E911BE9874B727CD67B95", "holders": [0]}], "elements": [], "aggregate": "8391B75C4ACE226798B29B4404C035CCE5AB32CDBE61E794E9699AF6B715AD237F8252DBFBCFFB68EEF59F887AA03A5A", "header": {"version": 0, "last": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78", "contents": "B3EB7A99EA5421D49D6D00998EE360B366F3C48E5A1D5D13D80B013B1800FCB2", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "513271EF4B1EE9E718BDD93AC973B8F35BAF74011AA76898F266F3EBCF179621", "miner": 0, "time": 14400, "proof": 0, "signature": "90D95A9689683749BF754CAD5F25D7CD556292A552AA98CE163FB16291BDC19F11BB262C3E4B42455FFDB4EAD900E010"}, "hash": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10"}, {"transactions": [{"hash": "775A77F43C5C8613C6CA27C5A200822FE7F7865EC19F05B1BDE8676A25D3761E", "holders": [0]}], "elements": [], "aggregate": "85558989C1C0061C57F816F668D1856293F350376E7D2A8DC2C2A65BB793A4B0391CE885C19F318E13E38DD91C424F83", "header": {"version": 0, "last": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10", "contents": "9D9AB4830989D1F9C40A92C0E69ED5BD8EF610C530C0391C180E663581534D0D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FDEEA3491C7B261560E7D340180B11DB522865602670483A0D3591F4FCB19FE8", "miner": 0, "time": 15600, "proof": 0, "signature": "ACB2C40A0BF40BC25EF6930ECD4C4A5927EF5C252DC0E2E0DEE2F6B9784032F3B3936A05BEE3396057259B765ED77391"}, "hash": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A"}, {"transactions": [{"hash": "CD083ED5396FB5CC7DD6FDC217A91579255A97099E70D94591ABA9E4CF544A4B", "holders": [0]}], "elements": [], "aggregate": "A2719780937C21736440CE95C7792C5A689F1EC60488ABBE62450BEC33BA727B9E8D0C88323BC5E730CF38AD4105AB0D", "header": {"version": 0, "last": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A", "contents": "811FB083989D6585F335B0AFFDD0F166428B28603A780DD966B6CE1D54B2F6E1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A795C21BC025C44937FDEE5BDDE19DFE543B7997675D26A4C7BA034FED5419F4", "miner": 0, "time": 16800, "proof": 0, "signature": "97D2DC9E1B5454886C2C7A6AC13C06D0ADE94F33CF0B127407642A4019E9ADEBD0028BF7FF46A77AACC5736B0F2BA4E4"}, "hash": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD"}, {"transactions": [{"hash": "CA17D0EE821EA30021EA4637FE0E07F205710142CDE20F7DA9E5E8E6B40C2AEB", "holders": [0]}], "elements": [], "aggregate": "AA3FF79A19A97594CD2BEC4231E7EF8B445C3BED12AF224FA6F46F49234AF116046700B8C51AC62182C86C567C232AF8", "header": {"version": 0, "last": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD", "contents": "A031FAA34DB6E84F0B327B9ACFCDA17725CE6E0CA7C17B4F7111B89069D6E39B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "32E686539EB9152C9F37A1E1786C5493B08385491CF00DA633100F4A63C62901", "miner": 0, "time": 18000, "proof": 0, "signature": "AEFB5604981AA60CDB8F7A0472A0F4CBF21B957A403A5F34B8AD1292363D697A7475D9D6574E996F0B71C37B95C93F8D"}, "hash": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615"}, {"transactions": [{"hash": "0C7486E9CEB3030F18DBD9945929CF92BE3E60C8A31C997FCDEB80898A40E082", "holders": [0]}], "elements": [], "aggregate": "8A9141DA89C315F921F241ECC9346B541BC619436CBD0775C8C900A7B65AA39EA20BA65673DD27FBD1D3012650964D4F", "header": {"version": 0, "last": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615", "contents": "5C93322261C6FF291512BEC2F9A544BC9B8CB0169A4B1A3D2B62C4995D12072C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "519724F8B3CFEA672C1B1D683FB72975563CD47F113791B8576BE16BA4B524B5", "miner": 0, "time": 19200, "proof": 0, "signature": "991C269C809FBF0ED4AB45D9758E376C71D2BFFE6D24B625CBEA88FB68A443A8E62A4A27E579827D6CB882E8D419DCF6"}, "hash": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40"}, {"transactions": [{"hash": "387FC354C6A3D12E40466B080A0CD2C3C97EF9913D892919E08B17CDEB6CFFFA", "holders": [0]}], "elements": [], "aggregate": "94516A29CB0FC365B65879E5C9F34B101C2B2BEBB228EAA2FCE79058A24C93D56C674931184F56C0FF14D5068D76B0DD", "header": {"version": 0, "last": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40", "contents": "DB352DFE3A5DDE7F6E578088377C2D94B39E231A2EA7FF8DBD88FD6DBC9CA8D7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "089B9D00365E3EAD207BEF01F083490CF1F3B1BF195CB7B1DDF5EC3E263F239A", "miner": 0, "time": 20400, "proof": 0, "signature": "8042C2B741D709FD403A830BA298FD2DF611C2CD085BE63E5E0B924D162E182A4AF287069750297CE45F3B7DBEB1E90D"}, "hash": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525"}, {"transactions": [{"hash": "5C3F6AF8E5D45304AD7F387FADF9E3AF131C56E653D65DA0510DCB4088610CAE", "holders": [0]}], "elements": [], "aggregate": "8CB2DE75C0EB4213BA9E6C68CBB839838DDCCB2F845CEEFF505F9C00704D256FE774339C1712B5106BD27D4E2E2E3B59", "header": {"version": 0, "last": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525", "contents": "DB4F7545C4235D98A55E018A9C607A0F3006E51F0ACFB50FFD0954C5BC509A05", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "5F86DF5E696115C6B5F5B3DD88A95F0E1FBEF991C3508B2C2FF8444E58F68E22", "miner": 0, "time": 21600, "proof": 0, "signature": "AEED882FAB73397288E5CE76B611AC6DD589CE7863500ED1D95C6D4CF3EF0528BDE1CDB729D6803CDB33DE57EFD08C37"}, "hash": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C"}, {"transactions": [{"hash": "D1FC6A19E245897AB2F37E33DDA544C5C702D75CAE323C0B9576E0AA49204C17", "holders": [0]}], "elements": [], "aggregate": "B8E70BA7DD0EB28B36A147309B71F29E290D95EFC8F1A02120EDCA434ADB29AF85083E5C9994B7D79A78AF36124D2D03", "header": {"version": 0, "last": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C", "contents": "6AC0A02AD6E722A8B68E9535D876F7DE9DD666EFA0C3F7965145CB588393563F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EA827E1705E6B10688301DB3BB0AFDD25E09313A96EE093616B2D2C4AD5A649C", "miner": 0, "time": 22800, "proof": 0, "signature": "A7E7BB684B26DAB5619093EACFB7D01D145C75DD4C9C2687F6E57C40D50BFCAB2910521BF09EBC56D5AF70729B940E46"}, "hash": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9"}, {"transactions": [{"hash": "4663CF896951DA74019FBB40ACA05D193FA36947DD00C6381A174EB7ED42459F", "holders": [0]}], "elements": [], "aggregate": "85FE98831A681C0052B0239CD186BA4197C0CF8680C46F3719DDB9A7F2DB016F791260DBFEA99AD55A15D8EBDE562C85", "header": {"version": 0, "last": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9", "contents": "D47F54489B54352B703153B00552D3D2BFBACF08977756F475119087F9C211A7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AEEA9B2AB377ACF84D2D25C1CC9A1E611CA13ECB337F8A8C3892C32905275F04", "miner": 0, "time": 24000, "proof": 0, "signature": "895C44B71029343EF6D92D320ED563F3F2E96F95742EFA14A794BB3E8A036C8EDEB6027F52B97C24EC68AE8D89E3B141"}, "hash": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361"}, {"transactions": [{"hash": "2CD1A225E20AAACE9A34B0CFC2F07C9FCF80B9D4A47812CEFCB23012962BFF5D", "holders": [0]}], "elements": [], "aggregate": "AD6E8902BF0C69248E65C34683E015831102B7753E661ED26364F67B7E53C756DCCBD683C1442BD4D9664520385739F8", "header": {"version": 0, "last": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361", "contents": "DD85BC873116FF0253FDE52723C7F0F55EBDEE71E5990CABA412440206CC7662", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1AC6CE760921B1B70AA1575CC7A625E0FFE8073D72711E8B976322275968A9CF", "miner": 0, "time": 25200, "proof": 0, "signature": "B1DBCCBBFDB8A9F41176A91AC7FB3105FDF05246A757B7C018F0A12EDE54896F1CEA1378CD83493D31BA1FB367EFBF2C"}, "hash": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25"}, {"transactions": [{"hash": "1914829F4A76D1ED019BF9D2E3198BE0B6A1B55E0170D4EEF8D3772937799B70", "holders": [0]}], "elements": [], "aggregate": "A579D7772E589647623C357F1FA8CB2AADAA8A348389B4F7345507F29D3DD5D9D6B74D52D8504E0AA802203315B57C65", "header": {"version": 0, "last": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25", "contents": "A1E5B19F192EA5BA11FB02EFC47C19731C4A61996A0DD8141E9250AFC702F42B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E895BBBAFA1388A73917ABA96B3D276468F54CAFA23D4E1C92604C780E4FA72E", "miner": 0, "time": 26400, "proof": 0, "signature": "B541F894A8B0F540F5FDF447747BEDCF5473026EFD38775CF022A6E6E71B5621807DF1C64520F1A74261CAD9F72B0404"}, "hash": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B"}, {"transactions": [{"hash": "886F3192EF28DB8E9476A85AC3E2BA505743198602E89F72339C96A6BC6CAEC0", "holders": [0]}], "elements": [], "aggregate": "AFB23913A0589BAD19804AB1984CF2AE40067483571B598043F188482F94E4218594F963632B5401CE9165C74A564A9F", "header": {"version": 0, "last": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B", "contents": "24E54640F6DC403551054EDA610E734D6A00332BD3847DC7A7DE17EA61C58718", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E52203148F778F29C26C4A5EED60316866F0C36F6735291D9E38DB897F83109B", "miner": 0, "time": 27600, "proof": 0, "signature": "B8A51E0BA8649D6EFF075F70E76C2879FF57F944ED6A05C424AB6861C57B7489CAB77DA3FD5FE640FBA9A8AE3D1C9E69"}, "hash": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B"}, {"transactions": [{"hash": "AA383287CBD1280772D520162020F4B89A44683FE12BFCBFB2203EA7A25D7D17", "holders": [0]}], "elements": [], "aggregate": "83275ECE29DA906B17730B1D83E30182D7A54B58FAEEE123F4C7C49F863C1A5AD0C46F5D61A80198B06054EE647AAEF1", "header": {"version": 0, "last": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B", "contents": "1D4FCE21C0FE6A24C386D40C92D26F0E8B4AB1CA1D52E281F552B6E8B31E6803", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "ADEEF2E532DFBF8B41384A61173FD3FF6BE2542080B106FA57CE6C546E4E176B", "miner": 0, "time": 28800, "proof": 0, "signature": "925B65382C0A8B26393EBE08F8B72E4BBD45E275CFC749A163858A582D9DD474EA60A527671D1280D7B76B4B9232BFF5"}, "hash": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF"}, {"transactions": [{"hash": "C9A65D759E9D37FF276B2733376F82140763F6FE90D2E2C6040A0BB62EF51152", "holders": [0]}], "elements": [], "aggregate": "AD3406EACE13826010E20C65D38AD688FCC28B24B3A7B78B9459AFDE89E5E831EE789B971B5175B0ED83E4C70AA42710", "header": {"version": 0, "last": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF", "contents": "B69147F0EEF64CEE66828986C8C9F06364FFEBC61C56680FF1659CCAF985CA9D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8C9811D818346EE6C40848C9F8B15D1E61227639EA2F0F1DD896AA3DA2C44441", "miner": 0, "time": 30000, "proof": 0, "signature": "A2AD4BE21A833707E3F42DF55E4300089C4FADCA6010008501BB4133F90793653B6C7E8D45DE7A320428A75F69DFD980"}, "hash": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583"}, {"transactions": [{"hash": "7ECE5339A3ADF8C9A93E348BE975C2161BB2CA99B0A95D5E7367E94534E1EF89", "holders": [0]}], "elements": [], "aggregate": "89E22645E0F5509B5AB620C71D146E3863209BB1FEAA1A98A2AA44685A788AC81B72F0791CFEB627E1BB33EB6767F198", "header": {"version": 0, "last": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583", "contents": "B684AEBF86A1F120E5A44A8C14A159973288246CCC77B44F7E554A5D0D8D67AB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "BE619866422407EDECCCF01D40CFEA4CD97B108AD4AC8A859565954F80355E4B", "miner": 0, "time": 31200, "proof": 0, "signature": "83693BA208148B3BBB92BA670BD6320EB343B3998064F3C65D1D3FFFDF34625B0B63875C37808597BD1B151B35CE64BB"}, "hash": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB"}, {"transactions": [{"hash": "7B0498874B886C771502AE3A40F848F706C8B412517C33DFCDE0A3EE24896C3D", "holders": [0]}], "elements": [], "aggregate": "9777F1ECC189743CF8495F1593A4B9ECBA8E24F0E377F238AFF267B00451FA24A2F55BA867A5A3C5D2A7724C500A5CA4", "header": {"version": 0, "last": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB", "contents": "380410891EE3A5845B0438A8C2281B92E902D94B4FE4F7A9F383849718D21168", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FCFD4349689F789D33570A5B0A42CA821F6FF8F320D1A700ABD3B538FEB25466", "miner": 0, "time": 32400, "proof": 0, "signature": "8A858B58B9C089077FE125A55B2EE89F9629E460FF628545677881F14AB623AB673D8E50A70924EE2359307DE4462A6E"}, "hash": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C"}, {"transactions": [{"hash": "CA1CE7CDF5B2F3128DFC1050B240E48E13D313D951FD20BD60F1C89C71227EEB", "holders": [0]}], "elements": [], "aggregate": "9789EFEAEAD52C813157D9E7ED237FA6217BFD92C98C6AF10E75040A090CD184B97F0BA67E1B561971DA0C3128D3DADE", "header": {"version": 0, "last": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C", "contents": "628BD47A0591ADBDC780A7ECC8C86977DB29493E072761E00C64B51AC92E0732", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B87CF0B569E2F08B381753FA8F6359AD314319056E14BD28CB129D457949A14", "miner": 0, "time": 33600, "proof": 0, "signature": "AC71514D519AE5B9CA1E5B9C90940FBFAB7AF802B4928CB661040D82B5DA6CBDC96515140EC07297EE7292F4F40091EF"}, "hash": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A"}, {"transactions": [{"hash": "17F10521957248191BFEDAF36E6543FC8FFE61D10644E037E93F3A555443A9FB", "holders": [0]}], "elements": [], "aggregate": "B97955DE18257B2B74B54F2F56C4E0F34A11AE75F9AF7F1E9BD8EC66017F15962264DC654D23C69BE97931976A4C35B2", "header": {"version": 0, "last": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A", "contents": "1375723AD673ED9974F0B5CAE015F691581759434D076DA469E2AACADFF37F1A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "17B2C89050F788BFD1134009C1A396C2EDC2F8919F11497A7F6F9BB9FC88B780", "miner": 0, "time": 34800, "proof": 0, "signature": "B711DDFDF6717189817C83AF1D760045F6BD9A928EF1F3C8B514766781138741C6301B5FC430BC7832A7AF4530CA3A97"}, "hash": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42"}, {"transactions": [{"hash": "503D0FE35CFDC7F3A15DFCBEFB1AE55495EB1FEC723591E023B6C854D55E494B", "holders": [0]}], "elements": [], "aggregate": "A08D9E3C46934EAF57AB1D31EDFCCD0C031A9D8E35638631E76963CD8420B1B44F60FFCEEE88EFFAE22EAD3D435FFC4B", "header": {"version": 0, "last": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42", "contents": "BDCD84DC8A3C6E41FB8D432F2F69E2565BDB20CDEA60048355C0572F19FE2C69", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EDAE53D97A0A81B7D39FD161435E0BE196D5DC8E860DA6F1A6D3E9DA87956CE9", "miner": 0, "time": 36000, "proof": 0, "signature": "B912FBA94F16BB9AF34DE53A7A65532E3AD5841862EE34C85172F85C3ED55BF0F8C136BF4114298B86E19A5BA7105910"}, "hash": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096"}, {"transactions": [{"hash": "CF82B3A47ACB0B38E9FA9A8A8FD1C89E3D0023F016A2F207D629D6FB3AC0FC2B", "holders": [0]}], "elements": [], "aggregate": "93996B99767DEBFB239E3FA3ECA1A901D1D07401ED3BB751878D8C9127ED58EE981A0A2E9A25BFE577E03707089ED000", "header": {"version": 0, "last": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096", "contents": "6B58318212B14ACDD98D0EE097D5614FD5CD21AF968A0AB538D6AC3A00AA3C67", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F98042571452CA386BD6BDF2638A6BD9ABE56627AFEFB49DBA2A41FCD9474745", "miner": 0, "time": 37200, "proof": 0, "signature": "ADE7BBD187AD8E9625230E92F813E5BFD19FF8FE000FF96E29129CF7498C6066A4E992EA82D21979D20CFA6B8A4A0F90"}, "hash": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F"}, {"transactions": [{"hash": "EFD9B2A90A15401AB3D1AAC9262E70C9B9AE448AC7560E96A7766449AE5446D2", "holders": [0]}], "elements": [], "aggregate": "93926EAD07F7A93C23990C6A41F23FA3288232F741C97D29182E0AFE90C3E6DFFF1E54EEF666F78FDE513945407E285B", "header": {"version": 0, "last": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F", "contents": "37CD8023C91A9ADF1512D69A23B77A418478C6F6EF66D966182C1E5C95F092AC", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7D2362B3488EC1A041755C7A14B20463502292442C4E2FE96222B264E43985E3", "miner": 0, "time": 38400, "proof": 0, "signature": "A92DABCDB4078E26DA81109E38AD82397555543264028383E94DDE5DB9BD5079B8385329759F28D3D81A21E620E51DDF"}, "hash": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92"}, {"transactions": [{"hash": "768D69AFADCC7A29E23663E8A66EB8D7CC1563079A216EAA29B9783DD528DB8A", "holders": [0]}], "elements": [], "aggregate": "AF48FB26E57F2D619487811009F1E43A50E073A439DE7DBF899F9E158F8C6F0B6432B2754347C1CE6510E538C5D47291", "header": {"version": 0, "last": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92", "contents": "44571AF7F71017BC02764E5416B9E8CCFF1597E3480143808E1BF8E5F9B8023F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F0FD8CC4454CF3F1E0B3636CD046FABC7291F16D2D0A7054C8C3100E10D8BEB1", "miner": 0, "time": 39600, "proof": 0, "signature": "A84C6D840E538E70EA4AC4831853F3EE70A384DEEA885EBC0684248DD1905C7FD6465E37E6B187A10C1E084249BA1D17"}, "hash": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436"}, {"transactions": [{"hash": "5028DDEEEE12CC6FB156E713F8446325DBE6DAF87C4926EC9E12E8B028DE5433", "holders": [0]}], "elements": [], "aggregate": "B2C2CD38A31D99177B263A311BD14718B1012F75987B8A653A1C2C20FC239A86F7597F2BD9CF50784B8BF4559740F126", "header": {"version": 0, "last": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436", "contents": "A20B36DA35E654AC8C2DE99EE1915EE1ADF583F8F0EBEDBAF6DD4BF4EF2F5382", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "B5802C1E8CE29BD6FBCA50DCB4506A2563CC030A453D971594DBB27E72D8E6FE", "miner": 0, "time": 40800, "proof": 0, "signature": "AC4C3BE796F9B6978079119F47E4A2616787EA843A54015A85D4F357A1A62DDE1380105BC8C86389A892A8B934349D30"}, "hash": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E"}, {"transactions": [{"hash": "01FA1B676C326343DF4B03F3840DE9383EAF0EAA13898A278029EF821E459089", "holders": [0]}], "elements": [], "aggregate": "91B51AFF6A3F9FB9BADBDCCECCE5093F80F83067D51E9CE660368B89F0C6B200A6ECF0D2E38BB0B9D90ACE4E3FBC4F41", "header": {"version": 0, "last": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E", "contents": "4E18ECCBF34A734136250BB94C7EC609FD424D1443860FE53A082FF59A8B87F0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "239668D5D0544F01BE9E0C39363AF70E78335F388495EC1BAF779425A09E3B71", "miner": 0, "time": 42000, "proof": 0, "signature": "8EB2D10893C3DF1B5331E4ED390AA2C44AED0B48FA0DC8A1329C797ECD561A10A6D0548B77A03942D8356C43B392FB94"}, "hash": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65"}, {"transactions": [{"hash": "1D932D69B193AD9246D4B61F946270940083FA5ED35459F4ABF41DB8F1C888BB", "holders": [0]}], "elements": [], "aggregate": "B2D9926E7F67112737ABB78DC0526F3F3FD02005AA984C9C33F9B53FEE2BBB70E6370570C511A0966C21AEAB46592D35", "header": {"version": 0, "last": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65", "contents": "0F6821A9C7D8A2005B6BA5FAE7B2A60EE0809E53AEA2540A269B4EAA01EBB12E", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "655CDDB0EAB8FCC0A36AFB7D970980E30CD65830CB7DD3C4A43D6F19A8669D05", "miner": 0, "time": 43200, "proof": 0, "signature": "8EC7D88E37A2490D7954DB86D6A7A67E887E0D6D5186CACCB3F189E4520CCA2623F72C234C1D0162C48709EDDE583CE2"}, "hash": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B"}, {"transactions": [{"hash": "52A09B2A03602D5536DEA58861D24E61D50F9756D9F402D9BC80D092194C8914", "holders": [0]}], "elements": [], "aggregate": "980FA53F64DF42DE83F30EEF35F1ECFABDD1E650C96BC7E9BCC485C1A0190F48D08C2FD98D8C2D8A13FA3CB9CE598D5E", "header": {"version": 0, "last": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B", "contents": "63DF40FF9CBAD4F329AF28C1E9D32D6AD817AB330962FC2A22A0E0747B5E70BA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C05C3FCAC02C04C52FF67D092421BDCB1087040F479181B4CABA1D7CC971E77E", "miner": 0, "time": 44400, "proof": 0, "signature": "98F92E503B9FDBDF51AC6022913D5ADB6EEE9A46475E19F6EA3D14B89AA7E3A4E0410646360C852B86E94A6BD94DDA59"}, "hash": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785"}, {"transactions": [{"hash": "715C4EB143D757F3D9578085A9A493B62A12E63979F981FF71D7D0E5797F963D", "holders": [0]}], "elements": [], "aggregate": "838886F6A7C95D91E0046704A5829BFE80953CA62CAFA25F4966B912C188B30FB29565C3DC959A371C27D1A67D802411", "header": {"version": 0, "last": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785", "contents": "76CB3B34A2ABBE67A018D9329412A4A76D67838D06308E4029B3A8FAA78F6692", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "75E79993E688F0BABEBA4E8AF7EAB2D23BA0B20E35B53C4352FF25464FBC2305", "miner": 0, "time": 45600, "proof": 0, "signature": "82E559E958A3630F5B240B0F82DD0D7C8B786ED0889DFEA7AFF0074DF3A2342E77D9400CE8BA83F1A8101EAE45F9C0BB"}, "hash": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3"}, {"transactions": [{"hash": "76CF7E02283072268B70821E34C60372B84B3CFBE8FB315B3540CD7FA48FF2D9", "holders": [0]}], "elements": [], "aggregate": "A5C043E438254B6D82445D3D5858F7E37873B1178A2FE8E0B7ABCF5903EAA1F7955711A64F16C4F9B2A5D5AB7AA91DFB", "header": {"version": 0, "last": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3", "contents": "05ECD46C6FFDB6CE064B1A7BDE217199694C779906269E29B686AAC2ECD4AFBB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EB0E425AFDDEC9A6511202015E98197A4B5FDC8F8E0EA83B5F6E9F091486F308", "miner": 0, "time": 46800, "proof": 0, "signature": "8148B9722127E142AF9014C182B3924EC4D2187B6B56A50D9B679C856864DE5A210F372B410DFCAE53D237F7F5B3B50E"}, "hash": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34"}, {"transactions": [{"hash": "9B8A47FC44420DB73CF46F0168B53CF93D495CBBAA9B36A9913298B8648574FD", "holders": [0]}], "elements": [], "aggregate": "84A52DB751EEB72E1D9F9DA54509134F7F5F54C409C77343E78925583E5FD1209E66DDDEAACDD60ADFE3E133C266307F", "header": {"version": 0, "last": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34", "contents": "0843B460D5841D73032DCCA971D096930EABEB343420D5CE790A0D79BC4577F1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7228382536A5A5B26CD248EA461978371776D786A4574023AED45FA7EC07CFBE", "miner": 0, "time": 48000, "proof": 0, "signature": "925EACECFEC61C713E1B5E07DD7B21A5D9BD75177E6AD97ADB5D93FEAF358975D334E738609C390220EEA72B514EF93C"}, "hash": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF"}, {"transactions": [{"hash": "A8B87D0CE244374CCC914F0086D2A5ED6C7D7947F76AF07518BC17DADFE61B99", "holders": [0]}], "elements": [], "aggregate": "9197DFD18F76C8574169D64E947A07C61AF53BF4334972DFFDC81765D7B0B80E11A7421755BABF67CC78B51F645F20C7", "header": {"version": 0, "last": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF", "contents": "F33F7C3B8383B04FB3458B3F09135AFB25821E0988D835E9E02065EA10613DFF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1B4CBCDBD72F25DFDEEE604A1402555B911270C58AD3EEE05B5339FB47709AB0", "miner": 0, "time": 49200, "proof": 0, "signature": "978D37E5B2262D811FFA5D2551C4F6812E448E8652942414981F161EC81B30C97C55F55782088F0798FCFFE6718A67E2"}, "hash": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553"}, {"transactions": [{"hash": "0DEC8740B4E9BC79F54827B7F179BB804866B62DD8FB89D5A3AAE91127179B0C", "holders": [0]}], "elements": [], "aggregate": "A1927B616F7FCBD04954B26122881383B90A18153EA39FF2B85C5006243E8F1EABE29BD9640044B2B157FE130D5710FD", "header": {"version": 0, "last": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553", "contents": "FD65AB96924037AE17DE36A779438D3E0F1D5B6AB86EB13294B4B74B0B3EBCFA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "81F9805AC3B6E2A7E2334F535FA802F144B11C2FCF36D7225354E6889B4B8362", "miner": 0, "time": 50400, "proof": 0, "signature": "A15685AFE8A8ABF35FE4E8DBC290284AB2941066A73AA8BE763E5A489F1FCF9EC6B425F892720726A2A3B7514A93FDAF"}, "hash": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D"}, {"transactions": [{"hash": "93851C8F4DD5348A50EB02FFA01D6948CB6E16819C0D7D13EB144281C17A0751", "holders": [0]}], "elements": [], "aggregate": "840A00EC75C90EAB1F50EA4718C87F189E21551BDA4C507058EBD3BAFFD916051688EC9E8DFD92A3F240BA27B474ADCE", "header": {"version": 0, "last": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D", "contents": "6EC6E7C25E62DD005D6FB65250FAB452453D9D9AD4CA93121C1275FFA8E4B207", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "79929DBA45BB3E810CB2847915609D2D9E3CEB998F2B09F6EB0B5E90C9D497F9", "miner": 0, "time": 51600, "proof": 0, "signature": "808F073633DF55ADBCD93035466E9254300AF1518E923477B6FC5D483CC8A2C75D87E6FF24CEE061FED050BE3CCE244E"}, "hash": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D"}, {"transactions": [{"hash": "0B85FF7C665D0229B14C2FCEB168D27E30A0E955B6BF5176EA9AE549042A2011", "holders": [0]}], "elements": [], "aggregate": "A03EED165302DE3DCE12645AF9FF5D2618B23454AD8D2429CFC582C5F9E8EDB6CB63B60DD0F0FC5A2AB1F44C7CAF4340", "header": {"version": 0, "last": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D", "contents": "D81C83FD51D107CD3F0020796868A59557628C9B6D62693ADCB6E3A4328159FD", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8415D8191D3B91B5B7ED68975872BE5EC17F030C2CA2EB056F84294E0382FA6A", "miner": 0, "time": 52800, "proof": 0, "signature": "ABAEBE124340D09ED8D202418FC060174C432D383F36FC5D25F3C0E81DD694CF74DA28AF1458A55693E3FFCD916684A1"}, "hash": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3"}, {"transactions": [{"hash": "F4001DCD38B0E23768108C1F0CEC8FAA1777AE0FD4EEB38E6DD3E0CDC9735696", "holders": [0]}], "elements": [], "aggregate": "A1DED17B59BFE2F34FB6011D9FAA2F60B5D557895BCB1B2D927B021E21EB22F7DEB115A78B350D486014ADDFF69EF391", "header": {"version": 0, "last": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3", "contents": "BBF99503895A2E37FE5599ABDB7C1C1672D003666B29E9F28BDEC380E35EF040", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B7E79ABB5483F92115182B9B5CDC30709D8192DE966E8B831E4A6314BD3D5D8", "miner": 0, "time": 54000, "proof": 0, "signature": "8DE8C11F66CDD2B75998A8936778DC624C382ACBEBFDFE322BEC0904F730F033AFE70748AE98F49308C3CE3D3E2981A7"}, "hash": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8"}, {"transactions": [{"hash": "602BA0D211013BDE8EB9619F8431EBDAE3BD4B5FD09F4266B19432A6AC6DDBC8", "holders": [0]}], "elements": [], "aggregate": "A3737B6AD6E12FB918672E22099A7C113A9590787723EBD5D387B7871EB60D424A8450B271DFFAAEEAF2C375E4D403FB", "header": {"version": 0, "last": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8", "contents": "478094914295E4311699033104C5BAB93F471B4EA137209F7DC3B10E97678068", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8D97A09A1AD26F8A100AB558630D930F60A42500F5AB309609ADC50B7F9EF07C", "miner": 0, "time": 55200, "proof": 0, "signature": "8C36F0B44D5120FBB910D3CE312F9BA135CF19ABD39D84EC5560642EF1FD4BAED46957CE94DBCC1B4FA7C2BC736CD0E0"}, "hash": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3"}, {"transactions": [{"hash": "2FD1583B1B31B0FE60632F556903257FC3734E39AEC237EDB5CCE784777A1A6B", "holders": [0]}], "elements": [], "aggregate": "B60B0A0084D20E0CCACCF0797884A1DBFB10EAA1D634584D0BE0299343285D137EF5CCE91DFB9B4D1D52791BD98FC40E", "header": {"version": 0, "last": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3", "contents": "6EF462AF646189E7E659FB67F082F67442B1ACB3D1439EF97B410C8D94975815", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8337618DDD6041028BE2E2F00A9D6592E0C80F6900F1DD5772856A59ED589736", "miner": 0, "time": 56400, "proof": 0, "signature": "A20D6B1BF11A779FCFC017DCCE8334DB9C7E5406FA8B718C8820BC5A07B3246ECA22D6455524D0F746872575AC19C979"}, "hash": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368"}, {"transactions": [{"hash": "E6AD11871A75D17643C9C208399B45CCE496FCE32E37EBE98D6AF09E77094397", "holders": [0]}], "elements": [], "aggregate": "84F89FB08A88F3F3197AEA45CBD2C095DBFAD15E1537E19A7159BBC0B285C5A7F5BC2CF164C60B8308FD5B0188B4E762", "header": {"version": 0, "last": "AFEBBA5A17422C44FB30800E833F7D7E690DB1E7F37139FA40CF635E07148368", "contents": "73FDB8E7FEA18F47DB89ABEB7F7BF375B5F994459F7855C40605AB07BDB17337", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1DBF1565853B1EA6467129426BE65B3A541BBBEEDF24E258559CA64F95967643", "miner": 0, "time": 57600, "proof": 0, "signature": "B76E1B0BD4277C4C3C708A9E28208123813C22C48CEC5A6C591A3C6CF4A2081C5FE6D5212990DC266FBA264B69C90795"}, "hash": "A7EFEF96DA3E639FF8871B6EF3E2462619CC8BE367B782FAAD821EC8CA71F6C2"}, {"transactions": [{"hash": "FF685B32345A09B80656FFEFFCC00077BDDAAFD7013608D07434A7874F393418", "holders": [0]}], "elements": [], "aggregate": "A03EB7A9DC1C11E1B8510C5E1AB902FCB9EA77DC36459A6AEE755C4CA9840406389CD2648D3D5111308282E59B63A602", "header": {"version": 0, "last": "A7EFEF96DA3E639FF8871B6EF3E2462619CC8BE367B782FAAD821EC8CA71F6C2", "contents": "D62E8DD01DB12E08E2C4E57E5ADD86E55C8DCB62145C4B3D588ECB4F6C00F48C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "026AB7E9112E3E233D0AE9A4E18F034C77412CE618570DBEAE8B496A62554ECC", "miner": 0, "time": 58799, "proof": 0, "signature": "943CEDB29EF4BBE602473106DE9A4097A8A92C07D0B4B121AD7B9F14F8CD55D4EB7767872846FC14BCF752FE88171B88"}, "hash": "AF3D060CF0D2A80E711A548C5A72D7630D3635CC01CDC6AF2274F433D7D878C6"}, {"transactions": [{"hash": "880C622C28741AEFD3608304E0BDD39567EE544F9278AE3092ECA03DC97C7A80", "holders": [0]}], "elements": [], "aggregate": "97467B5B7D1DB949BD557CA86345B1BEA4C99A5FFDEE06EFC47CEC1A640507B1C335AAE455CC1D16BBB70CF11B8F837F", "header": {"version": 0, "last": "AF3D060CF0D2A80E711A548C5A72D7630D3635CC01CDC6AF2274F433D7D878C6", "contents": "0FE5DE8D696949B869C2EE91C2C51FFB4772438ABC60E44898B0B044D187BA45", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "41B5CCA386C067786F5444D96F59C36575757CAF78EFA23DA386C8AD708EAB8B", "miner": 0, "time": 59997, "proof": 0, "signature": "A218F91DCFC64C64A90BF7A636C61DF88B8678A924392310088B3796F5442CFC60A194E684FE161CFDA2AEAEE8786BE3"}, "hash": "3F592711D543E023AADC98D58A772F86E45503199AA912B5DD2228E39F28D23A"}, {"transactions": [{"hash": "F35E3620E7078A2DAA408B5ABB2EE8DF60775E8FD496C38A7374D0B80DBEDC50", "holders": [0]}], "elements": [], "aggregate": "8B1CD79298A873B8232E930D7E92B74D4A85892AB742593FAB764D02FC6D4D356CC05F7EE361C3993634B7E0EF919612", "header": {"version": 0, "last": "3F592711D543E023AADC98D58A772F86E45503199AA912B5DD2228E39F28D23A", "contents": "3F8DF14AB6D85E8331F71B8920965DFFEAC3501DAE87E1F02A026B730077E9E0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F8A814784AA93D2E196CB7EE2BDF51B11D73F93015DCFE9058BE273D92EC738E", "miner": 0, "time": 61194, "proof": 0, "signature": "89828258AC657FE14B4A1BEFC80FCE53826975118F29AB9F5B481E978915EAC02C6AA270734E870B028CF40D08A13846"}, "hash": "AD5AC543EC9DE00C2BAB9B6792963A440B42AF273215E3505A774D3F37264340"}, {"transactions": [{"hash": "010C9770454D7ECE1034B920AE734DC63A0527B8C538DC7B543D18ECEA83013D", "holders": [0]}], "elements": [], "aggregate": "B5F76D68B236CC48AFA95D2C8639C46FD6A88FE5C4E0DB312BCBE17814405BCE1AD1CD42A37F95CDE047B9FB33F81D13", "header": {"version": 0, "last": "AD5AC543EC9DE00C2BAB9B6792963A440B42AF273215E3505A774D3F37264340", "contents": "8B628774FED89EB5C836F31CBFB8B34AAD73CB34D8BCEC1CFEEE87872598D9EA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A6639C6F3B1C0AA7BC02A03E2425C94A07E1F458EBC751A8232AF5B401807F7A", "miner": 0, "time": 62390, "proof": 0, "signature": "AFB6B10CB185A0AA101260BF609F94829B60034D79F08421A65AD1E23B03956798AEE0CD8A3925A7E4BBF65C4475C2E5"}, "hash": "6BF4990FC5DE8DB71FC803596AEC2CEFEC825091DB548CCA9E199192F2FFF473"}, {"transactions": [{"hash": "D5420092EE384E712500F4F8A079E5C1724355E92B5F015FABCF64BEA68E7B53", "holders": [0]}], "elements": [], "aggregate": "B699A564A182B897591D6D2F58E8D4C7A3AD63FB160AFF9A0C53A8E5D951E0A469D0939B2814FC19785A7ED73218F310", "header": {"version": 0, "last": "6BF4990FC5DE8DB71FC803596AEC2CEFEC825091DB548CCA9E199192F2FFF473", "contents": "4D6FD0AB9A334FCA898C91A3EFAF69DDEF62AFF915BA1F046944BBF9AB8E684E", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C803D179E86BF9090529C0C8A0E6F0B6C90B3A2CD423E69949EFB9AF7F915DBE", "miner": 0, "time": 63585, "proof": 0, "signature": "815E51DAC03D19466FCC0FF7C391F884AB9CCE03D37DBA03D493FFB9E5D91E93CCB78C090F146C5B256D0F41F9D94F59"}, "hash": "E2B2B911981A2236147729057762C9C43C1ED5781395003AC2BD56D0655C3D28"}, {"transactions": [{"hash": "C6BCA0495201A2806EBEDDB70AA8AABE7F15F0B3B415544ACE1BF18E6886C67B", "holders": [0]}], "elements": [], "aggregate": "8C4299E7374CD4A4B059C42B2D6A180DAF7FAC41F2F97CEAE174FB0318EAE7AE761F094587C4324C113A872F2F5DF88E", "header": {"version": 0, "last": "E2B2B911981A2236147729057762C9C43C1ED5781395003AC2BD56D0655C3D28", "contents": "5F11A798270EBD36EB50B0CD5CF27AA69F10806F2B5368BB81F17F272AECB580", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "3018D38909A629DF60253F049727DB78E10AD544E64C0008CB5D905B1914F8EA", "miner": 0, "time": 64779, "proof": 0, "signature": "95ACDF10D7783460EECA28E39CB0F6B94153E4E0001FF1CB14B0E64931307932C9F1876AF93FA6A54DF08881B5FF046D"}, "hash": "FCB3B806FBF07490F55710DE7CEE212D9A8CEB36FCAE585A01A6102AD2EBA03D"}, {"transactions": [{"hash": "FF055C241DAE42B8A3D77D22EC4E035B66E054F237984A74ED934640B9398609", "holders": [0]}], "elements": [], "aggregate": "A2DBD869CBE757CDAE1B65268A76737506742907501E3399DABA9DBC4954E9229C375B43586C7BA8626A6B2050A1475D", "header": {"version": 0, "last": "FCB3B806FBF07490F55710DE7CEE212D9A8CEB36FCAE585A01A6102AD2EBA03D", "contents": "EA78E4F0B4EEDA25E988AE8AD50F1D486307071874EB9E82B4C7CD22844374A5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "55E0324DD9BE7DCC2F86736A0B7233B38291743C8CAB67F8A83AFA0E30FBFA56", "miner": 0, "time": 65972, "proof": 0, "signature": "A3C782D70EFDC5162B5BAD5CC07206B4B2F1F6083700B759D78ABE5BE709F47090FB8134125E5429DB962D7D87006363"}, "hash": "BE95A9CBBE7E98FEC3AF50A625117E9B239115476E8726ED32CC11B051435C7B"}, {"transactions": [{"hash": "9BC32A516790D27044EED8ACADA06CE670754FCCB91A397630786CB0323DBB03", "holders": [0]}], "elements": [], "aggregate": "8D0EC49823DB96ECC3BD3340C451DECA6A9E8EC3DECAF02604AFFC5682B4AB6B0DC29A55783DF9D1E399D8B171DC6D4B", "header": {"version": 0, "last": "BE95A9CBBE7E98FEC3AF50A625117E9B239115476E8726ED32CC11B051435C7B", "contents": "582177C0BFF92D78C8FDFFE89252D76CB8FD2C3FA77CD62D1A8026EAF0425996", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E6C1110BB07890E5B97161D5D72A557B4951934999B47B6342C0B60CF235DD23", "miner": 0, "time": 67164, "proof": 0, "signature": "8AB4DCB50B7F044B8C0CD1A33E643DB18653A706CA39904D84E1D5A51F70BE4DDA79178207AA55E4F21B1FD999349523"}, "hash": "D0160D4AB8068B0F3BA2274234F7BA55658E62FFDD3109A8ADB4E631AE2C8A2C"}, {"transactions": [{"hash": "83396B853773EFA82B7F72FDCEC5355D79C8B3879EA56B4FCB4769BA4E966D7E", "holders": [0]}], "elements": [], "aggregate": "87885CFD04A2D706586765634287CE85532CD7CAABD7B35A611D6766ED3A5C24F6F9A32FF0B240E34903DC64129EE415", "header": {"version": 0, "last": "D0160D4AB8068B0F3BA2274234F7BA55658E62FFDD3109A8ADB4E631AE2C8A2C", "contents": "160996CDEC6B9494EF4AC9A311DEAADD056E059448EE9ABEAC37E9CD5065BCF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "44924176FD22CC23609F0428CD9A6D58A1DA467D654F955D0BB3E0FA8C199D44", "miner": 0, "time": 68355, "proof": 0, "signature": "929265D91A432B696F8CEA46D642CB3C128F0A31F1B0D02A3CE5E0D90FD41C7B656FFA52FFCA31F4CBB449B833F16850"}, "hash": "0470BC9A8F98CD20A1EB555ECAF5BFF3B52EB09CB98075A5087322B80ED2B7CE"}, {"transactions": [{"hash": "0F8D88ACC761842CB73524FE50E81D0C83F0C2CFC290F55864E58EC935AAFD9F", "holders": [0]}], "elements": [], "aggregate": "929C80AA9C704219B5D7198F54B5F309F68A9C3683595C77B555558BDFB587D5D9704DBA304D6FAE6137F925A457D1ED", "header": {"version": 0, "last": "0470BC9A8F98CD20A1EB555ECAF5BFF3B52EB09CB98075A5087322B80ED2B7CE", "contents": "246A68AACC573D059F6FB8374F59CBB08061A6E3D5972C54A376304CD1878B36", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "3245C1A56113645F33ED53328A31AEFC73DA0432B92F4B422EE9F5D03AAEACEE", "miner": 0, "time": 69545, "proof": 0, "signature": "8DC44A6369674D6AF3265F8C2F9D67D71AFE6F4FA09077AB52E8CDEA667D8D32EFC1600A18228A4FE43F972FC6A5DB95"}, "hash": "441B11C8BE8F01C4B82FB530C7CF97F6C746381C838170F9C9554ADDFCA2EFF5"}], "blocksWithoutOlderMint": [{"transactions": [], "elements": [], "aggregate": "C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "header": {"version": 0, "last": "360E204A3E870E34FC69B65536A6C8354966F95950D4C55A5C10FC276149FB2E", "contents": "0000000000000000000000000000000000000000000000000000000000000000", "packets": 0, "sketchSalt": "00000000", "sketchCheck": "0000000000000000000000000000000000000000000000000000000000000000", "miner": "B73B38B8D005DF431DB1CBF3462348FB8DCD213B3980A6C0E26D7B95A79F4E88324832A5ECB684436852ADCC8511C0BC0306EEB6D9E5E581AD279B633C0291157EB8ADC56D24750C95C8B676BA66C1FF5AAC4FA60EB5C8EEF0768067426602EC", "time": 1200, "proof": 162, "signature": "82021D8D5B5EF89C40548BF31C2890177B7CE874E306A62A0C1E6FD46F1583BD5E9E18299EC5D64A2D90E0B2CEC47B44"}, "hash": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700"}, {"transactions": [{"hash": "72EFE1EEF8D6372BBBA749E7CA910F210A2BC80C46A688D966F84C36AAFA3732", "holders": [0]}], "elements": [], "aggregate": "8B04E9F4BD7CC9A83D324ADCAC4FB2246A2A1A7AB87885A7F6B0A7A119CAB298D24ECE2A47239AB296E0A0505E2C478D", "header": {"version": 0, "last": "99CFD0A30A9C88ECCDA97868DAB5EB75F6D43F8C96F12B3E286257E3C89BD700", "contents": "C9521F92299A0B088B8141B012B42100E5FE6E72189F58DE714C22B1E910358B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7953C6E651693D49AEDDBB5FCDE84E3264648828330B8E8261C2EEBEDE4ED175", "miner": 0, "time": 2400, "proof": 218, "signature": "8CA439D808746224FAEB94D9E2980DE1B92E0947FB401CC458530A5E2D9E227C69FABC83E050EA5981F2AAFBF5F3A209"}, "hash": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602"}, {"transactions": [{"hash": "6281D2C3868D973A5EF0505A8AC35DCEB4C02671CF32195CDE3399D064AD3573", "holders": [0]}], "elements": [], "aggregate": "A070246256DB4536DEF33503DD7E63D4EE6B528BD445F9F671DA43C09945B3196AE4E65BE683EEAEAE41A0A26B3AB197", "header": {"version": 0, "last": "75BF9D54628A150616D25333694A3AA7440A59A6BD977DA3EEC98ED411B26602", "contents": "A8D4C85179A8C6D6A7538F1A182D12A22C3200F4F97C49E85CD3B2185D22F8DA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A9A9C38D542DFA0477E4A01DD1DCF9EC3390986E1F944C9A4B204412E133B288", "miner": 0, "time": 3600, "proof": 144, "signature": "96B09D483F134D88486DC1079ECEE623620A5186E54B6AD2802FAE542E2BA8F60A4DB8A5A95D6E853FAA0C52CC40B8E4"}, "hash": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02"}, {"transactions": [{"hash": "278C104FEB0EB60FDD554EA0157704706F5587B165DC6356749B4AB20781BC56", "holders": [0]}], "elements": [], "aggregate": "B6F88AB1861E8739DC9CFFFC5B91F8ED221E65C2E1CAE55C58B59A6D0F9B93123DE2C680F83FB629F87E49621E3385B0", "header": {"version": 0, "last": "545D2F99FB30C4B6143E882D4482BC8E10CFFCED38AB3F33B2419651DB6A0A02", "contents": "A809E61ABF343724E17003F6EDC3FC31604BFC8CAC89E733167D422CB355B429", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "791F73693CA57D52320A0227EC5359BD325CDDC300B9B94091F53A77233883C2", "miner": 0, "time": 4800, "proof": 30, "signature": "A2D8A2CAB1ECBBA004769A57E782F308E1308DF6D64D64C0D2A7122BF023A586CA7F3B2D60FE3CFD91B80621D6E42A0A"}, "hash": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700"}, {"transactions": [{"hash": "86E44D62E8D27DA57458A2DE80698A6EC88054C5ABA9083FBE54EE0141E93A93", "holders": [0]}], "elements": [], "aggregate": "8B129406C90F252DBA681AB51D48B0B4B3C99E681B477411DF137B7657103EF92C5FF48ACCD05B8E87807F1906D2BF96", "header": {"version": 0, "last": "69FB18FC4F2B96EAE38BE617CC061276852F349A726E9FE26BAD2EFB3EB58700", "contents": "0DD37CA5DDBFC97B394561381460ECA3212039FD592CBD232795C865E7C5CB6B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DB055B18061A1ED67925CC442B187CD56837434CB813865E70FDCBCF4480E426", "miner": 0, "time": 6000, "proof": 21, "signature": "B9685F27C2A79D71FCF6C14CBA94CD55F3E615F1BE5E27F23FB4F2DCC65A047928FC5C87B0115AAD791E40667C241286"}, "hash": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900"}, {"transactions": [{"hash": "018412CB2C17E3ADFEF52A98DD73EF145A46AAA76AA35A887246A83ABBFBEB89", "holders": [0]}], "elements": [], "aggregate": "8A6B7756526BDF6304907773176182EC8873536431E9F1B6001CEE8E6FC217B3AC34DB6FA3E8F8B46924685A14E5EDC5", "header": {"version": 0, "last": "7D831A527679670C93D51EA0F9989EA62EAB63200EA9F14A044C56A55DF22900", "contents": "E27C9370E5749F06BDEC855865682DFFF18767D1A5AFBAD3007A8FC62820F3E5", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "DE6F5EE882121224255A2D752B891B43FBEA7BF74D42E7CBF251C7C2F28DA123", "miner": 0, "time": 7200, "proof": 0, "signature": "AC47691383371FC7A5EBEA5961BD060CED69FB56BCBE68A83221FE23AAB8BD0C4290AD38736B551C33FD918D7C6BFB7D"}, "hash": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623"}, {"transactions": [{"hash": "19C4B0914783A2CE9846044FFE48AD35E1E173023A64D7BD340734184EEF04B4", "holders": [0]}], "elements": [], "aggregate": "81EA7597EE88E818FA43F480FE43CA24269AC9BA4B26CB6A93B0DADAF06B679EE76B3371E9F2335488960CE7FB85D17B", "header": {"version": 0, "last": "C9BD930533C570DF525399D296B90305C5B0456940B1578D910BE856211D8623", "contents": "7E6C29898A4764EA8A42B39D3DC51472834FE78A67778A379DB6345EC81D4CF9", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "729018FFA42AEF0122ADA8FEC04B4271CADB3E9175472A7B4C0447ED720B4BE2", "miner": 0, "time": 8400, "proof": 0, "signature": "9638880A8D68E4B770802307E4CB24BACE6AEC6CAE02F623AF423F2C53D027B7F31DC8EEE8E9BA910B600B0A9D48318D"}, "hash": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042"}, {"transactions": [{"hash": "A8963D7A7144FDE3F7B5B64C1B3E6D0B1B5DA983BE0486B2FC6A746585BEC0C7", "holders": [0]}], "elements": [], "aggregate": "AEB80694FFDCCFE47F38CF3C5AEEC3F9B435AF5B3C5CDE5931EA4D15BF5A5E617AA414CE2B3305F85BD493292E879BBD", "header": {"version": 0, "last": "CF1067A87E358C5DBA2871B3D8FAAB41894495C6FAC249DC107C80097A5F4042", "contents": "E1BC8CE92B23858A2FA0093D944DEE8A08C66FFF8B799FC94595D2C18B4F4F2F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "560BB2F0F92A8DAF15FE89394733AA845880957823FC081A64233DA40F54582D", "miner": 0, "time": 9600, "proof": 0, "signature": "95E3DD10E00C916B59E9FE95894AF3EB7C9889B1FA67B6D10D549C5AF7258EE5A9E23E928B62F54EB708059DDF09D9A8"}, "hash": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D"}, {"transactions": [{"hash": "C8727936A3F41AB82E5564D94C5AAAD205F0B349470240C10752370BC8AB5591", "holders": [0]}], "elements": [], "aggregate": "B8A43C71E3F1F66E9E2D918EFF60927E85CE2A13440F3E8CAB0509DAB8A955E37ED81A3DB02F3E81F4568085B0D1CEAE", "header": {"version": 0, "last": "2B6E766AD6F81160C3FF704E9BF09B782532F54D9D9A7C785E4E421A42858C6D", "contents": "9A09BA0879AF73C8F6762D6E17B6D1CD71DF45E882D26492671D89ABB17B4C3A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "9185C45438F953385D49F221070A1FD8DA98C3A8838B6DBC5DFFB3DEE4CF2B12", "miner": 0, "time": 10800, "proof": 0, "signature": "B736988A8A41D7EE74434414B3B3FD8DAD2725D2F45E6F28D8EBD19534DA37683869BA6CCBFAA3E6E076C2FFB0712107"}, "hash": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D"}, {"transactions": [{"hash": "BBF99A9FA8B5E3EDA2A9A01E9EF0216CA25BF1944E3C7BFE07E8AB76D79BF6E8", "holders": [0]}], "elements": [], "aggregate": "B7E6334C31D2C6ABA93103690CE1B97C807241F595E3BE37C14E6404A9DA9F18041B045EDE05EA8724ECE3DBE9DC3018", "header": {"version": 0, "last": "F6E0EAA41B7DA6F0C3102A176E0B7E4A7860752E49E0CC91BFA8A1240B67417D", "contents": "3EB66D63EC0762271C12F3BE3F9F17B99A27BEEE28DEE121B43CE38E52EE9410", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "2E4AB694CFF8FAD0742A3A24E821848DA2B8E2586A9090DC5B950A18C600D554", "miner": 0, "time": 12000, "proof": 0, "signature": "83A746E816FA1390BD30698A4811A6C1D6808A58C6FA97B4FE24D53D579F94AC6B90A149C2CB81612266C8452BB65742"}, "hash": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18"}, {"transactions": [{"hash": "60F3797E55CCE4C9EDAB148470A9127AAF368647F2757A9E52312239466C8D71", "holders": [0]}], "elements": [], "aggregate": "A9BDEAE67E27D4B4CD97E39EC93207A585675C29AB60BCA0F60A0624225BD1486B2326CF8C74D21569D5DE928F0340A5", "header": {"version": 0, "last": "60D8671B22AFD8C9220DE54B04428C4DCFEA781636322A749167B7A20637FB18", "contents": "8775AF24BD4FA0DCCB52F15283F214FCF4C32A76F2E9A03C3485D471D9DFAAE6", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AF47884FAA2EA0351B28450560297631E457194F63DE3E2C531ACF628575D97D", "miner": 0, "time": 13200, "proof": 0, "signature": "A84775CC6E97C7A86AB3EC7F2F0F6CA8368569A5ACAB5697EB0035B5C6E5A1F58FCF7C9EC34178617D5815942084A99F"}, "hash": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78"}, {"transactions": [{"hash": "351A83B6BD0129252F7064B983F5C6CDFD7D854B941E911BE9874B727CD67B95", "holders": [0]}], "elements": [], "aggregate": "8391B75C4ACE226798B29B4404C035CCE5AB32CDBE61E794E9699AF6B715AD237F8252DBFBCFFB68EEF59F887AA03A5A", "header": {"version": 0, "last": "886C29ACA6510002E2FC482D4F0BFC6AF1A8BD2794AC202DD5F42E76D7040D78", "contents": "B3EB7A99EA5421D49D6D00998EE360B366F3C48E5A1D5D13D80B013B1800FCB2", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "513271EF4B1EE9E718BDD93AC973B8F35BAF74011AA76898F266F3EBCF179621", "miner": 0, "time": 14400, "proof": 0, "signature": "90D95A9689683749BF754CAD5F25D7CD556292A552AA98CE163FB16291BDC19F11BB262C3E4B42455FFDB4EAD900E010"}, "hash": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10"}, {"transactions": [{"hash": "775A77F43C5C8613C6CA27C5A200822FE7F7865EC19F05B1BDE8676A25D3761E", "holders": [0]}], "elements": [], "aggregate": "85558989C1C0061C57F816F668D1856293F350376E7D2A8DC2C2A65BB793A4B0391CE885C19F318E13E38DD91C424F83", "header": {"version": 0, "last": "595FE4B3AD8C33E0614426ECB1924CD0B9700C3A8418C67598716D3F43C23D10", "contents": "9D9AB4830989D1F9C40A92C0E69ED5BD8EF610C530C0391C180E663581534D0D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FDEEA3491C7B261560E7D340180B11DB522865602670483A0D3591F4FCB19FE8", "miner": 0, "time": 15600, "proof": 0, "signature": "ACB2C40A0BF40BC25EF6930ECD4C4A5927EF5C252DC0E2E0DEE2F6B9784032F3B3936A05BEE3396057259B765ED77391"}, "hash": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A"}, {"transactions": [{"hash": "CD083ED5396FB5CC7DD6FDC217A91579255A97099E70D94591ABA9E4CF544A4B", "holders": [0]}], "elements": [], "aggregate": "A2719780937C21736440CE95C7792C5A689F1EC60488ABBE62450BEC33BA727B9E8D0C88323BC5E730CF38AD4105AB0D", "header": {"version": 0, "last": "5C1A96B0C3F8C251E318F773EC97B34846AAD1AEC82000D7E178D58B99CC890A", "contents": "811FB083989D6585F335B0AFFDD0F166428B28603A780DD966B6CE1D54B2F6E1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A795C21BC025C44937FDEE5BDDE19DFE543B7997675D26A4C7BA034FED5419F4", "miner": 0, "time": 16800, "proof": 0, "signature": "97D2DC9E1B5454886C2C7A6AC13C06D0ADE94F33CF0B127407642A4019E9ADEBD0028BF7FF46A77AACC5736B0F2BA4E4"}, "hash": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD"}, {"transactions": [{"hash": "CA17D0EE821EA30021EA4637FE0E07F205710142CDE20F7DA9E5E8E6B40C2AEB", "holders": [0]}], "elements": [], "aggregate": "AA3FF79A19A97594CD2BEC4231E7EF8B445C3BED12AF224FA6F46F49234AF116046700B8C51AC62182C86C567C232AF8", "header": {"version": 0, "last": "7619B8A92A123EED87E4FFEC8379126A10A5538F6134B00E391B96A83948DFDD", "contents": "A031FAA34DB6E84F0B327B9ACFCDA17725CE6E0CA7C17B4F7111B89069D6E39B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "32E686539EB9152C9F37A1E1786C5493B08385491CF00DA633100F4A63C62901", "miner": 0, "time": 18000, "proof": 0, "signature": "AEFB5604981AA60CDB8F7A0472A0F4CBF21B957A403A5F34B8AD1292363D697A7475D9D6574E996F0B71C37B95C93F8D"}, "hash": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615"}, {"transactions": [{"hash": "0C7486E9CEB3030F18DBD9945929CF92BE3E60C8A31C997FCDEB80898A40E082", "holders": [0]}], "elements": [], "aggregate": "8A9141DA89C315F921F241ECC9346B541BC619436CBD0775C8C900A7B65AA39EA20BA65673DD27FBD1D3012650964D4F", "header": {"version": 0, "last": "C5B2EE869BFA429964903A66FFF2EF5DD5095AB8307214F1D4A109953BE09615", "contents": "5C93322261C6FF291512BEC2F9A544BC9B8CB0169A4B1A3D2B62C4995D12072C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "519724F8B3CFEA672C1B1D683FB72975563CD47F113791B8576BE16BA4B524B5", "miner": 0, "time": 19200, "proof": 0, "signature": "991C269C809FBF0ED4AB45D9758E376C71D2BFFE6D24B625CBEA88FB68A443A8E62A4A27E579827D6CB882E8D419DCF6"}, "hash": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40"}, {"transactions": [{"hash": "387FC354C6A3D12E40466B080A0CD2C3C97EF9913D892919E08B17CDEB6CFFFA", "holders": [0]}], "elements": [], "aggregate": "94516A29CB0FC365B65879E5C9F34B101C2B2BEBB228EAA2FCE79058A24C93D56C674931184F56C0FF14D5068D76B0DD", "header": {"version": 0, "last": "34CA707D3C70E140B5808550A588FF7313CD1C797B9166D463A0BD5FD206DB40", "contents": "DB352DFE3A5DDE7F6E578088377C2D94B39E231A2EA7FF8DBD88FD6DBC9CA8D7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "089B9D00365E3EAD207BEF01F083490CF1F3B1BF195CB7B1DDF5EC3E263F239A", "miner": 0, "time": 20400, "proof": 0, "signature": "8042C2B741D709FD403A830BA298FD2DF611C2CD085BE63E5E0B924D162E182A4AF287069750297CE45F3B7DBEB1E90D"}, "hash": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525"}, {"transactions": [{"hash": "5C3F6AF8E5D45304AD7F387FADF9E3AF131C56E653D65DA0510DCB4088610CAE", "holders": [0]}], "elements": [], "aggregate": "8CB2DE75C0EB4213BA9E6C68CBB839838DDCCB2F845CEEFF505F9C00704D256FE774339C1712B5106BD27D4E2E2E3B59", "header": {"version": 0, "last": "8F30880B151FCFE9C20B2C38DDB95F5966AB6E59B85DA940D8E36EE47DD2C525", "contents": "DB4F7545C4235D98A55E018A9C607A0F3006E51F0ACFB50FFD0954C5BC509A05", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "5F86DF5E696115C6B5F5B3DD88A95F0E1FBEF991C3508B2C2FF8444E58F68E22", "miner": 0, "time": 21600, "proof": 0, "signature": "AEED882FAB73397288E5CE76B611AC6DD589CE7863500ED1D95C6D4CF3EF0528BDE1CDB729D6803CDB33DE57EFD08C37"}, "hash": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C"}, {"transactions": [{"hash": "D1FC6A19E245897AB2F37E33DDA544C5C702D75CAE323C0B9576E0AA49204C17", "holders": [0]}], "elements": [], "aggregate": "B8E70BA7DD0EB28B36A147309B71F29E290D95EFC8F1A02120EDCA434ADB29AF85083E5C9994B7D79A78AF36124D2D03", "header": {"version": 0, "last": "633F9A82580AF8705069C8003E2B309353288420BF07884096D5DC8A2920214C", "contents": "6AC0A02AD6E722A8B68E9535D876F7DE9DD666EFA0C3F7965145CB588393563F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EA827E1705E6B10688301DB3BB0AFDD25E09313A96EE093616B2D2C4AD5A649C", "miner": 0, "time": 22800, "proof": 0, "signature": "A7E7BB684B26DAB5619093EACFB7D01D145C75DD4C9C2687F6E57C40D50BFCAB2910521BF09EBC56D5AF70729B940E46"}, "hash": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9"}, {"transactions": [{"hash": "4663CF896951DA74019FBB40ACA05D193FA36947DD00C6381A174EB7ED42459F", "holders": [0]}], "elements": [], "aggregate": "85FE98831A681C0052B0239CD186BA4197C0CF8680C46F3719DDB9A7F2DB016F791260DBFEA99AD55A15D8EBDE562C85", "header": {"version": 0, "last": "638E525958ED18CD91FC7C613374ABBB0D41FE14B6CE636A79956DC39D0E3AD9", "contents": "D47F54489B54352B703153B00552D3D2BFBACF08977756F475119087F9C211A7", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "AEEA9B2AB377ACF84D2D25C1CC9A1E611CA13ECB337F8A8C3892C32905275F04", "miner": 0, "time": 24000, "proof": 0, "signature": "895C44B71029343EF6D92D320ED563F3F2E96F95742EFA14A794BB3E8A036C8EDEB6027F52B97C24EC68AE8D89E3B141"}, "hash": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361"}, {"transactions": [{"hash": "2CD1A225E20AAACE9A34B0CFC2F07C9FCF80B9D4A47812CEFCB23012962BFF5D", "holders": [0]}], "elements": [], "aggregate": "AD6E8902BF0C69248E65C34683E015831102B7753E661ED26364F67B7E53C756DCCBD683C1442BD4D9664520385739F8", "header": {"version": 0, "last": "D9BBC37DD578F29221525E8A0160A8E26D6BB0F9986C5D5718A1015CB701E361", "contents": "DD85BC873116FF0253FDE52723C7F0F55EBDEE71E5990CABA412440206CC7662", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1AC6CE760921B1B70AA1575CC7A625E0FFE8073D72711E8B976322275968A9CF", "miner": 0, "time": 25200, "proof": 0, "signature": "B1DBCCBBFDB8A9F41176A91AC7FB3105FDF05246A757B7C018F0A12EDE54896F1CEA1378CD83493D31BA1FB367EFBF2C"}, "hash": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25"}, {"transactions": [{"hash": "1914829F4A76D1ED019BF9D2E3198BE0B6A1B55E0170D4EEF8D3772937799B70", "holders": [0]}], "elements": [], "aggregate": "A579D7772E589647623C357F1FA8CB2AADAA8A348389B4F7345507F29D3DD5D9D6B74D52D8504E0AA802203315B57C65", "header": {"version": 0, "last": "29E8D6125481DF76C4B6F64A689F9E56B1030CD930B66ED3C7F6D38B21666C25", "contents": "A1E5B19F192EA5BA11FB02EFC47C19731C4A61996A0DD8141E9250AFC702F42B", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E895BBBAFA1388A73917ABA96B3D276468F54CAFA23D4E1C92604C780E4FA72E", "miner": 0, "time": 26400, "proof": 0, "signature": "B541F894A8B0F540F5FDF447747BEDCF5473026EFD38775CF022A6E6E71B5621807DF1C64520F1A74261CAD9F72B0404"}, "hash": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B"}, {"transactions": [{"hash": "886F3192EF28DB8E9476A85AC3E2BA505743198602E89F72339C96A6BC6CAEC0", "holders": [0]}], "elements": [], "aggregate": "AFB23913A0589BAD19804AB1984CF2AE40067483571B598043F188482F94E4218594F963632B5401CE9165C74A564A9F", "header": {"version": 0, "last": "193F69CD56982BD21FFDC6BB0C58A48D8989D921425EEA03252FA464AED5965B", "contents": "24E54640F6DC403551054EDA610E734D6A00332BD3847DC7A7DE17EA61C58718", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "E52203148F778F29C26C4A5EED60316866F0C36F6735291D9E38DB897F83109B", "miner": 0, "time": 27600, "proof": 0, "signature": "B8A51E0BA8649D6EFF075F70E76C2879FF57F944ED6A05C424AB6861C57B7489CAB77DA3FD5FE640FBA9A8AE3D1C9E69"}, "hash": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B"}, {"transactions": [{"hash": "AA383287CBD1280772D520162020F4B89A44683FE12BFCBFB2203EA7A25D7D17", "holders": [0]}], "elements": [], "aggregate": "83275ECE29DA906B17730B1D83E30182D7A54B58FAEEE123F4C7C49F863C1A5AD0C46F5D61A80198B06054EE647AAEF1", "header": {"version": 0, "last": "0E13D107BC22326E67BF2A57C680918A24BDB61A099A8BFB98694CF6DD72D73B", "contents": "1D4FCE21C0FE6A24C386D40C92D26F0E8B4AB1CA1D52E281F552B6E8B31E6803", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "ADEEF2E532DFBF8B41384A61173FD3FF6BE2542080B106FA57CE6C546E4E176B", "miner": 0, "time": 28800, "proof": 0, "signature": "925B65382C0A8B26393EBE08F8B72E4BBD45E275CFC749A163858A582D9DD474EA60A527671D1280D7B76B4B9232BFF5"}, "hash": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF"}, {"transactions": [{"hash": "C9A65D759E9D37FF276B2733376F82140763F6FE90D2E2C6040A0BB62EF51152", "holders": [0]}], "elements": [], "aggregate": "AD3406EACE13826010E20C65D38AD688FCC28B24B3A7B78B9459AFDE89E5E831EE789B971B5175B0ED83E4C70AA42710", "header": {"version": 0, "last": "822D83EF4B30D3F7A753CB93936D5C08557A2B1BD2836F54A63B42D55D9690EF", "contents": "B69147F0EEF64CEE66828986C8C9F06364FFEBC61C56680FF1659CCAF985CA9D", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8C9811D818346EE6C40848C9F8B15D1E61227639EA2F0F1DD896AA3DA2C44441", "miner": 0, "time": 30000, "proof": 0, "signature": "A2AD4BE21A833707E3F42DF55E4300089C4FADCA6010008501BB4133F90793653B6C7E8D45DE7A320428A75F69DFD980"}, "hash": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583"}, {"transactions": [{"hash": "7ECE5339A3ADF8C9A93E348BE975C2161BB2CA99B0A95D5E7367E94534E1EF89", "holders": [0]}], "elements": [], "aggregate": "89E22645E0F5509B5AB620C71D146E3863209BB1FEAA1A98A2AA44685A788AC81B72F0791CFEB627E1BB33EB6767F198", "header": {"version": 0, "last": "0ACBB6C7BB24D3B165BC48DD945E20757DC333EC16CDD7BE112604599192A583", "contents": "B684AEBF86A1F120E5A44A8C14A159973288246CCC77B44F7E554A5D0D8D67AB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "BE619866422407EDECCCF01D40CFEA4CD97B108AD4AC8A859565954F80355E4B", "miner": 0, "time": 31200, "proof": 0, "signature": "83693BA208148B3BBB92BA670BD6320EB343B3998064F3C65D1D3FFFDF34625B0B63875C37808597BD1B151B35CE64BB"}, "hash": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB"}, {"transactions": [{"hash": "7B0498874B886C771502AE3A40F848F706C8B412517C33DFCDE0A3EE24896C3D", "holders": [0]}], "elements": [], "aggregate": "9777F1ECC189743CF8495F1593A4B9ECBA8E24F0E377F238AFF267B00451FA24A2F55BA867A5A3C5D2A7724C500A5CA4", "header": {"version": 0, "last": "B1C48CE0AD5840C96EE8D5FF90030798399FCE6402D477C7261655B7014D81BB", "contents": "380410891EE3A5845B0438A8C2281B92E902D94B4FE4F7A9F383849718D21168", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "FCFD4349689F789D33570A5B0A42CA821F6FF8F320D1A700ABD3B538FEB25466", "miner": 0, "time": 32400, "proof": 0, "signature": "8A858B58B9C089077FE125A55B2EE89F9629E460FF628545677881F14AB623AB673D8E50A70924EE2359307DE4462A6E"}, "hash": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C"}, {"transactions": [{"hash": "CA1CE7CDF5B2F3128DFC1050B240E48E13D313D951FD20BD60F1C89C71227EEB", "holders": [0]}], "elements": [], "aggregate": "9789EFEAEAD52C813157D9E7ED237FA6217BFD92C98C6AF10E75040A090CD184B97F0BA67E1B561971DA0C3128D3DADE", "header": {"version": 0, "last": "78A5ED8DA5C76A771866928AD934342C52659E529BA69600687A51CECBDBD81C", "contents": "628BD47A0591ADBDC780A7ECC8C86977DB29493E072761E00C64B51AC92E0732", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B87CF0B569E2F08B381753FA8F6359AD314319056E14BD28CB129D457949A14", "miner": 0, "time": 33600, "proof": 0, "signature": "AC71514D519AE5B9CA1E5B9C90940FBFAB7AF802B4928CB661040D82B5DA6CBDC96515140EC07297EE7292F4F40091EF"}, "hash": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A"}, {"transactions": [{"hash": "17F10521957248191BFEDAF36E6543FC8FFE61D10644E037E93F3A555443A9FB", "holders": [0]}], "elements": [], "aggregate": "B97955DE18257B2B74B54F2F56C4E0F34A11AE75F9AF7F1E9BD8EC66017F15962264DC654D23C69BE97931976A4C35B2", "header": {"version": 0, "last": "10AAAC87311C0E9AD4C452AE0821FA5DC9037F2781982E66C15DDAED226FFC6A", "contents": "1375723AD673ED9974F0B5CAE015F691581759434D076DA469E2AACADFF37F1A", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "17B2C89050F788BFD1134009C1A396C2EDC2F8919F11497A7F6F9BB9FC88B780", "miner": 0, "time": 34800, "proof": 0, "signature": "B711DDFDF6717189817C83AF1D760045F6BD9A928EF1F3C8B514766781138741C6301B5FC430BC7832A7AF4530CA3A97"}, "hash": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42"}, {"transactions": [{"hash": "503D0FE35CFDC7F3A15DFCBEFB1AE55495EB1FEC723591E023B6C854D55E494B", "holders": [0]}], "elements": [], "aggregate": "A08D9E3C46934EAF57AB1D31EDFCCD0C031A9D8E35638631E76963CD8420B1B44F60FFCEEE88EFFAE22EAD3D435FFC4B", "header": {"version": 0, "last": "8502C37A14E061C67263AEA6D3661D268B25FD8D870E4CD4427074FEFE8F8E42", "contents": "BDCD84DC8A3C6E41FB8D432F2F69E2565BDB20CDEA60048355C0572F19FE2C69", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EDAE53D97A0A81B7D39FD161435E0BE196D5DC8E860DA6F1A6D3E9DA87956CE9", "miner": 0, "time": 36000, "proof": 0, "signature": "B912FBA94F16BB9AF34DE53A7A65532E3AD5841862EE34C85172F85C3ED55BF0F8C136BF4114298B86E19A5BA7105910"}, "hash": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096"}, {"transactions": [{"hash": "CF82B3A47ACB0B38E9FA9A8A8FD1C89E3D0023F016A2F207D629D6FB3AC0FC2B", "holders": [0]}], "elements": [], "aggregate": "93996B99767DEBFB239E3FA3ECA1A901D1D07401ED3BB751878D8C9127ED58EE981A0A2E9A25BFE577E03707089ED000", "header": {"version": 0, "last": "BD9B0597993B72592D980A97E3E1C07DD7884258C6EA612ADC50FF04ACA3D096", "contents": "6B58318212B14ACDD98D0EE097D5614FD5CD21AF968A0AB538D6AC3A00AA3C67", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F98042571452CA386BD6BDF2638A6BD9ABE56627AFEFB49DBA2A41FCD9474745", "miner": 0, "time": 37200, "proof": 0, "signature": "ADE7BBD187AD8E9625230E92F813E5BFD19FF8FE000FF96E29129CF7498C6066A4E992EA82D21979D20CFA6B8A4A0F90"}, "hash": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F"}, {"transactions": [{"hash": "EFD9B2A90A15401AB3D1AAC9262E70C9B9AE448AC7560E96A7766449AE5446D2", "holders": [0]}], "elements": [], "aggregate": "93926EAD07F7A93C23990C6A41F23FA3288232F741C97D29182E0AFE90C3E6DFFF1E54EEF666F78FDE513945407E285B", "header": {"version": 0, "last": "565CEC02ABA4F43FD527F407AE86985D2646136289BE598DDB0286FAB7DDA00F", "contents": "37CD8023C91A9ADF1512D69A23B77A418478C6F6EF66D966182C1E5C95F092AC", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7D2362B3488EC1A041755C7A14B20463502292442C4E2FE96222B264E43985E3", "miner": 0, "time": 38400, "proof": 0, "signature": "A92DABCDB4078E26DA81109E38AD82397555543264028383E94DDE5DB9BD5079B8385329759F28D3D81A21E620E51DDF"}, "hash": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92"}, {"transactions": [{"hash": "768D69AFADCC7A29E23663E8A66EB8D7CC1563079A216EAA29B9783DD528DB8A", "holders": [0]}], "elements": [], "aggregate": "AF48FB26E57F2D619487811009F1E43A50E073A439DE7DBF899F9E158F8C6F0B6432B2754347C1CE6510E538C5D47291", "header": {"version": 0, "last": "3061E0B25C57CABF778FC08D21E329649B6D70617FA832DAB41CAB696AE24A92", "contents": "44571AF7F71017BC02764E5416B9E8CCFF1597E3480143808E1BF8E5F9B8023F", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F0FD8CC4454CF3F1E0B3636CD046FABC7291F16D2D0A7054C8C3100E10D8BEB1", "miner": 0, "time": 39600, "proof": 0, "signature": "A84C6D840E538E70EA4AC4831853F3EE70A384DEEA885EBC0684248DD1905C7FD6465E37E6B187A10C1E084249BA1D17"}, "hash": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436"}, {"transactions": [{"hash": "5028DDEEEE12CC6FB156E713F8446325DBE6DAF87C4926EC9E12E8B028DE5433", "holders": [0]}], "elements": [], "aggregate": "B2C2CD38A31D99177B263A311BD14718B1012F75987B8A653A1C2C20FC239A86F7597F2BD9CF50784B8BF4559740F126", "header": {"version": 0, "last": "0B3ADCD0D6B9901CA62FCF7957F11A6B395446A0E84ACC1A8684717DCE92C436", "contents": "A20B36DA35E654AC8C2DE99EE1915EE1ADF583F8F0EBEDBAF6DD4BF4EF2F5382", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "B5802C1E8CE29BD6FBCA50DCB4506A2563CC030A453D971594DBB27E72D8E6FE", "miner": 0, "time": 40800, "proof": 0, "signature": "AC4C3BE796F9B6978079119F47E4A2616787EA843A54015A85D4F357A1A62DDE1380105BC8C86389A892A8B934349D30"}, "hash": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E"}, {"transactions": [{"hash": "01FA1B676C326343DF4B03F3840DE9383EAF0EAA13898A278029EF821E459089", "holders": [0]}], "elements": [], "aggregate": "91B51AFF6A3F9FB9BADBDCCECCE5093F80F83067D51E9CE660368B89F0C6B200A6ECF0D2E38BB0B9D90ACE4E3FBC4F41", "header": {"version": 0, "last": "84551F762E18C162A6420945D56937C043D8B9FC062FAF994AE17CE7D7958E7E", "contents": "4E18ECCBF34A734136250BB94C7EC609FD424D1443860FE53A082FF59A8B87F0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "239668D5D0544F01BE9E0C39363AF70E78335F388495EC1BAF779425A09E3B71", "miner": 0, "time": 42000, "proof": 0, "signature": "8EB2D10893C3DF1B5331E4ED390AA2C44AED0B48FA0DC8A1329C797ECD561A10A6D0548B77A03942D8356C43B392FB94"}, "hash": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65"}, {"transactions": [{"hash": "1D932D69B193AD9246D4B61F946270940083FA5ED35459F4ABF41DB8F1C888BB", "holders": [0]}], "elements": [], "aggregate": "B2D9926E7F67112737ABB78DC0526F3F3FD02005AA984C9C33F9B53FEE2BBB70E6370570C511A0966C21AEAB46592D35", "header": {"version": 0, "last": "055C3A544A894A6F9962B96848E1D80BF7ABD88E62686745512770CE25936A65", "contents": "0F6821A9C7D8A2005B6BA5FAE7B2A60EE0809E53AEA2540A269B4EAA01EBB12E", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "655CDDB0EAB8FCC0A36AFB7D970980E30CD65830CB7DD3C4A43D6F19A8669D05", "miner": 0, "time": 43200, "proof": 0, "signature": "8EC7D88E37A2490D7954DB86D6A7A67E887E0D6D5186CACCB3F189E4520CCA2623F72C234C1D0162C48709EDDE583CE2"}, "hash": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B"}, {"transactions": [{"hash": "52A09B2A03602D5536DEA58861D24E61D50F9756D9F402D9BC80D092194C8914", "holders": [0]}], "elements": [], "aggregate": "980FA53F64DF42DE83F30EEF35F1ECFABDD1E650C96BC7E9BCC485C1A0190F48D08C2FD98D8C2D8A13FA3CB9CE598D5E", "header": {"version": 0, "last": "5765FE67A9E714950E85D5D5AF628A838567CBD1654EDD844AAE04CB6CA0551B", "contents": "63DF40FF9CBAD4F329AF28C1E9D32D6AD817AB330962FC2A22A0E0747B5E70BA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C05C3FCAC02C04C52FF67D092421BDCB1087040F479181B4CABA1D7CC971E77E", "miner": 0, "time": 44400, "proof": 0, "signature": "98F92E503B9FDBDF51AC6022913D5ADB6EEE9A46475E19F6EA3D14B89AA7E3A4E0410646360C852B86E94A6BD94DDA59"}, "hash": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785"}, {"transactions": [{"hash": "715C4EB143D757F3D9578085A9A493B62A12E63979F981FF71D7D0E5797F963D", "holders": [0]}], "elements": [], "aggregate": "838886F6A7C95D91E0046704A5829BFE80953CA62CAFA25F4966B912C188B30FB29565C3DC959A371C27D1A67D802411", "header": {"version": 0, "last": "E5FD39FE3B827E8ABE665BD859C62E79F683F8FF30B067075CA380B474468785", "contents": "76CB3B34A2ABBE67A018D9329412A4A76D67838D06308E4029B3A8FAA78F6692", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "75E79993E688F0BABEBA4E8AF7EAB2D23BA0B20E35B53C4352FF25464FBC2305", "miner": 0, "time": 45600, "proof": 0, "signature": "82E559E958A3630F5B240B0F82DD0D7C8B786ED0889DFEA7AFF0074DF3A2342E77D9400CE8BA83F1A8101EAE45F9C0BB"}, "hash": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3"}, {"transactions": [{"hash": "76CF7E02283072268B70821E34C60372B84B3CFBE8FB315B3540CD7FA48FF2D9", "holders": [0]}], "elements": [], "aggregate": "A5C043E438254B6D82445D3D5858F7E37873B1178A2FE8E0B7ABCF5903EAA1F7955711A64F16C4F9B2A5D5AB7AA91DFB", "header": {"version": 0, "last": "ED159280BAEEFCD40543C29C35A26440C7D3E62C0031FD59017FC60F1793E8F3", "contents": "05ECD46C6FFDB6CE064B1A7BDE217199694C779906269E29B686AAC2ECD4AFBB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "EB0E425AFDDEC9A6511202015E98197A4B5FDC8F8E0EA83B5F6E9F091486F308", "miner": 0, "time": 46800, "proof": 0, "signature": "8148B9722127E142AF9014C182B3924EC4D2187B6B56A50D9B679C856864DE5A210F372B410DFCAE53D237F7F5B3B50E"}, "hash": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34"}, {"transactions": [{"hash": "9B8A47FC44420DB73CF46F0168B53CF93D495CBBAA9B36A9913298B8648574FD", "holders": [0]}], "elements": [], "aggregate": "84A52DB751EEB72E1D9F9DA54509134F7F5F54C409C77343E78925583E5FD1209E66DDDEAACDD60ADFE3E133C266307F", "header": {"version": 0, "last": "0C6549FFAD6529DF9C7E33F68FED7995944665977A44E93DBD723DD7EA266F34", "contents": "0843B460D5841D73032DCCA971D096930EABEB343420D5CE790A0D79BC4577F1", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7228382536A5A5B26CD248EA461978371776D786A4574023AED45FA7EC07CFBE", "miner": 0, "time": 48000, "proof": 0, "signature": "925EACECFEC61C713E1B5E07DD7B21A5D9BD75177E6AD97ADB5D93FEAF358975D334E738609C390220EEA72B514EF93C"}, "hash": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF"}, {"transactions": [{"hash": "A8B87D0CE244374CCC914F0086D2A5ED6C7D7947F76AF07518BC17DADFE61B99", "holders": [0]}], "elements": [], "aggregate": "9197DFD18F76C8574169D64E947A07C61AF53BF4334972DFFDC81765D7B0B80E11A7421755BABF67CC78B51F645F20C7", "header": {"version": 0, "last": "21FA8EAAB5452A91386743309D3D444214EF6896BFF94AF0E53B5D48FBEE6ADF", "contents": "F33F7C3B8383B04FB3458B3F09135AFB25821E0988D835E9E02065EA10613DFF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "1B4CBCDBD72F25DFDEEE604A1402555B911270C58AD3EEE05B5339FB47709AB0", "miner": 0, "time": 49200, "proof": 0, "signature": "978D37E5B2262D811FFA5D2551C4F6812E448E8652942414981F161EC81B30C97C55F55782088F0798FCFFE6718A67E2"}, "hash": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553"}, {"transactions": [{"hash": "0DEC8740B4E9BC79F54827B7F179BB804866B62DD8FB89D5A3AAE91127179B0C", "holders": [0]}], "elements": [], "aggregate": "A1927B616F7FCBD04954B26122881383B90A18153EA39FF2B85C5006243E8F1EABE29BD9640044B2B157FE130D5710FD", "header": {"version": 0, "last": "06BE3CFE8AEE76F848DCF85548EFAF47D450D1F8E2E76AE06CA99D475194F553", "contents": "FD65AB96924037AE17DE36A779438D3E0F1D5B6AB86EB13294B4B74B0B3EBCFA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "81F9805AC3B6E2A7E2334F535FA802F144B11C2FCF36D7225354E6889B4B8362", "miner": 0, "time": 50400, "proof": 0, "signature": "A15685AFE8A8ABF35FE4E8DBC290284AB2941066A73AA8BE763E5A489F1FCF9EC6B425F892720726A2A3B7514A93FDAF"}, "hash": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D"}, {"transactions": [{"hash": "93851C8F4DD5348A50EB02FFA01D6948CB6E16819C0D7D13EB144281C17A0751", "holders": [0]}], "elements": [], "aggregate": "840A00EC75C90EAB1F50EA4718C87F189E21551BDA4C507058EBD3BAFFD916051688EC9E8DFD92A3F240BA27B474ADCE", "header": {"version": 0, "last": "338A1398CAEDD5723713AD5AED2A4C88B70AB6A39F25D27ABF344C0CF6E7A59D", "contents": "6EC6E7C25E62DD005D6FB65250FAB452453D9D9AD4CA93121C1275FFA8E4B207", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "79929DBA45BB3E810CB2847915609D2D9E3CEB998F2B09F6EB0B5E90C9D497F9", "miner": 0, "time": 51600, "proof": 0, "signature": "808F073633DF55ADBCD93035466E9254300AF1518E923477B6FC5D483CC8A2C75D87E6FF24CEE061FED050BE3CCE244E"}, "hash": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D"}, {"transactions": [{"hash": "0B85FF7C665D0229B14C2FCEB168D27E30A0E955B6BF5176EA9AE549042A2011", "holders": [0]}], "elements": [], "aggregate": "A03EED165302DE3DCE12645AF9FF5D2618B23454AD8D2429CFC582C5F9E8EDB6CB63B60DD0F0FC5A2AB1F44C7CAF4340", "header": {"version": 0, "last": "EAC191A77606FEF6DF79CA62A078177B0585F6A11678441173F17503EF76578D", "contents": "D81C83FD51D107CD3F0020796868A59557628C9B6D62693ADCB6E3A4328159FD", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8415D8191D3B91B5B7ED68975872BE5EC17F030C2CA2EB056F84294E0382FA6A", "miner": 0, "time": 52800, "proof": 0, "signature": "ABAEBE124340D09ED8D202418FC060174C432D383F36FC5D25F3C0E81DD694CF74DA28AF1458A55693E3FFCD916684A1"}, "hash": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3"}, {"transactions": [{"hash": "F4001DCD38B0E23768108C1F0CEC8FAA1777AE0FD4EEB38E6DD3E0CDC9735696", "holders": [0]}], "elements": [], "aggregate": "A1DED17B59BFE2F34FB6011D9FAA2F60B5D557895BCB1B2D927B021E21EB22F7DEB115A78B350D486014ADDFF69EF391", "header": {"version": 0, "last": "ED57BBEC2B184223E254958950984DC6B90B9021E65D2CDA0413AFFC0D69BBB3", "contents": "BBF99503895A2E37FE5599ABDB7C1C1672D003666B29E9F28BDEC380E35EF040", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8B7E79ABB5483F92115182B9B5CDC30709D8192DE966E8B831E4A6314BD3D5D8", "miner": 0, "time": 54000, "proof": 0, "signature": "8DE8C11F66CDD2B75998A8936778DC624C382ACBEBFDFE322BEC0904F730F033AFE70748AE98F49308C3CE3D3E2981A7"}, "hash": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8"}, {"transactions": [{"hash": "602BA0D211013BDE8EB9619F8431EBDAE3BD4B5FD09F4266B19432A6AC6DDBC8", "holders": [0]}], "elements": [], "aggregate": "A3737B6AD6E12FB918672E22099A7C113A9590787723EBD5D387B7871EB60D424A8450B271DFFAAEEAF2C375E4D403FB", "header": {"version": 0, "last": "558F3C165A57F581A36777CD3F176192CA41BBC5DDEDCBF01C2403981183E8D8", "contents": "478094914295E4311699033104C5BAB93F471B4EA137209F7DC3B10E97678068", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8D97A09A1AD26F8A100AB558630D930F60A42500F5AB309609ADC50B7F9EF07C", "miner": 0, "time": 55200, "proof": 0, "signature": "8C36F0B44D5120FBB910D3CE312F9BA135CF19ABD39D84EC5560642EF1FD4BAED46957CE94DBCC1B4FA7C2BC736CD0E0"}, "hash": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3"}, {"transactions": [{"hash": "2FD1583B1B31B0FE60632F556903257FC3734E39AEC237EDB5CCE784777A1A6B", "holders": [0]}], "elements": [], "aggregate": "B60B0A0084D20E0CCACCF0797884A1DBFB10EAA1D634584D0BE0299343285D137EF5CCE91DFB9B4D1D52791BD98FC40E", "header": {"version": 0, "last": "EA4C0A3C1C21B8AC4652575CEE781C19B8BD7DA2CDDFBE6FF358FB37F307FAD3", "contents": "6EF462AF646189E7E659FB67F082F67442B1ACB3D1439EF97B410C8D94975815", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "8337618DDD6041028BE2E2F00A9D6592E0C80F6900F1DD5772856A59ED589736", "miner": 0, "time": 56399, "proof": 0, "signature": "819709ED53A6705EAEC12E6A2CD8D043261CB63B86E8EC649BD6710CC63273422EAD54DEF5AB8C7E4176E3B467D1981F"}, "hash": "39B31BA925FC2B941F6AAE16845F59033300EA3802D1E712C35A33C724691F73"}, {"transactions": [{"hash": "D16A4D797778E28E2A502564C36341DE7B1EC19AD559A0A9531C60C957119A95", "holders": [0]}], "elements": [], "aggregate": "827754AEEDD5FBE55D2699F85BF40F2ADDC0BAE621241E12BC8475F533D361A9B60BB86202F7A06D28E96CE342AF114C", "header": {"version": 0, "last": "39B31BA925FC2B941F6AAE16845F59033300EA3802D1E712C35A33C724691F73", "contents": "301E4960B4684F82A494B52471014512F7D112A31B2A480C508994F9AC617BFF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "048F0D2121E9792F0FCB066D47AB58B7E2808B4FE65839297A8DFA59308E3373", "miner": 0, "time": 57597, "proof": 0, "signature": "99A989F912E11D279D3B8FF95883534442BD30F238942FF5CE80B25D1419BF1311736B5763721070B63F8860EB68EF5B"}, "hash": "A8917347690CBF080881C502316566BB7FAA200E4B7AD8A89A70383777F419C7"}, {"transactions": [{"hash": "C617C3A1B1F023031E594814D2719D583239E3963C32C8B263C96E482B318136", "holders": [0]}], "elements": [], "aggregate": "A36AC381FD736E0C1DB95FA868D80B0CACB6F9C63C706C11893A076F005AF4D03F6536AF5F7FD1CA89679DC3D1B41ABA", "header": {"version": 0, "last": "A8917347690CBF080881C502316566BB7FAA200E4B7AD8A89A70383777F419C7", "contents": "7C459C7C07D924F9064A94ECC7985C17E0259CB91816DC9A2DD1D8642BF1FDAB", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "7F1DED0B838DFCCD578A58916E8ECE31CDEA9C7C3EDC0275CC9B7AA812FECD3F", "miner": 0, "time": 58794, "proof": 0, "signature": "93F3E4D6414BA7608016CAADB0161660AE33FA705FC41DC251A60108EBA4CB06B403330F484C46D835A446E60D5F0E9D"}, "hash": "57E7B90B70F241328B6E144747FF7EA9617EF18490FEC3F26AED1A857BBEC4CA"}, {"transactions": [{"hash": "AF84761088CE38F92985486285AA936D25EA5E66BA4777B9B4EC67199BE2377A", "holders": [0]}], "elements": [], "aggregate": "840E0E5BD8C88088D566C4E14ABFFBC09A3876ADBF3A67078FF35F4576DC313A6671DBE32F4EC337499A7CF1AB647F23", "header": {"version": 0, "last": "57E7B90B70F241328B6E144747FF7EA9617EF18490FEC3F26AED1A857BBEC4CA", "contents": "288F5987030EA0645CB577CB457D24A7EFB61009D9CCFC9D7A8907B5CE3F64FA", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "6EE1A47A5220FA45442C3B35BEBEA009FDB32DA90731F685CC232DB6281B7A82", "miner": 0, "time": 59990, "proof": 0, "signature": "B03A3DCBF8A1FB916A5198B0954FD5F2A5F9F3299D939E8A97CCBDBF7FBE02EF45A3996D47BCDAC889699A2AD02D0571"}, "hash": "9AEF387366FB38F413622E80D9612CB6DBC9F8A7A81100F0843C5E43374C0CC4"}, {"transactions": [{"hash": "44EAC5BA1597F80834041856BD1A606D833D6BC6FC3CBB578715F3C77C0E430D", "holders": [0]}], "elements": [], "aggregate": "B48E8DD21C67DBEE6ECD1F883D51FC167E76444A1EDA1FA5AB8D90FC23A6EB27BDF0E1B7541F48DF54F75B004134DC9B", "header": {"version": 0, "last": "9AEF387366FB38F413622E80D9612CB6DBC9F8A7A81100F0843C5E43374C0CC4", "contents": "8BDF0A5A38D3437FBF374B552DB3B905A467501759E7D01795CFFA2DECE87384", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "CF368235CAD1ED14263CF89F4F76878A47A2C21A3A1B06E293268F86393FD6A7", "miner": 0, "time": 61185, "proof": 0, "signature": "AEFAA6DF6CA145788D3BA7E1CB14AB6AD9C545303EEBFAE341AB6342D83BEA255C793ECFC4D23AAE3796323C5368AC5B"}, "hash": "AD1B774E34650A5473100F207E9555038D49898E3FE69A3810BDE27FE2C5D89D"}, {"transactions": [{"hash": "B8A27B655BAA4169B6F1BF366BB2F140D36CC8712C309E3AD39DB38B21D042EB", "holders": [0]}], "elements": [], "aggregate": "B3B60666140E492DD5A5506FD21A67AFD8CDA2BE21D5E4C08C4131A9CB65947F5AE05049DB2F30ECDECBD1FD50E108D9", "header": {"version": 0, "last": "AD1B774E34650A5473100F207E9555038D49898E3FE69A3810BDE27FE2C5D89D", "contents": "09C7E878DB9CB247FF0F6256A972060E1A803095C19D86E9D577668D0D7126D0", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "CC8F1DEDADA9EE529E6F1F09426CE62651BA2B4887C841DA2C8BE479E1F203B8", "miner": 0, "time": 62379, "proof": 0, "signature": "AE5FA414FD8464C0994FEBACCEF33029F58C5DF85CC77D954F6B047B272526F5C12E347C923835EAD7181ABEDF6C1E16"}, "hash": "F22D70F21DCE181A142BF4D2DBFAF3A1F67647B31C169E1CF19B144E71C1C4E6"}, {"transactions": [{"hash": "C6AE256DBC414BE2170F7CB3EB7D42A3DB3D667876792DA9CA5CB63782FEB63F", "holders": [0]}], "elements": [], "aggregate": "B7F916D307913D8EE69532D4742866A9525BF663FA3FECF64DB11F00519B408A86BBE9304FB96298903D3A7D4FD41F05", "header": {"version": 0, "last": "F22D70F21DCE181A142BF4D2DBFAF3A1F67647B31C169E1CF19B144E71C1C4E6", "contents": "D8AB94509D9353FE6040DFAD31F4DDE784FF13AAD8693C67EC88FF03EF7B8DCF", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "F0A3F4A0708DB8EE7835AA9F85201B3932391498A726447B03F2376E41AC2DAE", "miner": 0, "time": 63572, "proof": 0, "signature": "80B584A7038C00C11412C89D9CDE063CA19D8FD7D3E77E57B0416724590EA49876F956970AB71E0E4791BBF6C8E65537"}, "hash": "CA665B9232DAE4D09676D1B5BD33ABB0FD7047BE7587974D441B05C63D10C88A"}, {"transactions": [{"hash": "BF4BF9F6E68B4A219D00CE05ECB24974B94157C91EEF1DBB4FC391D64E98ABF7", "holders": [0]}], "elements": [], "aggregate": "89E8BF5ED10DF7684A08C203BF57E4064EAF59D573191C80383D6B225EED276F82B51C1FDA05025E355B0B641793041B", "header": {"version": 0, "last": "CA665B9232DAE4D09676D1B5BD33ABB0FD7047BE7587974D441B05C63D10C88A", "contents": "C8024B621D762431982F3F9AE4DCEB274D2EB822E5C25FB642F4AB90BD7D389C", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "C0F93253B47C703A63E4CCD26372DE679BA3145F52E2A5CE18B73CE12F0E351D", "miner": 0, "time": 64764, "proof": 0, "signature": "B1594FCE6BA9C2C4D45C7286527D753D62E523B55C9A775ACB331C2A705DE3697B873A8C7D5E8ACD44C4F1896214C182"}, "hash": "E6D39869513D084BCB182FDFED1142961DB252C1C8B77CBB6FCDB99F68FCA468"}, {"transactions": [{"hash": "F4947153B5E58BDE9BD8539164F6343FB650EB2B838E5A161790549566829182", "holders": [0]}], "elements": [], "aggregate": "87DF0E54D61FCDA831C0174161F80EB0A209C4BEF8C7421D98151B5D437859AE4D7BAAA867C09098C2303BCCADF39D09", "header": {"version": 0, "last": "E6D39869513D084BCB182FDFED1142961DB252C1C8B77CBB6FCDB99F68FCA468", "contents": "726F44A3547839636C102860656F754813C2A0C67DF797D6BD87CDFEFB6C0207", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A68A57F357AAE63B0430D67FECC9D8B5CBCA78119D4989C2A9D96272E580D6A7", "miner": 0, "time": 65955, "proof": 0, "signature": "822564C3F6164CF64644E13E65F6D6011ED0B1AC6F95C3E4AC317E00CFE51096EA7A384C4C7191A63F29F6B4BD13BB2A"}, "hash": "D51B5947E945E397C08651B0C1FD74BFB5BCF36EE588B79BC5DBB8BFCE91F110"}, {"transactions": [{"hash": "6DA6641F2E311BE34DFAA0084CAE5F366CE00768F2DBCC2F15588A2E0323FC0E", "holders": [0]}], "elements": [], "aggregate": "8CD7693F6EB3F7D030788E523AB9D4A96D3710FF569F5A3BC7EFB422548FACCBBA36AA362122019232A3E4796A4960BC", "header": {"version": 0, "last": "D51B5947E945E397C08651B0C1FD74BFB5BCF36EE588B79BC5DBB8BFCE91F110", "contents": "230B7810D94E96C09015F4F04C6D55D0590E1515E1971E35CE0E29DE40A9C6BC", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "91013FEB54FF4BBCAE0A861D294D7A4451789CBE8F721528E81915E0852F7221", "miner": 0, "time": 67145, "proof": 0, "signature": "A3E83C2C21D354A32AE92A1FB3AFD0C1A39B36B03324E79EA1BA22B1F9E0C9D0A015E2C2F60489D81A5E2B799DB82B04"}, "hash": "B66AC4E5AA44C921059ED9A22976F60D60F5A5B5D33BFB34CC6E37DDE093569E"}, {"transactions": [{"hash": "2B4241FA7E0C95661ED94F7D47631B26D0A50B4B662F4F893D032E1B8B43CB04", "holders": [0]}], "elements": [], "aggregate": "8968B8054C565D10E5AF688581A35E666E93D4BAAC344D2BC7C0681F71ABBC3C49DC2877B2319DED9438A676DAB9FD9A", "header": {"version": 0, "last": "B66AC4E5AA44C921059ED9A22976F60D60F5A5B5D33BFB34CC6E37DDE093569E", "contents": "47ADFEEEDE0AA76F3DEA62EEFA0030BCE465A726BA6E3808A99165FCAF9C5CE6", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "BB48978168A402C2B6375840F7B5807785F79FE572978FD4E8BDADBCF412DB7E", "miner": 0, "time": 68334, "proof": 0, "signature": "A2B0ED45E09D4E7B5414CB825DDCC8309C90CC3F44E31B635D914D4427232EBC4196E86BE793597D82AF66DA248BBA18"}, "hash": "3B3768AFF63D6D45AE34722D833E1E17842D626FEF64F44D6C17D314AAB4FCDE"}, {"transactions": [{"hash": "91ABEF222673835953358B6987D640A6CDC20E7E8E378A9A97FCB0376CE238B3", "holders": [0]}], "elements": [], "aggregate": "B9218F42969794368DFB27B8692A49DEA6A744ECF74B378FF55EE48B86156B80F5D54D7A34976C604097A1252B2901FE", "header": {"version": 0, "last": "3B3768AFF63D6D45AE34722D833E1E17842D626FEF64F44D6C17D314AAB4FCDE", "contents": "9AACC5935E2217D467182741787B5D38CB8DAA36C95760226A4312BB1EEB1DBD", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "A0E379EA9BEEE232CBB7F1660085CD3B8DA338CD74AA1D2D3C441291C37DFD73", "miner": 0, "time": 69522, "proof": 0, "signature": "A3D3FB9B964F7192E33F0BFDB4DC83B21B3354E0C6D42D44031C9CC0602597C6FCF1063F98A996AF5F3A783CB5AD8A52"}, "hash": "84A450235379E147EB06FDCB6E55800DEDC8C575B17F5EA79358D8AD695A9827"}, {"transactions": [{"hash": "891B0F955CC5304958B954C5304520F52AE236091BBE1720454A70E2B7759559", "holders": [0]}], "elements": [], "aggregate": "9605EA95953942EA05001FD40544CA5B3C54A7DCC0F3892F5198FCB26E41287B21871B731955649A99F5A817F1B2F38F", "header": {"version": 0, "last": "84A450235379E147EB06FDCB6E55800DEDC8C575B17F5EA79358D8AD695A9827", "contents": "DFFCFF5CEEF4B46E22C99DA607204072F6FE4DDBC0EC1BF5C555A3CBCD201D35", "packets": 1, "sketchSalt": "00000000", "sketchCheck": "465F49E3B306C52179149BDE4A76288C97EC631BBACAF57BE0B8E5A662B02239", "miner": 0, "time": 70709, "proof": 0, "signature": "8DD98B7A27C9348124FDAB5FBF8317F736FC0404F5B4601978DBE8F46A3100FE5411459C2E8E08585A2D58FF876C7CF1"}, "hash": "C5B00DDFF903B1D6C899AB52BDFFCFCB5E78CAEDC6FE8CB460A61367C372E0B9"}]} \ No newline at end of file diff --git a/e2e/requirements.txt b/e2e/requirements.txt index 4efe66a6f..56d029fe4 100644 --- a/e2e/requirements.txt +++ b/e2e/requirements.txt @@ -1,6 +1,8 @@ argon2-cffi==19.1.0 ed25519==1.5 bech32==1.2.0 +bip-utils==1.7.0 +requests==2.25.1 pytest==5.3.5 pytest-xdist==1.32.0 filelock==3.0.12 diff --git a/samples/DBDumpSample.nim b/samples/DBDumpSample.nim old mode 100755 new mode 100644 index ca45f12eb..ca37579f4 --- a/samples/DBDumpSample.nim +++ b/samples/DBDumpSample.nim @@ -1,41 +1,61 @@ import strutils import sets import json +import httpclient -import asyncdispatch - -import MerosRPC +type MerosError = object of CatchableError var - rpc: MerosRPC = waitFor newMerosRPC() db: JSONNode = %* { "blockchain": [], "transactions": {} } hashes: HashSet[string] = initHashSet[string]() +proc call( + module: string, + methodStr: string, + params: JSONNode = %* {} +): JSONNode = + var + client: HttpClient = newHttpClient() + res: JSONNode = parseJSON( + client.request( + "http://localhost:5133", + "POST", + $ %* { + "jsonrpc": "2.0", + "id": 0, + "method": module & "_" & methodStr, + "params": params + } + ).body + ) + if res.hasKey("error"): + raise newException(MerosError, "RPC threw an error: " & $res["error"]) + result = res["result"] + client.close() + #Get every Block. -for nonce in 0 ..< waitFor rpc.merit.getHeight(): - db["blockchain"].add(waitFor rpc.merit.getBlock(nonce)) +for nonce in 0 ..< call("merit", "getHeight").getInt(): + db["blockchain"].add(call("merit", "getBlock", %* {"block": nonce})) #Get the matching Mint. try: db["transactions"][ db["blockchain"][db["blockchain"].len - 1]["hash"].getStr() - ] = waitFor rpc.transactions.getTransaction( - parseHexStr(db["blockchain"][db["blockchain"].len - 1]["hash"].getStr()) - ) + ] = call("transactions", "getTransaction", % {"hash": db["blockchain"][db["blockchain"].len - 1]["hash"]}) #This will raise if there wasn't a Mint for this Block. except MerosError: discard #Mark every Transaction so we can grab them later. for tx in db["blockchain"][db["blockchain"].len - 1]["transactions"]: - hashes.incl(parseHexStr(tx["hash"].getStr())) + hashes.incl(tx["hash"].getStr()) #Get every Transaction. for hash in hashes: - db["transactions"][hash.toHex()] = waitFor rpc.transactions.getTransaction(hash) + db["transactions"][hash.toHex()] = call("transactions", "getTransaction", %* {"hash": hash}) #Write it to a file. "data/db.json".writeFile(db.pretty(4)) diff --git a/samples/RPCSample.nim b/samples/RPCSample.nim old mode 100755 new mode 100644 index 8e3d0c5c7..94310cbd4 --- a/samples/RPCSample.nim +++ b/samples/RPCSample.nim @@ -3,41 +3,53 @@ import strutils import tables import json -import asyncdispatch -import asyncnet +import httpclient #Argument types. -const ARGUMENTS: Table[string, seq[char]] = { +const ARGUMENTS: Table[string, seq[(string, char)]] = { "merit_getHeight": @[], "merit_getDifficulty": @[], - "merit_getBlock": @['i'], + "merit_getBlock": @[("block", 'i')], + "merit_getPublicKey": @[("nick", 'i')], + "merit_getNickname": @[("key", 'b')], "merit_getTotalMerit": @[], "merit_getUnlockedMerit": @[], - "merit_getMerit": @['i'], + "merit_getMerit": @[("nick", 'i')], - "merit_getBlockTemplate": @['b'], - "merit_publishBlock": @['i', 'b'], + "merit_getBlockTemplate": @[("miner", 'b')], + "merit_publishBlock": @[("id", 'i'), ("header", 'b')], "consensus_getSendDifficulty": @[], "consensus_getDataDifficulty": @[], - "consensus_getStatus": @['b'], + "consensus_getStatus": @[("hash", 'b')], - "transactions_getTransaction": @['b'], - "transactions_getUTXOs": @['s'], - "transactions_getBalance": @['s'], - "transactions_publishSend": @['s'], + "transactions_getTransaction": @[("hash", 'b')], + "transactions_getUTXOs": @[("address", 's')], + "transactions_getBalance": @[("address", 's')], - "network_connect": @['s', 'i'], - "network_getPeers": @[], + "transactions_publishTransaction": @[("type", 's'), ("transaction", 'b')], + "transactions_publishTransactionWithoutWork": @[("type", 's'), ("transaction", 'b')], - "personal_getMiner": @[], - "personal_setMnemonic": @['s', 's'], - "personal_getMnemonic": @[], - "personal_getAddress": @[], + "network_connect": @[("address", 's'), ("port", 'i')], + "network_getPeers": @[], + "network_broadcast": @[("transaction", 'b')], #Skips over Block rebroadcasting as it should be extremely rarely needed. - "personal_send": @['s', 's'], - "personal_data": @['s'], + "personal_setWallet": @[("mnemonic", 's'), ("password", 's')], + "personal_setAccount": @[("key", 'b'), ("chainCode", 'b')], + + "personal_getMnemonic": @[], + "personal_getMeritHolderKey": @[], + "personal_getMeritHolderNick": @[], + "personal_getAccount": @[], + "personal_getAddress": @[], + + "personal_send": @[("outputs", 'j'), ("password", 's')], + "personal_data": @[("data", 's'), ("password", 's')], + + "personal_getUTXOs": @[], + "personal_getTransactionTemplate": @[("outputs", 'j')], #Using this via the RPCSample would be incredibly pointless. + #That said, it is an RPC method, and could be used to demonstrate WatchWallet functionality. "system_quit": @[] }.toTable() @@ -48,16 +60,13 @@ proc readLine(): string = result.removeSuffix(Whitespace) var - client: AsyncSocket = newAsyncSocket() port: int = 5133 payload: JSONNode = %* { "jsonrpc": "2.0", "id": 0, - "params": [] + "params": {} } p: int = 1 - res: string - counter: int = 0 if paramCount() != 0: if (paramStr(p) == "-h") or (paramStr(p) == "--help"): @@ -65,7 +74,6 @@ if paramCount() != 0: Meros RPC Sample. Parameters can be specified via command line arguments or the interactive prompt. - ./build/Sample ./build/Sample ... ./build/Sample @@ -94,24 +102,34 @@ if paramCount() >= p: quit(1) while p <= paramCount(): - case ARGUMENTS[payload["method"].getStr()][payload["params"].len]: + let + fieldInfo: (string, char) = ARGUMENTS[payload["method"].getStr()][payload["params"].len] + fieldName: string = fieldInfo[0] + case fieldInfo[1]: of 's': - payload["params"].add(% paramStr(p)) + payload["params"][fieldName] = % paramStr(p) of 'b': try: - payload["params"].add(% parseHexStr(paramStr(p)).toHex()) + payload["params"][fieldName] = % parseHexStr(paramStr(p)).toHex() except ValueError: echo "Non-hex value passed at position ", p, "." quit(1) of 'i': try: - payload["params"].add(% parseInt(paramStr(p))) + payload["params"][fieldName] = % parseInt(paramStr(p)) except ValueError: echo "Non-integer value passed at position ", p, "." quit(1) + of 'j': + try: + payload["params"][fieldName] = parseJSON(paramStr(p)) + except ValueError: + echo "Non-JSON value passed at position ", p, "." + quit(1) + else: doAssert(false, "Unknown argument type declared.") inc(p) @@ -135,16 +153,17 @@ if not payload.hasKey("method"): #If the arguments weren't specificed via the CLI, get it via interactive prompt. if payload["params"].len == 0: for arg in ARGUMENTS[payload["method"].getStr()]: - case arg: + let fieldName: string = arg[0] + case arg[1]: of 's': echo "Please enter the next string argument for this method." - payload["params"].add(% readLine()) + payload["params"][fieldName] = % readLine() of 'b': echo "Please enter the next binary argument for this method as hex." while true: try: - payload["params"].add(% parseHexStr(readLine()).toHex()) + payload["params"][fieldName] = % parseHexStr(readLine()).toHex() break except ValueError: echo "Non-hex value passed. Please enter a hex value." @@ -153,33 +172,35 @@ if payload["params"].len == 0: echo "Please enter the next integer argument for this method." while true: try: - payload["params"].add(% parseInt(readLine())) + payload["params"][fieldName] = % parseInt(readLine()) break except ValueError: echo "Non-integer value passed. Please enter an integer value." + of 'j': + echo "Please enter the next JSON argument for this method." + while true: + try: + payload["params"][fieldName] = parseJSON(readLine()) + break + except ValueError: + echo "Non-JSON value passed. Please enter a JSON value." + else: doAssert(false, "Unknown argument type declared.") -#Connect to the server. -echo "Connecting..." -waitFor client.connect("127.0.0.1", Port(port)) -echo "Connected." - -#Send the JSON. -waitFor client.send($payload) -echo "Sent." - -#Get the response back. -while true: - res &= waitFor client.recv(1) - if res[^1] == res[0]: - inc(counter) - elif (res[^1] == ']') and (res[0] == '['): - dec(counter) - elif (res[^1] == '}') and (res[0] == '{'): - dec(counter) - if counter == 0: - break - +#Connect to the server, send the JSON, and get the response back. +var + client: HttpClient = newHttpClient() + headers: HttpHeaders = newHttpheaders() +headers["Authorization"] = "Bearer " & readFile("data/e2e/.token") +let res: JSONNode = parseJSON( + client.request( + "http://localhost:" & $port, + "POST", + $ payload, + headers + ).body +) echo res +client.close() diff --git a/samples/config.nims b/samples/config.nims old mode 100755 new mode 100644 index edf939711..de2b899a1 --- a/samples/config.nims +++ b/samples/config.nims @@ -1,6 +1,3 @@ -#Necessary flags. -switch("threads", "on") - #Optimize for size (which is faster than `opt=speed` for Meros (at least on x86_64)). switch("opt", "size") diff --git a/src/Database/Consensus/objects/ConsensusObj.nim b/src/Database/Consensus/objects/ConsensusObj.nim index 9ce8d903d..a47aa781d 100644 --- a/src/Database/Consensus/objects/ConsensusObj.nim +++ b/src/Database/Consensus/objects/ConsensusObj.nim @@ -427,7 +427,7 @@ proc finalize*( try: if consensus.getStatus(input.hash).merit == -1: - raise newException(UnfinalizedParents, "Family requires another family to finalize first.") + raise newLoggedException(UnfinalizedParents, "Family requires another family to finalize first.") except IndexError as e: #This would generally be a panic, yet there is a single case where it isn't. #If this input is a Mint, it won't have a status. diff --git a/src/Database/Filesystem/DB/TransactionsDB.nim b/src/Database/Filesystem/DB/TransactionsDB.nim index ea489c3a2..c811eea77 100644 --- a/src/Database/Filesystem/DB/TransactionsDB.nim +++ b/src/Database/Filesystem/DB/TransactionsDB.nim @@ -49,6 +49,11 @@ template OUTPUT( ): string = input.serialize() +template USED_KEY( + key: EdPublicKey +): string = + key.serialize() & "uk" + template DATA_SENDER( hash: Hash[256] ): string = @@ -158,6 +163,10 @@ proc save*( for o in 0 ..< tx.outputs.len: db.put(OUTPUT(tx.hash, o), tx.outputs[o].serialize()) + #Save Ed keys as used if they're sent to. + if (tx of Claim) or (tx of Send): + db.put(USED_KEY(cast[SendOutput](tx.outputs[o]).key), "") + proc mention*( db: DB, hash: Hash[256] @@ -225,6 +234,16 @@ proc load*( claim.outputs[0].amount = amount +proc loadIfKeyWasUsed*( + db: DB, + key: EdPublicKey +): bool {.forceCheck: [].} = + try: + discard db.get(USED_KEY(key)) + result = true + except DBReadError: + result = false + proc isBeaten*( db: DB, hash: Hash[256] @@ -294,13 +313,19 @@ proc loadSpendable*( raise newLoggedException(DBReadError, e.msg) for i in countup(0, spendable.len - 1, 33): - result.add( - newFundedInput( - spendable[i ..< i + 32].toHash[:256](), - int(spendable[i + 32]) - ) + let input: FundedInput = newFundedInput( + spendable[i ..< i + 32].toHash[:256](), + int(spendable[i + 32]) ) + #Spendable isn't guaranteed consistency for a few reasons. This manifests as spent Transactions reappearing. + #Without a lot more tracking code, which would be decently intensive, it can't be made consistent. + #That said, by following up with this check, it can be. + #Theoretically, we could also move this check to addToSpendable, yet addToSpendable is caused for all UTXOs. + #loadSpendable is solely triggered via the RPC and therefore should run much more infrequently. + if db.loadSpenders(input).len == 0: + result.add(input) + proc addToSpendable( db: DB, key: EdPublicKey, @@ -328,18 +353,20 @@ proc removeFromSpendable( except DBReadError: return - #Remove the specified output. - for o in countup(0, spendable.len - 1, 33): + #Remove all occurrences of the specified output. + var o: int = 0 + while o < spendable.len: if spendable[o ..< o + 33] == output: - db.put(SPENDABLE(key), spendable[0 ..< o] & spendable[o + 33 ..< spendable.len]) - break + spendable = spendable[0 ..< o] & spendable[o + 33 ..< spendable.len] + continue + o += 33 + db.put(SPENDABLE(key), spendable) -#Add the Transaction's outputs to spendable while removing spent inputs. +#Add the Transaction's outputs to spendable. proc verify*( db: DB, tx: Transaction ) {.forceCheck: [].} = - #Add spendable outputs. if (tx of Claim) or (tx of Send): for o in 0 ..< tx.outputs.len: db.addToSpendable( @@ -348,28 +375,12 @@ proc verify*( o ) - if tx of Send: - #Remove spent inputs. - for input in tx.inputs: - var key: EdPublicKey - try: - key = db.loadSendOutput(cast[FundedInput](input)).key - except DBReadError: - panic("Removing a non-existent output.") - - db.removeFromSpendable( - key, - input.hash, - cast[FundedInput](input).nonce - ) - -#Add a inputs back to spendable while removing unverified outputs. +#Removes outputs from spendable. proc unverify*( db: DB, tx: Transaction ) {.forceCheck: [].} = if (tx of Claim) or (tx of Send): - #Remove outputs. for o in 0 ..< tx.outputs.len: db.removeFromSpendable( cast[SendOutput](tx.outputs[o]).key, @@ -377,21 +388,6 @@ proc unverify*( o ) - #Restore inputs. - if tx of Send: - for input in tx.inputs: - var key: EdPublicKey - try: - key = db.loadSendOutput(cast[FundedInput](input)).key - except DBReadError: - panic("Restoring a non-existent output.") - - db.addToSpendable( - key, - input.hash, - cast[FundedInput](input).nonce - ) - #Mark a Transaction as beaten. proc beat*( db: DB, @@ -436,19 +432,6 @@ proc prune*( db.put(OUTPUT_SPENDERS(tx.inputs[i]), spenders) - #If we were the only spender of this output, restore the output as spendable. - if (spenders.len == 0) and (tx of Send): - try: - db.addToSpendable( - cast[SendOutput]( - db.load(tx.inputs[i].hash).outputs[cast[FundedInput](tx.inputs[i]).nonce] - ).key, - tx.inputs[i].hash, - cast[FundedInput](tx.inputs[i]).nonce - ) - except DBReadError: - panic("Couldn't load the Transaction the Transaction we're pruning spent.") - for o in 0 ..< tx.outputs.len: #Delete its outputs and their spenders. db.del(OUTPUT(hash, o)) @@ -459,6 +442,9 @@ proc prune*( discard db.del(OUTPUT_SPENDERS(newFundedInput(hash, o))) - #If it has no spenders and is tracked by spendable, remove it. - if (spenders == "") and (tx.outputs[o] of SendOutput): + #If it is tracked by spendable, remove it. + #Should be handled by unverify, so I'm not really sure why this is here. + #That said, it doesn't hurt to have, and it's safe to run even if the outputs aren't present in spendable. + #-- Kayaba + if tx.outputs[o] of SendOutput: db.removeFromSpendable(cast[SendOutput](tx.outputs[o]).key, hash, o) diff --git a/src/Database/Filesystem/Wallet/WalletDB.nim b/src/Database/Filesystem/Wallet/WalletDB.nim index e1077334c..ab61c97a6 100644 --- a/src/Database/Filesystem/Wallet/WalletDB.nim +++ b/src/Database/Filesystem/Wallet/WalletDB.nim @@ -1,12 +1,15 @@ +import options +import sets import tables import mc_lmdb import ../../../lib/[Errors, Util, Hash] -import ../../../Wallet/[MinerWallet, Wallet, HDWallet] +import ../../../Wallet/[MinerWallet, Wallet, Address] import ../../Transactions/objects/TransactionObj import ../../Transactions/Data as DataFile +import ../../Transactions/objects/TransactionsObj import ../../Merit/objects/EpochsObj @@ -17,6 +20,34 @@ import ../DB/Serialize/Transactions/[DBSerializeTransaction, ParseTransaction] template MNEMONIC(): string = "w" +template MINER_KEY(): string = + "m" + +template ACCOUNT_ZERO(): string = + "az" + +template CHAIN_CODE(): string = + "cc" + +template NEXT_ADDRESS_INDEX(): string = + "na" + +template CHANGE_INDEX(): string = + "c" + +template ADDRESS_CHANGE( + key: EdPublicKey +): string = + "ac" & key.serialize() + +template ADDRESS_INDEX( + key: EdPublicKey +): string = + "i" & key.serialize() + +template ADDRESSES(): string = + "a" + template DATA_TIP(): string = "d" @@ -26,9 +57,6 @@ template DATA_TX( ): string = "d" & hash.serialize() -template MINER_KEY(): string = - "m" - template MINER_NICK(): string = "n" @@ -58,22 +86,44 @@ type lmdb: LMDB - wallet*: Wallet + mnemonic: Mnemonic miner*: MinerWallet + accountZero*: EdPublicKey + chainCode*: Hash[256] when defined(merosTests): + nextIndex*: uint32 + changeIndex*: uint32 + addresses*: HashSet[uint32] + finalizedNonces*: int unfinalizedNonces*: int verified*: Table[string, int] elementNonce*: int else: + nextIndex: uint32 + changeIndex: uint32 + addresses: HashSet[uint32] + finalizedNonces: int unfinalizedNonces: int verified: Table[string, int] elementNonce: int + KeyIndex* = object + change*: bool + index*: uint32 + + UsableInput* = object + index*: KeyIndex + key*: EdPublicKey + address*: string + utxo*: FundedInput + +const ADDRESS_DISCOVERY_THRESHOLD: int = 10 + proc put( db: WalletDB, key: string, @@ -117,8 +167,9 @@ proc del( var tx: LMDBTransaction = db.lmdb.newTransaction() db.lmdb.delete(tx, "", key) tx.commit() - except Exception as e: - panic("Couldn't delete data from the Database: " & e.msg) + #Data doesn't exist. + except Exception: + discard proc commit*( db: WalletDB, @@ -177,8 +228,10 @@ proc newWalletDB*( lmdb: newLMDB(path, size, 1), - wallet: newWallet(""), - miner: newMinerWallet(), + mnemonic: newWallet("").mnemonic, + nextIndex: 0, + changeIndex: 0, + addresses: initHashSet[uint32](), finalizedNonces: 0, unfinalizedNonces: 0, @@ -186,31 +239,56 @@ proc newWalletDB*( elementNonce: 0 ) + result.miner = newMinerWallet(result.mnemonic.unlock("")[0 ..< 32]) + let wallet: HDWallet = newWallet(result.mnemonic.sentence, "").hd[0] + result.accountZero = wallet.publicKey + result.chainCode = wallet.chainCode result.lmdb.open() except Exception as e: raise newLoggedException(DBError, "Couldn't open the WalletDB: " & e.msg) - #Load the Wallet. + #Load the Wallets. try: - result.wallet = newWallet(result.get(MNEMONIC()), "") + let mnemonic: string = result.get(MNEMONIC()) + if mnemonic == "": + result.mnemonic = nil + else: + result.mnemonic = newMnemonic(mnemonic) + + let minerKey: string = result.get(MINER_KEY()) + if minerKey == "": + result.miner = nil + else: + result.miner = newMinerWallet(minerKey) + + result.accountZero = newEdPublicKey(result.get(ACCOUNT_ZERO())) + result.chainCode = result.get(CHAIN_CODE()).toHash[:256]() + result.nextIndex = cast[uint32](result.get(NEXT_ADDRESS_INDEX()).fromBinary()) + result.changeIndex = cast[uint32](result.get(CHANGE_INDEX()).fromBinary()) + + let addresses: string = result.get(ADDRESSES()) + result.addresses = initHashSet[uint32]() + for i in countup(0, addresses.len - 1, 4): + result.addresses.incl(cast[uint32](addresses[i ..< i + 4].fromBinary())) except ValueError as e: panic("Failed to load the Wallet from the Database: " & e.msg) - except DBReadError: - result.put(MNEMONIC(), $result.wallet.mnemonic) - - #Load the MinerWallet. - try: - result.miner = newMinerWallet(result.get(MINER_KEY())) except BLSError as e: panic("Failed to load the MinerWallet from the Database: " & e.msg) except DBReadError: + result.put(MNEMONIC(), $result.mnemonic) result.put(MINER_KEY(), result.miner.privateKey.serialize()) + result.put(ACCOUNT_ZERO(), result.accountZero.serialize()) + result.put(CHAIN_CODE(), result.chainCode.serialize()) + result.put(NEXT_ADDRESS_INDEX(), 0.toBinary()) + result.put(CHANGE_INDEX(), 0.toBinary()) + result.put(ADDRESSES(), "") - try: - result.miner.nick = uint16(result.get(MINER_NICK()).fromBinary()) - result.miner.initiated = true - except DBReadError: - discard + if not result.miner.isNil: + try: + result.miner.nick = uint16(result.get(MINER_NICK()).fromBinary()) + result.miner.initiated = true + except DBReadError: + discard #Load the input nonces. try: @@ -254,23 +332,308 @@ proc close*( except Exception as e: raise newLoggedException(DBError, "Couldn't close the WalletDB: " & e.msg) -#Set the Wallet's mnemonic. -proc setWallet*( +#Meant to encourage the non-usage of direct access by defining a getter returning its string form. +proc getMnemonic*( + db: WalletDB +): string {.forceCheck: [ + ValueError +].} = + if db.mnemonic.isNil: + raise newException(ValueError, "This is a WatchWallet node; no Mnemonic is set.") + result = $db.mnemonic + +proc getPublicKey*( db: WalletDB, - mnemonic: string, - password: string -) {.forceCheck: [ + index: Option[uint32], + used: proc ( + key: EdPublicKey + ): bool {.gcsafe, raises: [].} +): EdPublicKey {.forceCheck: [ ValueError ].} = - if mnemonic.len == 0: - db.wallet = newWallet(password) + var + external: HDPublic + child: HDPublic + #Get the external chain. + try: + external = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(0) + except ValueError as e: + panic("WalletDB has an unusable Wallet: " & e.msg) + + #Get the child. + if index.isSome(): + try: + child = external.derivePublic(index.unsafeGet()) + except ValueError as e: + raise e + + #Explicitly track this address. + db.addresses.incl(index.unsafeGet()) + try: + db.put(ADDRESSES(), db.get(ADDRESSES()) & index.unsafeGet().toBinary(INT_LEN)) + except DBReadError: + db.put(ADDRESSES(), index.unsafeGet().toBinary(INT_LEN)) else: try: - db.wallet = newWallet(mnemonic, password) + child = external.next(db.nextIndex) except ValueError as e: raise e - db.put(MNEMONIC(), $db.wallet.mnemonic) + #This will return the same address we returned last time. + #We want to do that UNLESS this address was used in the mean time. + #We also don't want to return this address if it was explicitly requested. + while child.key.used or db.addresses.contains(child.index): + #Track the used address. + if not db.addresses.contains(child.index): + db.addresses.incl(child.index) + try: + db.put(ADDRESSES(), db.get(ADDRESSES()) & child.index.toBinary(INT_LEN)) + except DBReadError: + db.put(ADDRESSES(), child.index.toBinary(INT_LEN)) + + try: + child = external.next(child.index + 1) + except ValueError as e: + raise e + + #Update the index in use. + db.nextIndex = child.index + db.put(NEXT_ADDRESS_INDEX(), db.nextIndex.toBinary()) + + db.put(ADDRESS_INDEX(child.key), child.index.toBinary()) + result = child.key + +proc getAddress*( + db: WalletDB, + index: Option[uint32], + used: proc ( + key: EdPublicKey + ): bool {.gcsafe, raises: [].} +): string {.forceCheck: [ + ValueError +].} = + try: + result = newAddress(AddressType.PublicKey, db.getPublicKey(index, used).serialize()) + except ValueError as e: + raise e + +proc getChangeKey*( + db: WalletDB, + used: proc ( + key: EdPublicKey + ): bool {.gcsafe, raises: [].} +): EdPublicKey {.forceCheck: [].} = + var + internal: HDPublic + child: HDPublic + #Get the internal chain. + try: + internal = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(1) + except ValueError as e: + panic("WalletDB has an unusable Wallet: " & e.msg) + + #Get the child. + try: + child = internal.derivePublic(db.changeIndex) + except ValueError as e: + panic("Either first or last change index was invalid: " & e.msg) + + while child.key.used: + inc(db.changeIndex) + try: + child = internal.derivePublic(db.changeIndex) + except ValueError: + continue + + db.put(ADDRESS_CHANGE(child.key), "") + db.put(ADDRESS_INDEX(child.key), db.changeIndex.toBinary()) + db.put(CHANGE_INDEX(), db.changeIndex.toBinary()) + result = child.key + +proc getKeyIndex*( + db: WalletDB, + key: EdPublicKey +): KeyIndex {.forceCheck: [ + IndexError +].} = + var change: bool + try: + discard db.get(ADDRESS_CHANGE(key)) + change = true + except DBReadError: + change = false + + try: + return KeyIndex( + change: change, + index: cast[uint32](db.get(ADDRESS_INDEX(key)).fromBinary()) + ) + except DBReadError: + raise newLoggedException(IndexError, "Asked for the address index of an address which doesn't belong to this Wallet.") + +#Clear the Miner and Mnemonic. +proc clearPrivateKeys*( + db: WalletDB +) {.forceCheck: [].} = + db.miner = nil + db.mnemonic = nil + +#Set the Miner and Mnemonic. +#Must have setAccount called after to commit it, not to mention finishing updating the RAM of the WalletDB. +#Only currently valid as Meros is single threaded. +proc setMinerAndMnemonic*( + db: WalletDB, + wallet: InsecureWallet +) {.forceCheck: [].} = + try: + db.miner = newMinerWallet(wallet.mnemonic.unlock(wallet.password)[0 ..< 32]) + except BLSError as e: + panic("Couldn't create a MinerWallet out of a 32-byte secret: " & e.msg) + db.mnemonic = wallet.mnemonic + +#Set the account. +proc setAccount*( + db: WalletDB, + key: EdPublicKey, + chainCode: Hash[256], + datas: seq[Data], + used: proc ( + key: EdPublicKey + ): bool {.gcsafe, raises: [].} +) {.forceCheck: [].} = + db.accountZero = key + db.chainCode = chainCode + + var items: seq[tuple[key: string, value: string]] = @[] + + #Save the Mnemonic. + if db.mnemonic.isNil: + items.add((key: MNEMONIC(), value: "")) + else: + items.add((key: MNEMONIC(), value: $db.mnemonic)) + + #Save the miner. + if db.miner.isNil: + items.add((key: MINER_KEY(), value: "")) + else: + items.add((key: MINER_KEY(), value: db.miner.privateKey.serialize())) + + #Save the account key and chain code. + items.add((key: ACCOUNT_ZERO(), value: db.accountZero.serialize())) + items.add((key: CHAIN_CODE(), value: db.chainCode.serialize())) + + #Recover the addresses. + db.nextIndex = 0 + db.addresses = initHashSet[uint32]() + + var + lastUsedIndex: uint32 = 0 + usedAny: bool = false + buffer: seq[string] = @[] + addressIndexes: seq[uint32] = @[] + flag: bool + + proc discoveryUsed( + key: EdPublicKey + ): bool {.gcsafe, forceCheck: [].} = + if key.used: + usedAny = true + lastUsedIndex = db.nextIndex + + #Return true the first time to get the next address. + result = flag + flag = false + + block outer: + while true: + flag = true + try: + buffer.add(db.getAddress(none(uint32), discoveryUsed)) + except ValueError as e: + panic("Tried to set a wallet which has billions of addresses used: " & e.msg) + try: + items.add((ADDRESS_INDEX(newEdPublicKey(cast[string](buffer[^1].getEncodedData().data))), db.nextIndex.toBinary())) + except ValueError as e: + panic("Generated an invalid address: " & e.msg) + + addressIndexes.add(db.nextIndex) + #If the buffer has overflown, remove the oldest entry. + #Moving to a queue would be optimal. + if buffer.len > ADDRESS_DISCOVERY_THRESHOLD: + buffer.delete(0) + addressIndexes.delete(0) + + #If the buffer is filled, and every address is used, break. + if buffer.len == ADDRESS_DISCOVERY_THRESHOLD: + for address in buffer: + try: + if newEdPublicKey(cast[string](address.getEncodedData().data)).used: + break + elif address == buffer[^1]: + break outer + except ValueError as e: + panic("getAddress returned an invalid address: " & e.msg) + + if usedAny: + #Set the index to the index after the last used index. + try: + db.nextIndex = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(0).next(lastUsedIndex + 1).index + + #Prune the last addresses as they're all unused. + for index in addressIndexes: + db.addresses.excl(index) + except ValueError as e: + panic("Could discover addresses yet couldn't get the next address: " & e.msg) + else: + db.nextIndex = 0 + db.addresses = initHashSet[uint32]() + + #Save the address data. + var addresses: string = "" + for address in db.addresses: + addresses &= address.toBinary(INT_LEN) + items.add((key: NEXT_ADDRESS_INDEX(), value: db.nextIndex.toBinary())) + items.add((key: ADDRESSES(), value: addresses)) + + #Also recover the change index. Thankfully, this is a much simpler algorithm. + db.changeIndex = 0 + while true: + try: + let key: EdPublicKey = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(1).next(db.changeIndex).key + if not key.used: + break + items.add((ADDRESS_CHANGE(key), "")) + items.add((ADDRESS_INDEX(key), db.changeIndex.toBinary())) + #An unusable key and a used key are the same here. + except ValueError: + discard + inc(db.changeIndex) + items.add((CHANGE_INDEX(), db.changeIndex.toBinary())) + + #Set the Datas. + for data in datas: + items.add((key: DATA_TX(data.hash), value: data.serialize())) + + #Update the Data tip. + if datas.len != 0: + items.add((key: DATA_TIP(), value: datas[0].hash.serialize())) + else: + items.add((key: DATA_TIP(), value: "")) + + #Actually commit all of this. + db.put(items) #Set our miner's nick. proc setMinerNick*( @@ -282,10 +645,66 @@ proc setMinerNick*( db.put(MINER_KEY(), db.miner.privateKey.serialize()) db.put(MINER_NICK(), nick.toBinary()) +proc unlock( + db: WalletDB, + password: string +): HDWallet {.forceCheck: [ + ValueError +].} = + if db.mnemonic.isNil: + raise newException(ValueError, "This is a WatchWallet node; no Mnemonic is set.") + try: + result = newHDWallet(SHA2_256(db.mnemonic.unlock(password)).serialize())[0] + if result.publicKey != db.accountZero: + raise newException(ValueError, "") + except ValueError: + raise newLoggedException(ValueError, "Invalid password.") + +#Doesn't return a usable HDWallet; that's just the generic private key + public key struct at this point. +proc getAggregateKey*( + db: WalletDB, + indexes: seq[KeyIndex], + password: string +): HDWallet {.forceCheck: [ + ValueError, + IndexError +].} = + var + wallet: HDWallet + internal: HDWallet + external: HDWallet + try: + wallet = db.unlock(password) + except ValueError as e: + raise e + + try: + internal = wallet.derive(1) + external = wallet.derive(0) + except ValueError as e: + panic("Unlocked the Wallet, yet it's unusable: " & e.msg) + + var keys: seq[EdPrivateKey] = @[] + for index in indexes: + try: + keys.add( + if index.change: + internal.derive(index.index).privateKey + else: + external.derive(index.index).privateKey + ) + except ValueError: + raise newLoggedException(IndexError, "Key index isn't valid.") + + result = HDWallet( + privateKey: keys.aggregate() + ) + result.publicKey = result.privateKey.toPublicKey() + proc stepData*( db: WalletDB, + password: string, dataStr: string, - wallet: HDWallet, difficulty: uint16 ) {.forceCheck: [ ValueError @@ -293,9 +712,21 @@ proc stepData*( var tip: Hash[256] data: Data + wallet: HDWallet + try: - tip = db.get(DATA_TIP()).toHash[:256]() + wallet = db.unlock(password).derive(0).first() + except ValueError as e: + raise e + + try: + let storedTip: string = db.get(DATA_TIP()) + #Length is 0 when a new Wallet without Datas is set. + if storedTip.len == 0: + raise newException(DBReadError, "") + tip = storedTip.toHash[:256]() except DBReadError: + #If there isn't a data tip, create the initial Data. try: data = newData(Hash[256](), wallet.publicKey.serialize()) except ValueError as e: @@ -305,9 +736,8 @@ proc stepData*( db.put(DATA_TX(data.hash), data.serialize()) db.put(DATA_TIP(), data.hash.serialize()) tip = data.hash - except ValueError as e: - panic("WalletDB didn't save a 32-byte hash as the Data tip: " & e.msg) + #Create this Data. try: data = newData(tip, dataStr) except ValueError as e: @@ -324,7 +754,10 @@ iterator loadDatasFromTip*( tip: Hash[256] done: bool = false try: - tip = db.get(DATA_TIP()).toHash[:256]() + let storedTip: string = db.get(DATA_TIP()) + if storedTip.len == 0: + raise newException(DBReadError, "") + tip = storedTip.toHash[:256]() except DBReadError: done = true except ValueError as e: @@ -344,6 +777,86 @@ iterator loadDatasFromTip*( done = true tip = data.inputs[0].hash +proc getUTXOs*( + db: WalletDB, + transactions: ref Transactions +): seq[UsableInput] {.forceCheck: [].} = + #Get both chains. + var + internal: HDPublic + external: HDPublic + try: + internal = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(1) + + external = HDPublic( + key: db.accountZero, + chainCode: db.chainCode + ).derivePublic(0) + except ValueError as e: + panic("WalletDB has an unusable Wallet: " & e.msg) + + for address in db.addresses: + var child: HDPublic + try: + child = external.derivePublic(address) + except ValueError as e: + panic("WalletDB has an unusable address: " & e.msg) + + for utxo in transactions[].getUTXOs(child.key): + result.add(UsableInput( + index: KeyIndex( + change: false, + index: address + ), + key: child.key, + address: newAddress(AddressType.PublicKey, child.key.serialize()), + utxo: utxo + )) + + #Get the UTXOs for the current address which generally isn't part of db.addresses. + #There is one known edge case to this, where an implicitly returned address is then explicitly returned. + #This wider check is useful for guaranteeing a lack of duplication. + #Also, the above edge case is handled properly by the implicit indexing code. + if db.nextIndex notin db.addresses: + var child: HDPublic + try: + child = external.derivePublic(db.nextIndex) + except ValueError as e: + panic("WalletDB has an unusable address: " & e.msg) + for utxo in transactions[].getUTXOs(child.key): + result.add(UsableInput( + index: KeyIndex( + change: false, + index: db.nextIndex + ), + key: child.key, + address: newAddress(AddressType.PublicKey, child.key.serialize()), + utxo: utxo, + )) + + #Get change UTXOs. + #Inclusive in order to support change addresses which have been used, yet we haven't since called getChangeKey. + for address in 0 .. db.changeIndex: + var child: HDPublic + try: + child = internal.derivePublic(address) + except ValueError: + continue + + for utxo in transactions[].getUTXOs(child.key): + result.add(UsableInput( + index: KeyIndex( + change: true, + index: address + ), + key: child.key, + address: newAddress(AddressType.PublicKey, child.key.serialize()), + utxo: utxo + )) + #Mark that we're verifying a Transaction. #Assumes if the function completes, the input was used. #If the function doesn't complete, none of its data is written. diff --git a/src/Database/Merit/State.nim b/src/Database/Merit/State.nim index 6dc3abebc..11770e863 100644 --- a/src/Database/Merit/State.nim +++ b/src/Database/Merit/State.nim @@ -195,6 +195,27 @@ proc processBlock*( state.statuses[h] = MeritStatus.Unlocked state.db.appendMeritStatus(uint16(h), blockchain.height, byte(state.statuses[h])) + var + tCopy = state.total + cCopy = state.counted + pCopy = state.pending + try: + for h in 0 ..< state.merit.len: + tCopy -= state.merit[h] + if state.statuses[h] == MeritStatus.Unlocked: + cCopy -= state.merit[h] + if state.statuses[h] == MeritStatus.Pending: + cCopy -= state.merit[h] + pCopy -= state.merit[h] + except Exception as e: + panic("Exception when checking Merit status of a holder: " & e.msg) + if tCopy != 0: + panic("Total is wrong.") + if cCopy != 0: + panic("Counted is wrong.") + if pCopy != 0: + panic("Pending is wrong.") + #Save the Merit amounts for the next Block. #This will be overwritten when we process the next Block, yet is needed for some statuses. state.saveMerits() @@ -210,15 +231,14 @@ proc protocolThreshold*( #[ Calculate the threshold for an Epoch that ends on the specified Block. -This is meant to return 80% of the amount of Merit at the time of finalization. -Thanks to truncation, it returns 55% in the worst case scenario (9). -Anything below 5 would return 1, which is 25%, hence the max. +This is meant to return 67% of the amount of Merit at the time of finalization. ]# proc nodeThresholdAt*( state: State, height: int ): int {.inline, forceCheck: [].} = (max(state.loadCounted(height), 5) div 5 * 4) + 1 + #(state.loadCounted(height) * 2 div 3) + 1 proc revert*( state: var State, diff --git a/src/Database/Transactions/Transactions.nim b/src/Database/Transactions/Transactions.nim index 2cb4d3827..32500d111 100644 --- a/src/Database/Transactions/Transactions.nim +++ b/src/Database/Transactions/Transactions.nim @@ -13,7 +13,7 @@ export Transaction import objects/TransactionsObj export TransactionsObj.Transactions, `[]` -export getUTXOs, loadSpenders, getAndPruneFamilyUnsafe, `==`, verify, unverify, beat, prune +export getUTXOs, loadSpenders, loadIfKeyWasUsed, getAndPruneFamilyUnsafe, `==`, verify, unverify, beat, prune when defined(merosTests): export getSender @@ -154,9 +154,8 @@ proc add*( except DBReadError: raise newLoggedException(ValueError, "Send spends a non-existant output.") - if not senders.contains(spent.key): + if amount != 0: senders.add(spent.key) - amount += spent.amount #Subtract the amount the outpts spend. diff --git a/src/Database/Transactions/objects/TransactionsObj.nim b/src/Database/Transactions/objects/TransactionsObj.nim index 9fdefe8c1..14e6570b0 100644 --- a/src/Database/Transactions/objects/TransactionsObj.nim +++ b/src/Database/Transactions/objects/TransactionsObj.nim @@ -19,7 +19,7 @@ type Transactions* = object #Copy of the Genesis. genesis*: Hash[256] #Wallet used to sign/verify Datas created by Blocks. - dataWallet*: Wallet + dataWallet*: HDWallet #Cache of transactions which have yet to leave Epochs. transactions*: Table[Hash[256], Transaction] #Family tracker: @@ -38,7 +38,7 @@ proc getSender*( result = newEdPublicKey(data.data) else: if data.inputs[0].hash == transactions.genesis: - return transactions.dataWallet.hd.publicKey + return transactions.dataWallet.publicKey try: result = transactions.db.loadDataSender(data.inputs[0].hash) except DBReadError: @@ -116,12 +116,13 @@ proc newTransactionsObj*( ) try: - result.dataWallet = newWallet(result.db.loadDataWallet(), "") + result.dataWallet = newWallet(result.db.loadDataWallet(), "").hd except ValueError as e: panic("Couldn't reload this node's Data Wallet: " & e.msg) except DBReadError: - result.dataWallet = newWallet("") - result.db.saveDataWallet($result.dataWallet.mnemonic) + let wallet: InsecureWallet = newWallet("") + result.dataWallet = wallet.hd + result.db.saveDataWallet($wallet.mnemonic) #Load the Transactions from the DB. try: @@ -259,3 +260,9 @@ proc loadSpenders*( input: Input ): seq[Hash[256]] {.inline, forceCheck: [].} = transactions.db.loadSpenders(input) + +proc loadIfKeyWasUsed*( + transactions: Transactions, + key: EdPublicKey +): bool {.inline, forceCheck: [].} = + transactions.db.loadIfKeyWasUsed(key) diff --git a/src/Interfaces/RPC/HTTP.nim b/src/Interfaces/RPC/HTTP.nim new file mode 100644 index 000000000..5a250cd2f --- /dev/null +++ b/src/Interfaces/RPC/HTTP.nim @@ -0,0 +1,391 @@ +import strutils +import tables + +import chronos + +import ../../lib/Errors + +import objects/RPCObj + +#text/plain is a legacy format, yet it's also used when context is unavailable, which this caters to. +const JSON_MIME_TYPES: seq[string] = @["application/json", "application/*", "text/plain", "text/*", "*/*"] + +const STATUSES: Table[int, string] = { + 100: "Continue", + 200: "OK", + 400: "Bad Request", + 401: "Unauthorized", + 405: "Method Not Allowed", + #406: "Not Acceptable", + 411: "Length Required", + #412: "Precondition Failed", + 413: "Payload Too Large", + 415: "Unsupported Media Type", + #416: "Range Not Satisfiable" + 417: "Expectation Failed", + 431: "Request Header Fields Too Large", + 505: "HTTP Version Not Supported" +}.toTable() + +proc supported( + supportedTypes: seq[string], + parts: seq[string] +): string {.forceCheck: [].} = + for i in 1 ..< parts.len: + if supportedTypes.contains(parts[i].split(";")[0].toLowerAscii()): + return parts[i] + +proc sendHTTP( + socket: RPCSocket, + code: int, + body: string = "" +) {.forceCheck: [], async.} = + var res: string = "HTTP/1.1 " & $code & " " + try: + res &= STATUSES[code] & "\r\n" + except KeyError as e: + panic("Couldn't get a status's message despite having a constant table and only using a select few: " & e.msg) + + socket.headers["Content-Length"] = $body.len + #Only send these headers when there's a body to refer to. + #Especially important for Content-Type as that can be disagreeable. + if code == 200: + if not socket.headers.hasKey("Content-Type"): + socket.headers["Content-Type"] = "application/json" + socket.headers["Transfer-Encoding"] = "identity" + socket.headers["Cache-Control"] = "no-store" + #Set a connection type of close. Avoids extensive socket tracking and a few headers. + if code != 100: + socket.headers["Connection"] = "close" + for header in socket.headers.keys(): + try: + #Don't send the Connection header for the default policy. + if (header == "Connection") and (socket.headers[header] == "keep-alive"): + continue + res &= header & ": " & socket.headers[header] & "\r\n" + except KeyError as e: + panic("Couldn't get a header despite confirming its existence: " & e.msg) + res &= "\r\n" & body + + try: + await socket.send(res) + #Supposed to close after this response. + if socket.headers.hasKey("Connection") and (socket.headers["Connection"] == "close"): + socket.close() + except KeyError as e: + panic("Couldn't get the Connection header despite defining it if it didn't exist: " & e.msg) + except Exception as e: + logWarn "Couldn't send a response to a RPC client; this may supposed to be fatal", err = e.msg + try: + socket.close() + #Move on. + except Exception: + discard + +proc httpStatus( + socket: RPCSocket, + code: int +) {.forceCheck: [], async.} = + if code == 401: + socket.headers["WWW-Authenticate"] = "Bearer realm=\"\", charset=\"UTF-8\"" + elif code == 405: + socket.headers["Allow"] = "HEAD, GET, POST" + + try: + await socket.sendHTTP(code) + except Exception as e: + panic("sendHTTP threw an Exception despite not naturally throwing anything: " & e.msg) + +template writeHTTP*( + socket: RPCSocket, + json: string +): Future[void] = + mixin sendHTTP + socket.sendHTTP(200, json) + +proc httpUnauthorized*( + socket: RPCSocket +) {.forceCheck: [], async.} = + try: + await socket.httpStatus(401) + except Exception as e: + panic("httpStatus threw an Exception despite not naturally throwing anything: " & e.msg) + +#Reads a RPC call over HTTP and returns it. +#Non-RPC calls, such as HEAD/GET, are handled without returning. +proc readHTTP*( + socket: RPCSocket +): Future[tuple[body: string, token: string]] {.forceCheck: [], async.} = + template HTTP_STATUS( + code: int + ) = + try: + await socket.httpStatus(code) + except Exception as e: + panic("Couldn't send a HTTP status despite httpStatus not naturally throwing anything: " & e.msg) + + while not socket.closed(): + block thisReq: + #Clear the result. + result = (body: "", token: "") + + #Needed to prevent an async lockup; I'm actually not sure where such lockup occurs. + #-- Kayaba + try: + await sleepAsync(1.milliseconds) + except Exception as e: + panic("Couldn't sleep before receving the next HTTP request: " & e.msg) + + #Clear the socket's last headers. + socket.headers = initTable[string, string]() + + var + chunked: bool = false + line: string + #Read the start line. + try: + line = await socket.readLine() + except Exception as e: + panic("Couldn't read the start line despite readLine not naturally throwing anything: " & e.msg) + if socket.closed: + return + #Verify this is a start line. If it's not, move on. + #Rather naive check, yet should work well enough. + #Generally used since we process headers as they come in, instead of reading the entire message first. + #Also handles misc newlines. + let startLine: seq[string] = line.split(" ") + if ( + (startLine.len != 3) or + (startLine[0] != startLine[0].toUpperAscii()) or + (not startLine[1].contains("/")) or + (startLine[2][0 ..< 7] != "HTTP/1.") + ): + continue + + #Now that we've confirmed it's the start line, handle it. + #This following check is pointless due to the above. + if startLine.len != 3: + HTTP_STATUS(400) + continue + #Ensure this is a POST. The HTTP spec technically requires GET/HEAD to never return 405. + #That said, we don't use them at all, so we just move on with the simpler solution. + if startLine[0] != "POST": + HTTP_STATUS(405) + continue + if startLine[2] != "HTTP/1.1": + HTTP_STATUS(505) + continue + + var + headerCount: int = 0 + expectContinue: bool = false + contentLength: int = -1 + while true: + #Read the header. + try: + line = await socket.readLine() + except Exception as e: + panic("Couldn't read a header despite readLine not naturally throwing anything: " & e.msg) + if socket.closed: + return + + if line == "": + break + + if line.len > 100: + HTTP_STATUS(431) + break thisReq + inc(headerCount) + if headerCount > 20: + HTTP_STATUS(431) + break thisReq + + var parts: seq[string] = line.split(" ").join("").split(":") + if parts.len < 2: + HTTP_STATUS(400) + break thisReq + + #Process this header. + parts[0] = parts[0].toLowerAscii() + if (parts[0] == "expect") and (parts[1].toLowerAscii() == "100-continue"): + expectContinue = true + continue + + #[ + if parts[0].contains("range"): + HTTP_STATUS(416) + continue + ]# + + case parts[0]: + #Used to figure out the best content type to use. + #If no content types work, the traditional solution is to move on anyways (despite not following the spec). + #Question is do generic, and specific, HTTP libs prefer text/plain or application/json... + of "accept": + var toUse: string = supported(JSON_MIME_TYPES, parts) + if toUse == "": + #HTTP_STATUS(406) + #break thisReq + toUse = "application/json" + + #Handle wildcard values. + if toUse == "text/*": + toUse = "text/plain" + #application/* and */* + if toUse.contains("*"): + toUse = "application/json" + + socket.headers["Content-Type"] = toUse + + #Commented as any ASCII compatible charset will work. + #This means numerous charsets will incorrectly decode, yet them being chosen is such an edge case... + #Easier to have wide support, yet this comment block serves as an ack to their existence. + #[ + of "accept-charset": + var toUse: string = supported(CHARSETS, parts): + if toUse == "": + HTTP_STATUS(406) + break thisReq + socket.headers["Charset"] = toUse + ]# + + #[ + #Even though a list of accepted encodings are defined, we ultimately decide which to use, which can be any. + #The identity should be universally accepted, especially given context of what this is. + #Hence why we don't needlessly error (or bother with this). + of "accept-encoding": + #If compression is required, error. + if ( + #Identity was disabled. + line.contains("identity;q=0,") or line.contains("identity;q=0 ") or + #All that weren't explicitly mentioned were disabled, and identity wasn't explicitly mentioned. + #Identity being explicitly mentioned yet also set to 0 is handled in the above check. + (line.contains("*;q=0") and (not line.contains("identity"))) + ): + HTTP_STATUS(406) + break thisReq + ]# + + #Don't accept compressed requests. + of "content-encoding": + HTTP_STATUS(415) + break thisReq + + #We only handle 100-continue, as defined above. + of "expect": + HTTP_STATUS(417) + break thisReq + + #curl defaults to x-www-form-urlencoded. + #We should really just try to handle the body no matter what. + #[ + of "content-type": + if not ["application/json", "text/plain"].contains(parts[1]): + HTTP_STATUS(415) + break thisReq + ]# + + of "content-length": + #Max of 9999 bytes, which would only come close during batch requests. + if parts[1].len > 4: + HTTP_STATUS(413) + break thisReq + try: + contentLength = int(parseUInt(parts[1])) + if $contentLength != parts[1]: + raise newException(ValueError, "") + except ValueError: + HTTP_STATUS(400) + break thisReq + + of "authorization": + var authParts: seq[string] = line.split(" ") + if authParts.len < 2: + HTTP_STATUS(400) + break thisReq + elif (authParts[^2] != "Bearer"): + HTTP_STATUS(401) + break thisReq + result.token = authParts[^1] + + of "connection": + if parts[1].split(",").contains("keep-alive"): + socket.headers["Connection"] = "keep-alive" + + of "transfer-encoding": + if parts[1] == "identity": + discard + elif parts[1] == "chunked": + chunked = true + else: + HTTP_STATUS(415) + + #[ + #If there's any conditional statement, we can assume it's invalid or ignore it. + #This comment block shows we're ignoring it. + if parts[0].contains("if-"): + HTTP_STATUS(412) + continue + ]# + + #Make sure the content length was provided. + if (not chunked) and (contentLength == -1): + HTTP_STATUS(411) + break thisReq + + #If the client was solely validating their headers, move on to the next message. + if expectContinue: + HTTP_STATUS(100) + + #Read the body. + if chunked: + while true: + var length: string + try: + length = await socket.readLine() + except Exception as e: + panic("Couldn't read the chunk length despite readLine not naturally throwing anything: " & e.msg) + if socket.closed: + return + + if length.len > 4: + HTTP_STATUS(413) + break thisReq + var parsedLen: int + try: + parsedLen = parseHexInt(length) + if parsedLen < 0: + #https://github.com/nim-lang/Nim/issues/17208 + HTTP_STATUS(413) + break thisReq + elif parsedLen == 0: + return + except ValueError: + HTTP_STATUS(400) + break thisReq + + if (result.body.len + parsedLen) > 9999: + HTTP_STATUS(413) + break thisReq + + try: + result.body &= await socket.recv(parsedLen) + except Exception as e: + panic("Couldn't read the chunk despite recv not naturally throwing anything: " & e.msg) + if socket.closed: + return + + try: + discard await socket.readLine() + except Exception as e: + panic("Couldn't read the new line characters despite readLine not naturally throwing anything: " & e.msg) + if socket.closed: + return + + else: + try: + #Doesn't check socket.closed as the calling function does. + result.body = await socket.recv(contentLength) + return + except Exception as e: + panic("Couldn't read the body despite recv not naturally throwing anything: " & e.msg) diff --git a/src/Interfaces/RPC/Modules/ConsensusModule.nim b/src/Interfaces/RPC/Modules/ConsensusModule.nim index a032b56bf..bf134b567 100644 --- a/src/Interfaces/RPC/Modules/ConsensusModule.nim +++ b/src/Interfaces/RPC/Modules/ConsensusModule.nim @@ -1,4 +1,8 @@ import sets +import options +import json + +import chronos import ../../../lib/[Errors, Util, Hash] @@ -10,45 +14,40 @@ import ../objects/RPCObj proc module*( functions: GlobalFunctionBox -): RPCFunctions {.forceCheck: [].} = +): RPCHandle {.forceCheck: [].} = try: - newRPCFunctions: - "getSendDifficulty" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.consensus.getSendDifficulty() - - "getDataDifficulty" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.consensus.getDataDifficulty() - - "getStatus" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, + result = newRPCHandle: + proc getSendDifficulty( + holder: Option[uint16] = none(uint16) + ): uint16 {.forceCheck: [ JSONRPCError ].} = - #Verify the parameters. - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") + if holder.isSome: + try: + result = functions.consensus.getSendDifficultyOfHolder(holder.unsafeGet()) + except IndexError: + raise newJSONRPCError(IndexError, "Holder doesn't have a SendDifficulty") + else: + result = functions.consensus.getSendDifficulty() - #Extract the parameter. - var hash: Hash[256] - try: - var strHash: string = parseHexStr(params[0].getStr()) - if strHash.len != 32: - raise newJSONRPCError(-3, "Invalid hash") - hash = strHash.toHash[:256]() - except ValueError: - raise newJSONRPCError(-3, "Invalid hash") + proc getDataDifficulty( + holder: Option[uint16] = none(uint16) + ): uint16 {.forceCheck: [ + JSONRPCError + ].} = + if holder.isSome: + try: + result = functions.consensus.getDataDifficultyOfHolder(holder.unsafeGet()) + except IndexError: + raise newJSONRPCError(IndexError, "Holder doesn't have a DataDifficulty") + else: + result = functions.consensus.getDataDifficulty() + proc getStatus( + hash: Hash[256] + ): JSONNode {.forceCheck: [ + JSONRPCError + ].} = #Get the Status. var status: TransactionStatus try: @@ -65,11 +64,12 @@ proc module*( if (status.merit == -1) and (not functions.consensus.isMalicious(holder)): merit += functions.merit.getMerit(holder, status.epoch) - res["result"] = %* { + result = %* { "verifiers": verifiers, "merit": merit, "threshold": functions.consensus.getThreshold(status.epoch), "verified": status.verified, + "finalized": status.merit != -1, "competing": status.competing, "beaten": status.beaten } diff --git a/src/Interfaces/RPC/Modules/MeritModule.nim b/src/Interfaces/RPC/Modules/MeritModule.nim index 521af34fb..63f7663d6 100644 --- a/src/Interfaces/RPC/Modules/MeritModule.nim +++ b/src/Interfaces/RPC/Modules/MeritModule.nim @@ -1,5 +1,8 @@ import strutils import tables +import json + +import chronos import ../../../lib/[Errors, Util, Hash] import ../../../lib/Sketcher @@ -22,6 +25,13 @@ import ../../../Network/Serialize/Merit/[ import ../objects/RPCObj +#BlockTemplate object, storing the info needed to create a template and publish a Block based off of one. +type BlockTemplate = object + sketchSalt: string + packets: seq[VerificationPacket] + body: string + contents: tuple[packets: Hash[256], contents: Hash[256]] + #Element -> JSON. #This wouldn't work with %, broke everything with %*, so now we have this symbol. proc `%**`( @@ -60,18 +70,19 @@ proc `%`( result = %* { "hash": $blockArg.header.hash, "header": { - "version": blockArg.header.version, - "last": $blockArg.header.last, - "contents": $blockArg.header.contents, + "version": blockArg.header.version, + "last": $blockArg.header.last, + "contents": $blockArg.header.contents, - "sketchSalt": blockArg.header.sketchSalt.toHex(), - "sketchCheck": $blockArg.header.sketchCheck, + "packets": blockArg.header.packetsQuantity, + "sketchSalt": blockArg.header.sketchSalt.toHex(), + "sketchCheck": $blockArg.header.sketchCheck, - "time": blockArg.header.time, - "proof": blockArg.header.proof, - "signature": $blockArg.header.signature + "time": blockArg.header.time, + "proof": blockArg.header.proof, + "signature": $blockArg.header.signature }, - "aggregate": $blockArg.body.aggregate + "aggregate": $blockArg.body.aggregate } #Add the miner to the header. @@ -112,166 +123,158 @@ proc `%`( proc module*( functions: GlobalFunctionBox -): RPCFunctions {.forceCheck: [].} = - #Table of usable Sketcher objects. - #Shared between the getBlockTemplate/publishBlock routes. +): RPCHandle {.forceCheck: [].} = var - sketchers: Table[int, seq[VerificationPacket]] - bodies: Table[int, string] - sketchID: int = 0 + templates: Table[uint32, BlockTemplate] lastTailUsedForTemplate: Hash[256] = Hash[256]() try: - newRPCFunctions: - "getHeight" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.merit.getHeight() - - "getDifficulty" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.merit.getDifficulty() - - "getBlock" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ + result = newRPCHandle: + proc getHeight(): int {.forceCheck: [].} = + functions.merit.getHeight() + + proc getDifficulty(): int {.forceCheck: [].} = + int(functions.merit.getDifficulty()) + + proc getBlock( + block_JSON: JSONNode + ): JSONNode {.forceCheck: [ ParamError, JSONRPCError ].} = - #Verify the parameters length. - if params.len != 1: - raise newException(ParamError, "") - - #Get the Block. - if params[0].kind == JInt: + if block_JSON.kind == JInt: try: - res["result"] = % functions.merit.getBlockByNonce(params[0].getInt()) + #Ensure this is within uint boundaries, raising a ParamError if not. + discard retrieveFromJSON(block_JSON, uint) + result = % functions.merit.getBlockByNonce(retrieveFromJSON(block_JSON, int)) + except ParamError as e: + raise e + except JSONRPCError as e: + panic("getBlock's retrieveFromJSON (int) call caused a JSONRPCError, when it shouldn't call any of those paths: " & e.msg) except IndexError: - raise newJSONRPCError(-2, "Block not found", %* { + raise newJSONRPCError(IndexError, "Block not found", %* { "height": functions.merit.getHeight() }) - elif params[0].kind == JString: + + else: try: - var strHash: string = parseHexStr(params[0].getStr()) - if strHash.len != 32: - raise newJSONRPCError(-3, "Invalid hash") - res["result"] = % functions.merit.getBlockByHash(strHash.toHash[:256]()) + result = % functions.merit.getBlockByHash(retrieveFromJSON(block_JSON, Hash[256])) + except ParamError as e: + raise e + except JSONRPCError as e: + panic("getBlock's retrieveFromJSON (Hash[256]) call caused a JSONRPCError, when it shouldn't call any of those paths: " & e.msg) except IndexError: - raise newJSONRPCError(-2, "Block not found") - except ValueError: - raise newJSONRPCError(-3, "Invalid hash") - else: - raise newException(ParamError, "") - - "getTotalMerit" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.merit.getTotalMerit() - - "getUnlockedMerit" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.merit.getUnlockedMerit() - - "getMerit" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError + raise newJSONRPCError(IndexError, "Block not found") + + proc getPublicKey( + nick: uint16 + ): string {.forceCheck: [ + JSONRPCError ].} = - #Verify the parameters length. - if ( - (params.len != 1) or - (params[0].kind != JInt) or - (params[0].getInt() >= 65536) - ): - raise newException(ParamError, "") + try: + result = $ functions.merit.getPublicKey(nick) + except IndexError: + raise newJSONRPCError(IndexError, "Nickname doesn't exist") + + proc getNickname( + key: BLSPublicKey + ): uint16 {.forceCheck: [ + JSONRPCError + ].} = + try: + result = functions.merit.getNickname(key) + except IndexError: + raise newJSONRPCError(IndexError, "Key doesn't have a nickname assigned") - #Extract the parameter. - var nick: uint16 = uint16(params[0].getInt()) + proc getTotalMerit(): int {.forceCheck: [].} = + functions.merit.getTotalMerit() - #Create the result. - res["result"] = %* { + proc getUnlockedMerit(): int {.forceCheck: [].} = + functions.merit.getUnlockedMerit() + + proc getMerit( + nick: uint16 + ): JSONNode {.forceCheck: [].} = + result = %* { "status": if functions.merit.isUnlocked(nick): "Unlocked" elif functions.merit.isPending(nick): "Pending" else: "Locked", "malicious": functions.consensus.isMalicious(nick), "merit": functions.merit.getRawMerit(nick) } - "getBlockTemplate" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, - JSONRPCError - ].} = - #Verify and extract the parameter. - if (params.len != 1) or (params[0].kind != JString): - raise newException(ParamError, "") - - var miner: BLSPublicKey - try: - miner = newBLSPublicKey(params[0].getStr().parseHexStr()) - except ValueError: - raise newJSONRPCError(-3, "Invalid miner") - except BLSError: - raise newJSONRPCError(-4, "Invalid miner") - - if lastTailUsedForTemplate != functions.merit.getTail(): - sketchers = initTable[int, seq[VerificationPacket]]() - bodies = initTable[int, string]() - lastTailusedForTemplate = functions.merit.getTail() + proc getBlockTemplate( + miner: BLSPublicKey + ): JSONNode {.forceCheck: [].} = + let tail: Hash[256] = functions.merit.getTail() + if lastTailUsedForTemplate != tail: + templates = initTable[uint32, BlockTemplate]() + lastTailusedForTemplate = tail + #Create a new template if needed, as determined by our second-accuracy. + #If we already created a template for this second, just use it (see https://github.com/MerosCrypto/Meros/issues/278). var - #Pending packets/elements. - pending: tuple[ - packets: seq[VerificationPacket], - elements: seq[BlockElement], - aggregate: BLSSignature - ] = functions.consensus.getPending() - - #ID for this Sketcher. - id: int = sketchID - #Sketch salt we're using with the packets. - sketchSaltNum: uint32 - #Actual sketch salt. - sketchSalt: string - sketchSalt = newString(4) - randomFill(sketchSalt) - sketchSaltNum = cast[uint32](sketchSalt.fromBinary()) - inc(sketchID) - - #Verify the packets don't collide with our salt. - while true: - try: - sketchers[id] = pending.packets + time: uint32 = getTime() + blockTemplate: BlockTemplate + if not templates.hasKey(time): + var + pending: tuple[ + packets: seq[VerificationPacket], + elements: seq[BlockElement], + aggregate: BLSSignature + ] = functions.consensus.getPending() + sketchSalt: string = newString(4) + + #Create the new template. + blockTemplate = BlockTemplate( + packets: pending.packets, + contents: newContents(pending.packets, pending.elements), + body: "" + ) - sketchSalt = sketchSaltNum.toBinary(INT_LEN) - if not sketchers[id].collides(sketchSalt): - break + #Randomize the sketch salt. Prevents malicious actors from trying to cause collisions against "\0\0\0\0". + randomFill(sketchSalt) + #Verify the packets don't collide with our salt. + while blockTemplate.packets.collides(sketchSalt): #This shouldn't be needed as sketchSaltNum is a uint32. {.push checks: off.} + var sketchSaltNum: uint32 = cast[uint32](sketchSalt.fromBinary()) inc(sketchSaltNum) {.pop.} + sketchSalt = sketchSaltNum.toBinary(INT_LEN) + + #Set the salt now that it's been proven valid. + blockTemplate.sketchSalt = sketchSalt + + #Create the body for the template. + try: + blockTemplate.body = newBlockBodyObj( + blockTemplate.contents.packets, + blockTemplate.packets, + pending.elements, + pending.aggregate, + {} + ).serialize(blockTemplate.sketchSalt, blockTemplate.packets.len) + except ValueError as e: + panic("BlockBody's sketch has a collision despite mining a salt which doesn't: " & e.msg) + + #Save the template. + templates[time] = blockTemplate + + else: + try: + blockTemplate = templates[time] except KeyError as e: - panic("Couldn't get a Sketcher we just created: " & e.msg) + panic("Couldn't get the Block Template for this second despite confirming its existence: " & e.msg) #Create the Header. var - contents: tuple[packets: Hash[256], contents: Hash[256]] = newContents(pending.packets, pending.elements) header: JSONNode = newJNull() - time: uint32 = getTime() difficulty: uint64 = functions.merit.getDifficulty() + headerTime: uint32 #Ensure the time is higher than the previous Block's. try: - time = max(time, functions.merit.getBlockByHash(functions.merit.getTail()).header.time + 1) + headerTime = max(time, functions.merit.getBlockByHash(tail).header.time + 1) except IndexError as e: panic("Couldn't get the last Block despite grabbing it by the chain's tail: " & e.msg) @@ -282,13 +285,13 @@ proc module*( except IndexError: header = % newBlockHeader( 0, - functions.merit.getTail(), - contents.contents, - uint32(pending.packets.len), - sketchSalt, - newSketchCheck(sketchSalt, pending.packets), + tail, + blockTemplate.contents.contents, + uint32(blockTemplate.packets.len), + blockTemplate.sketchSalt, + newSketchCheck(blockTemplate.sketchSalt, blockTemplate.packets), miner, - time, + headerTime, 0, newBLSSignature() ).serializeTemplate().toHex() @@ -297,13 +300,13 @@ proc module*( if header.kind == JNull: header = % newBlockHeader( 0, - functions.merit.getTail(), - contents.contents, - uint32(pending.packets.len), - sketchSalt, - newSketchCheck(sketchSalt, pending.packets), + tail, + blockTemplate.contents.contents, + uint32(blockTemplate.packets.len), + blockTemplate.sketchSalt, + newSketchCheck(blockTemplate.sketchSalt, blockTemplate.packets), nick, - time, + headerTime, 0, newBLSSignature() ).serializeTemplate().toHex() @@ -312,67 +315,50 @@ proc module*( except BLSError: panic("Couldn't create a temporary signature for a BlockHeader template.") - #Create the body. - try: - bodies[id] = newBlockBodyObj( - contents.packets, - pending.packets, - pending.elements, - pending.aggregate, - {} - ).serialize(sketchSalt, pending.packets.len) - except ValueError as e: - panic("Block Body had sketch collision: " & e.msg) - #Create the result. - res["result"] = %* { - "id": id, + result = %* { + "id": time, "key": functions.merit.getRandomXCacheKey().toHex(), "header": header, "difficulty": difficulty } - "publishBlock" = proc ( - res: JSONNode, - params: JSONNode - ): Future[void] {.forceCheck: [ - ParamError, + proc publishBlock( + id: uint32, + header: hex + ) {.forceCheck: [ JSONRPCError ], async.} = - #Verify the parameters. - if ( - (params.len != 2) or - (params[0].kind != JInt) or - (params[1].kind != JString) - ): - raise newException(ParamError, "") - - var sketchyBlock: SketchyBlock + var + blockTemplate: BlockTemplate + sketchyBlock: SketchyBlock try: - sketchyBlock = functions.merit.getRandomX().parseBlock(params[1].getStr().parseHexStr() & bodies[params[0].getInt()]) + blockTemplate = templates[id] except KeyError: - raise newJSONRPCError(-2, "Invalid ID") + raise newJSONRPCError(IndexError, "Invalid ID") + try: + sketchyBlock = functions.merit.getRandomX().parseBlock(header & blockTemplate.body) except ValueError: - raise newJSONRPCError(-3, "Invalid Block") + raise newJSONRPCError(ValueError, "Invalid Block") #Test the Block Header. try: functions.merit.testBlockHeader(sketchyBlock.data.header) except ValueError: - raise newJSONRPCError(-3, "Invalid Block") + raise newJSONRPCError(ValueError, "Invalid Block") try: await functions.merit.addBlock( sketchyBlock, - sketchers[params[0].getInt()], + blockTemplate.packets, false ) except KeyError: - raise newJSONRPCError(-2, "Invalid ID") + raise newJSONRPCError(KeyError, "Invalid ID") except ValueError: - raise newJSONRPCError(-3, "Invalid Block") + raise newJSONRPCError(ValueError, "Invalid Block") except DataMissing: - raise newJSONRPCError(-1, "Missing Block-referenced data") + panic("Missing Block-referenced data despite creating this Block's body") except Exception as e: panic("addBlock threw a raw Exception, despite catching all Exception types it naturally raises: " & e.msg) except Exception as e: diff --git a/src/Interfaces/RPC/Modules/NetworkModule.nim b/src/Interfaces/RPC/Modules/NetworkModule.nim index 2f62e5d11..edbd00e34 100644 --- a/src/Interfaces/RPC/Modules/NetworkModule.nim +++ b/src/Interfaces/RPC/Modules/NetworkModule.nim @@ -1,6 +1,18 @@ +import options +import strutils +import json + import chronos import ../../../lib/Errors +import ../../../lib/Hash + +import ../../../Database/Transactions/Transaction +import ../../../Database/Merit/BlockHeader + +import ../../../Network/objects/MessageObj +import ../../../Network/Serialize/Transactions/[SerializeClaim, SerializeSend, SerializeData] +import ../../../Network/Serialize/Merit/SerializeBlockHeader import ../../../objects/GlobalFunctionBoxObj @@ -11,58 +23,64 @@ const DEFAULT_PORT {.intdefine.}: int = 5132 proc module*( functions: GlobalFunctionBox -): RPCFunctions {.forceCheck: [].} = +): RPCHandle {.forceCheck: [].} = try: - newRPCFunctions: - "connect" = proc ( - res: JSONNode, - params: JSONNode - ): Future[void] {.forceCheck: [ - ParamError - ], async.} = - #Verify the parameters length. - if (params.len != 1) and (params.len != 2): - raise newException(ParamError, "") - - #Verify the paramters types. - if params[0].kind != JString: - raise newException(ParamError, "") - - #Supply the optional port argument if needed. - if params.len == 1: - params.add(% DEFAULT_PORT) - if params[1].kind != JInt: - raise newException(ParamError, "") - + result = newRPCHandle: + proc connect( + address: string, + port: Option[int] = some(DEFAULT_PORT) + ) {.requireAuth, forceCheck: [], async.} = try: - await functions.network.connect(params[0].getStr(), params[1].getInt()) + await functions.network.connect(address, port.unsafeGet()) except Exception as e: panic("MainNetwork's connect threw an Exception despite not naturally throwing anything: " & e.msg) - "getPeers" = proc ( - res: JSONNode, - params: JSONNode - ): Future[void] {.forceCheck: [], async.} = - res["result"] = % [] + proc getPeers(): JSONNode {.forceCheck: [].} = + result = % [] for client in functions.network.getPeers(): + result.add(%* { + "ip": ( + $int(client.ip[0]) & "." & + $int(client.ip[1]) & "." & + $int(client.ip[2]) & "." & + $int(client.ip[3]) + ), + "server": client.server + }) + if client.server: + result[result.len - 1]["port"] = % client.port + + proc broadcast( + transaction: Option[Hash[256]] = none(Hash[256]), + block_JSON: Option[Hash[256]] = none(Hash[256]) + ) {.forceCheck: [ + JSONRPCError + ].} = + if transaction.isSome: + var tx: Transaction try: - res["result"].add(%* { - "ip": ( - $int(client.ip[0]) & "." & - $int(client.ip[1]) & "." & - $int(client.ip[2]) & "." & - $int(client.ip[3]) - ), - "server": client.server - }) - except KeyError as e: - panic("Couldn't set the result: " & e.msg) + tx = functions.transactions.getTransaction(transaction.unsafeGet()) + except IndexError: + raise newJSONRPCError(IndexError, "Transaction not found") + case tx: + of Mint as _: + raise newJSONRPCError(ValueError, "Transaction is a Mint") + of Claim as _: + functions.network.broadcast(MessageType.Claim, tx.serialize()) + of Send as _: + functions.network.broadcast(MessageType.Send, tx.serialize()) + of Data as _: + functions.network.broadcast(MessageType.Data, tx.serialize()) + + if block_JSON.isSome(): + try: + let header: BlockHeader = functions.merit.getBlockByHash(block_JSON.unsafeGet()).header + if header.hash == functions.merit.getBlockByNonce(0).header.hash: + raise newJSONRPCError(ValueError, "Block is the genesis Block") + functions.network.broadcast(MessageType.BlockHeader, header.serialize()) + except IndexError: + raise newJSONRPCError(IndexError, "Block not found") - if client.server: - try: - res["result"][res["result"].len - 1]["port"] = % client.port - except KeyError as e: - panic("Couldn't add the port the result: " & e.msg) except Exception as e: panic("Couldn't create the Network Module: " & e.msg) diff --git a/src/Interfaces/RPC/Modules/PersonalModule.nim b/src/Interfaces/RPC/Modules/PersonalModule.nim index f65a9cc2d..9423a2f10 100644 --- a/src/Interfaces/RPC/Modules/PersonalModule.nim +++ b/src/Interfaces/RPC/Modules/PersonalModule.nim @@ -1,5 +1,14 @@ +import options +import strutils +import json + +import chronos + import ../../../lib/[Errors, Hash, Util] import ../../../Wallet/[MinerWallet, Wallet] +import ../../../Database/Transactions/Send + +from ../../../Database/Filesystem/Wallet/WalletDB import KeyIndex, UsableInput import ../../../objects/GlobalFunctionBoxObj @@ -7,132 +16,306 @@ import ../objects/RPCObj proc module*( functions: GlobalFunctionBox -): RPCFunctions {.forceCheck: [].} = +): RPCHandle {.forceCheck: [].} = try: - newRPCFunctions: - "getMiner" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % $functions.personal.getMinerWallet().privateKey - - "setMnemonic" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, + result = newRPCHandle: + proc setWallet( + mnemonic: Option[string] = some(""), + password: string = "" + ) {.requireAuth, forceCheck: [ JSONRPCError ].} = - #Verify the params len. - if params.len > 2: - raise newException(ParamError, "") - #Verify the params' types. - for param in params: - if param.kind != JString: - raise newException(ParamError, "") - - #Fill in optional params. - while params.len < 2: - params.add(% "") - - #Create the Wallet. try: - functions.personal.setMnemonic(params[0].getStr(), params[1].getStr()) + functions.personal.setWallet(mnemonic.unsafeGet(), password) except ValueError: - raise newJSONRPCError(-3, "Invalid Mnemonic") - - "getMnemonic" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [].} = - res["result"] = % functions.personal.getWallet().mnemonic.sentence - - "getAddress" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, + raise newJSONRPCError(ValueError, "Invalid mnemonic or password") + + proc setAccount( + key: EdPublicKey, + chainCode: Hash[256] + ) {.requireAuth, forceCheck: [].} = + functions.personal.setAccount(key, chainCode, true) + + proc getMnemonic(): string {.requireAuth, forceCheck: [ + JSONRPCError + ].} = + try: + result = functions.personal.getMnemonic() + except ValueError as e: + raise newJSONRPCError(ValueError, e.msg) + + proc getMeritHolderKey(): string {.requireAuth, forceCheck: [ JSONRPCError ].} = - #Supply optional parameters. - if params.len == 0: - params.add(% 0) - if params.len == 1: - params.add(% false) - - #Verify the params. - if ( - (params.len != 2) or - (params[0].kind != JInt) or - (params[1].kind != JBool) - ): - raise newException(ParamError, "") - - #Get the account in question. - var wallet: HDWallet = functions.personal.getWallet() try: - wallet = wallet[uint32(params[0].getInt())] + result = $functions.personal.getMinerWallet().privateKey except ValueError: - raise newJSONRPCError(-3, "Unusable account") + raise newJSONRPCError(ValueError, "Node is running as a WatchWallet and has no Merit Holder") - #Get the tree in question. + proc getMeritHolderNick(): uint16 {.requireAuth, forceCheck: [ + JSONRPCError + ].} = try: - if params[1].getBool(): - wallet = wallet.derive(1) - else: - wallet = wallet.derive(0) - except ValueError as e: - panic("Unusable external/internal trees despite checking for their validity: " & e.msg) + #Not the most optimal path given how the WalletDB tracks the nick. + result = functions.merit.getNickname(functions.personal.getMinerWallet().publicKey) + except ValueError: + raise newJSONRPCError(ValueError, "Node is running as a WatchWallet and has no Merit Holder") + except IndexError: + raise newJSONRPCError(IndexError, "Wallet doesn't have a Merit Holder nickname assigned") + + proc getAccount(): JSONNode {.requireAuth, forceCheck: [].} = + let data: tuple[key: EdPublicKey, chainCode: Hash[256]] = functions.personal.getAccount() + result = %* { + "key": $data.key, + "chainCode": $data.chainCode + } - #Get the child. + proc getAddress( + index: Option[uint32] = none(uint32) + ): string {.requireAuth, forceCheck: [ + JSONRPCError + ].} = + if index.isSome() and (index.unsafeGet() >= (1 shl 31)): + raise newJSONRPCError(ValueError, "Hardened index specified") try: - res["result"] = % wallet.next().address + result = functions.personal.getAddress(index) except ValueError: - raise newJSONRPCError(-3, "Tree has no valid children") + raise newJSONRPCError(ValueError, "Invalid index") - "send" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, + proc data( + hex: bool = false, + data_JSON: string, + password: string = "" + ): Future[string] {.requireAuth, forceCheck: [ JSONRPCError ], async.} = - #Verify the params. - if ( - (params.len != 2) or - (params[0].kind != JString) or - (params[1].kind != JString) - ): - raise newException(ParamError, "") + var data: string = data_JSON + if hex: + try: + data = parseHexStr(data) + except ValueError as e: + raise newJSONRPCError(ValueError, e.msg) try: - res["result"] = % $(await functions.personal.send(params[0].getStr(), params[1].getStr())) - except ValueError: - raise newJSONRPCError(-3, "Invalid address/amount") - except NotEnoughMeros: - raise newJSONRPCError(1, "Not enough Meros") + result = $(await functions.personal.data(data, password)) + except ValueError as e: + raise newJSONRPCError(ValueError, e.msg) except Exception as e: - panic("send threw an Exception despite catching everything: " & e.msg) + panic("personal.data threw an Exception despite catching all Exceptions: " & e.msg) + + proc getUTXOs(): JSONNode {.requireAuth, forceCheck: [].} = + result = % [] + for utxo in functions.personal.getUTXOs(): + result.add(%* { + "address": utxo.address, + "hash": $utxo.utxo.hash, + "nonce": utxo.utxo.nonce + }) + + proc getTransactionTemplate( + #_JSON is used to distinguish the name from the below variables, not out of necessity due to using a keyword. + outputs_JSON: seq[JSONNode], + from_JSON: Option[seq[string]] = none(seq[string]), + change_JSON: Option[string] = none(string) + ): JSONNode {.requireAuth, forceCheck: [ + ParamError, + JSONRPCError + ].} = + if outputs_JSON.len == 0: + raise newJSONRPCError(ValueError, "No outputs were provided") + + var + utxos: seq[UsableInput] + outputs: seq[SendOutput] + sum: uint64 + change: uint64 + + for output in outputs_JSON: + try: + if not ( + (output.kind == JObject) and + output.hasKey("address") and (output["address"].kind == JString) and + output.hasKey("amount") and output["amount"].kind == JString + ): + raise newLoggedException(ParamError, "Output didn't have address/amount as strings") + + try: + var amount: uint64 = 0 + #This should work without issue on x86 systems unless JS is the set target. + #Removing this dependency means using a BigInt parser here before converting to an uint64. + #That is feasble with StInt, yet shouldn't be neccessary. + when not (BiggestUInt is uint64): + {.error: "Lack of uint64 availability breaks JSON-RPC parsing.".} + amount = parseBiggestUInt(output["amount"].getStr()) + if amount == 0: + raise newJSONRPCError(ValueError, "0 value output was provided") + if $amount != output["amount"].getStr(): + raise newJSONRPCError(ValueError, "Amount exceeded the uint64 range and is not a valid Meros amount") + sum += amount + + let addy: Address = output["address"].getStr().getEncodedData() + case addy.addyType: + of AddressType.PublicKey: + outputs.add(newSendOutput(newEdPublicKey(cast[string](addy.data)), amount)) + except ValueError: + raise newJSONRPCError(ValueError, "Invalid address/amount") + except KeyError as e: + panic("Couldn't get the key from an output despite screening its fields: " & e.msg) + + #If we're explicitly told which addresses to use, directly call getUTXOs. + if from_JSON.isSome(): + for addyJSON in from_JSON.unsafeGet(): + var addy: Address + try: + addy = addyJSON.getEncodedData() + except ValueError: + raise newJSONRPCError(ValueError, "Invalid address to send from") + case addy.addyType: + of AddressType.PublicKey: + let key: EdPublicKey = newEdPublicKey(cast[string](addy.data)) + for utxo in functions.transactions.getUTXOs(key): + try: + utxos.add(UsableInput( + index: functions.personal.getKeyIndex(key), + key: key, + address: addyJSON, + utxo: utxo + )) + except IndexError: + raise newJSONRPCError(IndexError, "Asked to send from unknown address") + else: + utxos = functions.personal.getUTXOs() + + #Filter to a minimal UTXO set. + #Considering it doesn't sort highest amount to lowest, this isn't truly minimal. + var + keys: seq[EdPublicKey] = @[] + u: int = 0 + while u < utxos.len: + keys.add(utxos[u].key) + + var tx: Transaction + try: + tx = functions.transactions.getTransaction(utxos[u].utxo.hash) + except IndexError as e: + panic("Couldn't get a Transaction listed as a UTXO: " & e.msg) + + let amount: uint64 = cast[SendOutput](tx.outputs[utxos[u].utxo.nonce]).amount + if amount >= sum: + change = amount - sum + sum = 0 + break + sum -= amount + inc(u) + if sum != 0: + raise newJSONRPCError(NotEnoughMeros, "Wallet doesn't have enough Meros") + while (u + 1) != utxos.len: + utxos.del(u + 1) + + result = %* { + "type": "Send", + "inputs": [], + "outputs": [] + } + try: + for input in utxos: + result["inputs"].add(%* { + "hash": $input.utxo.hash, + "nonce": input.utxo.nonce, + "change": input.index.change, + "index": input.index.index + }) + for output in outputs: + result["outputs"].add(%* { + "key": $output.key, + "amount": $output.amount + }) + if change != 0: + if change_JSON.isSome(): + try: + let addy: Address = change_JSON.unsafeGet().getEncodedData() + case addy.addyType: + of AddressType.PublicKey: + result["outputs"].add(%* { + "key": $newEdPublicKey(cast[string](addy.data)), + "amount": $change + }) + except ValueError: + raise newJSONRPCError(ValueError, "Invalid change address specified") + else: + result["outputs"].add(%* { + "key": $functions.personal.getChangeKey(), + "amount": $change + }) + except KeyError as e: + panic("Couldn't add an input/output despite ensuring inputs/outputs exist: " & e.msg) + result["publicKey"] = % $keys.aggregate() - "data" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ + proc send( + outputs_JSON: seq[JSONNode], + password: Option[string] = none(string) + ): Future[string] {.requireAuth, forceCheck: [ ParamError, JSONRPCError ], async.} = - #Verify the params. - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") + #Outsource to getTransactionTemplate which already implements all this logic. + var txTemplate: JSONNode + try: + txTemplate = getTransactionTemplate(outputs_JSON, none(seq[string]), none(string)) + except ParamError as e: + raise e + except JSONRPCError as e: + raise e + var + inputs: seq[FundedInput] = @[] + keys: seq[KeyIndex] = @[] + outputs: seq[SendOutput] = @[] + send: Send try: - res["result"] = % $(await functions.personal.data(params[0].getStr())) - except ValueError: - raise newJSONRPCError(-3, "Invalid data length") + for input in txTemplate["inputs"]: + inputs.add(newFundedInput(input["hash"].getStr().parseHexStr().toHash[:256](), input["nonce"].getInt())) + keys.add(KeyIndex( + change: input["change"].getBool(), + index: uint32(input["index"].getInt()) + )) + for output in txTemplate["outputs"]: + #There's a note above about the use of parseBiggestUInt which applies here as well. + outputs.add(newSendOutput(newEdPublicKey(parseHexStr(output["key"].getStr())), parseBiggestUInt(output["amount"].getStr()))) + except KeyError, ValueError: + panic("personal_send failed due to personal_getTransactionTemplate not returning a valid template.") + send = newSend(inputs, outputs) + + #Create the proper object and call sign. + try: + functions.personal.sign(send, keys, password.get("")) + except IndexError as e: + panic("Tried to sign a template with an unusable key: " & e.msg) + except ValueError as e: + raise newJSONRPCError(ValueError, e.msg) + + #Mine it. + send.mine(functions.consensus.getSendDifficulty()) + + #Add it. + try: + await functions.transactions.addSend(send) + except ValueError as e: + panic("Created an invalid Send in personal_send: " & e.msg) + #This should be impossible since we use chronos. + #It wouldn't be impossible if we used the stdlib's async (or at least, old versions of it). + #Because we use chronos, Futures should be handled FIFO, meaning although the above loses flow control... + #Another personal_getUTXOs requestt couldn't snipe UTXOs from it. + #That leaves two nodes issuing this command at the same time, which sould also be impossible. + #This is because it's a single call to await at the end of the function. + #We don't have a good way to continue if this error did ever pop up anyways... + #Except possibly just trying the TX again with new UTXOs? Anyways. Just panic. + except DataExists as e: + panic("Created a Send that exists: " & e.msg) except Exception as e: - panic("send threw an Exception despite catching everything: " & e.msg) + panic("addSend threw an Exception despite catching all errors: " & e.msg) + + result = $send.hash + except Exception as e: panic("Couldn't create the Consensus Module: " & e.msg) diff --git a/src/Interfaces/RPC/Modules/SystemModule.nim b/src/Interfaces/RPC/Modules/SystemModule.nim new file mode 100644 index 000000000..8caa6f802 --- /dev/null +++ b/src/Interfaces/RPC/Modules/SystemModule.nim @@ -0,0 +1,32 @@ +import json + +import chronos + +import ../../../lib/Errors + +import ../../../objects/GlobalFunctionBoxObj + +import ../objects/RPCObj + +proc module*( + functions: GlobalFunctionBox +): RPCHandle {.forceCheck: [].} = + try: + result = newRPCHandle: + proc quit( + req: RPCRequest, + reply: RPCReplyFunction + ) {.requireAuth, forceCheck: [], async.} = + try: + await reply(%* { + "jsonrpc": "2.0", + "id": req["id"], + "result": true + }) + except Exception as e: + panic("Couldn't call reply about how we're quitting due to an Exception despite reply not naturally throwing anything: " & e.msg) + + functions.system.quit() + + except Exception as e: + panic("Couldn't create the System Module: " & e.msg) diff --git a/src/Interfaces/RPC/Modules/TransactionsModule.nim b/src/Interfaces/RPC/Modules/TransactionsModule.nim index 586c7b71e..5b15f053f 100644 --- a/src/Interfaces/RPC/Modules/TransactionsModule.nim +++ b/src/Interfaces/RPC/Modules/TransactionsModule.nim @@ -1,14 +1,22 @@ import strutils import json +import chronos + import ../../../lib/[Errors, Util, Hash] -import ../../../Wallet/[MinerWallet, Wallet, Address] -import ../../../Wallet/Address +import ../../../Wallet/[MinerWallet, Wallet] +import ../../../Wallet/Address as AddressFile import ../../../Database/Transactions/Transactions +import ../../../Network/Serialize/Transactions/ParseClaim import ../../../Network/Serialize/Transactions/ParseSend +import ../../../Network/Serialize/Transactions/ParseData + +#Used solely when generating work for Transactions about to be published. +import ../../../Network/Serialize/Transactions/SerializeSend +import ../../../Network/Serialize/Transactions/SerializeData import ../../../objects/GlobalFunctionBoxObj @@ -45,7 +53,7 @@ proc `%`( try: for o in 0 ..< result["outputs"].len: - result["outputs"][o]["key"] = % $cast[MintOutput](mint.outputs[o]).key + result["outputs"][o]["nick"] = % cast[MintOutput](mint.outputs[o]).key except KeyError as e: panic("Couldn't add a Mint's output's key to its output: " & e.msg) @@ -75,7 +83,6 @@ proc `%`( result["signature"] = % $send.signature result["proof"] = % send.proof - result["argon"] = % $send.argon of Data as data: result["descendant"] = % "Data" @@ -84,99 +91,47 @@ proc `%`( result["signature"] = % $data.signature result["proof"] = % data.proof - result["argon"] = % $data.argon proc module*( functions: GlobalFunctionBox -): RPCFunctions {.forceCheck: [].} = +): RPCHandle {.forceCheck: [].} = try: - newRPCFunctions: - "getTransaction" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError, + result = newRPCHandle: + proc getTransaction( + hash: Hash[256] + ): JSONNode {.forceCheck: [ JSONRPCError ].} = - #Verify the parameters. - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") - #Get the Transaction. try: - var strHash: string = parseHexStr(params[0].getStr()) - if strHash.len != 32: - raise newJSONRPCError(-3, "Invalid hash") - res["result"] = % functions.transactions.getTransaction(strHash.toHash[:256]()) + result = % functions.transactions.getTransaction(hash) except IndexError: - raise newJSONRPCError(-2, "Transaction not found") - except ValueError: - raise newJSONRPCError(-3, "Invalid hash") - - "getUTXOs" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError - ].} = - #Verify the parameters. - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") + raise newJSONRPCError(IndexError, "Transaction not found") + proc getUTXOs( + address: Address + ): JSONNode {.forceCheck: [].} = #Get the UTXOs. - var - decodedAddy: Address - utxos: seq[FundedInput] - try: - decodedAddy = Address.getEncodedData(params[0].getStr()) - except ValueError: - raise newException(ParamError, "") - - case decodedAddy.addyType: + var utxos: seq[FundedInput] + case address.addyType: of AddressType.PublicKey: - utxos = functions.transactions.getUTXOs(newEdPublicKey(cast[string](decodedAddy.data))) + utxos = functions.transactions.getUTXOs(newEdPublicKey(cast[string](address.data))) - res["result"] = % [] + result = % [] for utxo in utxos: - try: - res["result"].add(%* { - "hash": $utxo.hash, - "nonce": utxo.nonce - }) - except KeyError as e: - panic("Couldn't append to the list of UTXOs despite just creating it: " & e.msg) - - "getBalance" = proc ( - res: JSONNode, - params: JSONNode - ) {.forceCheck: [ - ParamError - ].} = - #Verify the parameters. - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") - + result.add(%* { + "hash": $utxo.hash, + "nonce": utxo.nonce + }) + + proc getBalance( + address: Address + ): string {.forceCheck: [].} = #Get the UTXOs. - var - decodedAddy: Address - utxos: seq[FundedInput] - try: - decodedAddy = Address.getEncodedData(params[0].getStr()) - except ValueError: - raise newException(ParamError, "") - - case decodedAddy.addyType: + var utxos: seq[FundedInput] + case address.addyType: of AddressType.PublicKey: - utxos = functions.transactions.getUTXOs(newEdPublicKey(cast[string](decodedAddy.data))) + utxos = functions.transactions.getUTXOs(newEdPublicKey(cast[string](address.data))) var balance: uint64 = 0 for utxo in utxos: @@ -184,34 +139,71 @@ proc module*( balance += cast[SendOutput](functions.transactions.getTransaction(utxo.hash).outputs[utxo.nonce]).amount except IndexError as e: panic("Failed to get a Transaction which was a spendable UTXO: " & e.msg) - res["result"] = % $balance + result = $balance - "publishSend" = proc ( - res: JSONNode, - params: JSONNode + proc publishTransaction( + type_JSON: string, + transaction: hex ) {.forceCheck: [ - ParamError, JSONRPCError ], async.} = - if ( - (params.len != 1) or - (params[0].kind != JString) - ): - raise newException(ParamError, "") - try: - await functions.transactions.addSend( - parseSend( - params[0].getStr().parseHexStr(), - functions.consensus.getSendDifficulty() - ) - ) + var difficulty: uint32 + case type_JSON: + of "Claim": + functions.transactions.addClaim(parseClaim(transaction)) + of "Send": + difficulty = functions.consensus.getSendDifficulty() + await functions.transactions.addSend( + parseSend(transaction, difficulty) + ) + of "Data": + difficulty = functions.consensus.getDataDifficulty() + await functions.transactions.addData( + parseData(transaction, functions.consensus.getDataDifficulty()) + ) + else: + raise newJSONRPCError(ValueError, "Invalid Transaction type specified") + except JSONRPCError as e: + raise e except ValueError as e: - raise newJSONRPCError(-3, "Invalid send: " & e.msg) + raise newJSONRPCError(ValueError, "Transaction is invalid: " & e.msg) except DataExists: - discard + return + except Spam as spam: + raise newJSONRPCError(Spam, "Transaction didn't beat the spam filter", %* { + "difficulty": spam.difficulty + }) + except Exception as e: + panic("Adding a Transaction raised an Exception despite catching all errors: " & e.msg) + + proc publishTransactionWithoutWork( + type_JSON: string, + transaction: hex + ) {.requireAuth, forceCheck: [ + JSONRPCError + ], async.} = + try: + case type_JSON: + of "Claim": + await publishTransaction(type_JSON, transaction) + of "Send": + let send: Send = parseSend(transaction & "".pad(4), uint32(0)) + send.mine(uint32(functions.consensus.getSendDifficulty())) + await publishTransaction(type_JSON, send.serialize()) + of "Data": + let data: Data = parseData(transaction & "".pad(4), uint32(0)) + data.mine(uint32(functions.consensus.getDataDifficulty())) + await publishTransaction(type_JSON, data.serialize()) + else: + raise newJSONRPCError(ValueError, "Invalid Transaction type specified") + except ValueError as e: + raise newJSONRPCError(ValueError, "Transaction is invalid: " & e.msg) + except Spam as e: + panic("Transaction we're generating work for was labelled Spam: " & e.msg) + except JSONRPCError as e: + raise e except Exception as e: - panic("addSend raised an Exception despite catching all errors: " & e.msg) - res["result"] = % true + panic("Calling publishTransactionWithoutWork raised an Exception despite catching all errors: " & e.msg) except Exception as e: panic("Couldn't create the Transactions Module: " & e.msg) diff --git a/src/Interfaces/RPC/RPC.nim b/src/Interfaces/RPC/RPC.nim index b0fb110fa..323679730 100644 --- a/src/Interfaces/RPC/RPC.nim +++ b/src/Interfaces/RPC/RPC.nim @@ -1,3 +1,6 @@ +import strutils +import json + import chronos import ../../lib/Errors @@ -5,9 +8,11 @@ import ../../lib/Errors import ../../objects/[ConfigObj, GlobalFunctionBoxObj] import objects/RPCObj -export RPC +export RPCObj.RPC +import HTTP import Modules/[ + SystemModule, TransactionsModule, ConsensusModule, MeritModule, @@ -15,42 +20,6 @@ import Modules/[ NetworkModule ] -proc newRPC*( - functions: GlobalFunctionBox, - toRPC: ptr Channel[JSONNode], - toGUI: ptr Channel[JSONNode] -): RPC {.forceCheck: [].} = - newRPCObj( - merge( - (prefix: "transactions_", rpc: TransactionsModule.module(functions)), - (prefix: "consensus_", rpc: ConsensusModule.module(functions)), - (prefix: "merit_", rpc: MeritModule.module(functions)), - (prefix: "personal_", rpc: PersonalModule.module(functions)), - (prefix: "network_", rpc: NetworkModule.module(functions)) - ), - functions.system.quit, - toRPC, - toGUI - ) - -#Add an error response to an existing JSONNode. -proc error( - res: JSONNode, - code: int, - msg: string, - data: JSONNode = nil -) {.forceCheck: [].} = - res["error"] = %* { - "code": code, - "message": msg - } - - try: - if not data.isNil: - res["error"]["data"] = data - except KeyError as e: - panic("Couldn't set an error's data field, despite just creating the data: " & e.msg) - #Create a new error response. proc newError( id: JSONNode, @@ -60,233 +29,180 @@ proc newError( ): JSONNode {.forceCheck: [].} = result = %* { "jsonrpc": "2.0", - "id": id + "id": id, + "error": { + "code": code, + #These shouldn't have periods, as exemplified by the spec's lack of periods on official messages. + #Meros follows that standard, yet its own exceptions use periods. + #That means quoted exceptions will create a message ending with a period. + #Hence the if statement. + "message": if msg[^1] == '.': msg[0 ..< msg.len - 1] else: msg + } } - error(result, code,msg, data) - -#Handle a request and store the result in res. -proc handle*( - rpc: RPC, - req: JSONNode, - res: ref JSONNode, - reply: proc ( - res: JSONNode - ): Future[void] {.gcsafe.} -) {.forceCheck: [], async.} = - #Verify the version. - try: - if (not req.hasKey("jsonrpc")) or (req["jsonrpc"].getStr() != "2.0"): - error(res[], -32600, "Invalid Request") - return - except KeyError as e: - panic("Couldn't check the RPC version despite confirming its existence: " & e.msg) - #Verify the method exists. try: - if (not req.hasKey("method")) or (req["method"].kind != JString): - error(res[], -32600, "Invalid Request") - return + if not data.isNil: + result["error"]["data"] = data except KeyError as e: - panic("Couldn't check the RPC method despite confirming its existence: " & e.msg) + panic("Couldn't set an error's data field, despite just creating the error: " & e.msg) - #Add params if it was omitted. - if (not req.hasKey("params")): - req["params"] = % [] +proc newRPC*( + functions: GlobalFunctionBox, + toRPC: ptr Channel[JSONNode], + toGUI: ptr Channel[JSONNode], + token: string +): RPC {.forceCheck: [].} = + var modules: seq[tuple[prefix: string, handle: RPCHandle]] = @[ + (prefix: "system_", handle: SystemModule.module(functions)), + (prefix: "transactions_", handle: TransactionsModule.module(functions)), + (prefix: "consensus_", handle: ConsensusModule.module(functions)), + (prefix: "merit_", handle: MeritModule.module(functions)), + (prefix: "personal_", handle: PersonalModule.module(functions)), + (prefix: "network_", handle: NetworkModule.module(functions)) + ] + + proc createHandler(): RPCHandle {.forceCheck: [].} = + result = proc ( + req: JSONNode, + reply: RPCReplyFunction, + authed: bool + ): Future[void] {.forceCheck: [], async.} = + if req.kind != JObject: + try: + await reply(newError(newJNull(), -32600, "Invalid Request")) + return + except Exception as e: + panic("Couldn't call reply about a Invalid Request due to an Exception despite reply not naturally throwing anything: " & e.msg) - #Make sure the param were an array. - try: - if req["params"].kind != JArray: - error(res[], -32600, "Invalid Request") - return - except KeyError as e: - panic("Couldn't check the RPC params despite confirming their existence: " & e.msg) + #Provide a params value if one wasn't supplied, as it can be omitted. + if not req.hasKey("params"): + req["params"] = newJObject() - #Override for system_quit. - try: - if req["method"].getStr() == "system_quit": - res[]["result"] = % true + #Check the request as a whole. try: - await reply(res[]) - except Exception as e: - panic("Couldn't call reply, despite catching all naturally thrown Exceptions: " & e.msg) - rpc.quit() - except KeyError as e: - panic("Couldn't get the RPC method despite confirming its existence: " & e.msg) - - try: - #Make sure the method exists. - if not rpc.functions.hasKey(req["method"].getStr()): - error(res[], -32601, "Method not found") - return - - #Call the method. - await rpc.functions[req["method"].getStr()](res[], req["params"]) - - #Handle KeyErrors. - except KeyError as e: - panic("Couldn't call a RPC method despite confirming its existence: " & e.msg) - - #If there was an invalid parameter, create the proper error response. - except ParamError: - try: - res[] = newError(req["id"], -32602, "Invalid params") - except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) - return - - #If a parameter had an invalid value, create the proper response. - except JSONRPCError as e: - try: - res[] = newError(req["id"], e.code, e.msg, e.data) - except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) - return - - #If we panic, make sure it bubbles up. - except AssertionError as e: - panic("RPC caused a panic: " & e.msg) - - #Else, respond that we had an internal error. - #Generally, we would panic here, yet the amount of custom data that can be entered makes that a worrysome prospect. - except Exception: - try: - res[] = newError(req["id"], -32603, "Internal error") - except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) - return - - #If the result isn't null and has no result field, provide a result field of true. - if (not res[].isNil) and (not res[].hasKey("result")): - res[]["result"] = % true - -#Handle a request and return the result. -proc handle*( - rpc: RPC, - req: JSONNode, - reply: proc ( - res: JSONNode - ): Future[void] {.gcsafe.} -): Future[ref JSONNode] {.forceCheck: [], async.} = - #Init the result. - result = new(ref JSONNode) - - #If this is a singular request... - if req.kind == JObject: - #Add an ID if it was omitted. - if not req.hasKey("id"): - req["id"] = % newJNull() - - #Create the response. - try: - result[] = %* { - "jsonrpc": "2.0", - "id": req["id"] - } - except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) - - #Handle it. - try: - await handle(rpc, req, result, reply) - except Exception as e: - panic("Couldn't handle the request (JSON; return res), despite catching all naturally thrown Exceptions: " & e.msg) - - #If this was a batch request... - elif req.kind == JArray: - #Prepare the result. - var results: ref JSONNode = new(ref JSONNode) - results[] = newJArray() + if not ( + #Only valid if this is a notification call, which we don't use. + #It may be better to use a different error, yet this is close enough. + req.hasKey("id") and + #Invalid version string. + req.hasKey("jsonrpc") and (req["jsonrpc"].kind == JString) and (req["jsonrpc"].getStr() == "2.0") and + #Invalid ID type. + req.hasKey("id") and ( + (req["id"].kind == JString) or + (req["id"].kind == JInt) or + (req["id"].kind == JFloat) or + (req["id"].kind == JNull) + ) and + #Technically invalid method field. + req.hasKey("method") and (req["method"].kind == JString) and + #Unstructured parameters. + ((not req.hasKey("params")) or ((req["params"].kind == JArray) or (req["params"].kind == JObject))) + ): + try: + await reply(newError(newJNull(), -32600, "Invalid Request")) + except Exception as e: + panic("Couldn't call reply about a Invalid Request due to an Exception despite reply not naturally throwing anything: " & e.msg) + return + except KeyError as e: + panic("Couldn't get a RPC request's field despite confirming its existence: " & e.msg) - #Iterate over each request. - for reqElem in req: - #Add an ID if it was omitted. - if not reqElem.hasKey("id"): - reqElem["id"] = % nil + #Supply params if they were omitted. + if not req.hasKey("params"): + req["params"] = newJObject() - #Prepare this specific result. + #While array parameters are technically valid, they aren't used by Meros. try: - result[] = %* { - "jsonrpc": "2.0", - "id": reqElem["id"] - } + if req["params"].kind != JObject: + try: + await reply(newError(req["id"], -32602, "Invalid params")) + return + except KeyError as e: + panic("Couldn't get the ID of the request despite confirming its existence: " & e.msg) + except Exception as e: + panic("Couldn't call reply about array params due to an Exception despite reply not naturally throwing anything: " & e.msg) except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) + panic("Couldn't get a RPC request's params field despite confirming its existence: " & e.msg) - #Check the request's type. - try: - if reqElem.kind != JObject: - results[].add(newError(reqElem["id"], -32600, "Invalid Request")) - continue - except KeyError as e: - panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) + #Check for extra fields. + #While we could let these slide, there isn't any good reason to allow them. + if req.len != 4: + try: + await reply(newError(req["id"], -32600, "Invalid Request", %* { + "reason": "Additional fields provided" + })) + return + except KeyError as e: + panic("Couldn't get the ID of the request despite confirming its existence: " & e.msg) + except Exception as e: + panic("Couldn't call reply about additional fields due to an Exception despite reply not naturally throwing anything: " & e.msg) - #Handle it. + var methodStr: string try: - await handle( - rpc, - reqElem, - result, - proc ( - res: JSONNode - ) {.forceCheck: [], async.} = - results[].add(res) + methodStr = req["method"].getStr() + except KeyError as e: + panic("Couldn't get the method of the request despite confirming its existence: " & e.msg) + + #Find the matching RPC module and pass it off. + var replied: bool = false + for rpc in modules: + if methodStr.startsWith(rpc.prefix): + #Remove the prefix so only the method is returned. + req["method"] = % methodStr[rpc.prefix.len ..< methodStr.len] + + #This has no raises pragma and should only raise ParamError/JSONRPCError. + #That said, we also have to bubble up AssertionErrors and handle Exceptions. + #There may also be a KeyError floating... + try: + await rpc.handle(req, reply, authed) + #Not authorized. + except RPCAuthorizationError: try: - await reply(results[]) + await reply(%* {"unauthorized": true}) except Exception as e: - panic("Couldn't call reply, despite catching all naturally thrown Exceptions: " & e.msg) - ) - except Exception as e: - panic("Couldn't handle the request (batch JSON; return res), despite catching all naturally thrown Exceptions: " & e.msg) + panic("reply threw an Exception despite not naturally throwing anything: " & e.msg) + #If there was an invalid parameter, create the proper error response. + except ParamError: + try: + await reply(newError(req["id"], -32602, "Invalid params")) + except KeyError as e: + panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) + except Exception as e: + panic("Couldn't call reply about a ParamError due to an Exception despite reply not naturally throwing anything: " & e.msg) - #If there was a result, add it. - if not result[].isNil: - results[].add(result[]) + #If there was an invalid value, create the proper response. + except JSONRPCError as e: + try: + await reply(newError(req["id"], e.code, e.msg, e.data)) + except KeyError as e: + panic("Couldn't get the ID despite guaranteeing its existence: " & e.msg) + except Exception as e: + panic("Couldn't call reply about a JSONRPCError due to an Exception despite reply not naturally throwing anything: " & e.msg) - #Set result to results. - result = results + #If we panic, make sure it bubbles up. + except AssertionError as e: + panic("RPC caused a panic: " & e.msg) - else: - error(result[], -32600, "Invalid Request") - return + #If we hit a raw Exception, likely from the async runtime, panic. + except Exception as e: + panic("Raw Exception from the RPC, which may be something OTHER than async: " & e.msg) -#Handle a string and return a string. -proc handle*( - rpc: RPC, - reqStr: string, - reply: proc ( - res: string - ): Future[void] {.gcsafe.} -): Future[string] {.forceCheck: [], async.} = - var - req: JSONNode - res: ref JSONNode - - #Parse the request. - try: - req = parseJSON(reqStr) - except Exception: - return $newError(newJNull(), -32700, "Parse error") + replied = true + break - #Handle it. - try: - res = await rpc.handle( - req, - proc ( - res: JSONNode - ) {.forceCheck: [], async.} = + if not replied: try: - await reply($res) + await reply(newError(req["id"], -32601, "Method not found")) except Exception as e: - panic("Couldn't call reply, despite catching all naturally thrown Exceptions: " & e.msg) - ) - except Exception as e: - panic("Couldn't handle the request (string), despite catching all naturally thrown Exceptions: " & e.msg) + panic("Couldn't call reply about the method not being found due to an Exception despite reply not naturally throwing anything: " & e.msg) + + result = RPC( + handle: createHandler(), + toRPC: toRPC, + toGUI: toGUI, - #Return the string. - if res[].isNil: - result = "" - else: - result = $res[] + token: token, + alive: true + ) #Start up the RPC's connection to the GUI. proc start*( @@ -295,7 +211,7 @@ proc start*( while rpc.alive: #Allow other async code to execute. try: - await sleepAsync(milliseconds(1)) + await sleepAsync(1.milliseconds) except Exception as e: panic("Couldn't sleep for 1ms before checking the GUI->RPC channel for data: " & e.msg) @@ -316,32 +232,27 @@ proc start*( continue #Handle the request. - var res: ref JSONNode try: - res = await rpc.handle( + #Can't directly inline due to an AST gen bug. + let rpcFuture: Future[void] = rpc.handle( data.msg, proc ( - replyArg: JSONNode + reply: JSONNode ) {.forceCheck: [], async.} = try: - rpc.toGUI[].send(replyArg) + rpc.toGUI[].send(reply) except DeadThreadError as e: panic("Couldn't send to a dead thread: " & e.msg) except Exception as e: panic("Sending over a channel threw an Exception: " & e.msg) + , true ) + await rpcFuture except Exception as e: panic("Couldn't handle the request from the GUI, despite catching all naturally thrown Exceptions: " & e.msg) - try: - rpc.toGUI[].send(res[]) - except DeadThreadError as e: - panic("Couldn't send to a dead thread: " & e.msg) - except Exception as e: - panic("Sending over a channel threw an Exception: " & e.msg) - #Create a function to handle a connection. -proc handle( +proc createSocketHandler( rpc: RPC ): proc ( server: StreamServer, @@ -349,72 +260,111 @@ proc handle( ): Future[void] {.gcsafe.} {.inline, forceCheck: [].} = result = proc ( server: StreamServer, - socket: StreamTransport + socketArg: StreamTransport ) {.forceCheck: [], async.} = + logTrace "RPC connection occurred" + + var socket: RPCSocket = newRPCSocket(socketArg) + #Handle the client. while not socket.closed(): #Read in a message. - var - data: string = "" - counter: int = 0 - oldLen: int = 1 - while true: - try: - data &= cast[string](await socket.read(1)) - except Exception: - try: - socket.close() - except Exception: - discard - return - - if data.len != oldLen: - try: - socket.close() - except Exception: - discard - return - inc(oldLen) - - if data[^1] == data[0]: - inc(counter) - elif (data[^1] == ']') and (data[0] == '['): - dec(counter) - elif (data[^1] == '}') and (data[0] == '{'): - dec(counter) - if counter == 0: + var httpReq: tuple[body: string, token: string] + try: + httpReq = await socket.readHTTP() + if socket.closed(): break + except Exception as e: + panic("readHTTP threw an error despite not naturally throwing anything: " & e.msg) #Handle the message. - var res: string + var parsedData: JSONNode try: - res = await rpc.handle( - data, - proc ( - replyArg: string - ) {.forceCheck: [], async.} = - try: - if (await socket.write(replyArg)) != replyArg.len: - raise newException(Exception, "") - except Exception: + parsedData = parseJSON(httpReq.body) + except Exception: + try: + await socket.writeHTTP($(newError(newJNull(), -32700, "Parse error"))) + continue + except Exception as e: + panic("writeHTTP threw an error despite not naturally throwing anything: " & e.msg) + + let batch: bool = parsedData.kind == JArray + #Convert this to a list of 'requests' to simplify the below handling code. + if not batch: + parsedData = % [parsedData] + + #If this is a batch request with length 0, this is an invalid request. + if parsedData.len == 0: + try: + await socket.writeHTTP($(newError(newJNull(), -32600, "Invalid Request"))) + continue + except Exception as e: + panic("writeHTTP threw an error despite not naturally throwing anything: " & e.msg) + + var + authorized: bool = httpReq.token == rpc.token + unauthorized: bool = false + res: JSONNode = newJArray() + try: + for req in parsedData: + if unauthorized: + break + + #See above instance for clarification. + let rpcFuture: Future[void] = rpc.handle( + req, + proc ( + replyArg: JSONNode + ) {.forceCheck: [], async.} = + if replyArg.hasKey("unauthorized"): + unauthorized = true + return + res.add(replyArg) + + #If we're quitting, send what we have. + #We only check for "quit" as a mutation occurs, dropping the module, during its processing. + #Technically non-compliant for batch requests, as all further requests in the batch won't be fulfilled. try: - socket.close() - except Exception: + if (req.kind == JObject) and (req["method"] == (% "quit")) and (replyArg["result"] == (% true)): + if not batch: + res = res[0] + try: + await socket.writeHTTP($res) + except Exception as e: + panic("writeHTTP threw an error despite not naturally throwing anything: " & e.msg) + #Invalid request (syntactically or semantically). + except KeyError: discard - return - ) + , + authorized + ) + await rpcFuture except Exception as e: panic("RPC's handle threw an Exception despite not naturally throwing anything: " & e.msg) - try: - if (await socket.write(res)) != res.len: - raise newException(Exception, "") - except Exception: + if unauthorized: try: - socket.close() - except Exception: - discard - return + await socket.httpUnauthorized() + except Exception as e: + panic("httpUnauthorized threw an error despite not naturally throwing anything: " & e.msg) + continue + + var resStr: string + #If this is an empty array, respond with nothing. + #Happens when all requests are for notifications, not for responses. + #Doesn't apply to Meros which doesn't use those (at least, not yet). + if res.len == 0: + #Redundant. + resStr = "" + else: + if not batch: + res = res[0] + resStr = $res + + try: + await socket.writeHTTP($res) + except Exception as e: + panic("writeHTTP threw an error despite not naturally throwing anything: " & e.msg) #Start up the RPC's server socket. proc listen*( @@ -423,7 +373,7 @@ proc listen*( ) {.forceCheck: [], async.} = #Create the server. try: - rpc.server = createStreamServer(initTAddress("0.0.0.0", config.rpcPort), handle(rpc), {ReuseAddr}) + rpc.server = createStreamServer(initTAddress("0.0.0.0", config.rpcPort), createSocketHandler(rpc), {ReuseAddr}) except OSError as e: panic("Couldn't create the RPC server due to an OSError: " & e.msg) except TransportAddressError as e: @@ -455,5 +405,8 @@ proc shutdown*( if not rpc.server.isNil: try: rpc.server.close() - except Exception: - discard + except Exception as e: + logWarn "Couldn't shutdown the RPC server", reason = e.msg + + #Set alive to false. + rpc.alive = false diff --git a/src/Interfaces/RPC/objects/RPCObj.nim b/src/Interfaces/RPC/objects/RPCObj.nim index 9af7c1fb6..4985923cc 100644 --- a/src/Interfaces/RPC/objects/RPCObj.nim +++ b/src/Interfaces/RPC/objects/RPCObj.nim @@ -1,100 +1,356 @@ import macros + +import options +import sequtils +import strutils import tables import json -export tables, json -import chronos -export chronos +import chronos except Socket import ../../../lib/Errors +import ../../../Network/objects/SocketObj +export recv type - RPCFunction = proc ( - res: JSONNode, - params: JSONNode + RPCSocket* = ref object + #Use the Networking socket which already has the proper helpies/safeties. + socket: Socket + #Headers to use in the response to the last request. + headers*: Table[string, string] + + RPCReplyFunction* = proc ( + res: JSONNode ): Future[void] {.gcsafe.} - RPCFunctions* = Table[string, RPCFunction] + RPCHandle* = proc ( + req: JSONNode, + reply: RPCReplyFunction, + authed: bool + ): Future[void] {.gcsafe.} RPC* = ref object - alive*: bool - - functions*: RPCFunctions - quit*: proc () {.gcsafe, raises: [].} + handle*: RPCHandle toRPC*: ptr Channel[JSONNode] toGUI*: ptr Channel[JSONNode] + token*: string server*: StreamServer + alive*: bool + + #Stub replaced with a string; used to signify to parse the string from hex. + hex* = object + +proc newRPCSocket*( + socket: StreamTransport +): RPCSocket {.forceCheck: [], inline.} = + RPCSocket( + socket: newSocket(socket), + headers: initTable[string, string]() + ) + +proc closed*( + socket: RPCSocket +): bool {.forceCheck: [], inline.} = + socket.socket.closed + +proc close*( + socket: RPCSocket +) {.forceCheck: [], inline.} = + socket.socket.safeClose("Told to close RPC socket") + +proc send*( + socket: RPCSocket, + msg: string +) {.forceCheck: [], async.} = + try: + await socket.socket.send(msg) + except SocketError: + socket.close() + except Exception as e: + panic("Sending to an RPC socket raised an Exception despite catching all Exceptions: " & e.msg) + +proc recv*( + socket: RPCSocket, + lenArg: int +): Future[string] {.forceCheck: [], async.} = + var len: int = lenArg + if socket.socket.readLineBuffer != char(0): + result = $socket.socket.readLineBuffer + socket.socket.readLineBuffer = char(0) + len -= 1 + + try: + result &= await socket.socket.recv(len) + except SocketError: + socket.close() + except Exception as e: + panic("recv raised an Exception despite catching all Exceptions: " & e.msg) + +proc readLine*( + socket: RPCSocket +): Future[string] {.forceCheck: [], async.} = + try: + result = await socket.socket.readLine() + except SocketError: + socket.close() + except Exception as e: + panic("Reading a line from an RPC socket raised an Exception despite catching all Exceptions: " & e.msg) + +template retrieveFromJSON*[T]( + value: JSONNode, + expectedType: typedesc[seq[T]] or typedesc[T] +#Auto as hex != string (and so on). +): auto = + when expectedType is Option: + some(retrieveFromJSON(value, type(T().get()))) + elif expectedType is seq: + if value.kind != JArray: + #This function uses ParamError + message, an oddity, as ParamError has a hardcoded error message. + #While that still applies to the actual RPC, this improves logging. + raise newLoggedException(ParamError, "retrieveFromJSON wanted a seq and didn't get a JSON array.") + + mapIt(toSeq(value.items), retrieveFromJSON(it, type(T))) + else: + #NOP for raw JSONNode. + when expectedType is JSONNode: + value + + elif expectedType is bool: + if value.kind != JBool: + raise newLoggedException(ParamError, "retrieveFromJSON expected bool.") + value.getBool() + + elif expectedType is SomeInteger: + if value.kind != JInt: + raise newLoggedException(ParamError, "retrieveFromJSON expected int.") + let num: int = value.getInt() + if num < int(low(T)): + raise newLoggedException(ParamError, "retrieveFromJSON expected an int within a specific range.") + + #Differentiate if this is an int or uint. Needed as high(uint) won't fit into an int. + when low(expectedType) != 0: + if (num > int(high(T))): + raise newLoggedException(ParamError, "retrieveFromJSON expected an int within a specific range.") + else: + if uint(num) > high(T): + raise newLoggedException(ParamError, "retrieveFromJSON expected a uint within a specific range.") + + T(num) + + elif expectedType is string: + if value.kind != JString: + raise newLoggedException(ParamError, "retrieveFromJSON expected a string.") + value.getStr() + + elif expectedType is hex: + var res: string + try: + res = retrieveFromJSON(value, string) + if res.substr(0, 1) == "0x": + res = res.substr(2, res.len).parseHexStr() + else: + res = res.parseHexStr() + except ValueError: + raise newLoggedException(ParamError, "retrieveFromJSON expected a hex string.") + res + + #Stops erroring when the Hash symbol isn't in scope. + elif $(expectedType) == "Hash[256]": + var res: string = retrieveFromJSON(value, hex) + if res.len != 32: + raise newLoggedException(ParamError, "retrieveFromJSON expected a 32-byte hex string (64 chars).") + res.toHash[:256]() + + elif $(expectedType) == "EdPublicKey": + var res: string = retrieveFromJSON(value, hex) + if res.len != 32: + raise newLoggedException(ParamError, "retrieveFromJSON expected a 32-byte hex string (64 chars).") + newEdPublicKey(res) + + #BLS Public Key. + elif $(expectedType) == "G2": + var resStr: string = retrieveFromJSON(value, hex) + if resStr.len != 96: + raise newLoggedException(ParamError, "retrieveFromJSON expected a 96-byte hex string (192 chars).") + + var res: BLSPublicKey + try: + res = newBLSPublicKey(resStr) + except BLSError as e: + raise newJSONRPCError(ValueError, "Invalid BLS Public Key: " & e.msg) + res -macro newRPCFunctions*( + elif $(expectedType) == "Address": + var res: Address + try: + res = retrieveFromJSON(value, string).getEncodedData() + except ValueError as e: + raise newLoggedException(ParamError, "retrieveFromJSON expected a string that is a valid address: " & e.msg) + res + + else: + {.error: "Trying to get an unknown type from JSON: " & $expectedType.} + +macro newRPCHandle*( routes: untyped ): untyped = - #Create a toTable call. - result = newNimNode(nnkAsgn).add( - ident("result"), - newCall( - ident("toTable") - ).add( - newNimNode(nnkTableConstr) + #The generated function is a RPCHandle. + #It needs to embody the functions passed in (routes), and also have a switch statement. + #Said switch must format the parameters for the target function. + #It finally needs to handle the reply logic. + + var + body: NimNode = newStmtList( + newEmptyNode(), + #Default result of true. + newVarStmt(ident("MACRO_res"), newCall(ident("%"), newLit(true))) + ) + switch: NimNode = newNimNode(nnkCaseStmt).add( + quote do: + getStr(MACRO_rawReq["method"]) ) - ) - #Add each route. for route in routes: - #Make sure they're closures. - route[1].addPragma(ident("closure")) - #Also add the gcsafe pragma for Chronos. - route[1].addPragma(ident("gcsafe")) - - #Make sure they're async. - var async: bool = false - for pragma in route[1][4]: - if (pragma.kind == nnkIdent) and (pragma.strVal == "async"): - async = true - if not async: - route[1].addPragma(ident("async")) - route[1][3][0] = newNimNode(nnkBracketExpr).add( - ident("Future"), - ident("void") + switch.add(newNimNode(nnkOfBranch)) + + var + argHandling: NimNode = newStmtList() + routeCall: NimNode = newCall(route[0]) + for argument in route[3][1 ..< route[3].len]: + var internalName: NimNode = ident("MACRO_ARGUMENT_" & argument[0].strVal) + + #If the argument isn't present, use the default value or fail. + var defaultOrFail: NimNode + if argument[2].kind != nnkEmpty: + defaultOrFail = newAssignment(internalName, argument[2]) + else: + defaultOrFail = quote do: + raise newLoggedException(ParamError, "") + + let + argumentName: string = argument[0].strVal.replace("_JSON") + argumentType: NimNode = argument[1] + + #Enable direct access to request/reply; used by quit. + if argumentType.kind == nnkIdent: + if argumentType.strVal == "RPCRequest": + routeCall.add(ident("MACRO_rawReq")) + continue + elif argumentType.strVal == "RPCReplyFunction": + routeCall.add(ident("MACRO_reply")) + continue + + var argumentActualType: NimNode = argumentType + #Doesn't support Option[hex], something currently unused and unsupported elsewhere as well. + if (argumentType.kind == nnkIdent) and (argumentType.strVal == "hex"): + argumentActualType = ident("string") + + argHandling.add( + quote do: + var `internalName`: `argumentActualType` + #Doesn't use a DotExpr for a more minimal AST. + if hasKey(MACRO_rawReq["params"], `argumentName`): + `internalName` = retrieveFromJSON(MACRO_rawReq["params"][`argumentName`], `argumentType`) + else: + `defaultOrFail` ) - result[1][1].add( - newNimNode(nnkExprColonExpr).add( - route[0], - route[1] + #Make sure it's passed to the function. + routeCall.add(internalName) + + var + hasAsyncPragma: bool = false + requiresAuth: bool = false + for pragma in route[4]: + if pragma.kind == nnkIdent: + if pragma.strVal == "async": + hasAsyncPragma = true + elif pragma.strVal == "requireAuth": + requiresAuth = true + continue + + var returnType: NimNode = route[3][0] + if hasAsyncPragma: + if returnType.kind == nnkBracketExpr: + returnType = returnType[0] + #If this is async, add an await. + routeCall = quote do: + await `routeCall` + + #If it's not void, set MACRO_res. + if ( + (returnType.kind != nnkEmpty) or + ( + (returnType.kind == nnkIdent) and + (returnType.strVal != "void") ) - ) + ): + routeCall = quote do: + MACRO_res = %(`routeCall`) -#Combine multiple RPCFunctions together. -proc merge*( - rpcs: varargs[ - tuple[prefix: string, rpc: RPCFunctions] - ] -): RPCFunctions {.raises: [].} = - result = initTable[string, RPCFunction]() + #Authorization check. + if requiresAuth: + argHandling = quote do: + if not MACRO_authed: + raise newLoggedException(RPCAuthorizationError, "401 Unauthorized") + `argHandling` - for rpc in rpcs: - for key in rpc.rpc.keys(): - try: - result[rpc.prefix & key] = rpc.rpc[key] - except KeyError as e: - panic("Couldn't get a value from the table despiting getting the key from .keys(): " & e.msg) - except Exception as e: - panic("Couldn't set a value in a table: " & e.msg) - -proc newRPCObj*( - functions: RPCFunctions, - quit: proc () {.gcsafe, raises: [].}, - toRPC: ptr Channel[JSONNode], - toGUI: ptr Channel[JSONNode] -): RPC {.inline, forceCheck: [].} = - RPC( - alive: true, - - functions: functions, - quit: quit, - - toRPC: toRPC, - toGUI: toGUI + let caseBody: NimNode = argHandling + caseBody.add(routeCall) + switch[^1].add(newStrLitNode(route[0].strVal), caseBody) + + switch.add(newNimNode(nnkElse)) + switch[^1].add( + quote do: + raise newJSONRPCError(-32601, "Method not found") + ) + + body[0] = routes + for r in 0 ..< body[0].len: + #Replace instances of artificial types/remove default argument values. + #Former are solely used as tags, latter is since they shouldn't be needed. + for i in 1 ..< body[0][r][3].len: + #Doesn't support Option[hex] which we don't use. + if body[0][r][3][i][1].kind == nnkIdent: + if body[0][r][3][i][1].strVal == "hex": + body[0][r][3][i][1] = ident("string") + elif body[0][r][3][i][1].strVal == "RPCRequest": + body[0][r][3][i][1] = ident("JSONNode") + body[0][r][3][i][2] = newNimNode(nnkEmpty) + + #Also remove requireAuth pragmas, since they're handled above and don't actually exist. + for p in 0 ..< body[0][r][4].len: + if (body[0][r][4][p].kind == nnkIdent) and (body[0][r][4][p].strVal == "requireAuth"): + body[0][r][4].del(p) + break + + body.add(switch) + + #Call reply. + body.add( + quote do: + await MACRO_reply(%* { + "jsonrpc": "2.0", + "id": MACRO_rawReq["id"], + "result": MACRO_res + }) + ) + + result = newProc( + newEmptyNode(), + @[ + newNimNode(nnkBracketExpr).add( + ident("Future"), + ident("void") + ), + newIdentDefs(ident("MACRO_rawReq"), ident("JSONNode")), + newIdentDefs(ident("MACRO_reply"), ident("RPCReplyFunction")), + newIdentDefs(ident("MACRO_authed"), ident("bool")) + ], + body, + nnkLambda, + quote do: + {.closure, async, gcsafe.} ) diff --git a/src/MainConsensus.nim b/src/MainConsensus.nim index f77cb5306..824bbc24d 100644 --- a/src/MainConsensus.nim +++ b/src/MainConsensus.nim @@ -69,8 +69,27 @@ proc mainConsensus( functions.consensus.getSendDifficulty = proc (): uint16 {.forceCheck: [].} = consensus.filters.send.difficulty + functions.consensus.getSendDifficultyOfHolder = proc ( + holder: uint16 + ): uint16 {.forceCheck: [ + IndexError + ].} = + try: + result = database.loadSendDifficulty(holder) + except DBReadError: + raise newLoggedException(IndexError, "Holder doesn't have a SendDifficulty.") + functions.consensus.getDataDifficulty = proc (): uint16 {.forceCheck: [].} = consensus.filters.data.difficulty + functions.consensus.getDataDifficultyOfHolder = proc ( + holder: uint16 + ): uint16 {.forceCheck: [ + IndexError + ].} = + try: + result = database.loadDataDifficulty(holder) + except DBReadError: + raise newLoggedException(IndexError, "Holder doesn't have a DataDifficulty.") functions.consensus.isMalicious = proc ( nick: uint16 @@ -147,7 +166,7 @@ proc mainConsensus( tx = await syncAwait network.syncManager.syncTransaction(verif.hash) except DataMissing: #At least the peer which gave us this Verification should have this Transaction. - raise newException(ValueError, "Verification is of a non-existent Transaction.") + raise newLoggedException(ValueError, "Verification is of a non-existent Transaction.") except Exception as e: panic("syncTransaction threw an error despite catching all errors: " & e.msg) try: @@ -158,11 +177,11 @@ proc mainConsensus( functions.transactions.addClaim(claim) of Send as send: if send.argon.overflows(send.getDifficultyFactor() * functions.consensus.getSendDifficulty()): - raise newException(ValueError, "Send doesn't pass the spam check.") + raise newLoggedException(ValueError, "Send doesn't pass the spam check.") await functions.transactions.addSend(send) of Data as data: if data.argon.overflows(data.getDifficultyFactor() * functions.consensus.getDataDifficulty()): - raise newException(ValueError, "Data doesn't pass the spam check.") + raise newLoggedException(ValueError, "Data doesn't pass the spam check.") await functions.transactions.addData(data) except ValueError as e: raise e diff --git a/src/MainImports.nim b/src/MainImports.nim index 087af674a..33ca273de 100644 --- a/src/MainImports.nim +++ b/src/MainImports.nim @@ -1,6 +1,10 @@ import os -import threadpool, locks -import sequtils +import locks +when not defined(nogui): + import threadpool + +import options +import strutils import sets, tables import json diff --git a/src/MainInterfaces.nim b/src/MainInterfaces.nim index 5a5665b0a..7aa182fae 100644 --- a/src/MainInterfaces.nim +++ b/src/MainInterfaces.nim @@ -15,9 +15,32 @@ proc mainRPC( functions: GlobalFunctionBox, rpc: var RPC ) {.forceCheck: [].} = - rpc = newRPC(functions, addr toRPC, addr toGUI) + #Don't bother if we'll never get any requests. + if not (config.rpc or config.gui): + return + + var token: string + if config.rpc: + #Grab the token if one was passed. + if config.token.isSome(): + token = config.token.unsafeGet() + #Generate one. + else: + token = newString(32) + randomFill(token) + token = token.toHex() + + try: + let tokenFile: File = open(config.dataDir / ".token", fmWrite) + tokenFile.write(token) + tokenFile.close() + except IOError as e: + panic("Couldn't write the RPC token to .token, under the data directory: " & e.msg) + + rpc = newRPC(functions, addr toRPC, addr toGUI, token) try: + #Start even if the RPC is disabled so we can still serve the RPC. asyncCheck rpc.start() if config.rpc: asyncCheck rpc.listen(config) diff --git a/src/MainMerit.nim b/src/MainMerit.nim index 4095a2dea..50f28d476 100644 --- a/src/MainMerit.nim +++ b/src/MainMerit.nim @@ -275,7 +275,7 @@ proc mainMerit( #If this header had a new miner, check if it was us. if newBlock.header.newMiner: - if newBlock.header.minerKey == wallet.miner.publicKey: + if (not wallet.miner.isNil) and (newBlock.header.minerKey == wallet.miner.publicKey): wallet.setMinerNick(uint16(merit.state.holders.len - 1)) logDebug "Minting Meros", hash = newBlock.header.hash @@ -289,7 +289,7 @@ proc mainMerit( transactions[].mint(newBlock.header.hash, rewards) #If we have a miner wallet, check if a mint was to us. - if wallet.miner.initiated: + if (not wallet.miner.isNil) and wallet.miner.initiated: for r in 0 ..< rewards.len: if wallet.miner.nick == rewards[r].nick: receivedMint = r @@ -326,16 +326,29 @@ proc mainMerit( #If we got a Mint... if receivedMint != -1: #Claim the Reward. - var claim: Claim - try: - claim = newClaim( - newFundedInput(newBlock.header.hash, receivedMint), - wallet.wallet.external.next().publicKey - ) - except ValueError as e: - panic("Created a Claim with a Mint yet newClaim raised a ValueError: " & e.msg) - except IndexError as e: - panic("Couldn't grab a Mint we just added: " & e.msg) + var + #Used to always claim to the first address. Using another has no value given the singular Merit Holder key. + firstAddress: uint32 = 0 + claim: Claim + while true: + try: + claim = newClaim( + newFundedInput(newBlock.header.hash, receivedMint), + wallet.getPublicKey( + some(firstAddress), + proc ( + key: EdPublicKey + ): bool {.gcsafe, forceCheck: [].} = + transactions[].loadIfKeyWasUsed(key) + ) + ) + break + except ValueError: + inc(firstAddress) + if firstAddress == 256: + panic("Generated a Wallet with 256 invalid child keys in a row OR Meros is trying to create a malformed Claim.") + except IndexError as e: + panic("Couldn't grab a Mint we just added: " & e.msg) wallet.miner.sign(claim) diff --git a/src/MainPersonal.nim b/src/MainPersonal.nim index f128f7f79..495b2d3a9 100644 --- a/src/MainPersonal.nim +++ b/src/MainPersonal.nim @@ -1,128 +1,159 @@ include MainTransactions proc mainPersonal( - wallet: WalletDB, + db: WalletDB, functions: GlobalFunctionBox, transactions: ref Transactions ) {.forceCheck: [].} = - functions.personal.getMinerWallet = proc (): MinerWallet {.forceCheck: [].} = - wallet.miner + functions.personal.getMinerWallet = proc (): MinerWallet {.forceCheck: [ + ValueError + ].} = + if db.miner.isNil: + raise newException(ValueError, "Meros is running as a WatchWallet and has no Merit Holder.") + result = db.miner + + functions.personal.getMnemonic = proc (): string {.forceCheck: [ + ValueError + ].} = + try: + result = db.getMnemonic() + except ValueError as e: + raise e - functions.personal.getWallet = proc (): Wallet {.forceCheck: [].} = - wallet.wallet + functions.personal.setAccount = proc ( + key: EdPublicKey, + chainCode: Hash[256], + clear: bool = false + ) {.forceCheck: [].} = + if clear: + db.clearPrivateKeys() + + var datas: seq[Data] + block handleDatas: + #Start with the initial data, discovering spenders until the tip. + var initial: Data + try: + initial = newData(Hash[256](), HDPublic( + key: key, + chainCode: chainCode + ).derivePublic(0).next(0).key.serialize()) + except ValueError as e: + panic("Couldn't create an initial Data to discover a Data tip: " & e.msg) + try: + discard transactions[][initial.hash] + #No Datas. + except IndexError: + break handleDatas - functions.personal.setMnemonic = proc ( + var + last: Hash[256] = initial.hash + spenders: seq[Hash[256]] = transactions[].loadSpenders(newInput(last)) + while spenders.len != 0: + last = spenders[0] + spenders = transactions[].loadSpenders(newInput(last)) + + #Grab the chain. + try: + datas = @[cast[Data](transactions[][last])] + while datas[^1].inputs[0].hash != Hash[256](): + datas.add(cast[Data](transactions[][datas[^1].inputs[0].hash])) + except IndexError as e: + panic("Couldn't get a Data chain from a discovered tip: " & e.msg) + + db.setAccount( + key, + chainCode, + datas, + proc ( + key: EdPublicKey + ): bool {.gcsafe, forceCheck: [].} = + transactions[].loadIfKeyWasUsed(key) + ) + + functions.personal.setWallet = proc ( mnemonic: string, password: string ) {.forceCheck: [ ValueError ].} = - try: - wallet.setWallet(mnemonic, password) - except ValueError as e: - raise e + var wallet: InsecureWallet + if mnemonic.len == 0: + wallet = newWallet(password) + else: + try: + wallet = newWallet(mnemonic, password) + except ValueError as e: + raise e - functions.personal.send = proc ( - destinationArg: string, - amountStr: string - ): Future[Hash[256]] {.forceCheck: [ - ValueError, - NotEnoughMeros - ], async.} = - var - #Wallet we're using. - child: HDWallet - #Spendable UTXOs. - utxos: seq[FundedInput] - destination: Address - amountIn: uint64 - amountOut: uint64 - send: Send + db.setMinerAndMnemonic(wallet) try: - destination = destinationArg.getEncodedData() + let account: HDWallet = wallet.hd[0] + functions.personal.setAccount(account.publicKey, account.chainCode) except ValueError as e: - raise e + panic("Account zero wasn't usable despite the above newWallet call making sure it was usable: " & e.msg) - #Grab a child. - try: - child = wallet.wallet.external.next() - except ValueError as e: - panic("Wallet has no usable keys: " & e.msg) - utxos = transactions[].getUTXOs(child.publicKey) + functions.personal.getAccount = proc (): tuple[key: EdPublicKey, chainCode: Hash[256]] {.forceCheck: [].} = + (key: db.accountZero, chainCode: db.chainCode) + + functions.personal.getAddress = proc ( + index: Option[uint32] + ): string {.gcsafe, forceCheck: [ + ValueError + ].} = try: - amountOut = parseUInt(amountStr) + result = db.getAddress( + index, + proc ( + key: EdPublicKey + ): bool {.gcsafe, forceCheck: [].} = + transactions[].loadIfKeyWasUsed(key) + ) except ValueError as e: raise e - #Grab the needed UTXOs. + functions.personal.getChangeKey = proc (): EdPublicKey {.gcsafe, forceCheck: [].} = + db.getChangeKey( + proc ( + key: EdPublicKey + ): bool {.gcsafe, forceCheck: [].} = + transactions[].loadIfKeyWasUsed(key) + ) + + functions.personal.getKeyIndex = proc ( + key: EdPublicKey + ): KeyIndex {.gcsafe, forceCheck: [ + IndexError + ].} = try: - var i: int = 0 - while i < utxos.len: - if transactions[].loadSpenders(utxos[i]).len != 0: - utxos.delete(i) - continue - - #Add this UTXO's amount to the amount in. - amountIn += transactions[][utxos[i].hash].outputs[utxos[i].nonce].amount - - #Remove uneeded UTXOs. - if amountIn >= amountOut: - if i + 1 < utxos.len: - utxos.delete(i + 1, utxos.len - 1) - break - - #Increment i. - inc(i) + result = db.getKeyIndex(key) except IndexError as e: - panic("Couldn't load a transaction we have an UTXO for: " & e.msg) - - #Make sure we have enough Meros. - if amountIn < amountOut: - raise newLoggedException(NotEnoughMeros, "Wallet didn't have enough money to create a Send.") - - #Create the outputs. - var outputs: seq[SendOutput] = @[ - newSendOutput(destination, amountOut) - ] - - #Add a change output. - if amountIn != amountOut: - outputs.add(newSendOutput(child.publicKey, amountIn - amountOut)) - - send = newSend(utxos, outputs) - child.sign(send) - send.mine(functions.consensus.getSendDifficulty()) + raise e - #Add the Send. + functions.personal.sign = proc ( + send: Send, + keys: seq[KeyIndex], + password: string + ) {.gcsafe, forceCheck: [ + IndexError, + ValueError + ].} = try: - await functions.transactions.addSend(send) + db.getAggregateKey(keys, password).sign(send) + except IndexError as e: + raise e except ValueError as e: - panic("Created a Send which was invalid: " & e.msg) - except DataExists as e: - panic("Created a Send which already existed: " & e.msg) - except Exception as e: - panic("addSend threw an Exception despite catching every Exception: " & e.msg) - - result = send.hash + raise e functions.personal.data = proc ( - dataStr: string + dataStr: string, + password: string ): Future[Hash[256]] {.forceCheck: [ ValueError ], async.} = - #Wallet we're using. - var child: HDWallet - try: - #Even though this call "next", this should always use the first Wallet. - #Just a note since our BIP 44 usage will change in the future. - child = wallet.wallet.external.next() - except ValueError as e: - panic("Wallet has no usable keys: " & e.msg) - #Create the Data. try: - wallet.stepData(dataStr, child, functions.consensus.getDataDifficulty()) + db.stepData(password, dataStr, functions.consensus.getDataDifficulty()) except ValueError as e: raise e @@ -134,12 +165,9 @@ proc mainPersonal( Because of that, the following iterative approach is used to add all 'new' Datas. ]# var toAdd: seq[Data] = @[] - for data in wallet.loadDatasFromTip(): + for data in db.loadDatasFromTip(): toAdd.add(data) - #Reached the initial Data. - if data.inputs[0].hash == Hash[256](): - break #We have the Data it relies on. try: discard transactions[][data.inputs[0].hash] @@ -161,3 +189,6 @@ proc mainPersonal( panic("addData threw an Exception despite catching every Exception: " & e.msg) result = toAdd[0].hash + + functions.personal.getUTXOs = proc (): seq[UsableInput] {.forceCheck: [].} = + db.getUTXOs(transactions) diff --git a/src/MainReorganization.nim b/src/MainReorganization.nim index 59fb8b964..befbab174 100644 --- a/src/MainReorganization.nim +++ b/src/MainReorganization.nim @@ -228,4 +228,4 @@ proc reorganize( result.headers.del(high(result.headers)) else: logInfo "Not reorganizing", oldWork = oldWorkStr, newWork = newWorkStr - raise newException(NotEnoughWork, "Chain didn't have enough work to be worth reorganizing to.") + raise newLoggedException(NotEnoughWork, "Chain didn't have enough work to be worth reorganizing to.") diff --git a/src/MainTransactions.nim b/src/MainTransactions.nim index 0daa6306e..74eb092ff 100644 --- a/src/MainTransactions.nim +++ b/src/MainTransactions.nim @@ -8,6 +8,10 @@ proc verify( consensus: ref Consensus, transaction: Transaction ) {.forceCheck: [], async.} = + #Make sure we're a Miner with Merit. + if wallet.miner.isNil or (not wallet.miner.initiated) or (merit.state.merit[wallet.miner.nick] == 0): + return + #Grab the Transaction's status. var status: TransactionStatus try: @@ -19,28 +23,26 @@ proc verify( if status.beaten: return - #Make sure we're a Miner with Merit. - if wallet.miner.initiated and (merit.state.merit[wallet.miner.nick] > 0): - #Inform the WalletDB were verifying a Transaction. - try: - wallet.verifyTransaction(transaction) - #We already verified a competitor. - except ValueError: - return + #Inform the WalletDB we're verifying a Transaction. + try: + wallet.verifyTransaction(transaction) + #We already verified a competitor. + except ValueError: + return - #Verify the Transaction. - var verif: SignedVerification = newSignedVerificationObj(transaction.hash) - wallet.miner.sign(verif) + #Verify the Transaction. + var verif: SignedVerification = newSignedVerificationObj(transaction.hash) + wallet.miner.sign(verif) - #Add the Verification, which calls broadcast. - try: - await functions.consensus.addSignedVerification(verif) - except ValueError as e: - panic("Created a Verification with an invalid signature: " & e.msg) - except DataExists as e: - panic("Created a Verification which already exists: " & e.msg) - except Exception as e: - panic("addSignedVerification threw an exception despite catching all errors: " & e.msg) + #Add the Verification, which calls broadcast. + try: + await functions.consensus.addSignedVerification(verif) + except ValueError as e: + panic("Created a Verification with an invalid signature: " & e.msg) + except DataExists as e: + panic("Created a Verification which already exists: " & e.msg) + except Exception as e: + panic("addSignedVerification threw an exception despite catching all errors: " & e.msg) proc syncPrevious( functions: GlobalFunctionBox, @@ -82,7 +84,7 @@ proc syncPrevious( ((final of Send) and (not ((tx of Claim) or (tx of Send)))) or ((final of Data) and (not (tx of Data))) ): - raise newException(ValueError, "Transaction has an invalid input.") + raise newLoggedException(ValueError, "Transaction has an invalid input.") if ( ( @@ -93,7 +95,7 @@ proc syncPrevious( cast[Data](tx).argon.overflows(cast[Data](tx).getDifficultyFactor() * functions.consensus.getDataDifficulty()) ) ): - raise newException(ValueError, "Transaction doesn't pass the spam check.") + raise newLoggedException(ValueError, "Transaction doesn't pass the spam check.") queue.add(tx) @@ -144,6 +146,11 @@ proc mainTransactions( except IndexError as e: raise e + functions.transactions.getUTXOs = proc ( + key: EdPublicKey + ): seq[FundedInput] {.forceCheck: [].} = + transactions[].getUTXOs(key) + functions.transactions.getSpenders = proc ( input: Input ): seq[Hash[256]] {.forceCheck: [].} = @@ -201,7 +208,7 @@ proc mainTransactions( except DataExists as e: raise e except DataMissing: - raise newException(ValueError, "Transaction has a non-existent input.") + raise newLoggedException(ValueError, "Transaction has a non-existent input.") except Exception as e: panic("syncPrevious threw an Exception despite catching everything: " & e.msg) @@ -244,7 +251,7 @@ proc mainTransactions( except DataExists as e: raise e except DataMissing: - raise newException(ValueError, "Transaction has a non-existent input.") + raise newLoggedException(ValueError, "Transaction has a non-existent input.") except Exception as e: panic("syncPrevious threw an Exception despite catching everything: " & e.msg) @@ -301,8 +308,3 @@ proc mainTransactions( hash: Hash[256] ) {.forceCheck: [].} = transactions[].prune(hash) - - functions.transactions.getUTXOs = proc ( - key: EdPublicKey - ): seq[FundedInput] {.forceCheck: [].} = - transactions[].getUTXOs(key) diff --git a/src/Meros.nim b/src/Meros.nim index 8639cec19..7c538d2e2 100644 --- a/src/Meros.nim +++ b/src/Meros.nim @@ -85,7 +85,7 @@ proc main() {.thread.} = database.close() wallet.close() except DBError as e: - echo "Couldn't shutdown the DB: " & e.msg + logWarn "Couldn't shutdown the DB", reson = e.msg #Quit. quit(0) diff --git a/src/Network/Serialize/Merit/ParseBlockBody.nim b/src/Network/Serialize/Merit/ParseBlockBody.nim index dc55ed83f..87aa2d03d 100644 --- a/src/Network/Serialize/Merit/ParseBlockBody.nim +++ b/src/Network/Serialize/Merit/ParseBlockBody.nim @@ -15,6 +15,9 @@ proc parseBlockBody*( ValueError ].} = #Packets Contents | Capacity | Sketch | Amount of Elements | Elements | Aggregate Signature + if bodyStr.len < HASH_LEN + INT_LEN: + raise newLoggedException(ValueError, "parseBlockBody not handed enough data to get the capacity.") + result.capacity = bodyStr[HASH_LEN ..< HASH_LEN + INT_LEN].fromBinary() var sketchLen: int = result.capacity * SKETCH_HASH_LEN diff --git a/src/Network/Serialize/Merit/ParseBlockHeader.nim b/src/Network/Serialize/Merit/ParseBlockHeader.nim index ae4e41364..bbbf73b5a 100644 --- a/src/Network/Serialize/Merit/ParseBlockHeader.nim +++ b/src/Network/Serialize/Merit/ParseBlockHeader.nim @@ -23,6 +23,17 @@ proc parseBlockHeader*( BYTE_LEN ) + const SHARED_LEN: int = INT_LEN + HASH_LEN + HASH_LEN + INT_LEN + INT_LEN + HASH_LEN + BYTE_LEN + INT_LEN + INT_LEN + BLS_SIGNATURE_LEN + if headerSeq[6] == "\0": + #< as the DBs call this with full blocks, so we can't just check for equality. + #Easier than detecting length on that end and passing a splice. + if headerStr.len < (SHARED_LEN + NICKNAME_LEN): + raise newLoggedException(ValueError, "parseBlockHeader not handed enough data for an existing miner header.") + #This also handles the edge case where the flag is empty, as the string wasn't even long enough for that. + else: + if headerStr.len < (SHARED_LEN + BLS_PUBLIC_KEY_LEN): + raise newLoggedException(ValueError, "parseBlockHeader not handed enough data for a new miner header.") + #Extract the rest of the header. headerSeq = headerSeq & headerStr[ BLOCK_HEADER_DATA_LEN ..< headerStr.len diff --git a/src/Network/Serialize/Transactions/ParseClaim.nim b/src/Network/Serialize/Transactions/ParseClaim.nim index e8c5722e6..492d0853f 100644 --- a/src/Network/Serialize/Transactions/ParseClaim.nim +++ b/src/Network/Serialize/Transactions/ParseClaim.nim @@ -13,6 +13,13 @@ proc parseClaim*( #Verify the input length. if claimStr.len < BYTE_LEN: raise newLoggedException(ValueError, "parseClaim not handed enough data to get the amount of inputs.") + if claimStr.len != ( + BYTE_LEN + + (int(claimStr[0]) * (HASH_LEN + BYTE_LEN)) + + ED_PUBLIC_KEY_LEN + + BLS_SIGNATURE_LEN + ): + raise newLoggedException(ValueError, "parseClaim handed the wrong amount of data.") #Inputs Length | Inputs | Output Ed25519 Key | BLS Signature var claimSeq: seq[string] = claimStr.deserialize( diff --git a/src/Network/Serialize/Transactions/ParseData.nim b/src/Network/Serialize/Transactions/ParseData.nim index 35dee3183..3a9fe93c1 100644 --- a/src/Network/Serialize/Transactions/ParseData.nim +++ b/src/Network/Serialize/Transactions/ParseData.nim @@ -15,9 +15,16 @@ proc parseData*( #Verify the input length. if dataStr.len < HASH_LEN + BYTE_LEN: raise newLoggedException(ValueError, "parseData not handed enough data to get the length of the data.") - if dataStr.len < HASH_LEN + BYTE_LEN + int(dataStr[HASH_LEN]) + BYTE_LEN: raise newLoggedException(ValueError, "parseData not handed enough data to get the data.") + if dataStr.len != ( + HASH_LEN + + BYTE_LEN + + (int(dataStr[HASH_LEN]) + 1) + + ED_SIGNATURE_LEN + + INT_LEN + ): + raise newLoggedException(ValueError, "parseData handed the wrong amount of data.") #Input | Data Length | Data | Signature | Proof var dataSeq: seq[string] = dataStr.deserialize( diff --git a/src/Network/Serialize/Transactions/ParseSend.nim b/src/Network/Serialize/Transactions/ParseSend.nim index 9f4323b9a..8aafbc183 100644 --- a/src/Network/Serialize/Transactions/ParseSend.nim +++ b/src/Network/Serialize/Transactions/ParseSend.nim @@ -5,6 +5,7 @@ import ../../../Database/Transactions/objects/SendObj import ../SerializeCommon +#A theoretical version of the function supporting missing work is available at https://gist.github.com/kayabaNerve/a03fdc506a00069e81c29afc5d6816bb. proc parseSend*( sendStr: string, diff: uint32 @@ -18,6 +19,15 @@ proc parseSend*( let outputLenPos: int = BYTE_LEN + (int(sendStr[0]) * (HASH_LEN + BYTE_LEN)) if sendStr.len < outputLenPos + BYTE_LEN: raise newLoggedException(ValueError, "parseSend not handed enough data to get the amount of outputs.") + if sendStr.len != ( + BYTE_LEN + + (sendStr[0].fromBinary() * (HASH_LEN + BYTE_LEN)) + + BYTE_LEN + + (sendStr[outputLenPos].fromBinary() * (ED_PUBLIC_KEY_LEN + MEROS_LEN)) + + ED_SIGNATURE_LEN + + INT_LEN + ): + raise newLoggedException(ValueError, "parseSend handed the wrong amount of data.") #Inputs Length | Inputs | Outputs Length | Signature | Proof var sendSeq: seq[string] = sendStr.deserialize( diff --git a/src/Network/objects/SocketObj.nim b/src/Network/objects/SocketObj.nim index a41828e66..59edcc4b2 100644 --- a/src/Network/objects/SocketObj.nim +++ b/src/Network/objects/SocketObj.nim @@ -7,6 +7,8 @@ type Socket* = ref object stream: StreamTransport alreadyClosed: bool + readLineBuffer*: char + proc newSocket*( addy: TransportAddress ): Future[Socket] {.forceCheck: [ @@ -105,3 +107,40 @@ proc safeClose*( if reason != "": logDebug "Closing raw socket", reason = reason + +#Used by the RPC, which shares this socket code. +proc readLine*( + socket: Socket +): Future[string] {.forceCheck: [], async.} = + try: + if socket.readLineBuffer != char(0): + let buffer: char = socket.readLineBuffer + socket.readLineBuffer = char(0) + + if buffer == '\r': + if not socket.stream.atEof: + socket.readLineBuffer = char((await socket.stream.read(1))[0]) + if socket.readLineBuffer == '\n': + socket.readLineBuffer = char(0) + return + result = $buffer + + while not socket.closed: + var temp: seq[byte] = await socket.stream.read(1) + if temp.len == 0: + raise newException(SocketError, "") + result &= char(temp[0]) + + if result[^1] == '\n': + result = result.substr(0, high(result) - 1) + break + elif result[^1] == '\r': + result = result.substr(0, high(result) - 1) + if not socket.stream.atEof: + socket.readLineBuffer = char((await socket.stream.read(1))[0]) + if socket.readLineBuffer == '\n': + socket.readLineBuffer = char(0) + break + except Exception: + socket.safeClose("Couldn't read a line from the socket.") + return "" diff --git a/src/Wallet/Address.nim b/src/Wallet/Address.nim index e48c2818a..da0c0ad65 100644 --- a/src/Wallet/Address.nim +++ b/src/Wallet/Address.nim @@ -29,16 +29,17 @@ const BCH_VALUES: array[5, uint32] = [ uint32(0X2A1462B3) ] -#AddressType enum. -#Right now, there's only PublicKey, yet in the future, there may PublicKeyHash/Stealth. -#Cannot have gaps due to the below address verification code. -type AddressType* = enum - PublicKey = 0 - -#Address object. Specifically stores a decoded address. -type Address* = object - addyType*: AddressType - data*: seq[byte] +type + #AddressType enum. + #Right now, there's only PublicKey, yet in the future, there may PublicKeyHash/Stealth. + #Cannot have gaps due to the below address verification code. + AddressType* = enum + PublicKey = 0 + + #Address object. Specifically stores a decoded address. + Address* = object + addyType*: AddressType + data*: seq[byte] #BCH Polymod function. func polymod( @@ -83,7 +84,7 @@ func verifyBCH( polymod(HRP.concat(data)) == 1 #Convert between two bases. -func convert( +proc convert( data: seq[byte], fromBits: int, to: int, @@ -110,7 +111,7 @@ func convert( if bits > 0: result.add(byte((acc shl (to - bits)) and maxv)) elif (bits >= fromBits) or (((acc shl (to - bits)) and maxv) != 0): - raise newException(ValueError, "Invalid padding.") + raise newLoggedException(ValueError, "Invalid address padding.") #Create a new address. proc newAddress*( diff --git a/src/Wallet/Ed25519.nim b/src/Wallet/Ed25519.nim index 989c533b3..3c430d3d0 100644 --- a/src/Wallet/Ed25519.nim +++ b/src/Wallet/Ed25519.nim @@ -109,47 +109,61 @@ func `$`*( ): string {.inline, forceCheck: [].} = data.serialize().toHex() +proc hasMultipleKeys*( + keys: seq[EdPrivateKey or EdPublicKey] +): bool {.forceCheck: [].} = + for key in keys: + if key != keys[0]: + return true + +#Generates the `a` value to use for each key. +#Returns a Hash[512] as we don't have a good scalar type and the datas already in a Hash[512]. +#The EdPrivateKey type, which is effectively a scalar, mirrors Hash[256]'s instantiated type definition. +#While this would save 32-bytes, it'll be pushed off the stack soon enough. +#Internally, pointers to raw bytes are used anyways. +proc generateAs( + keys: seq[EdPublicKey] +): seq[Hash.Hash[512]] {.forceCheck: [].} = + var L: string = "" + for key in keys: + L &= key.serialize() + L = Blake512(L).serialize() + + for key in keys: + result.add(Blake512("agg" & L & key.serialize())) + reduceScalar(cast[ptr cuchar](addr result[^1].data[0])) + #Aggregate Public Keys for MuSig. proc aggregate*( - keys: var seq[EdPublicKey] + keys: seq[EdPublicKey] ): EdPublicKey {.forceCheck: [].} = - if keys.len == 1: + if not keys.hasMultipleKeys: return keys[0] var - bytes: string - l: Hash.Hash[256] - keyHash: Hash.Hash[256] + As: seq[Hash.Hash[512]] = keys.generateAs() keyPoint: Point3 - a: Point2 + bytes: string = newString(64) + p2: Point2 tempRes: PointP1P1 tempCached: PointCached res: Point3 - for key in keys: - bytes &= key.serialize() - l = SHA2_256(bytes) - bytes = newString(64) - for k in 0 ..< keys.len: - keyHash = SHA2_256(l.serialize() & keys[k].serialize()) - copyMem(addr bytes[0], addr keyHash.data[0], 32) - reduceScalar(cast[ptr cuchar](addr bytes[0])) - copyMem(addr keyHash.data[0], addr bytes[0], 32) - - keyToNegativePoint(addr keyPoint, addr keys[k].data[0]) + var key: EdPublicKey = keys[k] + keyToNegativePoint(addr keyPoint, addr key.data[0]) serialize(addr bytes[0], addr keyPoint) keyToNegativePoint(addr keyPoint, cast[ptr cuchar](addr bytes[0])) var blankScalar: array[32, cuchar] multiplyScalar( - addr a, - cast[ptr cuchar](addr keyHash.data[0]), + addr p2, + cast[ptr cuchar](addr As[k].data[0]), addr keyPoint, addr blankScalar[0] ) - serialize(addr bytes[0], addr a) + serialize(addr bytes[0], addr p2) keyToNegativePoint(addr keyPoint, cast[ptr cuchar](addr bytes[0])) serialize(addr bytes[0], addr keyPoint) keyToNegativePoint(addr keyPoint, cast[ptr cuchar](addr bytes[0])) @@ -163,9 +177,37 @@ proc aggregate*( serialize(addr result.data[0], addr res) +#Private key aggregation to create a private key matching the MuSig public key aggregation. +#Insecure in the scope of MuSig as it is solely meant to be used by internally known private keys. +#Not even close to what MuSig does. +proc aggregate*( + keys: seq[EdPrivateKey] +): EdPrivateKey {.forceCheck: [].} = + if not keys.hasMultipleKeys: + return keys[0] + + var pubKeys: seq[EdPublicKey] = @[] + for key in keys: + pubKeys.add(key.toPublicKey()) + + var + As: seq[Hash.Hash[512]] = generateAs(pubKeys) + res: string = newString(32) + for k in 0 ..< keys.len: + var key: EdPrivateKey = keys[k] + mulAdd(cast[ptr cuchar](addr res[0]), addr key.data[0], cast[ptr cuchar](addr As[k].data[0]), cast[ptr cuchar](addr res[0])) + + #Traditional secret key expansion would be H512(secret), with the left half mod l. + #We have a scalar, not a secret. In response, H512(scalar). Then, the scalar is the left half already. + #This leaves us with just the right half left, which is still the right half of the H512 result. + #We could also call urandom, which wouldn't be deterministic, or call H256 and just use that. + var expanded: Hash.Hash[512] = Blake512(res) + copyMem(addr result.data[0], addr res[0], 32) + copyMem(addr result.data[32], addr expanded.data[32], 32) + proc hash*( key: EdPublicKey -): hashes.Hash {.inline, forceCheck: [].} = +): hashes.Hash {.forceCheck: [].} = for b in key.data: result = result !& int(b) result = !$ result diff --git a/src/Wallet/HDWallet.nim b/src/Wallet/HDWallet.nim index 2289dc025..fc6957d1f 100644 --- a/src/Wallet/HDWallet.nim +++ b/src/Wallet/HDWallet.nim @@ -1,7 +1,8 @@ -import math - import stint +#Directly import mc_ed25519 for more control over Elliptic curve operations. +import mc_ed25519 + import ../lib/[Errors, Util, Hash] import Ed25519, Address @@ -11,15 +12,25 @@ import ../Network/Serialize/SerializeCommon const #BIP 44 Coin Type. - COIN_TYPE {.intdefine.}: uint32 = 0 + COIN_TYPE {.intdefine.}: uint32 = 5132 #Ed25519's l value. l: StUInt[256] = "7237005577332262213973186563042994240857116359379907606001950938285454250989".parse(StUInt[256]) + #Hardened derivation threshold. + HARDENED_THRESHOLD: uint32 = 1 shl 31 + +type + HDWallet* = object + chainCode*: Hash[256] + privateKey*: EdPrivateKey + publicKey*: EdPublicKey + address*: string -type HDWallet* = object - chainCode*: Hash[256] - privateKey*: EdPrivateKey - publicKey*: EdPublicKey - address*: string + HDPublic* = object + #Key and matching chain code. + key*: EdPublicKey + chainCode*: Hash[256] + #Index this key was of its parent. + index*: uint32 func sign*( wallet: HDWallet, @@ -35,20 +46,11 @@ func verify*( wallet.publicKey.verify(msg, sig) proc newHDWallet*( - secretArg: string + secret: string ): HDWallet {.forceCheck: [ ValueError ].} = - #Parse the secret. - var secret: string = secretArg - if secret.len == 64: - try: - secret = secretArg.parseHexStr() - except ValueError: - raise newLoggedException(ValueError, "Hex-length secret with invalid Hex data passed to newHDWallet.") - elif secret.len == 32: - discard - else: + if secret.len != 32: raise newLoggedException(ValueError, "Invalid length secret passed to newHDWallet.") #Keys. @@ -95,7 +97,7 @@ proc derive*( #Child index, in little endian. child: string = childArg.toBinary(INT_LEN) #Is this a Hardened derivation? - hardened: bool = childArg >= (uint32(2) ^ 31) + hardened: bool = childArg >= HARDENED_THRESHOLD for i in 0 ..< 32: pPrivateKeyL[31 - i] = byte(pPrivateKey[i]) pPrivateKeyR[31 - i] = byte(pPrivateKey[32 + i]) @@ -126,7 +128,16 @@ proc derive*( zL[31 - i] = Z.data[i] zR[31 - i] = Z.data[i + 32] - #Calculate the Private Key. + #[ + Calculate the Private Key. + WARNING: kL should probably be mod l here, as it's used as a scalar. + That said, the codebase Meros uses as a reference, due to being an existing implementation, just uses the 32-byte variable. + That said, this definitely isn't right; zR is explicitly % 2^256, and zL will overflow 32-bytes with enough of a depth. + THAT said, the codebase says kL mod l must != 0, suggesting it's not naturally mod l. + TL;DR the paper has an ambiguity; same ambiguity is dangerous; this should probably be mod l; it isn't. + Why isn't Meros, a cryptocurrency, which needs to be secure and proper concerned? + https://github.com/MerosCrypto/Meros/issues/266 + ]# kL = (readUIntBE[256](zL) * 8) + readUIntBE[256](pPrivateKeyL) try: if kL mod l == 0: @@ -158,6 +169,69 @@ proc derive*( chainCode: chainCode ) +proc derivePublic*( + parent: HDPublic, + child: uint32 +): HDPublic {.forceCheck: [ + ValueError +].} = + result.index = child + + var + Z: Hash[512] + chainCodeExtended: Hash[512] + if child >= HARDENED_THRESHOLD: + panic("Asked to derive a public key with a hardened threshold.") + else: + Z = HMAC_SHA2_512(parent.chainCode.serialize(), '\2' & parent.key.serialize() & child.toBinary(INT_LEN)) + chainCodeExtended = HMAC_SHA2_512(parent.chainCode.serialize(), '\3' & parent.key.serialize() & child.toBinary(INT_LEN)) + copyMem(addr result.chainCode.data[0], addr chainCodeExtended.data[32], 32) + + var zL: array[32, byte] + for i in 0 ..< 28: + zL[31 - i] = Z.data[i] + + var + temp: EdPrivateKey + scalar: array[32, byte] = (readUIntBE[256](zL) * 8).toByteArrayBE() + for i in 0 ..< 32: + temp.data[31 - i] = cuchar(scalar[i]) + result.key = temp.toPublicKey() + + let + existingKey: ptr Point3 = cast[ptr Point3](alloc0(sizeof(Point3))) + offset: ptr Point3 = cast[ptr Point3](alloc0(sizeof(Point3))) + cached: ptr PointCached = cast[ptr PointCached](alloc0(sizeof(PointCached))) + resultKey: ptr PointP1P1 = cast[ptr PointP1P1](alloc0(sizeof(PointP1P1))) + var tempPub: EdPublicKey = parent.key + + keyToNegativePoint(existingKey, addr tempPub.data[0]) + serialize(addr tempPub.data[0], existingKey) + keyToNegativePoint(existingKey, addr tempPub.data[0]) + + keyToNegativePoint(offset, addr result.key.data[0]) + serialize(addr result.key.data[0], offset) + keyToNegativePoint(offset, addr result.key.data[0]) + p3ToCached(cached, offset) + + add(resultKey, existingKey, cached) + p1p1ToP3(offset, resultKey) + serialize(addr result.key.data[0], offset) + + dealloc(existingKey) + dealloc(offset) + dealloc(cached) + dealloc(resultKey) + + if result.key.data[0] == cuchar(1): + var identity: bool = true + for i in 1 ..< result.key.data.len: + if result.key.data[i] != cuchar(0): + identity = false + break + if identity: + raise newLoggedException(ValueError, "Deriving this child key produced an unusable PublicKey.") + #Derive a full path. proc derive*( wallet: HDWallet, @@ -167,7 +241,7 @@ proc derive*( ].} = if path.len == 0: return wallet - if path.len >= 2^20: + if path.len >= (1 shl 20): raise newLoggedException(ValueError, "Derivation path depth is too big.") try: @@ -186,9 +260,9 @@ proc `[]`*( ].} = try: result = wallet.derive(@[ - uint32(44) + (uint32(2) ^ 31), - COIN_TYPE + (uint32(2) ^ 31), - account + (uint32(2) ^ 31) + uint32(44) + HARDENED_THRESHOLD, + COIN_TYPE + HARDENED_THRESHOLD, + account + HARDENED_THRESHOLD ]) #Guarantee the external and internal chains are usable. @@ -197,27 +271,34 @@ proc `[]`*( except ValueError as e: raise e -#Grab the next valid key on this path. +#Grab the first valid key on this path. +proc first*( + wallet: HDWallet +): HDWallet {.forceCheck: [].} = + var i: uint32 = 0 + while true: + try: + return wallet.derive(i) + except ValueError: + inc(i) + if i == HARDENED_THRESHOLD: + panic("Couldn't derive the first account before hitting 2 ** 31.") + +#Grab the next key on this path. proc next*( - wallet: HDWallet, - path: seq[uint32] = @[], - last: uint32 = 0 -): HDWallet {.forceCheck: [ + parent: HDPublic, + start: uint32 +): HDPublic {.forceCheck: [ ValueError ].} = - var - pathWallet: HDWallet - i: uint32 = last + 1 - try: - pathWallet = wallet.derive(path) - except ValueError as e: - raise e - + var i: uint32 = start while true: try: - return pathWallet.derive(i) + result = parent.derivePublic(i) + break + #Keep going until we hit a valid address. except ValueError: - inc(i) - if i == (uint32(2) ^ 31): - raise newLoggedException(ValueError, "This path is out of non-hardened keys.") + i += 1 + if i >= (1 shl 31): + raise newLoggedException(ValueError, "Couldn't derive the next key as this account is out of non-hardened keys.") continue diff --git a/src/Wallet/MinerWallet.nim b/src/Wallet/MinerWallet.nim index 30756d427..f5bee3a38 100644 --- a/src/Wallet/MinerWallet.nim +++ b/src/Wallet/MinerWallet.nim @@ -5,7 +5,7 @@ from ../lib/Util import randomFill import BLS export BLS -type MinerWallet* = object +type MinerWallet* = ref object initiated*: bool privateKey*: BLSPrivateKey publicKey*: BLSPublicKey diff --git a/src/Wallet/Mnemonic.nim b/src/Wallet/Mnemonic.nim index 2d65f45cc..a3a279319 100644 --- a/src/Wallet/Mnemonic.nim +++ b/src/Wallet/Mnemonic.nim @@ -10,12 +10,14 @@ const LISTFILE: string = staticRead("WordLists/English.txt") LIST: seq[string] = LISTFILE.splitLines() -type Mnemonic* = object +type Mnemonic* = ref object entropy*: string checksum*: string sentence*: string proc newMnemonic*(): Mnemonic {.forceCheck: [].} = + result = Mnemonic() + #Create the entropy. result.entropy = newString(32) randomFill(result.entropy) @@ -50,13 +52,17 @@ proc newMnemonic*(): Mnemonic {.forceCheck: [].} = #Increase the bit by 11. bit += 11 - result.sentence = result.sentence[ 0 ..< result.sentence.len - 1] + + #Remove the extra space at the end. + result.sentence = result.sentence[0 ..< result.sentence.len - 1] proc newMnemonic*( sentence: string -): Mnemonic {.forceCheck: [ +): Mnemonic {.forceCheck: [ ValueError ].} = + result = Mnemonic() + #Split the sentence. var words: seq[string] = sentence.split(" ").filter( proc ( @@ -68,6 +74,10 @@ proc newMnemonic*( #Set the sentence in the mnemonic. result.sentence = words.join(" ") + when not defined(merosTests): + if words.len != 24: + raise newException(ValueError, "Mnemonic has too little entropy.") + #Decode the sentence. var #Bits in the sentence. @@ -160,7 +170,7 @@ proc newMnemonic*( #Generate a secret using the Mnemonic and the password. proc unlock*( mnemonic: Mnemonic, - password: string = "" + password: string ): string {.inline, forceCheck: [].} = PDKDF2_HMAC_SHA2_512(mnemonic.sentence.toNFKD(), ("mnemonic" & password.toNFKD())).serialize() diff --git a/src/Wallet/Wallet.nim b/src/Wallet/Wallet.nim index b0a7a79b8..5fde1f83b 100644 --- a/src/Wallet/Wallet.nim +++ b/src/Wallet/Wallet.nim @@ -1,60 +1,47 @@ -import ../lib/Errors +import ../lib/[Errors, Hash] import Mnemonic, HDWallet -export Mnemonic.Mnemonic, `$`, HDWallet +export Mnemonic, HDWallet -type Wallet* = ref object +type InsecureWallet* = ref object mnemonic*: Mnemonic + password*: string hd*: HDWallet - external*: HDWallet - internal*: HDWallet +#Create a new Wallet. proc newWallet*( password: string -): Wallet {.forceCheck: [].} = - result = Wallet() - try: - result.mnemonic = newMnemonic() - result.hd = newHDWallet(result.mnemonic.unlock(password)[0 ..< 32]) - - #Guarantee account 0 is usable. - discard result.hd[0] - result.external = result.hd[0].derive(0) - result.internal = result.hd[0].derive(1) - except ValueError: - result = newWallet(password) - +): InsecureWallet {.forceCheck: [].} = + while true: + try: + result = InsecureWallet( + mnemonic: newMnemonic(), + password: password + ) + result.hd = newHDWallet(SHA2_256(result.mnemonic.unlock(password)).serialize()) + + #Guarantee account 0 is usable. + #This getter automatically checks the internal/external chains as well. + discard result.hd[0] + + break + except ValueError: + continue + +#Load an existing Wallet. proc newWallet*( - mnemonic: string, + mnemonicArg: string, password: string -): Wallet {.forceCheck: [ +): InsecureWallet {.forceCheck: [ ValueError ].} = - result = Wallet() try: - result.mnemonic = newMnemonic(mnemonic) - result.hd = newHDWallet(result.mnemonic.unlock(password)[0 ..< 32]) - result.external = result.hd[0].derive(0) - result.internal = result.hd[0].derive(1) + let mnemonic: Mnemonic = newMnemonic(mnemonicArg) + result = InsecureWallet( + mnemonic: mnemonic, + password: password, + hd: newHDWallet(SHA2_256(mnemonic.unlock(password)).serialize()) + ) + discard result.hd[0] except ValueError as e: raise e - -converter toHDWallet*( - wallet: Wallet -): HDWallet {.forceCheck: [].} = - wallet.hd - -proc privateKey*( - wallet: Wallet -): EdPrivateKey {.forceCheck: [].} = - wallet.hd.privateKey - -proc publicKey*( - wallet: Wallet -): EdPublicKey {.forceCheck: [].} = - wallet.hd.publicKey - -proc address*( - wallet: Wallet -): string {.forceCheck: [].} = - wallet.hd.address diff --git a/src/lib/Errors.nim b/src/lib/Errors.nim index 047295953..c3c2bf625 100644 --- a/src/lib/Errors.nim +++ b/src/lib/Errors.nim @@ -111,13 +111,26 @@ proc newSpam*( result.argon = argon result.difficulty = difficulty -proc newJSONRPCError*( - code: int, +proc newJSONRPCError*[T: Exception or int]( + error: typedesc[T] or T, msg: string, data: JSONNode = nil ): ref JSONRPCError = result = newLoggedException(JSONRPCError, msg) - result.code = code + when error is int: + result.code = error + elif error is Spam: + result.code = 2 + elif error is NotEnoughMeros: + result.code = 1 + elif error is DataMissing: + result.code = -1 + elif error is IndexError: + result.code = -2 + elif error is ValueError: + result.code = -3 + else: + {.error: "Unknown Exception type passed to newJSONRPCError".} result.data = data #Getter for the MaliciousMeritHolder's removal as a MeritRemoval. diff --git a/src/lib/Hash.nim b/src/lib/Hash.nim index 3c5fbee0b..c21008316 100644 --- a/src/lib/Hash.nim +++ b/src/lib/Hash.nim @@ -7,11 +7,18 @@ export HashCommon, Blake2, SHA2, Argon, RandomX template Blake64*( input: string -): uint64 = Blake2_64(input) +): uint64 = + Blake2_64(input) template Blake256*( input: string -): HashCommon.Hash[256] = Blake2_256(input) +): HashCommon.Hash[256] = + Blake2_256(input) + +template Blake512*( + input: string +): HashCommon.Hash[512] = + Blake2_512(input) proc hash*[L]( hash: HashCommon.Hash[L] diff --git a/src/lib/Hash/Blake2.nim b/src/lib/Hash/Blake2.nim index 5091ffbb0..bc59e2b3b 100644 --- a/src/lib/Hash/Blake2.nim +++ b/src/lib/Hash/Blake2.nim @@ -27,42 +27,36 @@ proc finalize( ): cint {.importc: "blake2b_final".} {.pop.} -proc Blake2_64*( +proc Blake[bits: static int]( bytes: string -): uint64 {.forceCheck: [].} = +): Hash[bits] {.forceCheck: [].} = var dataPtr: ptr char state: ptr Blake2bState = cast[ptr Blake2bState](alloc0(sizeof(Blake2bState))) if bytes.len != 0: dataPtr = unsafeAddr bytes[0] - if state.init(8) != 0: - panic("Failed to init an 8-byte Blake2b State.") + if state.init(bits div 8) != 0: + panic("Failed to init a " & $(bits div 8) & "-byte Blake2b State.") if state.update(dataPtr, bytes.len) != 0: panic("Failed to update a Blake2b State.") - var hash: string = newString(8) - if state.finalize(unsafeAddr hash[0], 8) != 0: + if state.finalize(addr result.data[0], bits div 8) != 0: panic("Failed to finalize a Blake2b State.") - result = uint64(hash.fromBinary()) dealloc(state) -proc Blake2_256*( +proc Blake2_64*( bytes: string -): Hash[256] {.forceCheck: [].} = - var - dataPtr: ptr char - state: ptr Blake2bState = cast[ptr Blake2bState](alloc0(sizeof(Blake2bState))) - if bytes.len != 0: - dataPtr = unsafeAddr bytes[0] +): uint64 {.inline, forceCheck: [].} = + uint64(Blake[64](bytes).serialize().fromBinary()) - if state.init(32) != 0: - panic("Failed to init a 32-byte Blake2b State.") - if state.update(dataPtr, bytes.len) != 0: - panic("Failed to update a Blake2b State.") - - if state.finalize(addr result.data[0], 32) != 0: - panic("Failed to finalize a Blake2b State.") +proc Blake2_256*( + bytes: string +): Hash[256] {.inline, forceCheck: [].} = + Blake[256](bytes) - dealloc(state) +proc Blake2_512*( + bytes: string +): Hash[512] {.inline, forceCheck: [].} = + Blake[512](bytes) diff --git a/src/lib/Util.nim b/src/lib/Util.nim index b80d09c17..2f85e4b0c 100644 --- a/src/lib/Util.nim +++ b/src/lib/Util.nim @@ -2,7 +2,7 @@ import bitops import times import strutils -export toHex, parseHexStr, parseHexInt, parseUInt +export toHex, parseHexStr import stint import nimcrypto diff --git a/src/lib/objects/ErrorObjs.nim b/src/lib/objects/ErrorObjs.nim index a6fbf3c87..c5e1d0dd3 100644 --- a/src/lib/objects/ErrorObjs.nim +++ b/src/lib/objects/ErrorObjs.nim @@ -51,14 +51,15 @@ type difficulty*: uint32 #Interfaces/RPC Errors. - ParamError* = object of CatchableError #Used when an invalid parameter is passed. - JSONRPCError* = object of CatchableError #Used when the RPC call errors. + RPCAuthorizationError* = object of CatchableError #Used when a method which requires authorization is called without it. + ParamError* = object of CatchableError #Used when an invalid parameter is passed. + JSONRPCError* = object of CatchableError #Used when the RPC call errors. code*: int data*: JSONNode #Interfaces/GUI Errors. WebViewError* = object of CatchableError #Used when WebView fails. - RPCError* = object of CatchableError #Used when the GUI makes an invalid RPC call. + RPCError* = object of CatchableError #Used when the GUI makes an invalid RPC call. #Interfaces Statuses. NotEnoughMeros* = object of CatchableError #Used when the RPC is instructed to create a Send for more Meros than it can. diff --git a/src/objects/ConfigObj.nim b/src/objects/ConfigObj.nim index 9284b6870..8fa3ac431 100644 --- a/src/objects/ConfigObj.nim +++ b/src/objects/ConfigObj.nim @@ -7,6 +7,7 @@ CLI options will override options from the settings file which will override the ]# import os +import options import strutils import tables import json @@ -26,9 +27,10 @@ OPTIONS: -l, --log-file File to save the log to. --db Name for the database file. -n, --network Network to connect to. - -ns, --no-server Don't accept incoming connections. + --token Bearer authorization token to use for the RPC. -t, --tcp-port Port to listen for connections on. -r, --rpc-port Port the RPC should listen on. + -ns, --no-server Don't accept incoming connections. -nr, --no-rpc Don't listen on the RPC socket. -ng, --no-gui Don't start the GUI.""" @@ -38,9 +40,9 @@ OPTIONS: "d": "data-dir", "l": "log-file", "n": "network", - "ns": "no-server", "t": "tcp-port", "r": "rpc-port", + "ns": "no-server", "nr": "no-rpc", "ng": "no-gui" }.toTable() @@ -51,9 +53,10 @@ OPTIONS: "log-file": 1, "db": 1, "network": 1, - "no-server": 0, + "token": 1, "tcp-port": 1, "rpc-port": 1, + "no-server": 0, "no-rpc": 0, "no-gui": 0 }.toTable() @@ -64,14 +67,16 @@ type Config* = object db*: string network*: string + token*: Option[string] - #Listening for Meros connections or not. - server*: bool #Port for our server to listen on. tcpPort*: int #Port for the RPC to listen on. rpcPort*: int + #Listening for Meros connections or not. + server*: bool + #Listen on the RPC socket or not. rpc*: bool @@ -107,11 +112,12 @@ proc newConfig*(): Config {.forceCheck: [].} = db: "db", network: "testnet", + token: none(string), - server: true, tcpPort: 5132, rpcPort: 5133, + server: true, rpc: true, gui: true ) @@ -222,14 +228,17 @@ proc newConfig*(): Config {.forceCheck: [].} = doAssert(false, "Either couldn't read or parse the settings file despite it existing: " & e.msg) #Handle the settings. - template setParameter[X]( - variable: var X, + template setParameter[T]( + variable: var T, parameter: string, value: untyped, overrideValue: untyped ): untyped = try: - variable = value + when T is Option: + variable = some(value) + else: + variable = value except ValueError: echo "Invalid ", parameter, " value in the JSON settings. Please run --help for more info." quit(0) @@ -237,7 +246,10 @@ proc newConfig*(): Config {.forceCheck: [].} = discard try: - variable = overrideValue + when T is Option: + variable = some(overrideValue) + else: + variable = overrideValue except KeyError: discard except ValueError: @@ -256,6 +268,12 @@ proc newConfig*(): Config {.forceCheck: [].} = options["network"][0] ) + result.token.setParameter( + "token", + settings.get("token", JString).getStr(), + options["token"][0] + ) + result.logFile &= result.network & ".log" result.logFile.setParameter( "log-file", diff --git a/src/objects/GlobalFunctionBoxObj.nim b/src/objects/GlobalFunctionBoxObj.nim index 850592f9d..3ac837df2 100644 --- a/src/objects/GlobalFunctionBoxObj.nim +++ b/src/objects/GlobalFunctionBoxObj.nim @@ -1,11 +1,13 @@ import locks import sets +import options import chronos import ../lib/[Errors, Hash] import ../Wallet/[MinerWallet, Wallet] -import ../Wallet/Wallet + +import ../Database/Filesystem/Wallet/WalletDB import ../Database/Merit/objects/[BlockHeaderObj, BlockObj] @@ -93,7 +95,18 @@ type ConsensusFunctionBox* = ref object getSendDifficulty*: proc (): uint16 {.gcsafe, raises: [].} + getSendDifficultyOfHolder*: proc ( + holder: uint16 + ): uint16 {.gcsafe, raises: [ + IndexError + ].} + getDataDifficulty*: proc (): uint16 {.gcsafe, raises: [].} + getDataDifficultyOfHolder*: proc ( + holder: uint16 + ): uint16 {.gcsafe, raises: [ + IndexError + ].} isMalicious*: proc ( nick: uint16, @@ -265,26 +278,59 @@ type ].} PersonalFunctionBox* = ref object - getMinerWallet*: proc(): MinerWallet {.gcsafe, raises: [].} + getMinerWallet*: proc(): MinerWallet {.gcsafe, raises: [ + ValueError + ].} - getWallet*: proc (): Wallet {.gcsafe, raises: [].} + getMnemonic*: proc (): string {.gcsafe, raises: [ + ValueError + ].} - setMnemonic*: proc ( + getAccount*: proc (): tuple[key: EdPublicKey, chainCode: Hash[256]] {.gcsafe, raises: [].} + + setAccount*: proc ( + key: EdPublicKey, + chainCode: Hash[256], + clear: bool = false + ) {.gcsafe, raises: [].} + + setWallet*: proc ( mnemonic: string, - paassword: string + password: string ) {.gcsafe, raises: [ ValueError ].} - send*: proc ( - destination: string, - amount: string - ): Future[Hash[256]] {.gcsafe.} + getAddress*: proc ( + index: Option[uint32] + ): string {.gcsafe, raises: [ + ValueError + ].} + + getChangeKey*: proc (): EdPublicKey {.gcsafe, raises: [].} + + getKeyIndex*: proc ( + key: EdPublicKey + ): KeyIndex {.gcsafe, raises: [ + IndexError + ].} + + sign*: proc ( + send: Send, + keys: seq[KeyIndex], + password: string + ) {.gcsafe, raises: [ + IndexError, + ValueError + ].} data*: proc ( - data: string + data: string, + password: string ): Future[Hash[256]] {.gcsafe.} + getUTXOs*: proc (): seq[UsableInput] {.gcsafe, raises: [].} + NetworkFunctionBox* = ref object connect*: proc ( ip: string, diff --git a/tests/Database/Consensus/ConsensusRevertTest.nim b/tests/Database/Consensus/ConsensusRevertTest.nim index 82d1c86a3..4cee3c06f 100644 --- a/tests/Database/Consensus/ConsensusRevertTest.nim +++ b/tests/Database/Consensus/ConsensusRevertTest.nim @@ -60,7 +60,7 @@ suite "ConsensusRevert": -1 ] - wallets: seq[Wallet] = @[] + wallets: seq[HDWallet] = @[] #Planned Sends. plans: Table[int, seq[seq[SendOutput]]] = initTable[int, seq[seq[SendOutput]]]() @@ -670,7 +670,7 @@ suite "ConsensusRevert": for b in 1 .. 155: #Create a random amount of Wallets. for _ in 0 ..< rand(2) + 2: - wallets.add(newWallet("")) + wallets.add(newWallet("").hd) utxos[wallets[^1].publicKey] = @[] #For each selected Wallet, create a random amount of Transactions. diff --git a/tests/Database/Filesystem/DB/Serialize/Transactions/SerializeSendOutputTest.nim b/tests/Database/Filesystem/DB/Serialize/Transactions/SerializeSendOutputTest.nim index 652f88760..ac04f0c66 100644 --- a/tests/Database/Filesystem/DB/Serialize/Transactions/SerializeSendOutputTest.nim +++ b/tests/Database/Filesystem/DB/Serialize/Transactions/SerializeSendOutputTest.nim @@ -17,7 +17,7 @@ suite "SerializeSendOutput": lowFuzzTest "Serialize and parse.": var output: SendOutput = newSendOutput( - newWallet("").publicKey, + newWallet("").hd.publicKey, uint64(rand(int32.high)) ) reloaded: SendOutput = output.serialize().parseSendOutput() diff --git a/tests/Database/Filesystem/DB/TransactionsDB/SpendableTest.nim b/tests/Database/Filesystem/DB/TransactionsDB/SpendableTest.nim index 0edce1503..80813d3dc 100644 --- a/tests/Database/Filesystem/DB/TransactionsDB/SpendableTest.nim +++ b/tests/Database/Filesystem/DB/TransactionsDB/SpendableTest.nim @@ -17,7 +17,7 @@ suite "Spendable": midFuzzTest "Saving UTXOs, checking which UTXOs an account can spend, and deleting UTXOs.": var db = newTestDatabase() - wallets: seq[Wallet] = @[] + wallets: seq[HDWallet] = @[] outputs: seq[SendOutput] = @[] send: Send @@ -63,7 +63,7 @@ suite "Spendable": #Generate 10 wallets. for _ in 0 ..< 10: - wallets.add(newWallet("")) + wallets.add(newWallet("").hd) #Test 100 Transactions. for _ in 0 .. 100: @@ -88,57 +88,56 @@ suite "Spendable": compare() #Spend outputs. - var queue: seq[(EdPublicKey, FundedInput)] = @[] for key in spendable.keys(): if spendable[key].len == 0: continue inputs = @[] var i: int = 0 - while true: + while i != spendable[key].len: if rand(1) == 0: inputs.add(spendable[key][i]) spendable[key].delete(i) else: inc(i) - if i == spendable[key].len: - break - if inputs.len != 0: var outputKey: EdPublicKey = wallets[rand(10 - 1)].publicKey send = newSend(inputs, newSendOutput(outputKey, 0)) db.save(send) - db.verify(send) sends.add(send) - - queue.add((outputKey, newFundedInput(send.hash, 0))) spenders[send.hash.serialize() & char(0)] = outputKey - for output in queue: - if not spendable.hasKey(output[0]): - spendable[output[0]] = @[] - spendable[output[0]].add(output[1]) - compare() #Unverify a Send. if sends.len != 0: var s: int = rand(sends.high) db.unverify(sends[s]) - for input in sends[s].inputs: - spendable[ - spenders[input.hash.serialize() & char(cast[FundedInput](input).nonce)] - ].add(cast[FundedInput](input)) for o1 in 0 ..< sends[s].outputs.len: - var output: SendOutput = cast[SendOutput](sends[s].outputs[o1]) - for o2 in 0 ..< spendable[output.key].len: - if ( - (spendable[output.key][o2].hash == sends[s].hash) and - (spendable[output.key][o2].nonce == o1) - ): - spendable[output.key].delete(o2) - break - - compare() + var + output: SendOutput = cast[SendOutput](sends[s].outputs[o1]) + o2: int = 0 + if spendable.hasKey(output.key): + while o2 < spendable[output.key].len: + if ( + (spendable[output.key][o2].hash == sends[s].hash) and + (spendable[output.key][o2].nonce == o1) + ): + spendable[output.key].delete(o2) + else: + inc(o2) + + compare() + + #Prune a Send. + db.prune(sends[sends.high].hash) + for input in sends[sends.high].inputs: + if not spendable.hasKey(spenders[input.hash.serialize() & char(cast[FundedInput](input).nonce)]): + spendable[spenders[input.hash.serialize() & char(cast[FundedInput](input).nonce)]] = @[] + spendable[ + spenders[input.hash.serialize() & char(cast[FundedInput](input).nonce)] + ].add(cast[FundedInput](input)) + + compare() diff --git a/tests/Database/Filesystem/Wallet/WalletDBTest.nim b/tests/Database/Filesystem/Wallet/WalletDBTest.nim index 7c05d6762..00242efba 100644 --- a/tests/Database/Filesystem/Wallet/WalletDBTest.nim +++ b/tests/Database/Filesystem/Wallet/WalletDBTest.nim @@ -1,3 +1,7 @@ +#Specifically tests the Merit Holder side of things. +#Address management, including reloading, is extensively tested via Python. +#That said, this test should still be expanded at some point in the future. + import os import random import sets, tables @@ -38,11 +42,22 @@ suite "WalletDB": w2: WalletDB ) = check: - w1.wallet.privateKey == w2.wallet.privateKey + w1.miner.initiated == w2.miner.initiated w1.miner.privateKey == w2.miner.privateKey + w1.miner.publicKey == w2.miner.publicKey + w1.miner.nick == w2.miner.nick + + w1.accountZero == w2.accountZero + w2.chainCode == w2.chainCode + + w1.nextIndex == w2.nextIndex + w1.changeIndex == w2.changeIndex + w2.addresses == w2.addresses + w1.finalizedNonces == w2.finalizedNonces w1.unfinalizedNonces == w2.unfinalizedNonces w1.verified.len == w2.verified.len + w1.elementNonce == w2.elementNonce for v in w1.verified.keys(): diff --git a/tests/Database/Transactions/TransactionsTest.nim b/tests/Database/Transactions/TransactionsTest.nim index 9973a4e5d..023776722 100644 --- a/tests/Database/Transactions/TransactionsTest.nim +++ b/tests/Database/Transactions/TransactionsTest.nim @@ -35,7 +35,7 @@ suite "Transactions": ) holder: MinerWallet = newMinerWallet() - wallets: seq[Wallet] = @[] + wallets: seq[HDWallet] = @[] #Reverse lookup Table. walletsLookup: Table[EdPublicKey, int] = initTable[EdPublicKey, int]() @@ -95,8 +95,9 @@ suite "Transactions": claim, proc ( h: uint16 - ): BLSPublicKey = - holder.publicKey + ): BLSPublicKey {.gcsafe.} = + {.gcsafe.}: + holder.publicKey ) of Send as send: transactions.add(send) @@ -227,8 +228,9 @@ suite "Transactions": claim, proc ( h: uint16 - ): BLSPublicKey = - holder.publicKey + ): BLSPublicKey {.gcsafe.} = + {.gcsafe.}: + holder.publicKey ) of Send as send: transactions.add(send) @@ -272,8 +274,9 @@ suite "Transactions": claim, proc ( h: uint16 - ): BLSPublicKey = - holder.publicKey + ): BLSPublicKey {.gcsafe.} = + {.gcsafe.}: + holder.publicKey ) of Send as send: transactions.add(send) @@ -308,7 +311,7 @@ suite "Transactions": for b in 1 .. 30: #Create a random amount of Wallets. for _ in 0 ..< rand(2) + 2: - wallets.add(newWallet("")) + wallets.add(newWallet("").hd) walletsLookup[wallets[^1].publicKey] = wallets.len - 1 #For each Wallet, create a random amount of Transactions. diff --git a/tests/Network/Serialize/Merit/SerializeBlockHeaderTest.nim b/tests/Network/Serialize/Merit/SerializeBlockHeaderTest.nim index 3eb467cc5..8164d78e4 100644 --- a/tests/Network/Serialize/Merit/SerializeBlockHeaderTest.nim +++ b/tests/Network/Serialize/Merit/SerializeBlockHeaderTest.nim @@ -23,15 +23,12 @@ suite "SerializeBlockHeader": last: Hash[256] = newRandomHash() contents: Hash[256] = newRandomHash() sketchCheck: Hash[256] = newRandomHash() - miner: MinerWallet + miner: MinerWallet = newMinerWallet() header: BlockHeader reloaded: BlockHeader #Create the BlockHeaader. if newMiner: - #Get a new miner. - miner = newMinerWallet() - header = newBlockHeader( uint32(rand(high(int32))), last, diff --git a/tests/Network/Serialize/Transactions/SerializeClaimTest.nim b/tests/Network/Serialize/Transactions/SerializeClaimTest.nim index e85a01670..892d44f94 100644 --- a/tests/Network/Serialize/Transactions/SerializeClaimTest.nim +++ b/tests/Network/Serialize/Transactions/SerializeClaimTest.nim @@ -20,14 +20,14 @@ suite "SerializeClaim": inputs: seq[FundedInput] claim: Claim reloaded: Claim - wallet: Wallet = newWallet("") + wallet: HDWallet = newWallet("").hd midFuzzTest "Serialize and parse.": inputs = newSeq[FundedInput](rand(254) + 1) for i in 0 ..< inputs.len: inputs[i] = newFundedInput(newRandomHash(), rand(255)) - claim = newClaim(inputs, wallet.next(last = uint32(rand(200) * 1000)).publicKey) + claim = newClaim(inputs, wallet.publicKey) #The Meros protocol requires this signature be produced by the aggregate of every unique MinerWallet paid via the Mints. #Serialization/Parsing doesn't care at all. diff --git a/tests/Network/Serialize/Transactions/SerializeDataTest.nim b/tests/Network/Serialize/Transactions/SerializeDataTest.nim index 672f069a5..d3ed60921 100644 --- a/tests/Network/Serialize/Transactions/SerializeDataTest.nim +++ b/tests/Network/Serialize/Transactions/SerializeDataTest.nim @@ -19,7 +19,7 @@ suite "SerializeData": dataStr: string data: Data reloaded: Data - wallet: Wallet = newWallet("") + wallet: HDWallet = newWallet("").hd midFuzzTest "Serialize and parse.": #Create the data string. @@ -29,7 +29,7 @@ suite "SerializeData": #Create the Data. data = newData(newRandomHash(), dataStr) - wallet.next(last = uint32(rand(200) * 1000)).sign(data) + wallet.sign(data) data.mine(uint32(5)) reloaded = data.serialize().parseData(uint32(0)) diff --git a/tests/Network/Serialize/Transactions/SerializeSendTest.nim b/tests/Network/Serialize/Transactions/SerializeSendTest.nim index 436951317..ec7c6199d 100644 --- a/tests/Network/Serialize/Transactions/SerializeSendTest.nim +++ b/tests/Network/Serialize/Transactions/SerializeSendTest.nim @@ -20,7 +20,7 @@ suite "SerializeSend": outputs: seq[SendOutput] send: Send reloaded: Send - wallet: Wallet = newWallet("") + wallet: HDWallet = newWallet("").hd midFuzzTest "Serialize and parse.": #Create the inputs. @@ -31,18 +31,14 @@ suite "SerializeSend": #Create the outputs. outputs = newSeq[SendOutput](rand(254) + 1) for o in 0 ..< outputs.len: - outputs[o] = newSendOutput( - wallet - .next(last = uint32(rand(200) * 1000)) - .next(last = uint32(o * 1000)).publicKey, - uint64(rand(high(int32)))) + outputs[o] = newSendOutput(wallet.publicKey, uint64(rand(high(int32)))) #Create the Send. send = newSend(inputs, outputs) #The Meros protocol requires this signature be produced by the MuSig of every unique Wallet paid via the inputs. #Serialization/Parsing doesn't care at all. - wallet.next(last = uint32(rand(200) * 1000)).sign(send) + wallet.sign(send) #mine the Send. send.mine(uint32(3)) diff --git a/tests/Wallet/AddressTest.nim b/tests/Wallet/AddressTest.nim index 53f17374c..b378e3329 100644 --- a/tests/Wallet/AddressTest.nim +++ b/tests/Wallet/AddressTest.nim @@ -22,7 +22,7 @@ suite "Address": noFuzzTest "Wallet address.": for _ in 0 ..< 100: - var wallet: Wallet = newWallet("") + var wallet: HDWallet = newWallet("").hd check: wallet.address.isValidAddress() wallet.address.getEncodedData().addyType == AddressType.PublicKey diff --git a/tests/Wallet/Ed25519Test.nim b/tests/Wallet/Ed25519Test.nim index b00d6e4c8..4475ce22f 100644 --- a/tests/Wallet/Ed25519Test.nim +++ b/tests/Wallet/Ed25519Test.nim @@ -1,27 +1,31 @@ import strutils import ../../src/Wallet/Ed25519 +import ../../src/Wallet/Wallet import ../Fuzzed suite "Ed25519": setup: var keys: seq[EdPublicKey] = @[ - #Private Key 5234E072EAE6E27E31A2C615278992383E4DEACB262A0D61ECF31030BE63B004. - newEdPublicKey(parseHexStr("84C55D653C6AFC1FAC7FCF98501ADE2702A905E9D374780408576268CEB44C06")), - #Private Key 388C3C3E2E253FD1CE38ED9566989CEFEA4A7928414A9FC148409F709505170D. - newEdPublicKey(parseHexStr("CCE542F0E500C85088E9A253946D7EF5DD0867B7655B79EA3919B46CE6346128")) + newEdPublicKey(parseHexStr("0210EEDF740C1EFD7727BE80458ECA7D171EDD1CDCA77A97A9DBE8B7BDCDF28B")), + newEdPublicKey(parseHexStr("91841823E21F80D0514027DA146888DE26A37FD5FC41E8632CC91414602E2F9F")) ] noFuzzTest "Aggregate.": - check keys.aggregate() == newEdPublicKey(parseHexStr("58E6AF7B9C4FC89380CC84B4D478725A39A05EC9B3F2BE2E149EDD5C857B7371")) - - noFuzzTest "Verify.": - check keys.aggregate().verify( - "test", - newEdSignature( - parseHexStr( - "F529B567F152BD037A8EFD0BE58F0D1864E642E2FDA9FD1074A8F2D22B2387404E144C02DE18CCF984D354339928AF63DFDED2518FEF32FC2C0F376A607A250D" - ) - ) - ) + check keys.aggregate() == newEdPublicKey(parseHexStr("F19E50A37AB431A12E2DB0E1214A40D7845A1551FFAB50A0D7D25BC5D1E72AFC")) + + noFuzzTest "Doesn't aggregate a single key.": + check @[keys[0]].aggregate() == keys[0] + + noFuzzTest "Doesn't aggregate the same key.": + check @[keys[0], keys[0]].aggregate() == keys[0] + + noFuzzTest "Does aggregate the same key with other keys.": + check @[keys[0], keys[1], keys[0]].aggregate() == newEdPublicKey(parseHexStr("63408405F1D65043158A56B48C849A865AAAF6D8E7D0CE3A67C623D32E634019")) + + noFuzzTest "Can create the private key for an aggregated public key.": + let + a: EdPrivateKey = newWallet("").hd.privateKey + b: EdPrivateKey = newWallet("").hd.privateKey + check @[a, b].aggregate().toPublicKey() == @[a.toPublicKey(), b.toPublicKey()].aggregate() diff --git a/tests/Wallet/HDWalletTest.nim b/tests/Wallet/HDWalletTest.nim index 4609bc247..9cf714487 100644 --- a/tests/Wallet/HDWalletTest.nim +++ b/tests/Wallet/HDWalletTest.nim @@ -1,11 +1,10 @@ import os -import math import strutils import json import ../../src/lib/[Util, Hash] -import ../../src/Wallet/HDWallet +import ../../src/Wallet/Wallet import ../Fuzzed @@ -30,7 +29,7 @@ suite "HDWallet": child = childArg i = 0 if child[^1] == '\'': - i = uint32(2^31) + i = uint32(1 shl 31) child = child.substr(0, child.len - 2) i += uint32(parseUInt(child)) path.add(i) @@ -38,14 +37,25 @@ suite "HDWallet": #Make sure invalid secrets/paths are invalid. if vector["node"].kind == JNull: expect ValueError: - wallet = newHDWallet(vector["secret"].getStr()).derive(path) + wallet = newHDWallet(parseHexStr(vector["secret"].getStr())).derive(path) continue #If this wallet is valid, load and derive it. - wallet = newHDWallet(vector["secret"].getStr()).derive(path) + wallet = newHDWallet(parseHexStr(vector["secret"].getStr())).derive(path) #Compare the Wallet with the vector. check: $wallet.privateKey == (vector["node"]["kLP"].getStr() & vector["node"]["kRP"].getStr()).toUpper() $wallet.publicKey == vector["node"]["AP"].getStr().toUpper() $wallet.chainCode == vector["node"]["cP"].getStr().toUpper() + + highFuzzTest "Public key derivation": + wallet = newWallet("").hd[0] + check HDPublic( + key: wallet.derive(0).publicKey, + chainCode: wallet.derive(0).chainCode, + index: 0 + ) == HDPublic( + key: wallet.publicKey, + chainCode: wallet.chainCode + ).derivePublic(0) diff --git a/tests/Wallet/WalletTest.nim b/tests/Wallet/WalletTest.nim index d8d7cd550..e132a7300 100644 --- a/tests/Wallet/WalletTest.nim +++ b/tests/Wallet/WalletTest.nim @@ -7,8 +7,10 @@ import ../../src/Wallet/[Address, Wallet] import ../Fuzzed proc verify( - wallet: Wallet + insecure: InsecureWallet ) = + let wallet: HDWallet = insecure.hd + #Test recreating the Public Key. check: newEdPublicKey(wallet.publicKey.serialize()).serialize() == wallet.publicKey.serialize() diff --git a/tests/lib/UtilTest.nim b/tests/lib/UtilTest.nim index d6b7b0278..73aecb489 100644 --- a/tests/lib/UtilTest.nim +++ b/tests/lib/UtilTest.nim @@ -1,4 +1,5 @@ import random +import strutils import ../../src/lib/Util