From 44a7e61feec87a3db2e51a0c6f71612959615f48 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:29 -0500 Subject: [PATCH 01/15] Create contract handler interface and contract data structures --- src/Makefile.am | 4 +- src/Makefile.test.include | 1 + src/neuralnet/contract.cpp | 857 ++++++++++++++++ src/neuralnet/contract.h | 810 +++++++++++++++ src/test/neuralnet/contract_tests.cpp | 1346 +++++++++++++++++++++++++ 5 files changed, 3017 insertions(+), 1 deletion(-) create mode 100644 src/neuralnet/contract.cpp create mode 100644 src/neuralnet/contract.h create mode 100644 src/test/neuralnet/contract_tests.cpp diff --git a/src/Makefile.am b/src/Makefile.am index e3ce8261ab..a70d2830d3 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -100,6 +100,7 @@ GRIDCOIN_CORE_H = \ neuralnet/accrual/research_age.h \ neuralnet/accrual/snapshot.h \ neuralnet/claim.h \ + neuralnet/contract.h \ neuralnet/cpid.h \ neuralnet/magnitude.h \ neuralnet/project.h \ @@ -119,7 +120,7 @@ GRIDCOIN_CORE_H = \ scheduler.h \ scraper_net.h \ scraper/fwd.h \ - scraper/http.h \ + scraper/http.h \ scraper/scraper.h \ script.h \ scrypt.h \ @@ -174,6 +175,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ main.cpp \ miner.cpp \ neuralnet/claim.cpp \ + neuralnet/contract.cpp \ neuralnet/cpid.cpp \ neuralnet/project.cpp \ neuralnet/quorum.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index d8676f99e6..9c6fe25cd5 100755 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -42,6 +42,7 @@ GRIDCOIN_TESTS =\ test/multisig_tests.cpp \ test/netbase_tests.cpp \ test/neuralnet/claim_tests.cpp \ + test/neuralnet/contract_tests.cpp \ test/neuralnet/cpid_tests.cpp \ test/neuralnet/magnitude_tests.cpp \ test/neuralnet/project_tests.cpp \ diff --git a/src/neuralnet/contract.cpp b/src/neuralnet/contract.cpp new file mode 100644 index 0000000000..e3ee59ab7e --- /dev/null +++ b/src/neuralnet/contract.cpp @@ -0,0 +1,857 @@ +#include "appcache.h" +#include "main.h" +#include "neuralnet/contract.h" +#include "neuralnet/beacon.h" +#include "neuralnet/project.h" +#include "util.h" + +using namespace NN; + +// Parses the XML-like elements from contract messages: +std::string ExtractXML(const std::string& XMLdata, const std::string& key, const std::string& key_end); + +namespace +{ +//! +//! \brief Temporary interface implementation that reads and writes contracts +//! to AppCache to use while we refactor away each of the AppCache sections: +//! +class AppCacheContractHandler : public IContractHandler +{ + void Add(const Contract& contract) override + { + WriteCache( + StringToSection(contract.m_type.ToString()), + contract.m_key, + contract.m_value, + contract.m_tx_timestamp); + + // Update global current poll title displayed in UI: + // TODO: get rid of this global and make the UI fetch it from the + // voting contract handler (doesn't exist yet). + if (contract.m_type == ContractType::POLL) { + msPoll = contract.ToString(); + } + } + + void Delete(const Contract& contract) override + { + DeleteCache(StringToSection(contract.m_type.ToString()), contract.m_key); + } +}; + +//! +//! \brief Handles unknown contract message types by logging a message. +//! +//! After the mandatory switch to version 2 contracts, this class becomes +//! unnecessary--nodes will simply reject transactions with unknown contract +//! types. +//! +class UnknownContractHandler : public IContractHandler +{ + //! + //! \brief Handle a contract addition. + //! + //! \param contract A contract message with an unknown type. + //! + void Add(const Contract& contract) override + { + contract.Log("WARNING: Add unknown contract type ignored"); + } + + //! + //! \brief Handle a contract deletion. + //! + //! \param contract A contract message with an unknown type. + //! + void Delete(const Contract& contract) override + { + contract.Log("WARNING: Delete unknown contract type ignored"); + } + + //! + //! \brief Handle a contract reversal. + //! + //! \param contract A contract message with an unknown type. + //! + void Revert(const Contract& contract) override + { + contract.Log("WARNING: Revert unknown contract type ignored"); + } +}; + +//! +//! \brief Processes contracts from transaction messages by routing them to the +//! appropriate contract handler implementations. +//! +class Dispatcher +{ +public: + //! + //! \brief Validate the provided contract and forward it to the appropriate + //! contract handler. + //! + //! \param contract As received from a transaction message. + //! + void Apply(const Contract& contract) + { + if (contract.m_action == ContractAction::ADD) { + contract.Log("INFO: Add contract"); + GetHandler(contract.m_type.Value()).Add(contract); + return; + } + + if (contract.m_action == ContractAction::REMOVE) { + contract.Log("INFO: Delete contract"); + GetHandler(contract.m_type.Value()).Delete(contract); + return; + } + + contract.Log("WARNING: Unknown contract action ignored"); + } + + //! + //! \brief Revert a previously-applied contract from a transaction message + //! by passing it to the appropriate contract handler. + //! + //! \param contract Typically parsed from a message in a transaction from + //! a disconnected block. + //! + void Revert(const Contract& contract) + { + contract.Log("INFO: Revert contract"); + + // The default implementation of IContractHandler reverses an action + // (addition or deletion) declared in the contract argument, but the + // type-specific handlers may override this behavior as needed: + GetHandler(contract.m_type.Value()).Revert(contract); + } + + //! + //! \brief Check that the provided contract does not match an existing + //! contract to protect against replay attacks. + //! + //! The application calls this method when it receives a new transaction + //! from another node. The return value determines whether we should keep + //! or discard the transaction. If the received contract is too old (as + //! defined by \c REPLAY_RETENTION_PERIOD), or if the contract matches an + //! existing contract in the cache, this method returns \c false, and the + //! calling code shall reject the transaction containing the contract. + //! + //! Version 2+ contracts can be checked for replay. Version 1 contracts do + //! not contain the data necessary to determine uniqueness. + //! + //! Replay protection relies on the contract's signing timestamp and nonce + //! values captured in the contract hash. A contract received with a signing + //! time earlier than the configured number of seconds from now shall be + //! considered invalid. The addition of a nonce prevents replay of recent + //! contracts received within this window. + //! + //! \param contract Contract to check. Received in a transaction. + //! + //! \return \c true if the check deteremines that the contract is unique. + //! + bool CheckReplay(const Contract& contract) + { + int64_t valid_after_time = Contract::ReplayPeriod(); + + // Reject any contracts timestamped earlier than a reasonable window. + // By invalidating contracts older than a cut-off threshold, we only + // need to store contracts newer than REPLAY_RETENTION_PERIOD in memory + // for replay detection: + if (contract.m_timestamp < valid_after_time) { + return false; + } + + std::list::iterator item = m_replay_pool.begin(); + + while (item != m_replay_pool.end()) { + // If a contract hash matches an entry in the pool, we can assume + // that it was replayed: + if (item->m_hash == contract.GetHash()) { + return false; + } + + // Lazily purge old entries from the pool whenever we check a new + // contract. Any expired entries eventualy pass out when we receive + // a valid contract: + if (item->m_timestamp < valid_after_time) { + item = m_replay_pool.erase(item); + continue; + } + + ++item; + } + + return true; + } + + //! + //! \brief Add a contract to the replay tracking pool. + //! + //! \param contract A newly-received contract from a transaction. + //! + void TrackForReplay(const Contract& contract) + { + ReplayPoolItem new_item; + + new_item.m_timestamp = contract.m_timestamp; + new_item.m_hash = contract.GetHash(); + + m_replay_pool.push_back(std::move(new_item)); + } + +private: + //! + //! \brief Contains a hash of contract data for replay checks. + //! + struct ReplayPoolItem + { + int64_t m_timestamp; //!< To cull entries older than retention period. + uint256 m_hash; //!< Hash of a contract compared to new contracts. + }; + + //! + //! \brief Contains a rolling cache of recently-received contract hashes + //! used to compare with contracts received in transaction messages for + //! replay protection. + //! + //! Calling \c CheckReplay() will purge the old entries from the cache as + //! it checks a valid contract. + //! + std::list m_replay_pool; + + AppCacheContractHandler m_appcache_handler; //& contracts) +{ + for (const auto& contract : contracts) { + gateway.TrackForReplay(contract); + } +} + +// ----------------------------------------------------------------------------- +// Class: Contract +// ----------------------------------------------------------------------------- + +Contract::Contract() + : m_version(Contract::CURRENT_VERSION) + , m_type(Contract::Type(ContractType::UNKNOWN)) + , m_action(Contract::Action(ContractAction::UNKNOWN)) + , m_key(std::string()) + , m_value(std::string()) + , m_signature(Contract::Signature()) + , m_public_key(Contract::PublicKey()) + , m_nonce(0) + , m_timestamp(0) + , m_tx_timestamp(0) +{ +} + +Contract::Contract( + Contract::Type type, + Contract::Action action, + std::string key, + std::string value) + : m_version(Contract::CURRENT_VERSION) + , m_type(std::move(type)) + , m_action(std::move(action)) + , m_key(std::move(key)) + , m_value(std::move(value)) + , m_signature(Contract::Signature()) + , m_public_key(Contract::PublicKey()) + , m_nonce(0) + , m_timestamp(0) + , m_tx_timestamp(0) +{ +} + +Contract::Contract( + int version, + Contract::Type type, + Contract::Action action, + std::string key, + std::string value, + Contract::Signature signature, + Contract::PublicKey public_key, + unsigned int nonce, + long timestamp, + int64_t tx_timestamp) + : m_version(version) + , m_type(std::move(type)) + , m_action(std::move(action)) + , m_key(std::move(key)) + , m_value(std::move(value)) + , m_signature(std::move(signature)) + , m_public_key(std::move(public_key)) + , m_nonce(std::move(nonce)) + , m_timestamp(std::move(timestamp)) + , m_tx_timestamp(std::move(tx_timestamp)) +{ +} + +const CPubKey& Contract::MasterPublicKey() +{ + // If the master key changes, add a conditional entry to this method that + // returns the new key for the appropriate height. + + // 049ac003b3318d9fe28b2830f6a95a2624ce2a69fb0c0c7ac0b513efcc1e93a6a + // 6e8eba84481155dd82f2f1104e0ff62c69d662b0094639b7106abc5d84f948c0a + static const CPubKey since_block_0(std::vector { + 0x04, 0x9a, 0xc0, 0x03, 0xb3, 0x31, 0x8d, 0x9f, 0xe2, 0x8b, 0x28, + 0x30, 0xf6, 0xa9, 0x5a, 0x26, 0x24, 0xce, 0x2a, 0x69, 0xfb, 0x0c, + 0x0c, 0x7a, 0xc0, 0xb5, 0x13, 0xef, 0xcc, 0x1e, 0x93, 0xa6, 0xa6, + 0xe8, 0xeb, 0xa8, 0x44, 0x81, 0x15, 0x5d, 0xd8, 0x2f, 0x2f, 0x11, + 0x04, 0xe0, 0xff, 0x62, 0xc6, 0x9d, 0x66, 0x2b, 0x00, 0x94, 0x63, + 0x9b, 0x71, 0x06, 0xab, 0xc5, 0xd8, 0x4f, 0x94, 0x8c, 0x0a + }); + + return since_block_0; +} + +const CPrivKey Contract::MasterPrivateKey() +{ + std::vector key = ParseHex(GetArgument("masterprojectkey", "")); + + return CPrivKey(key.begin(), key.end()); +} + +const CPubKey& Contract::MessagePublicKey() +{ + // If the message key changes, add a conditional entry to this method that + // returns the new key for the appropriate height. + + // 044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a25 + // 2467f692ec9c5ba7e5eaa898ab99cbd9949496f7e3cafbf56304b1cc2e5bdf06e + static const CPubKey since_block_0 ({ + 0x04, 0x4b, 0x29, 0x38, 0xfb, 0xc3, 0x80, 0x71, 0xf2, 0x4b, 0xed, + 0xe2, 0x1e, 0x83, 0x8a, 0x07, 0x58, 0xa5, 0x2a, 0x00, 0x85, 0xf2, + 0xe0, 0x34, 0xe7, 0xf9, 0x71, 0xdf, 0x44, 0x54, 0x36, 0xa2, 0x52, + 0x46, 0x7f, 0x69, 0x2e, 0xc9, 0xc5, 0xba, 0x7e, 0x5e, 0xaa, 0x89, + 0x8a, 0xb9, 0x9c, 0xbd, 0x99, 0x49, 0x49, 0x6f, 0x7e, 0x3c, 0xaf, + 0xbf, 0x56, 0x30, 0x4b, 0x1c, 0xc2, 0xe5, 0xbd, 0xf0, 0x6e + }); + + return since_block_0; +} + +const CPrivKey& Contract::MessagePrivateKey() +{ + // If the message key changes, add a conditional entry to this method that + // returns the new key for the appropriate height. + + // 308201130201010420fbd45ffb02ff05a3322c0d77e1e7aea264866c24e81e5ab + // 6a8e150666b4dc6d8a081a53081a2020101302c06072a8648ce3d0101022100ff + // fffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300 + // 604010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28 + // d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a685541 + // 99c47d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af + // 48a03bbfd25e8cd0364141020101a144034200044b2938fbc38071f24bede21e8 + // 38a0758a52a0085f2e034e7f971df445436a252467f692ec9c5ba7e5eaa898ab9 + // 9cbd9949496f7e3cafbf56304b1cc2e5bdf06e + static const CPrivKey since_block_0 { + 0x30, 0x82, 0x01, 0x13, 0x02, 0x01, 0x01, 0x04, 0x20, 0xfb, 0xd4, + 0x5f, 0xfb, 0x02, 0xff, 0x05, 0xa3, 0x32, 0x2c, 0x0d, 0x77, 0xe1, + 0xe7, 0xae, 0xa2, 0x64, 0x86, 0x6c, 0x24, 0xe8, 0x1e, 0x5a, 0xb6, + 0xa8, 0xe1, 0x50, 0x66, 0x6b, 0x4d, 0xc6, 0xd8, 0xa0, 0x81, 0xa5, + 0x30, 0x81, 0xa2, 0x02, 0x01, 0x01, 0x30, 0x2c, 0x06, 0x07, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x01, 0x01, 0x02, 0x21, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xfc, 0x2f, 0x30, 0x06, 0x04, + 0x01, 0x00, 0x04, 0x01, 0x07, 0x04, 0x41, 0x04, 0x79, 0xbe, 0x66, + 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, + 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, + 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98, 0x48, 0x3a, 0xda, 0x77, + 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc, 0x0e, 0x11, 0x08, + 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, + 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8, 0x02, 0x21, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, + 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41, 0x02, 0x01, 0x01, + 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x4b, 0x29, 0x38, 0xfb, 0xc3, + 0x80, 0x71, 0xf2, 0x4b, 0xed, 0xe2, 0x1e, 0x83, 0x8a, 0x07, 0x58, + 0xa5, 0x2a, 0x00, 0x85, 0xf2, 0xe0, 0x34, 0xe7, 0xf9, 0x71, 0xdf, + 0x44, 0x54, 0x36, 0xa2, 0x52, 0x46, 0x7f, 0x69, 0x2e, 0xc9, 0xc5, + 0xba, 0x7e, 0x5e, 0xaa, 0x89, 0x8a, 0xb9, 0x9c, 0xbd, 0x99, 0x49, + 0x49, 0x6f, 0x7e, 0x3c, 0xaf, 0xbf, 0x56, 0x30, 0x4b, 0x1c, 0xc2, + 0xe5, 0xbd, 0xf0, 0x6e + }; + + return since_block_0; +} + +const std::string Contract::BurnAddress() +{ + return fTestNet + ? "mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ" + : "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; +} + +int64_t Contract::ReplayPeriod() +{ + return GetAdjustedTime() - REPLAY_RETENTION_PERIOD; +} + +bool Contract::Detect(const std::string& message) +{ + return !message.empty() + && Contains(message, "") + // Superblock currently handled elsewhere: + && !Contains(message, "superblock"); +} + +Contract Contract::Parse(const std::string& message, const int64_t timestamp) +{ + if (message.empty()) { + return Contract(); + } + + return Contract( + 1, // Legacy XML-like string contracts always parse to a v1 contract. + Contract::Type::Parse(ExtractXML(message, "", "")), + Contract::Action::Parse(ExtractXML(message, "", "")), + ExtractXML(message, "", ""), + ExtractXML(message, "", ""), + Contract::Signature::Parse(ExtractXML(message, "", "")), + // None of the currently-valid contract types support signing with a + // user-supplied private key, so we can skip parsing the public keys + // altogether. We verify contracts with the master and message keys: + //Contract::PublicKey::Parse(ExtractXML(message, "", "")), + Contract::PublicKey(), + 0, // Nonce unused in v1 contracts. + 0, // Signing timestamp unused in v1 contracts. + timestamp); +} + +bool Contract::RequiresMasterKey() const +{ + switch (m_type.Value()) { + case ContractType::BEACON: return m_action == ContractAction::REMOVE; + case ContractType::POLL: return m_action == ContractAction::REMOVE; + case ContractType::PROJECT: return true; + case ContractType::PROTOCOL: return true; + case ContractType::SCRAPER: return true; + case ContractType::VOTE: return m_action == ContractAction::REMOVE; + default: return false; + } +} + +bool Contract::RequiresMessageKey() const +{ + switch (m_type.Value()) { + case ContractType::BEACON: return m_action == ContractAction::ADD; + case ContractType::POLL: return m_action == ContractAction::ADD; + case ContractType::VOTE: return m_action == ContractAction::ADD; + default: return false; + } +} + +bool Contract::RequiresSpecialKey() const +{ + return RequiresMessageKey() || RequiresMasterKey(); +} + +const CPubKey& Contract::ResolvePublicKey() const +{ + if (RequiresMessageKey()) { + return MessagePublicKey(); + } + + if (RequiresMasterKey()) { + return MasterPublicKey(); + } + + return m_public_key.Key(); +} + +bool Contract::WellFormed() const +{ + return m_version > 0 && m_version <= Contract::CURRENT_VERSION + && m_type != ContractType::UNKNOWN + && m_action != ContractAction::UNKNOWN + && !m_key.empty() + && !m_value.empty() + && m_signature.Viable() + && (RequiresSpecialKey() || m_public_key.Viable()) + && m_tx_timestamp > 0 + && (m_version == 1 || (m_timestamp > 0 && m_nonce > 0)); +} + +bool Contract::Validate() const +{ + return WellFormed() && VerifySignature(); +} + +bool Contract::Sign(CKey& private_key) +{ + std::vector output; + m_hash_cache = 0; // Invalidate the cached hash, if any. + + if (m_version > 1) { + m_timestamp = GetAdjustedTime(); + RAND_bytes(reinterpret_cast(&m_nonce), sizeof(m_nonce)); + } + + if (!private_key.Sign(GetHash(), output)) { + Log("ERROR: Failed to sign contract"); + return false; + } + + m_signature = std::move(output); + + if (!RequiresSpecialKey()) { + m_public_key = private_key.GetPubKey(); + } + + return true; +} + +bool Contract::SignWithMessageKey() +{ + CKey key; + + key.SetPrivKey(MessagePrivateKey()); + + return Sign(key); +} + +bool Contract::VerifySignature() const +{ + CKey key; + + if (!key.SetPubKey(ResolvePublicKey())) { + Log("ERROR: Failed to set contract public key"); + return false; + } + + return key.Verify(GetHash(), m_signature.Raw()); +} + +uint256 Contract::GetHash() const +{ + // Nodes use the hash at least twice when validating a received contract + // (once to verify the signature, once to track the contract for replay + // protection, and one or more times to compare the contract to any other + // contracts in the replay protection cache), so we cache the value to + // avoid re-computing it. Once cached, the hash is only invalidated when + // re-signing a contract, so avoid calling this method on a contract in + // an intermediate state. + + if (m_hash_cache > 0) { + return m_hash_cache; + } + + if (m_version > 1) { + m_hash_cache = SerializeHash(*this); + return m_hash_cache; + } + + const std::string type_string = m_type.ToString(); + + m_hash_cache = Hash( + type_string.begin(), + type_string.end(), + m_key.begin(), + m_key.end(), + m_value.begin(), + m_value.end()); + + return m_hash_cache; +} + +std::string Contract::ToString() const +{ + return "" + m_type.ToString() + "" + + "" + m_key + "" + + "" + m_value + "" + + "" + m_action.ToString() + "" + + "" + m_public_key.ToString() + "" + + "" + m_signature.ToString() + ""; +} + +void Contract::Log(const std::string& prefix) const +{ + // TODO: temporary... needs better logging + if (!fDebug) { + return; + } + + LogPrintf( + ": %s: v%d, %d, %d, %s, %s, %s, %s, %s, %s, %d", + prefix, + m_version, + m_tx_timestamp, + m_timestamp, + m_type.ToString(), + m_action.ToString(), + m_key, + m_value, + m_public_key.ToString(), + m_signature.ToString(), + m_nonce); +} + +// ----------------------------------------------------------------------------- +// Class: Contract::Type +// ----------------------------------------------------------------------------- + +Contract::Type::Type(ContractType type) + : EnumVariant(type, boost::none) +{ +} + +Contract::Type::Type(std::string other) + : EnumVariant(ContractType::UNKNOWN, std::move(other)) +{ +} + +Contract::Type::Type(ContractType type, std::string other) + : EnumVariant(type, std::move(other)) +{ +} + +Contract::Type Contract::Type::Parse(std::string input) +{ + if (input.empty()) return ContractType::UNKNOWN; + + // Ordered by frequency: + if (input == "beacon") return ContractType::BEACON; + if (input == "vote") return ContractType::VOTE; + if (input == "poll") return ContractType::POLL; + if (input == "project") return ContractType::PROJECT; + if (input == "scraper") return ContractType::SCRAPER; + if (input == "protocol") return ContractType::PROTOCOL; + + // Currently handled elsewhere: + if (input == "superblock") return ContractType::SUPERBLOCK; + + // Legacy type for "project" (found at height 267504, 410257): + if (input == "projectmapping") { + return Contract::Type(ContractType::PROJECT, "projectmapping"); + } + + return Contract::Type(std::move(input)); +} + +std::string Contract::Type::ToString() const +{ + if (const EnumVariant::OptionalString other = m_other) { + return *other; + } + + switch (m_value) { + case ContractType::BEACON: return "beacon"; + case ContractType::POLL: return "poll"; + case ContractType::PROJECT: return "project"; + case ContractType::PROTOCOL: return "protocol"; + case ContractType::SCRAPER: return "scraper"; + case ContractType::SUPERBLOCK: return "superblock"; + case ContractType::VOTE: return "vote"; + default: return ""; + } +} + +// ----------------------------------------------------------------------------- +// Class: Contract::Action +// ----------------------------------------------------------------------------- + +Contract::Action::Action(ContractAction action) + : EnumVariant(action, boost::none) +{ +} + +Contract::Action::Action(std::string other) + : EnumVariant(ContractAction::UNKNOWN, std::move(other)) +{ +} + +Contract::Action Contract::Action::Parse(std::string input) +{ + if (input.empty()) return ContractAction::UNKNOWN; + if (input == "A") return ContractAction::ADD; + if (input == "D") return ContractAction::REMOVE; + + return Contract::Action(std::move(input)); +} + +std::string Contract::Action::ToString() const +{ + if (const EnumVariant::OptionalString other = m_other) { + return *other; + } + + switch (m_value) { + case ContractAction::ADD: return "A"; + case ContractAction::REMOVE: return "D"; + default: return ""; + } +} + +// ----------------------------------------------------------------------------- +// Class: Contract::Signature +// ----------------------------------------------------------------------------- + +Contract::Signature::Signature() : m_bytes(std::vector(0)) +{ +} + +Contract::Signature::Signature(std::vector bytes) + : m_bytes(std::move(bytes)) +{ +} + +Contract::Signature Contract::Signature::Parse(const std::string& input) +{ + if (input.empty()) { + return Contract::Signature(); + } + + bool invalid; + std::vector decoded = DecodeBase64(input.c_str(), &invalid); + + if (invalid) { + return Contract::Signature(); + } + + return Contract::Signature(std::move(decoded)); +} + +bool Contract::Signature::Viable() const +{ + // The DER-encoded ASN.1 ECDSA signatures typically contain 70 or 71 bytes, + // but may hold up to 73. Sizes as low as 68 bytes seen on mainnet. We only + // check the number of bytes here as an early step: + return m_bytes.size() >= 64 && m_bytes.size() <= 73; +} + +const std::vector& Contract::Signature::Raw() const +{ + return m_bytes; +} + +std::string Contract::Signature::ToString() const +{ + if (m_bytes.empty()) { + return std::string(); + } + + return EncodeBase64(m_bytes.data(), m_bytes.size()); +} + +// ----------------------------------------------------------------------------- +// Class: Contract::PublicKey +// ----------------------------------------------------------------------------- + +Contract::PublicKey::PublicKey() : m_key(CPubKey()) +{ +} + +Contract::PublicKey::PublicKey(CPubKey key) + : m_key(std::move(key)) +{ +} + +Contract::PublicKey Contract::PublicKey::Parse(const std::string& input) +{ + if (input.empty()) { + return Contract::PublicKey(); + } + + return Contract::PublicKey(CPubKey(ParseHex(input))); +} + +bool Contract::PublicKey::operator==(const CPubKey& other) const +{ + return m_key == other; +} + +bool Contract::PublicKey::operator!=(const CPubKey& other) const +{ + return m_key != other; +} + +bool Contract::PublicKey::Viable() const +{ + return m_key.IsValid(); +} + +const CPubKey& Contract::PublicKey::Key() const +{ + return m_key; +} + +std::string Contract::PublicKey::ToString() const +{ + return HexStr(m_key.Raw()); +} + +// ----------------------------------------------------------------------------- +// Abstract Class: IContractHandler +// ----------------------------------------------------------------------------- + +void IContractHandler::Revert(const Contract& contract) +{ + if (contract.m_action == ContractAction::ADD) { + Delete(contract); + return; + } + + if (contract.m_action == ContractAction::REMOVE) { + Add(contract); + return; + } + + error("Unknown contract action ignored: %s", contract.m_action.ToString()); +} diff --git a/src/neuralnet/contract.h b/src/neuralnet/contract.h new file mode 100644 index 0000000000..5d58ac2ff7 --- /dev/null +++ b/src/neuralnet/contract.h @@ -0,0 +1,810 @@ +#pragma once + +#include "key.h" +#include "serialize.h" +#include "uint256.h" + +#include +#include +#include + +namespace NN { +//! +//! \brief Represents the type of a Gridcoin contract. +//! +//! CONSENSUS: Do not remove an item from this enum or change or repurpose the +//! byte value. +//! +enum class ContractType : uint8_t +{ + UNKNOWN = 0x00, //!< An invalid, non-standard, or empty contract type. + BEACON = 0x01, //!< Beacon advertisement or deletion. + POLL = 0x02, //!< Submission of a new poll. + PROJECT = 0x03, //!< Project whitelist addition or removal. + PROTOCOL = 0x04, //!< Network control message or configuration directive. + SCRAPER = 0x05, //!< Scraper node authorization grants and revocations. + SUPERBLOCK = 0x06, //!< The result of superblock quorum consensus. + VOTE = 0x07, //!< A vote cast by a wallet for a poll. + MAX_VALUE = 0x07, //!< Increment this when adding items to the enum. +}; + +//! +//! \brief The type of action that a contract declares. +//! +//! CONSENSUS: Do not remove an item from this enum or change or repurpose the +//! byte value. +//! +enum class ContractAction : uint8_t +{ + UNKNOWN = 0x00, //!< An invalid, non-standard, or empty contract action. + ADD = 0x01, //!< Handle a new contract addition (A). + REMOVE = 0x02, //!< Remove an existing contract (D). + MAX_VALUE = 0x02, //!< Increment this when adding items to the enum. +}; + +//! +//! \brief Represents a Gridcoin contract embedded in a transaction message. +//! +//! Gridcoin contracts are not "smart". These messages contain metadata that +//! control the operation of the research reward protocol and other Gridcoin +//! features. +//! +//! The legacy protocol defined these messages as an XML-like string format. +//! Block versions 11+ changed the contract protocol to a binary format. The +//! contract classes include a significant amount of code that converts that +//! legacy format into objects for compatibility with historical blocks. +//! +//! The application defines "contract handler" services that process each of +//! the different contract types when a node receives contract messages from +//! transactions. These classes share the IContractHandler interface. +//! +class Contract +{ +private: + //! + //! \brief A wrapper around an enum type that contains a valid enum value + //! or a string that represents a value not present in the enum. + //! + //! \tparam E The enum type wrapped by this class. + //! + template + struct EnumVariant + { + // Replace with E::underlying_type_t when moving to C++14: + using EnumUnderlyingType = typename std::underlying_type::type; + + //! + //! \brief Compare a supplied enum value for equality. + //! + //! \param other An enum value to check equality for. + //! + //! \return \c true if the suppled value matches the wrapped enum value. + //! + bool operator==(const E& other) const + { + return m_value == other; + } + + //! + //! \brief Compare a supplied enum value for inequality. + //! + //! \param other An enum value to check inequality for. + //! + //! \return \c true if the suppled value does not match the wrapped + //! enum value. + //! + bool operator!=(const E& other) const + { + return m_value != other; + } + + //! + //! \brief Get the wrapped enum value. + //! + //! \return A value enumerated on enum \c E. + //! + E Value() const + { + return m_value; + } + + //! + //! \brief Get the wrapped enum value as a value of the underlying type. + //! + //! \return For example, an unsigned char for an enum that represents + //! an underlying byte value. + //! + EnumUnderlyingType Raw() const + { + return static_cast(m_value); + } + + //! + //! \brief Get the string representation of the wrapped enum value. + //! + //! \return The string as it would appear in a transaction message or + //! the captured string if parsed from an unrecognized value. + //! + virtual std::string ToString() const = 0; + + //! + //! \brief Get the size of the data to serialize/deserialize. + //! + //! \param nType Target protocol type (network, disk, etc.). + //! \param nVersion Protocol version. + //! + //! \return Size of the data in bytes. + //! + unsigned int GetSerializeSize(int nType, int nVersion) const + { + return ::GetSerializeSize(Raw(), nType, nVersion); + } + + //! + //! \brief Serialize the wrapped enum value to the provided stream. + //! + //! \param stream The output stream. + //! \param nType Target protocol type (network, disk, etc.). + //! \param nVersion Protocol version. + //! + template + void Serialize(Stream& stream) const + { + ::Serialize(stream, Raw()); + } + + //! + //! \brief Deserialize an enum value from the provided stream. + //! + //! \param stream The input stream. + //! \param nType Target protocol type (network, disk, etc.). + //! \param nVersion Protocol version. + //! + template + void Unserialize(Stream& stream) + { + EnumUnderlyingType value; + + ::Unserialize(stream, value); + + if (value > static_cast(E::MAX_VALUE)) { + m_value = E::UNKNOWN; + } else { + m_value = static_cast(value); + } + } + + protected: + //! + //! \brief Contains the string representation of a non-standard or + //! invalid contract type or contract action. + //! + //! \c Contract::Type or \c Contract::Action objects may parse string + //! values that do not exist on the corresponding \c ContractType and + //! \c ContractAction enums. These objects store that non-standard or + //! invalid string value behind a construct of this type. + //! + typedef boost::optional OptionalString; + + //! + //! \brief Delegated constructor called by child types. + //! + //! \param value The enum value to wrap. + //! \param other Unknown, non-standard, or empty enum value, if any. + //! + EnumVariant(E value, OptionalString other) + : m_value(value), m_other(std::move(other)) + { + } + + E m_value; //!< The wrapped enum value. + OptionalString m_other; //!< Holds invalid or non-standard types. + }; // Contract::EnumVariant + + //! + //! \brief Caches a contract's hash value. + //! + //! Prevents repeated, expensive hash calculations. However, once cached, + //! this value is only invalidated when re-signing a contract. + //! + mutable uint256 m_hash_cache = 0; + +public: + //! + //! \brief Version number of the current format for a serialized contract. + //! + //! CONSENSUS: Increment this value when introducing a breaking change and + //! ensure that the serialization/deserialization routines also handle all + //! of the previous versions. + //! + static constexpr int CURRENT_VERSION = 2; // TODO: int32_t + + //! + //! \brief The amount of time in seconds to store received contracts in + //! memory for replay detection. Reject contracts timestamped earlier than + //! this window. + //! + static constexpr int64_t REPLAY_RETENTION_PERIOD = 60 * 60; + + //! + //! \brief A contract type from a transaction message. + //! + //! \c Contract::Type objects directly represent values enumerated on the + //! \c ContractType enum but provide some internal machinery to store the + //! string values of invalid or non-standard types found in a message. + //! + struct Type : public EnumVariant + { + //! + //! \brief Initialize an instance for a known \c ContractType value. + //! + //! \param type A value enumerated on \c ContractType. + //! + Type(ContractType type); + + //! + //! \brief Initialize an instance for an unknown contract type. + //! + //! \param other An unknown, invalid, or non-standard contract type + //! parsed from a transaction message. + //! + Type(std::string other); + + //! + //! \brief Parse a \c ContractType value from its legacy string + //! representation. + //! + //! \param input String representation of a contract type. + //! + //! \return A value enumerated on \c ContractType. Returns the value of + //! \c ContractType::UNKNOWN for an unrecognized contract type. + //! + static Type Parse(std::string input); + + //! + //! \brief Get the string representation of the wrapped contract type. + //! + //! \return The string as it would appear in a transaction message. + //! + std::string ToString() const override; + + private: + //! + //! \brief Initialize an instance for a contract type that has a legacy + //! string representation. + //! + //! \param type A valid, known contract type described by the string. + //! \param legacy Described the provided contract type in the past. + //! + Type(ContractType type, std::string legacy); + }; // Contract::Type + + //! + //! \brief A contract action from a transaction message. + //! + //! \c Contract::Action objects directly represent values enumerated on the + //! \c ContractAction enum but provide some internal machinery to store the + //! string values of invalid or non-standard actions found in a message. + //! + struct Action : public EnumVariant + { + //! + //! \brief Initialize an instance for a known \c ContractAction value. + //! + //! \param type A value enumerated on \c ContractAction. + //! + Action(ContractAction action); + + //! + //! \brief Initialize an instance for an unknown contract action. + //! + //! \param other An unknown, invalid, or non-standard contract action + //! parsed from a transaction message. + //! + Action(std::string other); + + //! + //! \brief Parse a \c ContractAction value from its legacy string + //! representation. + //! + //! \param input String representation of a contract action. + //! + //! \return A value enumerated on \ContractAction. Returns the value of + //! \c ContractAction::UNKNOWN for an unrecognized contract action. + //! + static Action Parse(std::string input); + + //! + //! \brief Get the string representation of the wrapped contract action. + //! + //! \return The string as it would appear in a transaction message. + //! + std::string ToString() const override; + }; // Contract::Action + + //! + //! \brief Parses and stores a contract message signature in binary format. + //! + struct Signature + { + //! + //! \brief Initialize an empty, invalid \c Signature object. + //! + Signature(); + + //! + //! \brief Initialize a \c Signature object from a series of bytes. + //! + //! \param bytes As DER-encoded ASN.1 ECDSA. + //! + Signature(std::vector bytes); + + //! + //! \brief Create a \c Signature object from its string representation. + //! + //! \param input Base64 encoding of the binary signature. Typically + //! 96 characters. + //! + static Signature Parse(const std::string& input); + + //! + //! \brief Determine whether the object contains a viable signature. + //! + //! This method does NOT verify the signature against a public key. Use + //! only for early checks to determine whether to continue verification. + //! + //! \return \c true if resembles a DER-encoded ASN.1 ECDSA signature. + //! + bool Viable() const; + + //! + //! \brief Get the bytes in the signature. + //! + //! \return Typically 70 to 72 bytes. + //! + const std::vector& Raw() const; + + //! + //! \brief Get the string representation of the signature. + //! + //! \return Base64 encoding of the binary signature. + //! + std::string ToString() const; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_bytes); + } + + private: + std::vector m_bytes; //!< As DER-encoded ASN.1 ECDSA. + }; // Contract::Signature + + //! + //! \brief Parses and stores the contract public key in binary format. + //! + struct PublicKey + { + //! + //! \brief Initialize an empty, invalid \c PublicKey object. + //! + PublicKey(); + + //! + //! \brief Wrap a \c PublicKey object around a \c CPubKey instance. + //! + //! \param key The public key to wrap. + //! + PublicKey(CPubKey key); + + //! + //! \brief Create a \c PublicKey object from its string representation. + //! + //! \param input Hex string representation of the bytes in the key. + //! + static PublicKey Parse(const std::string& input); + + //! + //! \brief Compare a supplied \CPubKey object for equality. + //! + //! \param other A public key to check equality for. + //! + //! \return \c true if the supplied public key's bytes match. + //! + bool operator==(const CPubKey& other) const; + + //! + //! \brief Compare a supplied \CPubKey object for inequality. + //! + //! \param other A public key to check inequality for. + //! + //! \return \c true if the supplied public key's bytes do not match. + //! + bool operator!=(const CPubKey& other) const; + + //! + //! \brief Determine whether the object contains a viable public key. + //! + //! This method does NOT verify the key's structure. Use only for early + //! checks to determine whether to continue verification. + //! + //! \return \true if resembles a full or compressed public key. + //! + bool Viable() const; + + //! + //! \brief Get the wrapped \c CPubKey object. + //! + //! \return A reference to the wrapped key object. + //! + const CPubKey& Key() const; + + //! + //! \brief Get the string representation of the public key. + //! + //! \return Hex string representation of the bytes in the key. + //! + std::string ToString() const; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_key); + } + + private: + CPubKey m_key; //!< The wrapped public key. + }; // Contract::PublicKey + + //! + //! \brief Version number of the serialized contract format. + //! + //! Defaults to the most recent version for a new contract instance. + //! + //! Version 1: Legacy string XML-like contract message parsed from the + //! \c hashBoinc field of a transaction. Contains no nonce or signing + //! timestamp. + //! + //! Version 2: Contract data serializable in binary format. Stored in a + //! transaction's \c vContracts field. Must contain a nonce and signing + //! timestamp which protect against contract replay. + //! + int m_version = CURRENT_VERSION; + + Type m_type; //!< Determines how to handle the contract. + Action m_action; //!< Action to perform with the contract. + std::string m_key; //!< Uniquely identifies the contract subject. + std::string m_value; //!< Body containing any specialized data. + Signature m_signature; //!< Proves authenticity of the contract. + PublicKey m_public_key; //!< Verifies the contract signature. + unsigned int m_nonce; //!< Random. Prevents contract replay. + long m_timestamp; //!< Signing time. Prevents contract replay. + int64_t m_tx_timestamp; //!< Timestamp of the contract's transaction. + + //! + //! \brief Initialize an empty \c Contract object. + //! + //! The new contract is invalid until populated and signed. + //! + Contract(); + + //! + //! \brief Initialize a new, unsigned \c Contract object to publish in a + //! transaction. + //! + //! \param type The type of contract to publish. + //! \param action The action of the contract to publish. + //! \param key Uniquely identifies the contract target. + //! \param value Data specialized for the contract type. + //! + Contract(Type type, Action action, std::string key, std::string value); + + //! + //! \brief Initialize a \c Contract object by supplying each of the fields + //! from a contract message. + //! + //! \param version Version of the serialized contract format. + //! \param type Contract type parsed from the transaction message. + //! \param action Contract action parsed from the transaction message. + //! \param key The contract key as it exists in the transaction. + //! \param value The contract value as it exists in the transaction. + //! \param signature Proves authenticity of the contract message. + //! \param public_key Optional for some types. Verifies the signature. + //! \param nonce Random. Prevents contract replay. Version 2+. + //! \param timestamp Contract signing time. Prevents replay. Version 2+. + //! \param tx_timestamp Timestamp of the transaction containing the contract. + //! + Contract( + int version, + Type type, + Action action, + std::string key, + std::string value, + Signature signature, + PublicKey public_key, + unsigned int nonce, + long timestamp, + int64_t tx_timestamp); + + //! + //! \brief Get the master public key used to verify administrative contracts. + //! + //! \return A \c CPubKey object containing the master public key. + //! + static const CPubKey& MasterPublicKey(); + + //! + //! \brief Get the master private key provided by configuration or command- + //! line option. + //! + //! \return An empty key (vector) when no master key configured. + //! + static const CPrivKey MasterPrivateKey(); + + //! + //! \brief Get the message public key used to verify public contracts. + //! + //! \return A \c CPubKey object containing the message public key. + //! + static const CPubKey& MessagePublicKey(); + + //! + //! \brief Get the message private key used to sign public contracts. + //! + //! The private key is revealed by design, for public messages only. + //! + //! \return The message private key as a secure vector of bytes. + //! + static const CPrivKey& MessagePrivateKey(); + + //! + //! \brief Get the destination burn address for transactions that contain + //! contract messages. + //! + //! \return Burn address for transactions that contain contract messages. + //! + static const std::string BurnAddress(); + + //! + //! \brief Get the time after which newly-received contracts must pass + //! replay checking. + //! + //! \return Number of seconds before the current time. + //! + static int64_t ReplayPeriod(); + + //! + //! \brief Determine whether the supplied message might contain a contract. + //! + //! Call \c Contract::WellFormed() or \c Contract::VerifySignature() to + //! check whether a contract is actually viable. + //! + //! \param message A message as it exists in a transaction. + //! + //! \return \c true if the message appears to contain a contract. + //! + static bool Detect(const std::string& message); + + //! + //! \brief Create a contract instance by parsing the supplied message. + //! + //! \param message Extracted from the \c hashboinc field of a transaction. + //! \param timestamp Timestamp of the transaction containing the contract. + //! + //! \return The message parsed into a \c Contract instance. + //! + static Contract Parse(const std::string& message, const int64_t timestamp); + + //! + //! \brief Determine whether the contract shall sign the message or verify + //! the signature using the administrative master keys. + //! + //! \return \c true for administrative contract type/action pairs. + //! + bool RequiresMasterKey() const; + + //! + //! \brief Determine whether the contract shall sign the message or verify + //! the signature using the embedded, shared message keys. + //! + //! \return \c true for certain public actions (add poll, vote, beacon...). + //! + bool RequiresMessageKey() const; + + //! + //! \brief Determine whether the contract shall sign the message or verify + //! the signature using a special (non-user-supplied) key. + //! + //! \return \c true when a contract requires the master or message keys. + //! + bool RequiresSpecialKey() const; + + //! + //! \brief Get the public key used to verify the contract's signature. + //! + //! \return The appropriate public key for the contract type. + //! + const CPubKey& ResolvePublicKey() const; + + //! + //! \brief Determine whether the instance represents a complete contract. + //! + //! The results of these method calls do NOT guarantee that a contract is + //! valid for the specific Proof-of-Research component that handles it. A + //! return value of \c true only indicates that the contract includes all + //! of the pieces of data needed for a well-formed contract message. + //! + //! \return \c true if the contract contains each of the required elements. + //! + bool WellFormed() const; + + //! + //! \brief Determine whether a received contract is completely valid. + //! + //! \return \c true if the contract is well-formed and contains a valid + //! signature. + //! + bool Validate() const; + + //! + //! \brief Sign the contract using the provided private key. + //! + //! \param private_key The key to sign the message with. + //! + //! \return \c true if the signature was successfully created. + //! + bool Sign(CKey& private_key); + + //! + //! \brief Sign the contract using the shared message private key. + //! + //! \return \c true if the signature was successfully created. + //! + bool SignWithMessageKey(); + + //! + //! \brief Validate the integrity and authenticity of the contract message + //! by verifying its digital signature. + //! + //! \return \c true if the signature validates the contract's claims. + //! + bool VerifySignature() const; + + //! + //! \brief Generate a hash of the contract data as the input to create or + //! verify the contract signature. + //! + //! \return Hash of the contract type, key, and value. Versions 2+ also + //! include the action, nonce, and timestamp in the hash. + //! + uint256 GetHash() const; + + //! + //! \brief Get the string representation of the contract. + //! + //! \return The contract string in an XML-like format as published in a + //! transaction message. + //! + std::string ToString() const; + + //! + //! \brief Write a message to the debug log with the contract data. + //! + //! \param prefix Message to prepend to the log entry before the contract + //! data elements. + //! + void Log(const std::string& prefix) const; + + // + // Serialize and deserialize the contract in binary format instead of + // parsing and formatting the legacy XML-like string representation. + // + // For CTransaction::nVersion >= 2. + // + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + if (!(s.GetType() & SER_GETHASH)) { + READWRITE(m_version); + } + + READWRITE(m_type); + READWRITE(m_action); + READWRITE(m_key); + READWRITE(m_value); + READWRITE(m_nonce); + READWRITE(m_timestamp); + + if (!(s.GetType() & SER_GETHASH)) { + READWRITE(m_public_key); + READWRITE(m_signature); + } + } +}; // Contract + +//! +//! \brief Stores or processes neural network contract messages. +//! +//! Typically, contract handler implementations only mutate the data they store +//! when the application processes contract messages embedded in a transaction. +//! Consumers of the data will usually access it in a read-only fashion because +//! the data is based on immutable values stored in connected blocks. +//! +//! For this reason, contract handlers NEED NOT to implement the interface in a +//! thread-safe manner. The application applies contract messages from only one +//! thread. However, a contract handler MUST guarantee thread-safety for any of +//! its methods that provide the read-only access to the contract data imported +//! by this interface. +//! +struct IContractHandler +{ + //! + //! \brief Destructor. + //! + virtual ~IContractHandler() {} + + //! + //! \brief Handle an contract addition. + //! + //! \param contract A contract message that describes the addition. + //! + virtual void Add(const Contract& contract) = 0; + + //! + //! \brief Handle a contract deletion. + //! + //! \param contract A contract message that describes the deletion. + //! + virtual void Delete(const Contract& contract) = 0; + + //! + //! \brief Revert a contract found in a disconnected block. + //! + //! The application calls this method for each contract in the disconnected + //! blocks during chain reorganization. The default implementation forwards + //! the contract object to an appropriate \c Add() or \c Delete() method by + //! reversing the action specified in the contract. + //! + //! \param contract A contract message that describes the action to revert. + //! + virtual void Revert(const Contract& contract); +}; + +//! +//! \brief Apply a contract from a transaction message by passing it to the +//! appropriate contract handler. +//! +//! \param contract Received in a transaction message. +//! +void ProcessContract(const Contract& contract); + +//! +//! \brief Revert a previously-applied contract from a transaction message by +//! passing it to the appropriate contract handler. +//! +//! \param contract Received in a transaction message. +//! +void RevertContract(const Contract& contract); + +//! +//! \brief Check that the provided contract does not match an existing contract +//! to protect against replay attacks. +//! +//! \param contract Contract to check. Received in a transaction. +//! +bool CheckContractReplay(const Contract& contract); + +//! +//! \brief Add contracts from a transaction to the replay tracking pool. +//! +//! \param contracts A set of newly-received contracts from a transaction. +//! +void TrackContracts(const std::vector& contracts); +} diff --git a/src/test/neuralnet/contract_tests.cpp b/src/test/neuralnet/contract_tests.cpp new file mode 100644 index 0000000000..6b1b61725d --- /dev/null +++ b/src/test/neuralnet/contract_tests.cpp @@ -0,0 +1,1346 @@ +#include "neuralnet/contract.h" + +#include +#include + +namespace { +//! +//! \brief Provides various public and private key representations for tests. +//! +//! Keys match the shared message keys embedded in the application. +//! +struct TestKey +{ + //! + //! \brief Create a valid private key for tests. + //! + //! \return This is actually the shared message private key. + //! + static CKey Private() + { + std::vector private_key = ParseHex( + "308201130201010420fbd45ffb02ff05a3322c0d77e1e7aea264866c24e81e5ab6" + "a8e150666b4dc6d8a081a53081a2020101302c06072a8648ce3d0101022100ffff" + "fffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604" + "010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959" + "f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47" + "d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03b" + "bfd25e8cd0364141020101a144034200044b2938fbc38071f24bede21e838a0758" + "a52a0085f2e034e7f971df445436a252467f692ec9c5ba7e5eaa898ab99cbd9949" + "496f7e3cafbf56304b1cc2e5bdf06e"); + + CKey key; + key.SetPrivKey(CPrivKey(private_key.begin(), private_key.end())); + + return key; + } + + //! + //! \brief Create a valid public key for tests. + //! + //! \return Complements the private key above. + //! + static CPubKey Public() + { + return CPubKey(std::vector { + 0x04, 0x4b, 0x29, 0x38, 0xfb, 0xc3, 0x80, 0x71, 0xf2, 0x4b, 0xed, + 0xe2, 0x1e, 0x83, 0x8a, 0x07, 0x58, 0xa5, 0x2a, 0x00, 0x85, 0xf2, + 0xe0, 0x34, 0xe7, 0xf9, 0x71, 0xdf, 0x44, 0x54, 0x36, 0xa2, 0x52, + 0x46, 0x7f, 0x69, 0x2e, 0xc9, 0xc5, 0xba, 0x7e, 0x5e, 0xaa, 0x89, + 0x8a, 0xb9, 0x9c, 0xbd, 0x99, 0x49, 0x49, 0x6f, 0x7e, 0x3c, 0xaf, + 0xbf, 0x56, 0x30, 0x4b, 0x1c, 0xc2, 0xe5, 0xbd, 0xf0, 0x6e + }); + } + + //! + //! \brief Get a serialized representation of a public key. + //! + //! \return Public key as bytes with one additional byte at the beginning + //! for the size of the key. + //! + static std::vector PublicSerialized() + { + std::vector key = Public().Raw(); + unsigned char size = key.size(); + + std::vector serialized { size }; + + serialized.insert(serialized.end(), key.begin(), key.end()); + + return serialized; + } + + //! + //! \brief Create a valid public key for tests. + //! + //! \return Hex-encoded uncompressed key that complements the private key + //! above (and same key as the public key object above). + //! + static std::string PublicString() + { + return "044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a25" + "2467f692ec9c5ba7e5eaa898ab99cbd9949496f7e3cafbf56304b1cc2e5bdf06e"; + } + + //! + //! \brief Create some invalid hex-encoded public key strings for tests. + //! + //! \return A set of various malformed public key strings that should fail + //! validation after parsing. + //! + static std::vector GarbagePublicStrings() + { + return std::vector { + // Too short: 32 bytes (not a real key): + "044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a25", + // Too long: 66 bytes (not a real key): + "044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a252" + "467f692ec9c5ba7e5eaa898ab99cbd9949496f7e3cafbf56304b1cc2e5bdf06e11", + // Garbage: invalid hex characters + "zz4b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a252", + }; + } +}; // struct TestKey + +//! +//! \brief Provides various signature representations for tests. +//! +//! Valid signatures created by signing the contracts below with the keys above. +//! +struct TestSig +{ + //! + //! \brief Create a valid signature of a version 1 contract for tests. + //! + //! \return Base64-encoded signature. Created by signing the v1 contract + //! message body in the class below with the private key above. + //! + static std::string V1String() + { + return "MEQCIHCvttZzIrBqFty9ED/fn5yMSzTzuch4Xf4KxiNJjwvM" + "AiAp7QvHHhf1+xqEB57/k00jZW5RIITaxI9jXaR51Lmfnw=="; + } + + //! + //! \brief Create a valid signature of a version 1 contract for tests. + //! + //! \return Signature as a vector of bytes. Same signature as the base64 + //! string version above. + //! + static std::vector V1Bytes() + { + return std::vector { + 0x30, 0x44, 0x02, 0x20, 0x70, 0xaf, 0xb6, 0xd6, 0x73, 0x22, 0xb0, + 0x6a, 0x16, 0xdc, 0xbd, 0x10, 0x3f, 0xdf, 0x9f, 0x9c, 0x8c, 0x4b, + 0x34, 0xf3, 0xb9, 0xc8, 0x78, 0x5d, 0xfe, 0x0a, 0xc6, 0x23, 0x49, + 0x8f, 0x0b, 0xcc, 0x02, 0x20, 0x29, 0xed, 0x0b, 0xc7, 0x1e, 0x17, + 0xf5, 0xfb, 0x1a, 0x84, 0x07, 0x9e, 0xff, 0x93, 0x4d, 0x23, 0x65, + 0x6e, 0x51, 0x20, 0x84, 0xda, 0xc4, 0x8f, 0x63, 0x5d, 0xa4, 0x79, + 0xd4, 0xb9, 0x9f, 0x9f + }; + } + + //! + //! \brief Get a serialized representation of a valid version 1 signature. + //! + //! \return Same signature above as bytes with one additional byte at the + //! beginning for the signature size. + //! + static std::vector V1Serialized() + { + std::vector signature = V1Bytes(); + unsigned char size = signature.size(); + + std::vector serialized { size }; + + serialized.insert(serialized.end(), signature.begin(), signature.end()); + + return serialized; + } + + //! + //! \brief Get a hash of a valid version 1 contract body. + //! + //! \return Hash of the version 1 contract body below. + //! + static uint256 V1Hash() + { + return uint256S( + "484e6c63845cd102b86b75d1c0cb36dd15ae41f8ad00690cdddbdade666b41b6"); + } + + //! + //! \brief Create a valid signature of a version 2 contract for tests. + //! + //! \return Base64-encoded signature. Created by signing the v2 contract + //! message body in the class below with the private key above. + //! + static std::string V2String() + { + return "MEUCIQCJAVz+8CfEmbeHsx7mbBXP0LEISnsuHjC8UVD/rLEx" + "SAIgXmouutnnad8B+4+MYfHc/WXysckwvwheR7Y0+7i2yCM="; + } + + //! + //! \brief Create a valid signature of a version 2 contract for tests. + //! + //! \return Signature as a vector of bytes. Same signature as the base64 + //! string version above. + //! + static std::vector V2Bytes() + { + return std::vector { + 0x30, 0x45, 0x02, 0x21, 0x00, 0x89, 0x01, 0x5c, 0xfe, 0xf0, 0x27, + 0xc4, 0x99, 0xb7, 0x87, 0xb3, 0x1e, 0xe6, 0x6c, 0x15, 0xcf, 0xd0, + 0xb1, 0x08, 0x4a, 0x7b, 0x2e, 0x1e, 0x30, 0xbc, 0x51, 0x50, 0xff, + 0xac, 0xb1, 0x31, 0x48, 0x02, 0x20, 0x5e, 0x6a, 0x2e, 0xba, 0xd9, + 0xe7, 0x69, 0xdf, 0x01, 0xfb, 0x8f, 0x8c, 0x61, 0xf1, 0xdc, 0xfd, + 0x65, 0xf2, 0xb1, 0xc9, 0x30, 0xbf, 0x08, 0x5e, 0x47, 0xb6, 0x34, + 0xfb, 0xb8, 0xb6, 0xc8, 0x23 + }; + } + + //! + //! \brief Get a serialized representation of a valid version 2 signature. + //! + //! \return Signature as bytes with one additional byte at the beginning for + //! the signature size. + //! + static std::vector V2Serialized() + { + std::vector signature = V2Bytes(); + unsigned char size = signature.size(); + + std::vector serialized { size }; + + serialized.insert(serialized.end(), signature.begin(), signature.end()); + + return serialized; + } + + //! + //! \brief Get a hash of a valid version 2 contract body. + //! + //! \return Hash of the version 2 contract body below. + //! + static uint256 V2Hash() + { + return uint256S( + "bb434a253430c3b829ff8497ea7d497cf1f053a7af000b6c46ecf8b98dbe4046"); + } + + //! + //! \brief Create an invalid signature for tests. + //! + //! \return Base64-encoded signature. Same invalid signature as the bytes + //! below. + //! + static std::string InvalidString() + { + return "MEQCIHCvttZzIrBqFty9ED/fn5yMSzTzuch4Xf4KxiNJjwvM" + "AiAp7QvHHhf1+xqEB57/k00jZW5RIITaxI9jXaR51LmfDw=="; + } + + //! + //! \brief Create an invalid signature for tests. + //! + //! \return Signature as a vector of bytes. Same signature as the valid + //! version 1 above with the last byte changed. + //! + static std::vector InvalidBytes() + { + return std::vector { + 0x30, 0x44, 0x02, 0x20, 0x70, 0xaf, 0xb6, 0xd6, 0x73, 0x22, 0xb0, + 0x6a, 0x16, 0xdc, 0xbd, 0x10, 0x3f, 0xdf, 0x9f, 0x9c, 0x8c, 0x4b, + 0x34, 0xf3, 0xb9, 0xc8, 0x78, 0x5d, 0xfe, 0x0a, 0xc6, 0x23, 0x49, + 0x8f, 0x0b, 0xcc, 0x02, 0x20, 0x29, 0xed, 0x0b, 0xc7, 0x1e, 0x17, + 0xf5, 0xfb, 0x1a, 0x84, 0x07, 0x9e, 0xff, 0x93, 0x4d, 0x23, 0x65, + 0x6e, 0x51, 0x20, 0x84, 0xda, 0xc4, 0x8f, 0x63, 0x5d, 0xa4, 0x79, + 0xd4, 0xb9, 0x9f, 0x0f + }; + } + + + //! + //! \brief Create some invalid base64-encoded signatures for tests. + //! + //! \return A set of various malformed signature strings that should fail + //! validation after parsing. + //! + static std::vector GarbageStrings() + { + return std::vector { + // Too short: 63 bytes, base64-encoded (not a real signature): + "OGU0ZjQwNTE4ODA2NjEyMTIxMDJiYmJlMDMzOTM3ZTJkMTcyNDdjYmQzMDE5OTg5MzI3NTlhNjJkMjNlMGNl", + // Too long: 74 bytes, base64-encoded (not a real signature): + "OGU0ZjQwNTE4ODA2NjEyMTIxMDJiYmJlMDMzOTM3ZTJkMTcyNDdjYmQzMDE5OTg5MzI3NTlhNjJkMjNlMGNlMDk0MGU4Y2EzMjA=", + // Garbage base64-encoded string (one padding removed): + "MEQCIBQji0VFbMdiD5urHGgeq0UaiMB6IfI6+JKCC3Y9gMxCAiBornelAZ2bFusBPiD4DL+HS2SbiVU4j4pmMW4dQiiDIA=", + // Garbage base64-encoded string (one character removed): + "MEQCIBQji0VFbMdiD5urHGgeq0UaiMB6IfI6+JKCC3Y9gMxCAiBornelAZ2bFusBPiD4DL+HS2SbiVU4j4pmMW4dQiiDI==", + // Garbage base64-encoded string (non-base64 character): + "^EQCIBQji0VFbMdiD5urHGgeq0UaiMB6IfI6+JKCC3Y9gMxCAiBornelAZ2bFusBPiD4DL+HS2SbiVU4j4pmMW4dQiiDIA==", + }; + } +}; // struct TestSig + +//! +//! \brief Provides various contract message representations for tests. +//! +struct TestMessage +{ + //! + //! \brief Create a complete, signed contract object for the latest contract + //! version. + //! + //! \return Contains the default content used to create the v2 signature + //! above and includes that signature. + //! + static NN::Contract Current() + { + return NN::Contract( + NN::Contract::CURRENT_VERSION, + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "test", + "test", + NN::Contract::Signature(TestSig::V2Bytes()), + NN::Contract::PublicKey(), + 123, // Nonce + 234, // Signing timestamp + 123); // Tx timestamp is never part of a message + } + + //! + //! \brief Create a complete, signed, legacy, version 1 contract object. + //! + //! \return Contains the default content used to create the v1 signature + //! above and includes that signature. + //! + static NN::Contract V1() + { + return NN::Contract( + 1, // Version 1 for legacy, XML-like string contracts + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "test", + "test", + NN::Contract::Signature(TestSig::V1Bytes()), + NN::Contract::PublicKey(), + 0, // Unused in v1 (nonce) + 0, // Unused in v1 (signing timestamp) + 123); // Tx timestamp is never part of a message + } + + //! + //! \brief Create a legacy version 1 contract message string for tests. + //! + //! \return Signed by the private key above. + //! + static std::string V1String() + { + // This message was signed by the message private key and contains a valid + // signature: + return std::string( + "beacon" + "test" + "test" + "A" + "" + "" + TestSig::V1String() + ""); + } + + //! + //! \brief Get a serialized representation of a valid version 2 contract + //! message. + //! + //! \return As bytes. Matches the contract message string above (without + //! tags) and includes version 2 components. + //! + static std::vector V2Serialized() + { + std::vector serialized { + 0x02, 0x00, 0x00, 0x00, // Version: 32-bit int (little-endian) + 0x01, // Type: BEACON + 0x01, // Action: ADD + 0x04, // Length of the contract key + 0x74, 0x65, 0x73, 0x74, // Key: "test" as ASCII bytes + 0x04, // Length of the contract value + 0x74, 0x65, 0x73, 0x74, // Value: "test" as ASCII bytes + 0x7b, 0x00, 0x00, 0x00, // Nonce: 123 as 32-bit int (little-endian) + 0xEA, 0x00, 0x00, 0x00, // Signing timestamp: 234 as 64-bit int... + 0x00, 0x00, 0x00, 0x00, // ...(little-endian) + 0x00, // Length of the empty public key + }; + + std::vector signature = TestSig::V2Serialized(); + serialized.insert(serialized.end(), signature.begin(), signature.end()); + + return serialized; + } + + //! + //! \brief Create a legacy, version 1 contract message string for tests. + //! + //! \return A contract message with an invalid signature. + //! + static std::string InvalidV1String() + { + return std::string( + "beacon" + "test" + "test" + "A" + "" + "" + TestSig::InvalidString() + ""); + } + + //! + //! \brief Create an invalid, partial, legacy, version 1 contract message + //! string for tests. + //! + //! \return Contains a message type for detection, but missing items that + //! must fail to validate. + //! + static std::string PartialV1String() + { + return "beacontestA"; + } +}; // struct TestMessage +} // anonymous namespace + +// ----------------------------------------------------------------------------- +// Contract::Type +// ----------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(Contract__Type) + +BOOST_AUTO_TEST_CASE(it_initializes_to_a_provided_type) +{ + NN::Contract::Type type(NN::ContractType::BEACON); + + BOOST_CHECK(type == NN::ContractType::BEACON); +} + +BOOST_AUTO_TEST_CASE(it_stores_unknown_or_non_standard_type_strings) +{ + NN::Contract::Type type("foo"); + + BOOST_CHECK(type == NN::ContractType::UNKNOWN); + BOOST_CHECK(type.ToString() == "foo"); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_contract_type_from_a_string) +{ + NN::Contract::Type type = NN::Contract::Type::Parse("beacon"); + + BOOST_CHECK(type == NN::ContractType::BEACON); +} + +BOOST_AUTO_TEST_CASE(it_parses_and_stores_an_unknown_type_from_a_string) +{ + NN::Contract::Type type = NN::Contract::Type::Parse("foo"); + + BOOST_CHECK(type == NN::ContractType::UNKNOWN); + BOOST_CHECK(type.ToString() == "foo"); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_wrapped_contract_type_enum_value) +{ + NN::Contract::Type type(NN::ContractType::BEACON); + + BOOST_CHECK(type.Value() == NN::ContractType::BEACON); +} + +BOOST_AUTO_TEST_CASE(it_represents_itself_as_a_string) +{ + NN::Contract::Type type(NN::ContractType::BEACON); + + BOOST_CHECK(type.ToString() == "beacon"); +} + +BOOST_AUTO_TEST_CASE(it_handles_the_legacy_contract_types) +{ + NN::Contract::Type type = NN::Contract::Type::Parse("projectmapping"); + + BOOST_CHECK(type == NN::ContractType::PROJECT); + BOOST_CHECK(type.ToString() == "projectmapping"); +} + +BOOST_AUTO_TEST_CASE(it_supports_equality_operators_with_contract_type_enums) +{ + NN::Contract::Type type(NN::ContractType::BEACON); + + BOOST_CHECK(type == NN::ContractType::BEACON); + BOOST_CHECK(type != NN::ContractType::PROJECT); +} + +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) +{ + NN::Contract::Type type(NN::ContractType::BEACON); // BEACON == 0x01 + + BOOST_CHECK(GetSerializeSize(type, SER_NETWORK, 1) == 1); + + CDataStream stream(SER_NETWORK, 1); + stream << type; + std::vector output(stream.begin(), stream.end()); + + BOOST_CHECK(output == std::vector { 0x01 }); +} + +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) +{ + NN::Contract::Type type(NN::ContractType::UNKNOWN); + + std::vector bytes { 0x01 }; + CDataStream stream(bytes, SER_NETWORK, 1); + + stream >> type; + + BOOST_CHECK(type == NN::ContractType::BEACON); // BEACON == 0x01 +} + +BOOST_AUTO_TEST_CASE(it_deserializes_unknown_values_from_a_stream) +{ + // Start with a valid contract type: + NN::Contract::Type type(NN::ContractType::BEACON); + + std::vector bytes { 0xEE }; // Invalid + CDataStream stream(bytes, SER_NETWORK, 1); + + stream >> type; + + BOOST_CHECK(type == NN::ContractType::UNKNOWN); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ----------------------------------------------------------------------------- +// Contract::Action +// ----------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(Contract__Action) + +BOOST_AUTO_TEST_CASE(it_initializes_to_a_provided_action) +{ + NN::Contract::Action action(NN::ContractAction::ADD); + + BOOST_CHECK(action == NN::ContractAction::ADD); +} + +BOOST_AUTO_TEST_CASE(it_stores_unknown_or_non_standard_action_strings) +{ + NN::Contract::Action action("foo"); + + BOOST_CHECK(action == NN::ContractAction::UNKNOWN); + BOOST_CHECK(action.ToString() == "foo"); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_contract_action_from_a_string) +{ + NN::Contract::Action action = NN::Contract::Action::Parse("A"); + + BOOST_CHECK(action == NN::ContractAction::ADD); +} + +BOOST_AUTO_TEST_CASE(it_parses_and_stores_an_unknown_action_from_a_string) +{ + NN::Contract::Action action = NN::Contract::Action::Parse("foo"); + + BOOST_CHECK(action == NN::ContractAction::UNKNOWN); + BOOST_CHECK(action.ToString() == "foo"); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_wrapped_contract_action_enum_value) +{ + NN::Contract::Action action(NN::ContractAction::REMOVE); + + BOOST_CHECK(action.Value() == NN::ContractAction::REMOVE); +} + +BOOST_AUTO_TEST_CASE(it_represents_itself_as_a_string) +{ + NN::Contract::Action action(NN::ContractAction::REMOVE); + + BOOST_CHECK(action.ToString() == "D"); +} + +BOOST_AUTO_TEST_CASE(it_supports_equality_operators_with_contract_action_enums) +{ + NN::Contract::Action action(NN::ContractAction::ADD); + + BOOST_CHECK(action == NN::ContractAction::ADD); + BOOST_CHECK(action != NN::ContractAction::REMOVE); +} + +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) +{ + NN::Contract::Action action(NN::ContractAction::ADD); // ADD == 0x01 + + BOOST_CHECK(GetSerializeSize(action, SER_NETWORK, 1) == 1); + + CDataStream stream(SER_NETWORK, 1); + + stream << action; + std::vector output(stream.begin(), stream.end()); + + BOOST_CHECK(output == std::vector { 0x01 }); +} + +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) +{ + NN::Contract::Action action(NN::ContractAction::UNKNOWN); + + CDataStream stream(std::vector { 0x02 }, SER_NETWORK, 1); + + stream >> action; + + BOOST_CHECK(action == NN::ContractAction::REMOVE); // REMOVE == 0x02 +} + +BOOST_AUTO_TEST_CASE(it_deserializes_unknown_values_from_a_stream) +{ + // Start with a valid contract action: + NN::Contract::Action action(NN::ContractAction::ADD); + + std::vector bytes { 0xEE }; // Invalid + CDataStream stream(bytes, SER_NETWORK, 1); + + stream >> action; + + BOOST_CHECK(action == NN::ContractAction::UNKNOWN); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ----------------------------------------------------------------------------- +// Contract::Signature +// ----------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(Contract__Signature) + +BOOST_AUTO_TEST_CASE(it_initializes_to_an_invalid_signature_by_default) +{ + NN::Contract::Signature signature; + + BOOST_CHECK(signature.Viable() == false); +} + +BOOST_AUTO_TEST_CASE(it_initializes_with_bytes_in_a_signature) +{ + NN::Contract::Signature signature(std::vector { 0x05 }); + + BOOST_CHECK(signature.Raw() == std::vector { 0x05 }); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_signature_from_a_v1_base64_encoded_string) +{ + std::string input = TestSig::V1String(); + + NN::Contract::Signature signature = NN::Contract::Signature::Parse(input); + + BOOST_CHECK(signature.ToString() == input); + BOOST_CHECK(signature.Raw() == TestSig::V1Bytes()); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_signature_from_a_v2_base64_string_if_needed) +{ + std::string input = TestSig::V2String(); + + NN::Contract::Signature signature = NN::Contract::Signature::Parse(input); + + BOOST_CHECK(signature.ToString() == input); + BOOST_CHECK(signature.Raw() == TestSig::V2Bytes()); +} + +BOOST_AUTO_TEST_CASE(it_gives_an_invalid_signature_when_parsing_an_empty_string) +{ + NN::Contract::Signature signature = NN::Contract::Signature::Parse(""); + + BOOST_CHECK(signature.Viable() == false); + BOOST_CHECK(signature.ToString() == ""); +} + +BOOST_AUTO_TEST_CASE(it_supports_a_basic_check_for_signature_viability) +{ + // OK: 70 bytes + NN::Contract::Signature signature(TestSig::V1Bytes()); + + BOOST_CHECK(signature.Viable() == true); + + // OK: 70 bytes (not a real signature) + // Invalid signatures with correct length pass but won't verify against the + // public key when checking the contract. This is just an early check. + signature = NN::Contract::Signature(TestSig::InvalidBytes()); + + BOOST_CHECK(signature.Viable() == true); + + // BAD: Check some invalid base64-encoded inputs: + for (const auto& garbage : TestSig::GarbageStrings()) { + signature = NN::Contract::Signature::Parse(garbage); + + BOOST_CHECK(signature.Viable() == false); + } +} + +BOOST_AUTO_TEST_CASE(it_provides_the_bytes_in_the_signature) +{ + NN::Contract::Signature signature(TestSig::V1Bytes()); + + BOOST_CHECK(signature.Raw() == TestSig::V1Bytes()); +} + +BOOST_AUTO_TEST_CASE(it_represents_itself_as_a_string) +{ + std::vector input = TestSig::V1Bytes(); + + NN::Contract::Signature signature(input); + + BOOST_CHECK(signature.ToString() == TestSig::V1String()); +} + +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) +{ + NN::Contract::Signature signature(TestSig::V1Bytes()); + + // 71 bytes = 70 signature bytes + 1 leading byte with the size + BOOST_CHECK(GetSerializeSize(signature, SER_NETWORK, 1) == 71); + + CDataStream stream(SER_NETWORK, 1); + + stream << signature; + std::vector output(stream.begin(), stream.end()); + + BOOST_CHECK(output == TestSig::V1Serialized()); +} + +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) +{ + NN::Contract::Signature signature; + + CDataStream stream(TestSig::V1Serialized(), SER_NETWORK, 1); + + stream >> signature; + + BOOST_CHECK(signature.Raw() == TestSig::V1Bytes()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ----------------------------------------------------------------------------- +// Contract::PublicKey +// ----------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(Contract__PublicKey) + +BOOST_AUTO_TEST_CASE(it_initializes_to_an_invalid_key_by_default) +{ + NN::Contract::PublicKey key; + + BOOST_CHECK(key.Key() == CPubKey()); +} + +BOOST_AUTO_TEST_CASE(it_initializes_by_wrapping_a_provided_key_object) +{ + NN::Contract::PublicKey key(TestKey::Public()); + + BOOST_CHECK(key.Key() == TestKey::Public()); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_public_key_from_a_hex_encoded_string) +{ + std::string input = TestKey::PublicString(); + + NN::Contract::PublicKey key = NN::Contract::PublicKey::Parse(input); + + BOOST_CHECK(key.Key() == TestKey::Public()); +} + +BOOST_AUTO_TEST_CASE(it_gives_an_invalid_key_when_parsing_an_empty_string) +{ + NN::Contract::PublicKey key = NN::Contract::PublicKey::Parse(""); + + BOOST_CHECK(key.Viable() == false); + BOOST_CHECK(key.ToString() == ""); +} + +BOOST_AUTO_TEST_CASE(it_supports_a_basic_check_for_key_viability) +{ + // OK: 65 bytes, uncompressed + std::string full_length = TestKey::PublicString(); + NN::Contract::PublicKey key = NN::Contract::PublicKey::Parse(full_length); + + BOOST_CHECK(key.Viable() == true); + + // OK: 33 bytes, compressed (not a real key) + key = NN::Contract::PublicKey::Parse( + "044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a252"); + + BOOST_CHECK(key.Viable() == true); + + // BAD: Check some invalid hex-encoded inputs: + for (const auto& garbage : TestKey::GarbagePublicStrings()) { + key = NN::Contract::PublicKey::Parse(garbage); + + BOOST_CHECK(key.Viable() == false); + } +} + +BOOST_AUTO_TEST_CASE(it_provides_the_wrapped_key) +{ + NN::Contract::PublicKey key(TestKey::Public()); + + BOOST_CHECK(key.Key() == TestKey::Public()); +} + +BOOST_AUTO_TEST_CASE(it_represents_itself_as_a_string) +{ + NN::Contract::PublicKey key(TestKey::Public()); + + BOOST_CHECK(key.ToString() == TestKey::PublicString()); +} + +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) +{ + NN::Contract::PublicKey key(TestKey::Public()); + + // 66 bytes = 65 key bytes + 1 leading byte with the size + BOOST_CHECK(GetSerializeSize(key, SER_NETWORK, 1) == 66); + + CDataStream stream(SER_NETWORK, 1); + + stream << key; + std::vector output(stream.begin(), stream.end()); + + BOOST_CHECK(output == TestKey::PublicSerialized()); +} + +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) +{ + NN::Contract::PublicKey key; + + CDataStream stream(TestKey::PublicSerialized(), SER_NETWORK, 1); + + stream >> key; + + BOOST_CHECK(key == TestKey::Public()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ----------------------------------------------------------------------------- +// Contract +// ----------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(Contract) + +BOOST_AUTO_TEST_CASE(it_initializes_to_an_invalid_contract_by_default) +{ + NN::Contract contract; + + BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); + BOOST_CHECK(contract.m_type == NN::ContractType::UNKNOWN); + BOOST_CHECK(contract.m_action == NN::ContractAction::UNKNOWN); + BOOST_CHECK(contract.m_key.empty() == true); + BOOST_CHECK(contract.m_value.empty() == true); + BOOST_CHECK(contract.m_signature.Raw().empty() == true); + BOOST_CHECK(contract.m_public_key.Key() == CPubKey()); + BOOST_CHECK(contract.m_nonce == 0); + BOOST_CHECK(contract.m_timestamp == 0); + BOOST_CHECK(contract.m_tx_timestamp == 0); +} + +BOOST_AUTO_TEST_CASE(it_initializes_with_components_for_a_new_contract) +{ + NN::Contract contract( + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "foo", + "bar"); + + BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); + BOOST_CHECK(contract.m_type == NN::ContractType::BEACON); + BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); + BOOST_CHECK(contract.m_key == "foo"); + BOOST_CHECK(contract.m_value == "bar"); + BOOST_CHECK(contract.m_signature.Raw().empty() == true); + BOOST_CHECK(contract.m_public_key.Key() == CPubKey()); + BOOST_CHECK(contract.m_nonce == 0); + BOOST_CHECK(contract.m_timestamp == 0); + BOOST_CHECK(contract.m_tx_timestamp == 0); +} + +BOOST_AUTO_TEST_CASE(it_initializes_with_components_from_a_contract_message) +{ + NN::Contract contract( + NN::Contract::CURRENT_VERSION, + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "foo", + "bar", + NN::Contract::Signature(TestSig::V2Bytes()), + NN::Contract::PublicKey(TestKey::Public()), + 123, // Nonce + 456, // Signing timestamp + 789); // Tx timestamp + + BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); + BOOST_CHECK(contract.m_type == NN::ContractType::BEACON); + BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); + BOOST_CHECK(contract.m_key == "foo"); + BOOST_CHECK(contract.m_value == "bar"); + BOOST_CHECK(contract.m_signature.Raw() == TestSig::V2Bytes()); + BOOST_CHECK(contract.m_public_key.Key() == TestKey::Public()); + BOOST_CHECK(contract.m_nonce == 123); + BOOST_CHECK(contract.m_timestamp == 456); + BOOST_CHECK(contract.m_tx_timestamp == 789); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_master_and_message_keys) +{ + // Master private key read from configuration or command-line option: + SetArgument("masterprojectkey", "1234"); + + BOOST_CHECK(NN::Contract::MasterPrivateKey().size() == 2); + BOOST_CHECK(NN::Contract::MasterPublicKey().Raw().size() == 65); + BOOST_CHECK(NN::Contract::MessagePrivateKey().size() == 279); + BOOST_CHECK(NN::Contract::MessagePublicKey().Raw().size() == 65); + + SetArgument("masterprojectkey", ""); + BOOST_CHECK(NN::Contract::MasterPrivateKey().empty() == true); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_contract_burn_address) +{ + std::string testnet = "mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ"; + std::string mainnet = "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; + + fTestNet = true; + BOOST_CHECK(NN::Contract::BurnAddress() == testnet); + + fTestNet = false; + BOOST_CHECK(NN::Contract::BurnAddress() == mainnet); +} + +BOOST_AUTO_TEST_CASE(it_detects_a_contract_in_a_transaction_message) +{ + BOOST_CHECK(NN::Contract::Detect(TestMessage::V1String()) == true); + BOOST_CHECK(NN::Contract::Detect("") == false); + BOOST_CHECK(NN::Contract::Detect("") == false); +} + +BOOST_AUTO_TEST_CASE(it_ignores_superblocks_during_legacy_v1_contract_detection) +{ + std::string message( + "superblock" + "test" + "test" + "A" + "" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + + BOOST_CHECK(NN::Contract::Detect(message) == false); +} + +BOOST_AUTO_TEST_CASE(it_parses_a_legacy_v1_contract_from_a_transaction_message) +{ + NN::Contract contract = NN::Contract::Parse(TestMessage::V1String(), 123); + + BOOST_CHECK(contract.m_version == 1); // Legacy strings always parse to v1 + BOOST_CHECK(contract.m_type == NN::ContractType::BEACON); + BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); + BOOST_CHECK(contract.m_key == "test"); + BOOST_CHECK(contract.m_value == "test"); + BOOST_CHECK(contract.m_signature.Raw().size() == 70); + BOOST_CHECK(contract.m_public_key.Key().Raw().size() == 0); + BOOST_CHECK(contract.m_tx_timestamp == 123); +} + +BOOST_AUTO_TEST_CASE(it_gives_an_invalid_contract_when_parsing_an_empty_message) +{ + NN::Contract contract = NN::Contract::Parse("", 123); + + BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); + BOOST_CHECK(contract.m_type == NN::ContractType::UNKNOWN); + BOOST_CHECK(contract.m_action == NN::ContractAction::UNKNOWN); + BOOST_CHECK(contract.m_key == ""); + BOOST_CHECK(contract.m_value == ""); + BOOST_CHECK(contract.m_signature.Raw().size() == 0); + BOOST_CHECK(contract.m_public_key.Key().Raw().size() == 0); + BOOST_CHECK(contract.m_tx_timestamp == 0); +} + +BOOST_AUTO_TEST_CASE(it_gives_an_invalid_contract_when_parsing_a_non_contract) +{ + NN::Contract contract = NN::Contract::Parse("", 123); + + BOOST_CHECK(contract.m_version == 1); // Legacy strings always parse to v1 + BOOST_CHECK(contract.m_type == NN::ContractType::UNKNOWN); + BOOST_CHECK(contract.m_action == NN::ContractAction::UNKNOWN); + BOOST_CHECK(contract.m_key == ""); + BOOST_CHECK(contract.m_value == ""); + BOOST_CHECK(contract.m_signature.Raw().size() == 0); + BOOST_CHECK(contract.m_public_key.Key().Raw().size() == 0); + BOOST_CHECK(contract.m_tx_timestamp == 123); +} + +BOOST_AUTO_TEST_CASE(it_determines_whether_a_contract_is_complete) +{ + NN::Contract contract = TestMessage::Current(); + BOOST_CHECK(contract.WellFormed() == true); + + // WellFormed() does NOT verify the signature: + contract = TestMessage::Current(); + contract.m_signature = NN::Contract::Signature(TestSig::InvalidBytes()); + BOOST_CHECK(contract.WellFormed() == true); + + // In addition to v1, a contract must contain a nonce: + contract = TestMessage::Current(); + contract.m_nonce = 0; + BOOST_CHECK(contract.WellFormed() == false); + + // In addition to v1, a contract must contain a signing timestamp: + contract = TestMessage::Current(); + contract.m_timestamp = 0; + BOOST_CHECK(contract.WellFormed() == false); +} + +BOOST_AUTO_TEST_CASE(it_determines_whether_a_legacy_v1_contract_is_complete) +{ + NN::Contract contract = NN::Contract::Parse(TestMessage::V1String(), 123); + BOOST_CHECK(contract.WellFormed() == true); + + // WellFormed() does NOT verify the signature: + contract = NN::Contract::Parse(TestMessage::InvalidV1String(), 123); + BOOST_CHECK(contract.WellFormed() == true); + + contract = NN::Contract::Parse(TestMessage::PartialV1String(), 123); + BOOST_CHECK(contract.WellFormed() == false); + + contract = NN::Contract::Parse("", 123); + BOOST_CHECK(contract.WellFormed() == false); + + contract = NN::Contract::Parse("", 123); + BOOST_CHECK(contract.WellFormed() == false); +} + +BOOST_AUTO_TEST_CASE(it_determines_whether_a_contract_is_valid) +{ + NN::Contract contract = TestMessage::Current(); + BOOST_CHECK(contract.Validate() == true); + + // Valid() DOES verify the signature: + contract = TestMessage::Current(); + contract.m_signature = NN::Contract::Signature(TestSig::InvalidBytes()); + BOOST_CHECK(contract.Validate() == false); + + // In addition to v1, a contract must contain a nonce: + contract = TestMessage::Current(); + contract.m_nonce = 0; + BOOST_CHECK(contract.Validate() == false); + + // In addition to v1, a contract must contain a signing timestamp: + contract = TestMessage::Current(); + contract.m_timestamp = 0; + BOOST_CHECK(contract.Validate() == false); +} + +BOOST_AUTO_TEST_CASE(it_determines_whether_a_legacy_v1_contract_is_valid) +{ + NN::Contract contract = NN::Contract::Parse(TestMessage::V1String(), 123); + BOOST_CHECK(contract.Validate() == true); + + // Valid() DOES verify the signature: + contract = NN::Contract::Parse(TestMessage::InvalidV1String(), 123); + BOOST_CHECK(contract.Validate() == false); + + contract = NN::Contract::Parse(TestMessage::PartialV1String(), 123); + BOOST_CHECK(contract.Validate() == false); + + contract = NN::Contract::Parse("", 123); + BOOST_CHECK(contract.Validate() == false); + + contract = NN::Contract::Parse("", 123); + BOOST_CHECK(contract.Validate() == false); +} + +BOOST_AUTO_TEST_CASE(it_determines_whether_a_contract_needs_a_special_key) +{ + // Note: currently all contract types require either the master or message + // public/private keys. + + NN::Contract contract; + + // The following tests are not exhaustive for every type/action pair: + contract.m_type = NN::ContractType::BEACON; + contract.m_action = NN::ContractAction::ADD; + + BOOST_CHECK(contract.RequiresSpecialKey() == true); + BOOST_CHECK(contract.RequiresMasterKey() == false); + BOOST_CHECK(contract.RequiresMessageKey() == true); + + contract.m_type = NN::ContractType::PROJECT; + + BOOST_CHECK(contract.RequiresSpecialKey() == true); + BOOST_CHECK(contract.RequiresMasterKey() == true); + BOOST_CHECK(contract.RequiresMessageKey() == false); +} + +BOOST_AUTO_TEST_CASE(it_resolves_the_appropriate_public_key_for_a_contract) +{ + // Note: currently all contracts types require either the master or message + // public/private keys. + + NN::Contract contract; + + contract.m_type = NN::ContractType::BEACON; + contract.m_action = NN::ContractAction::ADD; + + BOOST_CHECK(contract.ResolvePublicKey() == NN::Contract::MessagePublicKey()); + + contract.m_type = NN::ContractType::PROJECT; + + BOOST_CHECK(contract.ResolvePublicKey() == NN::Contract::MasterPublicKey()); +} + +BOOST_AUTO_TEST_CASE(it_signs_a_message_with_a_supplied_private_key) +{ + NN::Contract contract( + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "test", + "test"); + + CKey private_key = TestKey::Private(); + + BOOST_CHECK(contract.Sign(private_key) == true); + BOOST_CHECK(contract.m_nonce > 0); + + // This assertion will fail if it takes longer than 1 second to sign: + BOOST_CHECK(contract.m_timestamp > GetAdjustedTime() - 1); + + // Build the message body to hash to verify the new signature: + std::vector body { + 0x01, // ContractType::BEACON + 0x01, // ContractAction::ADD + 0x04, 0x74, 0x65, 0x73, 0x74, // Key: "test" preceeded by length + 0x04, 0x74, 0x65, 0x73, 0x74, // Value: "test" preceeded by length + }; + + // Add contract nonce bytes for hash (32-bit int, little-endian) + body.push_back(contract.m_nonce & 0xFF); + body.push_back((contract.m_nonce >> 8) & 0xFF); + body.push_back((contract.m_nonce >> 16) & 0xFF); + body.push_back((contract.m_nonce >> 24) & 0xFF); + + // Add contract timestamp bytes for hash (64-bit int, little-endian) + body.push_back(contract.m_timestamp & 0xFF); + body.push_back((contract.m_timestamp >> 8) & 0xFF); + body.push_back((contract.m_timestamp >> 16) & 0xFF); + body.push_back((contract.m_timestamp >> 24) & 0xFF); + body.push_back((contract.m_timestamp >> 32) & 0xFF); + body.push_back((contract.m_timestamp >> 40) & 0xFF); + body.push_back((contract.m_timestamp >> 48) & 0xFF); + body.push_back((contract.m_timestamp >> 56) & 0xFF); + + uint256 hashed = Hash(body.begin(), body.end()); + + BOOST_CHECK(contract.m_signature.Viable() == true); + BOOST_CHECK(TestKey::Private().Verify(hashed, contract.m_signature.Raw())); +} + +BOOST_AUTO_TEST_CASE(it_signs_a_legacy_v1_message_with_a_supplied_private_key) +{ + NN::Contract contract; + contract.m_version = 1; // make this a legacy contract + contract.m_type = NN::ContractType::BEACON, + contract.m_key = "test", + contract.m_value = "test"; + + CKey private_key = TestKey::Private(); + + BOOST_CHECK(contract.Sign(private_key) == true); + + // Nonce and signing timestamp not generated for legacy v1 contracts: + BOOST_CHECK(contract.m_nonce == 0); + BOOST_CHECK(contract.m_timestamp == 0); + + // Build the message body to hash to verify the new signature: + std::string body = "beacontesttest"; + uint256 hashed = Hash(body.begin(), body.end()); + + BOOST_CHECK(TestKey::Private().Verify(hashed, contract.m_signature.Raw())); +} + +BOOST_AUTO_TEST_CASE(it_signs_a_message_with_the_shared_message_private_key) +{ + NN::Contract contract( + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "test", + "test"); + + BOOST_CHECK(contract.SignWithMessageKey() == true); + + // Build the message body to hash to verify the new signature: + std::vector body = { + 0x01, // ContractType::BEACON + 0x01, // ContractAction::ADD + 0x04, 0x74, 0x65, 0x73, 0x74, // Key: "test" preceeded by length + 0x04, 0x74, 0x65, 0x73, 0x74, // Value: "test" preceeded by length + }; + + // Add contract nonce bytes for hash (32-bit int, little-endian) + body.push_back(contract.m_nonce & 0xff); + body.push_back((contract.m_nonce >> 8) & 0xff); + body.push_back((contract.m_nonce >> 16) & 0xff); + body.push_back((contract.m_nonce >> 24) & 0xff); + + // Add contract timestamp bytes for hash (64-bit int, little-endian) + body.push_back(contract.m_timestamp & 0xff); + body.push_back((contract.m_timestamp >> 8) & 0xff); + body.push_back((contract.m_timestamp >> 16) & 0xff); + body.push_back((contract.m_timestamp >> 24) & 0xff); + body.push_back((contract.m_timestamp >> 32) & 0xff); + body.push_back((contract.m_timestamp >> 40) & 0xff); + body.push_back((contract.m_timestamp >> 48) & 0xff); + body.push_back((contract.m_timestamp >> 56) & 0xff); + + uint256 hashed = Hash(body.begin(), body.end()); + CKey key; + key.SetPrivKey(NN::Contract::MessagePrivateKey()); + + BOOST_CHECK(key.Verify(hashed, contract.m_signature.Raw())); +} + +BOOST_AUTO_TEST_CASE(it_refuses_to_sign_a_message_with_an_invalid_private_key) +{ + NN::Contract contract( + NN::ContractType::BEACON, + NN::ContractAction::ADD, + "test", + "test"); + + CKey key; // Empty key + + BOOST_CHECK(contract.Sign(key) == false); + BOOST_CHECK(contract.m_signature.Raw().size() == 0); +} + +BOOST_AUTO_TEST_CASE(it_verifies_a_contract_signature) +{ + // Test a message with a valid signature: + NN::Contract contract = TestMessage::Current(); + BOOST_CHECK(contract.VerifySignature() == true); + + // Change the previously-signed content: + contract.m_type = NN::ContractType::PROJECT; + BOOST_CHECK(contract.VerifySignature() == false); + + // Test a message with an invalid signature: + contract = TestMessage::Current(); + contract.m_signature = NN::Contract::Signature(TestSig::InvalidBytes()); + BOOST_CHECK(contract.VerifySignature() == false); + + // V2 contracts must additionally sign the action: + contract = TestMessage::Current(); + contract.m_action = NN::ContractAction::REMOVE; + BOOST_CHECK(contract.VerifySignature() == false); + + // V2 contracts must additionally sign the nonce: + contract = TestMessage::Current(); + contract.m_nonce = 999; + BOOST_CHECK(contract.VerifySignature() == false); + + // V2 contracts must additionally sign the signing timestamp: + contract = TestMessage::Current(); + contract.m_timestamp = 999; + BOOST_CHECK(contract.VerifySignature() == false); +} + +BOOST_AUTO_TEST_CASE(it_verifies_a_legacy_v1_contract_signature) +{ + // Test a message with a valid signature: + NN::Contract contract = NN::Contract::Parse(TestMessage::V1String(), 123); + BOOST_CHECK(contract.VerifySignature() == true); + + // Change the previously-signed content: + contract.m_type = NN::ContractType::PROJECT; + BOOST_CHECK(contract.VerifySignature() == false); + + // Test a message with an invalid signature: + contract = NN::Contract::Parse(TestMessage::InvalidV1String(), 123); + BOOST_CHECK(contract.VerifySignature() == false); + + // V1 contracts must NOT sign the action, nonce, or signing timestamp + // (but we can't test action without the master private key): + contract = NN::Contract::Parse(TestMessage::V1String(), 123); + contract.m_nonce = 999; + contract.m_timestamp = 999; + BOOST_CHECK(contract.VerifySignature() == true); +} + +BOOST_AUTO_TEST_CASE(it_generates_a_hash_of_a_contract_body) +{ + NN::Contract contract = TestMessage::Current(); + + BOOST_CHECK(contract.GetHash() == TestSig::V2Hash()); +} + +BOOST_AUTO_TEST_CASE(it_generates_a_hash_of_a_legacy_v1_contract_body) +{ + NN::Contract contract = TestMessage::V1(); + + BOOST_CHECK(contract.GetHash() == TestSig::V1Hash()); +} + +BOOST_AUTO_TEST_CASE(it_represents_itself_as_a_legacy_string) +{ + NN::Contract contract = TestMessage::V1(); + + BOOST_CHECK(contract.ToString() == TestMessage::V1String()); +} + +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) +{ + NN::Contract contract = TestMessage::Current(); + + // 101 bytes = 4 bytes for the serialization format version + // + 1 byte each for the type and action + // + 4 bytes for the nonce (32-bit int) + // + 8 bytes for the signing timestamp (64-bit int) + // + 5 bytes each for key and value (4 for string, 1 for size) + // + 1 byte for the empty public key size + // + 72 bytes for the signature (71 + 1 for the size) + BOOST_CHECK(GetSerializeSize(contract, SER_NETWORK, 1) == 101); + + CDataStream stream(SER_NETWORK, 1); + + stream << contract; + std::vector output(stream.begin(), stream.end()); + + BOOST_CHECK(output == TestMessage::V2Serialized()); +} + +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) +{ + NN::Contract contract; + + CDataStream stream(TestMessage::V2Serialized(), SER_NETWORK, 1); + + stream >> contract; + contract.m_tx_timestamp = 123; // So that contract.Validate() passes + + BOOST_CHECK(contract.Validate() == true); // Verifies signature + BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); + BOOST_CHECK(contract.m_type == NN::ContractType::BEACON); + BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); + BOOST_CHECK(contract.m_key == "test"); + BOOST_CHECK(contract.m_value == "test"); + BOOST_CHECK(contract.m_public_key == CPubKey()); + BOOST_CHECK(contract.m_signature.Raw() == TestSig::V2Bytes()); + BOOST_CHECK(contract.m_nonce == 123); + BOOST_CHECK(contract.m_timestamp == 234); +} + +BOOST_AUTO_TEST_SUITE_END() From 0adf85886ddb92b915581ccfd4ace976879b018d Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:34 -0500 Subject: [PATCH 02/15] Refactor project whitelist into a contract handler implementation --- src/neuralnet/project.cpp | 43 +++++++++++------ src/neuralnet/project.h | 33 +++++++++---- src/test/neuralnet/project_tests.cpp | 71 ++++++++++++++++++++++++---- 3 files changed, 113 insertions(+), 34 deletions(-) diff --git a/src/neuralnet/project.cpp b/src/neuralnet/project.cpp index 9f3cba6e11..517efafd37 100644 --- a/src/neuralnet/project.cpp +++ b/src/neuralnet/project.cpp @@ -1,4 +1,4 @@ -#include "project.h" +#include "neuralnet/project.h" #include #include @@ -19,8 +19,15 @@ Whitelist& NN::GetWhitelist() // Class: Project // ----------------------------------------------------------------------------- -Project::Project(const std::string name, const std::string url, const int64_t ts) - : m_name(name), m_url(url), m_timestamp(ts) +Project::Project(std::string name, std::string url, int64_t ts) + : m_name(std::move(name)), m_url(std::move(url)), m_timestamp(std::move(ts)) +{ +} + +Project::Project(const Contract& contract) + : m_name(contract.m_key) + , m_url(contract.m_value) + , m_timestamp(contract.m_tx_timestamp) { } @@ -59,12 +66,21 @@ std::string Project::StatsUrl(const std::string& type) const return BaseUrl() + "stats/" + type + ".gz"; } +Contract Project::IntoContract(ContractAction action) +{ + return Contract( + ContractType::PROJECT, + action, // defaults to ContractAction::ADD + std::move(m_name), + std::move(m_url)); +} + // ----------------------------------------------------------------------------- // Class: WhitelistSnapshot // ----------------------------------------------------------------------------- -WhitelistSnapshot::WhitelistSnapshot(const ProjectListPtr projects) - : m_projects(projects) +WhitelistSnapshot::WhitelistSnapshot(ProjectListPtr projects) + : m_projects(std::move(projects)) { } @@ -110,7 +126,7 @@ WhitelistSnapshot WhitelistSnapshot::Sorted() const { ProjectList sorted(m_projects->begin(), m_projects->end()); - auto predicate = [](const Project& a, const Project& b) { + auto ascending_by_name = [](const Project& a, const Project& b) { return std::lexicographical_compare( a.m_name.begin(), a.m_name.end(), @@ -121,7 +137,7 @@ WhitelistSnapshot WhitelistSnapshot::Sorted() const }); }; - std::sort(sorted.begin(), sorted.end(), predicate); + std::sort(sorted.begin(), sorted.end(), ascending_by_name); return WhitelistSnapshot(std::make_shared(sorted)); } @@ -141,23 +157,20 @@ WhitelistSnapshot Whitelist::Snapshot() const return WhitelistSnapshot(std::atomic_load(&m_projects)); } -void Whitelist::Add( - const std::string& name, - const std::string& url, - const int64_t& timestamp) +void Whitelist::Add(const Contract& contract) { - ProjectListPtr copy = CopyFilteredWhitelist(name); + ProjectListPtr copy = CopyFilteredWhitelist(contract.m_key); - copy->emplace_back(name, url, timestamp); + copy->emplace_back(contract); // With C++20, use std::atomic>::store() instead: std::atomic_store(&m_projects, std::move(copy)); } -void Whitelist::Delete(const std::string& name) +void Whitelist::Delete(const Contract& contract) { // With C++20, use std::atomic>::store() instead: - std::atomic_store(&m_projects, CopyFilteredWhitelist(name)); + std::atomic_store(&m_projects, CopyFilteredWhitelist(contract.m_key)); } ProjectListPtr Whitelist::CopyFilteredWhitelist(const std::string& name) const diff --git a/src/neuralnet/project.h b/src/neuralnet/project.h index 8040357af6..8810f9ae73 100644 --- a/src/neuralnet/project.h +++ b/src/neuralnet/project.h @@ -1,5 +1,7 @@ #pragma once +#include "neuralnet/contract.h" + #include #include #include @@ -18,7 +20,14 @@ struct Project //! \param url Project URL from contract message value. //! \param ts Contract timestamp. //! - Project(const std::string name, const std::string url, const int64_t ts); + Project(std::string name, std::string url, int64_t ts); + + //! + //! \brief Initialize a \c Project from the provided contract. + //! + //! \param contract Contains the project data. + //! + Project(const Contract& contract); std::string m_name; //!< As it exists in the contract key field. std::string m_url; //!< As it exists in the contract value field. @@ -46,6 +55,14 @@ struct Project //! Otherwise, return a URL to the specified export archive. //! std::string StatsUrl(const std::string& type = "") const; + + //! + //! \brief Consume the project object and move the data into a new, + //! unsigned \c Contract instance for publication in a transaction. + //! + //! \return A project-type contract containing the project data. + //! + Contract IntoContract(ContractAction action = ContractAction::ADD); }; //! @@ -73,7 +90,7 @@ class WhitelistSnapshot //! //! \param projects A copy of the smart pointer to the project list. //! - WhitelistSnapshot(const ProjectListPtr projects); + WhitelistSnapshot(ProjectListPtr projects); //! //! \brief Returns an iterator to the beginning. @@ -154,7 +171,7 @@ class WhitelistSnapshot //! only modifies this data from one thread now. The implementation needs more //! coarse locking if it will support multiple writers in the future. //! -class Whitelist +class Whitelist : public IContractHandler { public: //! @@ -170,18 +187,16 @@ class Whitelist //! //! \brief Add a project to the whitelist from contract data. //! - //! \param name Project name as it exists in the contract key. - //! \param url Project URL as it exists in the contract value. - //! \param ts Timestamp of the contract. + //! \param contract Contains information about the project to add. //! - void Add(const std::string& name, const std::string& url, const int64_t& ts); + void Add(const Contract& contract) override; //! //! \brief Remove the specified project from the whitelist. //! - //! \param name Project name as it exists in the contract key. + //! \param contract Contains information about the project to remove. //! - void Delete(const std::string& name); + void Delete(const Contract& contract) override; private: // With C++20, use std::atomic> instead: diff --git a/src/test/neuralnet/project_tests.cpp b/src/test/neuralnet/project_tests.cpp index b3c5360e2f..8b5c3298a8 100644 --- a/src/test/neuralnet/project_tests.cpp +++ b/src/test/neuralnet/project_tests.cpp @@ -2,6 +2,36 @@ #include +namespace { +//! +//! \brief Generate a mock project contract. +//! +//! \param key A fake project name as it might appear in a contract. +//! \param value A fake project URL as it might appear in a contract. +//! +//! \return A mock project contract. +//! +NN::Contract contract(std::string key, std::string value) +{ + return NN::Contract( + NN::Contract::CURRENT_VERSION, + NN::ContractType::PROJECT, + // Add or delete checked before passing to handler, so we don't need + // to give a specific value here: + NN::ContractAction::UNKNOWN, + key, + value, + // Signature checked before passing to handler, so we don't need to + // give specific signature, public key, nonce, or signing timestamp + // values here: + NN::Contract::Signature(), + NN::Contract::PublicKey(), + 0, + 0, + 1234567); +} +} // anonymous namespace + // ----------------------------------------------------------------------------- // Project // ----------------------------------------------------------------------------- @@ -17,6 +47,15 @@ BOOST_AUTO_TEST_CASE(it_provides_access_to_project_contract_data) BOOST_CHECK(project.m_timestamp == 1234567); } +BOOST_AUTO_TEST_CASE(it_initializes_from_a_contract) +{ + NN::Project project(contract("Enigma", "http://enigma.test/@")); + + BOOST_CHECK(project.m_name == "Enigma"); + BOOST_CHECK(project.m_url == "http://enigma.test/@"); + BOOST_CHECK(project.m_timestamp == 1234567); +} + BOOST_AUTO_TEST_CASE(it_formats_the_user_friendly_display_name) { NN::Project project("Enigma_at_Home", "http://enigma.test/@", 1234567); @@ -53,6 +92,18 @@ BOOST_AUTO_TEST_CASE(it_formats_a_project_stats_archive_url) BOOST_CHECK(project.StatsUrl("team") == "http://enigma.test/stats/team.gz"); } +BOOST_AUTO_TEST_CASE(it_converts_itself_into_a_contract) +{ + NN::Project project("Enigma", "http://enigma.test/@", 1234567); + + NN::Contract contract = project.IntoContract(); + + BOOST_CHECK(contract.m_type == NN::ContractType::PROJECT); + BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); + BOOST_CHECK(contract.m_key == "Enigma"); + BOOST_CHECK(contract.m_value == "http://enigma.test/@"); +} + BOOST_AUTO_TEST_SUITE_END() // ----------------------------------------------------------------------------- @@ -65,7 +116,7 @@ BOOST_AUTO_TEST_CASE(it_is_iterable) { NN::WhitelistSnapshot s(std::make_shared(NN::ProjectList { NN::Project("Enigma", "http://enigma.test/@", 1234567), - NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567) + NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567), })); auto counter = 0; @@ -86,7 +137,7 @@ BOOST_AUTO_TEST_CASE(it_counts_the_number_of_projects) NN::WhitelistSnapshot s2(std::make_shared(NN::ProjectList { NN::Project("Enigma", "http://enigma.test/@", 1234567), - NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567) + NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567), })); BOOST_CHECK(s2.size() == 2); @@ -100,7 +151,7 @@ BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_projects) NN::WhitelistSnapshot s2(std::make_shared(NN::ProjectList { NN::Project("Enigma", "http://enigma.test/@", 1234567), - NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567) + NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567), })); BOOST_CHECK(s2.Populated() == true); @@ -148,7 +199,7 @@ BOOST_AUTO_TEST_CASE(it_adds_whitelisted_projects_from_contract_data) BOOST_CHECK(whitelist.Snapshot().size() == 0); BOOST_CHECK(whitelist.Snapshot().Contains("Enigma") == false); - whitelist.Add("Enigma", "http://enigma.test", 1234567); + whitelist.Add(contract("Enigma", "http://enigma.test")); BOOST_CHECK(whitelist.Snapshot().size() == 1); BOOST_CHECK(whitelist.Snapshot().Contains("Enigma") == true); @@ -157,12 +208,12 @@ BOOST_AUTO_TEST_CASE(it_adds_whitelisted_projects_from_contract_data) BOOST_AUTO_TEST_CASE(it_removes_whitelisted_projects_from_contract_data) { NN::Whitelist whitelist; - whitelist.Add("Enigma", "http://enigma.test", 1234567); + whitelist.Add(contract("Enigma", "http://enigma.test")); BOOST_CHECK(whitelist.Snapshot().size() == 1); BOOST_CHECK(whitelist.Snapshot().Contains("Enigma") == true); - whitelist.Delete("Enigma"); + whitelist.Delete(contract("Enigma", "http://enigma.test")); BOOST_CHECK(whitelist.Snapshot().size() == 0); BOOST_CHECK(whitelist.Snapshot().Contains("Enigma") == false); @@ -171,10 +222,10 @@ BOOST_AUTO_TEST_CASE(it_removes_whitelisted_projects_from_contract_data) BOOST_AUTO_TEST_CASE(it_does_not_mutate_existing_snapshots) { NN::Whitelist whitelist; - whitelist.Add("Enigma", "http://enigma.test", 1234567); + whitelist.Add(contract("Enigma", "http://enigma.test")); auto snapshot = whitelist.Snapshot(); - whitelist.Delete("Enigma"); + whitelist.Delete(contract("Enigma", "http://enigma.test")); BOOST_CHECK(snapshot.Contains("Enigma") == true); } @@ -182,8 +233,8 @@ BOOST_AUTO_TEST_CASE(it_does_not_mutate_existing_snapshots) BOOST_AUTO_TEST_CASE(it_overwrites_projects_with_the_same_name) { NN::Whitelist whitelist; - whitelist.Add("Enigma", "http://enigma.test", 1234567); - whitelist.Add("Enigma", "http://new.enigma.test", 1234567); + whitelist.Add(contract("Enigma", "http://enigma.test")); + whitelist.Add(contract("Enigma", "http://new.enigma.test")); auto snapshot = whitelist.Snapshot(); BOOST_CHECK(snapshot.size() == 1); From f711176d612645b2f07e47916d6a41b1f5ebfc0e Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:36 -0500 Subject: [PATCH 03/15] Add placeholder contract handler implementation for beacons --- src/Makefile.am | 2 + src/neuralnet/beacon.cpp | 92 ++++++++++++++++++++++++++++++++++++++ src/neuralnet/beacon.h | 42 +++++++++++++++++ src/neuralnet/claim.cpp | 2 +- src/neuralnet/contract.cpp | 2 +- 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/neuralnet/beacon.cpp create mode 100644 src/neuralnet/beacon.h diff --git a/src/Makefile.am b/src/Makefile.am index a70d2830d3..c9554a4426 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -99,6 +99,7 @@ GRIDCOIN_CORE_H = \ neuralnet/accrual/null.h \ neuralnet/accrual/research_age.h \ neuralnet/accrual/snapshot.h \ + neuralnet/beacon.h \ neuralnet/claim.h \ neuralnet/contract.h \ neuralnet/cpid.h \ @@ -174,6 +175,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ logging.cpp \ main.cpp \ miner.cpp \ + neuralnet/beacon.cpp \ neuralnet/claim.cpp \ neuralnet/contract.cpp \ neuralnet/cpid.cpp \ diff --git a/src/neuralnet/beacon.cpp b/src/neuralnet/beacon.cpp new file mode 100644 index 0000000000..6e157c1bed --- /dev/null +++ b/src/neuralnet/beacon.cpp @@ -0,0 +1,92 @@ +#include "appcache.h" +#include "../beacon.h" // temporary +#include "neuralnet/beacon.h" +#include "util.h" + +using namespace NN; + +namespace { + BeaconRegistry beacons; +} + +BeaconRegistry& NN::GetBeaconRegistry() +{ + return beacons; +} + +// ----------------------------------------------------------------------------- +// Class: BeaconRegistry +// +// TODO: This placeholder contract handler just writes messages into the +// AppCache. Refactor the implementation to use strongly-typed structures and +// organize the beacon logic behind this API. +// ----------------------------------------------------------------------------- + +void BeaconRegistry::Add(const Contract& contract) +{ + if (Contains(contract.m_value, "INVESTOR")) { + return; + } + + std::string out_cpid = ""; + std::string out_address = ""; + std::string out_publickey = ""; + + GetBeaconElements(contract.m_value, out_cpid, out_address, out_publickey); + + WriteCache( + Section::BEACONALT, + contract.m_key + "." + ToString(contract.m_tx_timestamp), + out_publickey, + contract.m_tx_timestamp); + + WriteCache( + Section::BEACON, + contract.m_key, + contract.m_value, + contract.m_tx_timestamp); + + if (fDebug10) { + LogPrintf( + "BEACON add %s %s %s", + contract.m_key, + DecodeBase64(contract.m_value), + TimestampToHRDate(contract.m_tx_timestamp)); + } +} + +void BeaconRegistry::Delete(const Contract& contract) +{ + if (Contains(contract.m_value, "INVESTOR")) { + return; + } + + // We use beacons to verify blocks, so we cannot also delete it from the + // BEACONALT AppCache section. + DeleteCache(Section::BEACON, contract.m_key); + + if (fDebug10) { + LogPrintf("BEACON DEL %s - %s", contract.m_key, TimestampToHRDate(contract.m_tx_timestamp)); + } +} + +void BeaconRegistry::Revert(const Contract& contract) +{ + if (Contains(contract.m_value, "INVESTOR")) { + return; + } + + IContractHandler::Revert(contract); + + // Revert the entry used to verify blocks with beacon keys. Reorganizing + // the chain will add this entry back: + if (contract.m_action == ContractAction::ADD) { + DeleteCache( + Section::BEACONALT, + contract.m_key + "." + ToString(contract.m_tx_timestamp)); + } + + // Ignore ContractAction::REMOVE -- we don't need to add the beacon back + // to the BEACONALT section because we won't delete it from this section + // in the first place. +} diff --git a/src/neuralnet/beacon.h b/src/neuralnet/beacon.h new file mode 100644 index 0000000000..b78680f352 --- /dev/null +++ b/src/neuralnet/beacon.h @@ -0,0 +1,42 @@ +#pragma once + +#include "neuralnet/contract.h" + +namespace NN +{ +//! +//! \brief Stores and manages researcher beacons. +//! +class BeaconRegistry : public IContractHandler +{ +public: + //! + //! \brief Register a beacon from contract data. + //! + //! \param contract Contains information about the beacon to add. + //! + void Add(const Contract& contract) override; + + //! + //! \brief Deregister the beacon specified by contract data. + //! + //! \param contract Contains information about the beacon to remove. + //! + void Delete(const Contract& contract) override; + + //! + //! \brief Reverse a beacon registration or deregistration. + //! + //! \param contract Contains the action and CPID of the beacon entry to + //! reverse. + //! + void Revert(const Contract& contract) override; +}; + +//! +//! \brief Get the global beacon registry. +//! +//! \return Current global beacon manager instance. +//! +BeaconRegistry& GetBeaconRegistry(); +} diff --git a/src/neuralnet/claim.cpp b/src/neuralnet/claim.cpp index 9778aa7762..c3da13cf10 100644 --- a/src/neuralnet/claim.cpp +++ b/src/neuralnet/claim.cpp @@ -1,4 +1,4 @@ -#include "beacon.h" +#include "../beacon.h" #include "key.h" #include "neuralnet/claim.h" #include "util.h" diff --git a/src/neuralnet/contract.cpp b/src/neuralnet/contract.cpp index e3ee59ab7e..91d9485502 100644 --- a/src/neuralnet/contract.cpp +++ b/src/neuralnet/contract.cpp @@ -236,7 +236,7 @@ class Dispatcher // TODO: build contract handlers for the remaining contract types: // TODO: refactor to dynamic registration for easier testing: switch (type) { - case ContractType::BEACON: return GetBeaconDirectory(); + case ContractType::BEACON: return GetBeaconRegistry(); case ContractType::POLL: return m_appcache_handler; case ContractType::PROJECT: return GetWhitelist(); case ContractType::PROTOCOL: return m_appcache_handler; From 3071e04c03e718a75ec446a7bb9989e84043bc90 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:37 -0500 Subject: [PATCH 04/15] Unravel contracts: separate message sending functions --- src/Makefile.am | 2 + src/contract/contract.cpp | 82 ------------------------------- src/contract/contract.h | 7 --- src/contract/message.cpp | 101 ++++++++++++++++++++++++++++++++++++++ src/contract/message.h | 28 +++++++++++ src/contract/polls.cpp | 2 +- src/main.cpp | 1 + src/rpcblockchain.cpp | 1 + src/test/beacon_tests.cpp | 1 + 9 files changed, 135 insertions(+), 90 deletions(-) create mode 100644 src/contract/message.cpp create mode 100644 src/contract/message.h diff --git a/src/Makefile.am b/src/Makefile.am index c9554a4426..46d810f74b 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -75,6 +75,7 @@ GRIDCOIN_CORE_H = \ compat.h \ compat/byteswap.h \ compat/endian.h \ + contract/message.h \ contract/polls.h \ contract/contract.h \ crypter.h \ @@ -162,6 +163,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ block.cpp \ boinc.cpp \ checkpoints.cpp \ + contract/message.cpp \ contract/polls.cpp \ contract/contract.cpp \ crypter.cpp \ diff --git a/src/contract/contract.cpp b/src/contract/contract.cpp index 46de5fdcae..c3b0ec7812 100644 --- a/src/contract/contract.cpp +++ b/src/contract/contract.cpp @@ -7,36 +7,6 @@ double GetTotalBalance(); -std::string GetBurnAddress() { return fTestNet ? "mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ" : "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; - } - -bool CheckMessageSignature(std::string sAction,std::string messagetype, std::string sMsg, std::string sSig, std::string strMessagePublicKey) -{ - std::string strMasterPubKey = ""; - if (messagetype=="project" || messagetype=="projectmapping") - { - strMasterPubKey= msMasterProjectPublicKey; - } - else - { - strMasterPubKey = msMasterMessagePublicKey; - } - - if (!strMessagePublicKey.empty()) strMasterPubKey = strMessagePublicKey; - if (sAction=="D" && messagetype=="beacon") strMasterPubKey = msMasterProjectPublicKey; - if (sAction=="D" && messagetype=="poll") strMasterPubKey = msMasterProjectPublicKey; - if (sAction=="D" && messagetype=="vote") strMasterPubKey = msMasterProjectPublicKey; - if (messagetype == "protocol" || messagetype == "scraper") strMasterPubKey = msMasterProjectPublicKey; - - std::string db64 = DecodeBase64(sSig); - CKey key; - if (!key.SetPubKey(ParseHex(strMasterPubKey))) return false; - std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); - std::vector vchSig = std::vector(db64.begin(), db64.end()); - if (!key.Verify(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) return false; - return true; -} - bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature) { std::string sBeaconPublicKey = GetBeaconPublicKey(sCPID, false); @@ -48,58 +18,6 @@ bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string return bValid; } -std::string SignMessage(const std::string& sMsg, CKey& key) -{ - std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); - std::vector vchSig; - if (!key.Sign(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) - { - return "Unable to sign message, check private key."; - } - const std::string sig(vchSig.begin(), vchSig.end()); - std::string SignedMessage = EncodeBase64(sig); - return SignedMessage; -} - -std::string SignMessage(std::string sMsg, std::string sPrivateKey) -{ - CKey key; - std::vector vchPrivKey = ParseHex(sPrivateKey); - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - return SignMessage(sMsg, key); -} - -std::string SendMessage(bool bAdd, std::string sType, std::string sPrimaryKey, std::string sValue, - std::string sMasterKey, int64_t MinimumBalance, double dFees, std::string strPublicKey) -{ - std::string sAddress = GetBurnAddress(); - CBitcoinAddress address(sAddress); - if (!address.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Gridcoin address"); - int64_t nAmount = AmountFromValue(dFees); - // Wallet comments - CWalletTx wtx; - if (pwalletMain->IsLocked()) throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); - std::string sMessageType = "" + sType + ""; //Project or Smart Contract - std::string sMessageKey = "" + sPrimaryKey + ""; - std::string sMessageValue = "" + sValue + ""; - std::string sMessagePublicKey = ""+ strPublicKey + ""; - std::string sMessageAction = bAdd ? "A" : "D"; //Add or Delete - //Sign Message - std::string sSig = SignMessage(sType+sPrimaryKey+sValue,sMasterKey); - std::string sMessageSignature = "" + sSig + ""; - wtx.hashBoinc = sMessageType+sMessageKey+sMessageValue+sMessageAction+sMessagePublicKey+sMessageSignature; - std::string strError = pwalletMain->SendMoneyToDestinationWithMinimumBalance(address.Get(), nAmount, MinimumBalance, wtx); - if (!strError.empty()) throw JSONRPCError(RPC_WALLET_ERROR, strError); - return wtx.GetHash().GetHex().c_str(); -} - -std::string SendContract(std::string sType, std::string sName, std::string sContract) -{ - std::string sPass = (sType=="project" || sType=="projectmapping" || sType=="smart_contract") ? GetArgument("masterprojectkey", msMasterMessagePrivateKey) : msMasterMessagePrivateKey; - std::string result = SendMessage(true,sType,sName,sContract,sPass,AmountFromValue(1),.00001,""); - return result; -} - bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, std::string& sSignature, std::string& sError, bool bAdvertising=false) { // Check if there is a beacon for this user diff --git a/src/contract/contract.h b/src/contract/contract.h index 610d31b6b3..ea8fb7c0bb 100644 --- a/src/contract/contract.h +++ b/src/contract/contract.h @@ -1,12 +1,5 @@ #pragma once -std::string SendContract(std::string sType, std::string sName, std::string sContract); -std::string SendMessage(bool bAdd, std::string sType, std::string sPrimaryKey, std::string sValue, - std::string sMasterKey, int64_t MinimumBalance, double dFees, std::string strPublicKey); -std::string GetBurnAddress(); -std::string SignMessage(std::string sMsg, std::string sPrivateKey); -std::string SignMessage(const std::string& sMsg, CKey& key); bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature); bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, std::string& sSignature, std::string& sError, bool bAdvertising=false); -bool CheckMessageSignature(std::string sAction,std::string messagetype, std::string sMsg, std::string sSig, std::string opt_pubkey); std::string executeRain(std::string sRecipients); diff --git a/src/contract/message.cpp b/src/contract/message.cpp new file mode 100644 index 0000000000..4a64cf36e1 --- /dev/null +++ b/src/contract/message.cpp @@ -0,0 +1,101 @@ +#include "init.h" +#include "message.h" +#include "rpcclient.h" +#include "rpcserver.h" +#include "rpcprotocol.h" +#include "keystore.h" + + +std::string GetBurnAddress() { return fTestNet ? "mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ" : "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; + } + +bool CheckMessageSignature( + std::string sAction, + std::string messagetype, + std::string sMsg, + std::string sSig, + std::string strMessagePublicKey) +{ + std::string strMasterPubKey = ""; + if (messagetype=="project" || messagetype=="projectmapping") + { + strMasterPubKey= msMasterProjectPublicKey; + } + else + { + strMasterPubKey = msMasterMessagePublicKey; + } + + if (!strMessagePublicKey.empty()) strMasterPubKey = strMessagePublicKey; + if (sAction=="D" && messagetype=="beacon") strMasterPubKey = msMasterProjectPublicKey; + if (sAction=="D" && messagetype=="poll") strMasterPubKey = msMasterProjectPublicKey; + if (sAction=="D" && messagetype=="vote") strMasterPubKey = msMasterProjectPublicKey; + if (messagetype == "protocol" || messagetype == "scraper") strMasterPubKey = msMasterProjectPublicKey; + + std::string db64 = DecodeBase64(sSig); + CKey key; + if (!key.SetPubKey(ParseHex(strMasterPubKey))) return false; + std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); + std::vector vchSig = std::vector(db64.begin(), db64.end()); + if (!key.Verify(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) return false; + return true; +} + +std::string SignMessage(const std::string& sMsg, CKey& key) +{ + std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); + std::vector vchSig; + if (!key.Sign(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) + { + return "Unable to sign message, check private key."; + } + const std::string sig(vchSig.begin(), vchSig.end()); + std::string SignedMessage = EncodeBase64(sig); + return SignedMessage; +} + +std::string SignMessage(std::string sMsg, std::string sPrivateKey) +{ + CKey key; + std::vector vchPrivKey = ParseHex(sPrivateKey); + key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash + return SignMessage(sMsg, key); +} + +std::string SendMessage( + bool bAdd, + std::string sType, + std::string sPrimaryKey, + std::string sValue, + std::string sMasterKey, + int64_t MinimumBalance, + double dFees, + std::string strPublicKey) +{ + std::string sAddress = GetBurnAddress(); + CBitcoinAddress address(sAddress); + if (!address.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Gridcoin address"); + int64_t nAmount = AmountFromValue(dFees); + // Wallet comments + CWalletTx wtx; + if (pwalletMain->IsLocked()) throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); + std::string sMessageType = "" + sType + ""; //Project or Smart Contract + std::string sMessageKey = "" + sPrimaryKey + ""; + std::string sMessageValue = "" + sValue + ""; + std::string sMessagePublicKey = ""+ strPublicKey + ""; + std::string sMessageAction = bAdd ? "A" : "D"; //Add or Delete + //Sign Message + std::string sSig = SignMessage(sType+sPrimaryKey+sValue,sMasterKey); + std::string sMessageSignature = "" + sSig + ""; + wtx.hashBoinc = sMessageType+sMessageKey+sMessageValue+sMessageAction+sMessagePublicKey+sMessageSignature; + std::string strError = pwalletMain->SendMoneyToDestinationWithMinimumBalance(address.Get(), nAmount, MinimumBalance, wtx); + if (!strError.empty()) throw JSONRPCError(RPC_WALLET_ERROR, strError); + return wtx.GetHash().GetHex().c_str(); +} + +std::string SendContract(std::string sType, std::string sName, std::string sContract) +{ + std::string sPass = (sType=="project" || sType=="projectmapping" || sType=="smart_contract") ? GetArgument("masterprojectkey", msMasterMessagePrivateKey) : msMasterMessagePrivateKey; + std::string result = SendMessage(true,sType,sName,sContract,sPass,AmountFromValue(1),.00001,""); + return result; +} diff --git a/src/contract/message.h b/src/contract/message.h new file mode 100644 index 0000000000..5c9c313469 --- /dev/null +++ b/src/contract/message.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +bool CheckMessageSignature( + std::string sAction, + std::string messagetype, + std::string sMsg, + std::string sSig, + std::string opt_pubkey); + +std::string SendContract(std::string sType, std::string sName, std::string sContract); + +std::string SendMessage( + bool bAdd, + std::string sType, + std::string sPrimaryKey, + std::string sValue, + std::string sMasterKey, + int64_t MinimumBalance, + double dFees, + std::string strPublicKey); + +std::string GetBurnAddress(); + +std::string SignMessage(std::string sMsg, std::string sPrivateKey); + +std::string SignMessage(const std::string& sMsg, CKey& key); diff --git a/src/contract/polls.cpp b/src/contract/polls.cpp index 3aea6f74a0..03c2daac94 100644 --- a/src/contract/polls.cpp +++ b/src/contract/polls.cpp @@ -3,7 +3,7 @@ #include "main.h" #include "polls.h" -#include "contract.h" +#include "message.h" #include "rpcclient.h" #include "rpcserver.h" #include "appcache.h" diff --git a/src/main.cpp b/src/main.cpp index 9a2b1508b4..da0c72526e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "backup.h" #include "appcache.h" #include "contract/contract.h" +#include "contract/message.h" #include "scraper_net.h" #include "gridcoin.h" diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 804818ecd1..d861406d6c 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -17,6 +17,7 @@ #include "neuralnet/tally.h" #include "backup.h" #include "appcache.h" +#include "contract/message.h" #include "contract/contract.h" #include "util.h" diff --git a/src/test/beacon_tests.cpp b/src/test/beacon_tests.cpp index 6eb741b3d2..2c11c8dbb1 100644 --- a/src/test/beacon_tests.cpp +++ b/src/test/beacon_tests.cpp @@ -4,6 +4,7 @@ #include "beacon.h" #include "appcache.h" #include "contract/contract.h" +#include "contract/message.h" #include #include From 5ff2246f843b368ead2f642b23ae766e51e71821 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:38 -0500 Subject: [PATCH 05/15] Unravel contracts: separate rain functions --- src/Makefile.am | 2 + src/contract/contract.cpp | 94 ----------------------------------- src/contract/contract.h | 1 - src/contract/rain.cpp | 101 ++++++++++++++++++++++++++++++++++++++ src/contract/rain.h | 5 ++ src/rpcblockchain.cpp | 1 + 6 files changed, 109 insertions(+), 95 deletions(-) create mode 100644 src/contract/rain.cpp create mode 100644 src/contract/rain.h diff --git a/src/Makefile.am b/src/Makefile.am index 46d810f74b..6a16ddece4 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -77,6 +77,7 @@ GRIDCOIN_CORE_H = \ compat/endian.h \ contract/message.h \ contract/polls.h \ + contract/rain.h \ contract/contract.h \ crypter.h \ db.h \ @@ -165,6 +166,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ checkpoints.cpp \ contract/message.cpp \ contract/polls.cpp \ + contract/rain.cpp \ contract/contract.cpp \ crypter.cpp \ db.cpp \ diff --git a/src/contract/contract.cpp b/src/contract/contract.cpp index c3b0ec7812..85eb084b13 100644 --- a/src/contract/contract.cpp +++ b/src/contract/contract.cpp @@ -5,7 +5,6 @@ #include "keystore.h" #include "beacon.h" -double GetTotalBalance(); bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature) { @@ -45,96 +44,3 @@ bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, } return true; } - -int64_t AmountFromDouble(double dAmount) -{ - if (dAmount <= 0.0 || dAmount > MAX_MONEY) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount"); - int64_t nAmount = roundint64(dAmount * COIN); - if (!MoneyRange(nAmount)) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount"); - return nAmount; -} - -std::string executeRain(std::string sRecipients) -{ - CWalletTx wtx; - wtx.mapValue["comment"] = "Rain"; - std::set setAddress; - std::vector > vecSend; - std::string sRainCommand = ExtractXML(sRecipients,"",""); - std::string sRainMessage = MakeSafeMessage(ExtractXML(sRecipients,"","")); - std::string sRain = "Project Rain: " + sRainMessage + ""; - - if (!sRainCommand.empty()) - sRecipients = sRainCommand; - - wtx.hashBoinc = sRain; - int64_t totalAmount = 0; - double dTotalToSend = 0; - std::vector vRecipients = split(sRecipients.c_str(),""); - LogPrintf("Creating Rain transaction with %" PRId64 " recipients. ", vRecipients.size()); - - for (unsigned int i = 0; i < vRecipients.size(); i++) - { - std::string sRow = vRecipients[i]; - std::vector vReward = split(sRow.c_str(),""); - - if (vReward.size() > 1) - { - std::string sAddress = vReward[0]; - std::string sAmount = vReward[1]; - - if (sAddress.length() > 10 && sAmount.length() > 0) - { - double dAmount = RoundFromString(sAmount,4); - if (dAmount > 0) - { - CBitcoinAddress address(sAddress); - if (!address.IsValid()) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Gridcoin address: ")+sAddress); - - if (setAddress.count(address)) - throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ")+sAddress); - - setAddress.insert(address); - dTotalToSend += dAmount; - int64_t nAmount = AmountFromDouble(dAmount); - CScript scriptPubKey; - scriptPubKey.SetDestination(address.Get()); - totalAmount += nAmount; - vecSend.push_back(std::make_pair(scriptPubKey, nAmount)); - } - } - } - } - - EnsureWalletIsUnlocked(); - // Check funds - double dBalance = GetTotalBalance(); - - if (dTotalToSend > dBalance) - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Account has insufficient funds"); - // Send - CReserveKey keyChange(pwalletMain); - int64_t nFeeRequired = 0; - bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired); - LogPrintf("Transaction Created."); - - if (!fCreated) - { - if (totalAmount + nFeeRequired > pwalletMain->GetBalance()) - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); - throw JSONRPCError(RPC_WALLET_ERROR, "Transaction creation failed"); - } - - LogPrintf("Committing."); - // Rain the recipients - if (!pwalletMain->CommitTransaction(wtx, keyChange)) - { - LogPrintf("Commit failed."); - - throw JSONRPCError(RPC_WALLET_ERROR, "Transaction commit failed"); - } - std::string sNarr = "Rain successful: Sent " + wtx.GetHash().GetHex() + "."; - LogPrintf("Success %s",sNarr.c_str()); - return sNarr; -} diff --git a/src/contract/contract.h b/src/contract/contract.h index ea8fb7c0bb..ff788f5663 100644 --- a/src/contract/contract.h +++ b/src/contract/contract.h @@ -2,4 +2,3 @@ bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature); bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, std::string& sSignature, std::string& sError, bool bAdvertising=false); -std::string executeRain(std::string sRecipients); diff --git a/src/contract/rain.cpp b/src/contract/rain.cpp new file mode 100644 index 0000000000..307d230e68 --- /dev/null +++ b/src/contract/rain.cpp @@ -0,0 +1,101 @@ +#include "init.h" +#include "rain.h" +#include "rpcprotocol.h" +#include "rpcserver.h" + +double GetTotalBalance(); + +namespace { +int64_t AmountFromDouble(double dAmount) +{ + if (dAmount <= 0.0 || dAmount > MAX_MONEY) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount"); + int64_t nAmount = roundint64(dAmount * COIN); + if (!MoneyRange(nAmount)) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount"); + return nAmount; +} +} // anonymous namespace + +std::string executeRain(std::string sRecipients) +{ + CWalletTx wtx; + wtx.mapValue["comment"] = "Rain"; + std::set setAddress; + std::vector > vecSend; + std::string sRainCommand = ExtractXML(sRecipients,"",""); + std::string sRainMessage = MakeSafeMessage(ExtractXML(sRecipients,"","")); + std::string sRain = "Project Rain: " + sRainMessage + ""; + + if (!sRainCommand.empty()) + sRecipients = sRainCommand; + + wtx.hashBoinc = sRain; + int64_t totalAmount = 0; + double dTotalToSend = 0; + std::vector vRecipients = split(sRecipients.c_str(),""); + LogPrintf("Creating Rain transaction with %" PRId64 " recipients. ", vRecipients.size()); + + for (unsigned int i = 0; i < vRecipients.size(); i++) + { + std::string sRow = vRecipients[i]; + std::vector vReward = split(sRow.c_str(),""); + + if (vReward.size() > 1) + { + std::string sAddress = vReward[0]; + std::string sAmount = vReward[1]; + + if (sAddress.length() > 10 && sAmount.length() > 0) + { + double dAmount = RoundFromString(sAmount,4); + if (dAmount > 0) + { + CBitcoinAddress address(sAddress); + if (!address.IsValid()) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Gridcoin address: ")+sAddress); + + if (setAddress.count(address)) + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ")+sAddress); + + setAddress.insert(address); + dTotalToSend += dAmount; + int64_t nAmount = AmountFromDouble(dAmount); + CScript scriptPubKey; + scriptPubKey.SetDestination(address.Get()); + totalAmount += nAmount; + vecSend.push_back(std::make_pair(scriptPubKey, nAmount)); + } + } + } + } + + EnsureWalletIsUnlocked(); + // Check funds + double dBalance = GetTotalBalance(); + + if (dTotalToSend > dBalance) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Account has insufficient funds"); + // Send + CReserveKey keyChange(pwalletMain); + int64_t nFeeRequired = 0; + bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired); + LogPrintf("Transaction Created."); + + if (!fCreated) + { + if (totalAmount + nFeeRequired > pwalletMain->GetBalance()) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); + throw JSONRPCError(RPC_WALLET_ERROR, "Transaction creation failed"); + } + + LogPrintf("Committing."); + // Rain the recipients + if (!pwalletMain->CommitTransaction(wtx, keyChange)) + { + LogPrintf("Commit failed."); + + throw JSONRPCError(RPC_WALLET_ERROR, "Transaction commit failed"); + } + std::string sNarr = "Rain successful: Sent " + wtx.GetHash().GetHex() + "."; + LogPrintf("Success %s",sNarr.c_str()); + return sNarr; +} diff --git a/src/contract/rain.h b/src/contract/rain.h new file mode 100644 index 0000000000..6a61d6a6b3 --- /dev/null +++ b/src/contract/rain.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string executeRain(std::string sRecipients); diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index d861406d6c..87f83635f2 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -18,6 +18,7 @@ #include "backup.h" #include "appcache.h" #include "contract/message.h" +#include "contract/rain.h" #include "contract/contract.h" #include "util.h" From 602a49e9e36aa4c2e0084b3812d551c9e6e17633 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:39 -0500 Subject: [PATCH 06/15] Unravel contracts: separate CPID functions --- src/Makefile.am | 4 ++-- src/beacon.cpp | 1 - src/contract/contract.h | 4 ---- src/contract/{contract.cpp => cpid.cpp} | 23 +++++++++++++++-------- src/contract/cpid.h | 12 ++++++++++++ src/contract/polls.cpp | 3 ++- src/contract/rpccontract.cpp | 1 - src/main.cpp | 1 - src/miner.cpp | 1 - src/qt/bitcoingui.cpp | 1 - src/rpcblockchain.cpp | 2 +- src/test/beacon_tests.cpp | 2 +- 12 files changed, 33 insertions(+), 22 deletions(-) delete mode 100644 src/contract/contract.h rename src/contract/{contract.cpp => cpid.cpp} (79%) create mode 100644 src/contract/cpid.h diff --git a/src/Makefile.am b/src/Makefile.am index 6a16ddece4..00f2961be1 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -75,10 +75,10 @@ GRIDCOIN_CORE_H = \ compat.h \ compat/byteswap.h \ compat/endian.h \ + contract/cpid.h \ contract/message.h \ contract/polls.h \ contract/rain.h \ - contract/contract.h \ crypter.h \ db.h \ fs.h \ @@ -164,10 +164,10 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ block.cpp \ boinc.cpp \ checkpoints.cpp \ + contract/cpid.cpp \ contract/message.cpp \ contract/polls.cpp \ contract/rain.cpp \ - contract/contract.cpp \ crypter.cpp \ db.cpp \ fs.cpp \ diff --git a/src/beacon.cpp b/src/beacon.cpp index 6b5fb7ace8..4f14432905 100644 --- a/src/beacon.cpp +++ b/src/beacon.cpp @@ -6,7 +6,6 @@ #include "main.h" #include "init.h" #include "appcache.h" -#include "contract/contract.h" #include "key.h" #include "neuralnet/researcher.h" #include "neuralnet/quorum.h" diff --git a/src/contract/contract.h b/src/contract/contract.h deleted file mode 100644 index ff788f5663..0000000000 --- a/src/contract/contract.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature); -bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, std::string& sSignature, std::string& sError, bool bAdvertising=false); diff --git a/src/contract/contract.cpp b/src/contract/cpid.cpp similarity index 79% rename from src/contract/contract.cpp rename to src/contract/cpid.cpp index 85eb084b13..d9cd2473d5 100644 --- a/src/contract/contract.cpp +++ b/src/contract/cpid.cpp @@ -1,10 +1,7 @@ -#include "init.h" -#include "rpcclient.h" -#include "rpcserver.h" -#include "rpcprotocol.h" -#include "keystore.h" #include "beacon.h" - +#include "contract/cpid.h" +#include "contract/message.h" +#include "key.h" bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature) { @@ -17,7 +14,12 @@ bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string return bValid; } -bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, std::string& sSignature, std::string& sError, bool bAdvertising=false) +bool SignBlockWithCPID( + const std::string& sCPID, + const std::string& sBlockHash, + std::string& sSignature, + std::string& sError, + bool bAdvertising) { // Check if there is a beacon for this user // If not then return false as GetStoresBeaconPrivateKey grabs from the config @@ -26,15 +28,19 @@ bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, sError = "No active beacon"; return false; } + // Returns the Signature of the CPID+BlockHash message. CKey keyBeacon; - if(!GetStoredBeaconPrivateKey(sCPID, keyBeacon)) + + if (!GetStoredBeaconPrivateKey(sCPID, keyBeacon)) { sError = "No beacon key"; return false; } + std::string sMessage = sCPID + sBlockHash; sSignature = SignMessage(sMessage,keyBeacon); + // If we failed to sign then return false if (sSignature == "Unable to sign message, check private key.") { @@ -42,5 +48,6 @@ bool SignBlockWithCPID(const std::string& sCPID, const std::string& sBlockHash, sSignature = ""; return false; } + return true; } diff --git a/src/contract/cpid.h b/src/contract/cpid.h new file mode 100644 index 0000000000..dd2490978b --- /dev/null +++ b/src/contract/cpid.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature); + +bool SignBlockWithCPID( + const std::string& sCPID, + const std::string& sBlockHash, + std::string& sSignature, + std::string& sError, + bool bAdvertising = false); diff --git a/src/contract/polls.cpp b/src/contract/polls.cpp index 03c2daac94..42a818b035 100644 --- a/src/contract/polls.cpp +++ b/src/contract/polls.cpp @@ -3,7 +3,8 @@ #include "main.h" #include "polls.h" -#include "message.h" +#include "contract/cpid.h" +#include "contract/message.h" #include "rpcclient.h" #include "rpcserver.h" #include "appcache.h" diff --git a/src/contract/rpccontract.cpp b/src/contract/rpccontract.cpp index b261ef94ed..2dc7d62e17 100644 --- a/src/contract/rpccontract.cpp +++ b/src/contract/rpccontract.cpp @@ -2,7 +2,6 @@ #include "main.h" #include "rpcserver.h" #include "rpcclient.h" -#include "contract/contract.h" #include "contract/polls.h" #include diff --git a/src/main.cpp b/src/main.cpp index da0c72526e..527516319b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,6 @@ #include "neuralnet/tally.h" #include "backup.h" #include "appcache.h" -#include "contract/contract.h" #include "contract/message.h" #include "scraper_net.h" #include "gridcoin.h" diff --git a/src/miner.cpp b/src/miner.cpp index 2302c83c00..06a394a015 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -12,7 +12,6 @@ #include "neuralnet/quorum.h" #include "neuralnet/researcher.h" #include "neuralnet/tally.h" -#include "contract/contract.h" #include "util.h" #include diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index ed60249d3b..b7275d8ab6 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -83,7 +83,6 @@ #include "rpcclient.h" #include "rpcprotocol.h" #include "contract/polls.h" -#include "contract/contract.h" #include "neuralnet/researcher.h" #include "beacon.h" diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 87f83635f2..3c761c1e44 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -17,9 +17,9 @@ #include "neuralnet/tally.h" #include "backup.h" #include "appcache.h" +#include "contract/cpid.h" #include "contract/message.h" #include "contract/rain.h" -#include "contract/contract.h" #include "util.h" #include diff --git a/src/test/beacon_tests.cpp b/src/test/beacon_tests.cpp index 2c11c8dbb1..a94bf4ef8e 100644 --- a/src/test/beacon_tests.cpp +++ b/src/test/beacon_tests.cpp @@ -3,7 +3,7 @@ #include "main.h" #include "beacon.h" #include "appcache.h" -#include "contract/contract.h" +#include "contract/cpid.h" #include "contract/message.h" #include From 1dd8eecbd5e636f91bd29b1bb6735fcabcdd138b Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:40 -0500 Subject: [PATCH 07/15] Overload VerifyCPIDSignature for IsCPIDValidV2, use const references --- src/contract/cpid.cpp | 20 ++++++++++++++++++-- src/contract/cpid.h | 11 ++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/contract/cpid.cpp b/src/contract/cpid.cpp index d9cd2473d5..d82373a056 100644 --- a/src/contract/cpid.cpp +++ b/src/contract/cpid.cpp @@ -3,17 +3,33 @@ #include "contract/message.h" #include "key.h" -bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature) +bool VerifyCPIDSignature( + const std::string& sCPID, + const std::string& sBlockHash, + const std::string& sSignature, + const std::string& sBeaconPublicKey) { - std::string sBeaconPublicKey = GetBeaconPublicKey(sCPID, false); std::string sConcatMessage = sCPID + sBlockHash; bool bValid = CheckMessageSignature("R","cpid", sConcatMessage, sSignature, sBeaconPublicKey); + if(!bValid) LogPrintf("VerifyCPIDSignature: invalid signature sSignature=%s, cached key=%s" ,sSignature, sBeaconPublicKey); return bValid; } +bool VerifyCPIDSignature( + const std::string& sCPID, + const std::string& sBlockHash, + const std::string& sSignature) +{ + return VerifyCPIDSignature( + sCPID, + sBlockHash, + sSignature, + GetBeaconPublicKey(sCPID, false)); +} + bool SignBlockWithCPID( const std::string& sCPID, const std::string& sBlockHash, diff --git a/src/contract/cpid.h b/src/contract/cpid.h index dd2490978b..e8f7a50539 100644 --- a/src/contract/cpid.h +++ b/src/contract/cpid.h @@ -2,7 +2,16 @@ #include -bool VerifyCPIDSignature(std::string sCPID, std::string sBlockHash, std::string sSignature); +bool VerifyCPIDSignature( + const std::string& sCPID, + const std::string& sBlockHash, + const std::string& sSignature, + const std::string& sBeaconPublicKey); + +bool VerifyCPIDSignature( + const std::string& sCPID, + const std::string& sBlockHash, + const std::string& sSignature); bool SignBlockWithCPID( const std::string& sCPID, From d858068c376588bb1648980e20f70eb5503dd7a9 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:41 -0500 Subject: [PATCH 08/15] Refactor application to use contract class and contract handler API --- src/contract/cpid.cpp | 45 +++++--- src/contract/message.cpp | 105 +++-------------- src/contract/message.h | 25 +---- src/contract/polls.cpp | 56 +++++++--- src/init.cpp | 7 +- src/main.cpp | 229 ++++++++------------------------------ src/main.h | 4 - src/rpcblockchain.cpp | 156 ++++++++++++++++---------- src/scraper/scraper.cpp | 1 - src/test/beacon_tests.cpp | 8 -- 10 files changed, 236 insertions(+), 400 deletions(-) diff --git a/src/contract/cpid.cpp b/src/contract/cpid.cpp index d82373a056..54e1df163c 100644 --- a/src/contract/cpid.cpp +++ b/src/contract/cpid.cpp @@ -1,6 +1,5 @@ #include "beacon.h" #include "contract/cpid.h" -#include "contract/message.h" #include "key.h" bool VerifyCPIDSignature( @@ -9,13 +8,21 @@ bool VerifyCPIDSignature( const std::string& sSignature, const std::string& sBeaconPublicKey) { - std::string sConcatMessage = sCPID + sBlockHash; - bool bValid = CheckMessageSignature("R","cpid", sConcatMessage, sSignature, sBeaconPublicKey); + CKey key; - if(!bValid) - LogPrintf("VerifyCPIDSignature: invalid signature sSignature=%s, cached key=%s" - ,sSignature, sBeaconPublicKey); - return bValid; + bool valid = key.SetPubKey(ParseHex(sBeaconPublicKey)) + && key.Verify( + Hash(sCPID.begin(), sCPID.end(), sBlockHash.begin(), sBlockHash.end()), + DecodeBase64(sSignature.c_str())); + + if (!valid) { + LogPrintf( + "VerifyCPIDSignature: invalid signature sSignature=%s, cached key=%s", + sSignature, + sBeaconPublicKey); + } + + return valid; } bool VerifyCPIDSignature( @@ -39,31 +46,33 @@ bool SignBlockWithCPID( { // Check if there is a beacon for this user // If not then return false as GetStoresBeaconPrivateKey grabs from the config - if (!HasActiveBeacon(sCPID) && !bAdvertising) - { + if (!HasActiveBeacon(sCPID) && !bAdvertising) { sError = "No active beacon"; return false; } - // Returns the Signature of the CPID+BlockHash message. CKey keyBeacon; - if (!GetStoredBeaconPrivateKey(sCPID, keyBeacon)) - { + if (!GetStoredBeaconPrivateKey(sCPID, keyBeacon)) { sError = "No beacon key"; return false; } - std::string sMessage = sCPID + sBlockHash; - sSignature = SignMessage(sMessage,keyBeacon); + std::vector signature_bytes; - // If we failed to sign then return false - if (sSignature == "Unable to sign message, check private key.") - { - sError = sSignature; + // Returns the Signature of the CPID+BlockHash message. + bool result = keyBeacon.Sign( + Hash(sCPID.begin(), sCPID.end(), sBlockHash.begin(), sBlockHash.end()), + signature_bytes); + + if (!result) { + sError = "Unable to sign message, check private key."; sSignature = ""; + return false; } + sSignature = EncodeBase64(signature_bytes.data(), signature_bytes.size()); + return true; } diff --git a/src/contract/message.cpp b/src/contract/message.cpp index 4a64cf36e1..ce7459e41c 100644 --- a/src/contract/message.cpp +++ b/src/contract/message.cpp @@ -1,101 +1,32 @@ -#include "init.h" +#include "base58.h" #include "message.h" -#include "rpcclient.h" +#include "neuralnet/contract.h" #include "rpcserver.h" -#include "rpcprotocol.h" -#include "keystore.h" +#include "wallet.h" +extern CWallet* pwalletMain; -std::string GetBurnAddress() { return fTestNet ? "mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ" : "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; - } - -bool CheckMessageSignature( - std::string sAction, - std::string messagetype, - std::string sMsg, - std::string sSig, - std::string strMessagePublicKey) +std::pair SendContract(NN::Contract contract, int64_t min_balance, double fee) { - std::string strMasterPubKey = ""; - if (messagetype=="project" || messagetype=="projectmapping") - { - strMasterPubKey= msMasterProjectPublicKey; - } - else - { - strMasterPubKey = msMasterMessagePublicKey; - } + CWalletTx wtx; + wtx.hashBoinc = contract.ToString(); - if (!strMessagePublicKey.empty()) strMasterPubKey = strMessagePublicKey; - if (sAction=="D" && messagetype=="beacon") strMasterPubKey = msMasterProjectPublicKey; - if (sAction=="D" && messagetype=="poll") strMasterPubKey = msMasterProjectPublicKey; - if (sAction=="D" && messagetype=="vote") strMasterPubKey = msMasterProjectPublicKey; - if (messagetype == "protocol" || messagetype == "scraper") strMasterPubKey = msMasterProjectPublicKey; + std::string error = pwalletMain->SendMoneyToDestinationWithMinimumBalance( + CBitcoinAddress(NN::Contract::BurnAddress()).Get(), + AmountFromValue(fee), + AmountFromValue(min_balance), + wtx); - std::string db64 = DecodeBase64(sSig); - CKey key; - if (!key.SetPubKey(ParseHex(strMasterPubKey))) return false; - std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); - std::vector vchSig = std::vector(db64.begin(), db64.end()); - if (!key.Verify(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) return false; - return true; + return std::make_pair(wtx, error); } -std::string SignMessage(const std::string& sMsg, CKey& key) +std::string SendPublicContract(NN::Contract contract) { - std::vector vchMsg = std::vector(sMsg.begin(), sMsg.end()); - std::vector vchSig; - if (!key.Sign(Hash(vchMsg.begin(), vchMsg.end()), vchSig)) - { - return "Unable to sign message, check private key."; + if (!contract.SignWithMessageKey()) { + return "Failed to sign contract with shared message key."; } - const std::string sig(vchSig.begin(), vchSig.end()); - std::string SignedMessage = EncodeBase64(sig); - return SignedMessage; -} - -std::string SignMessage(std::string sMsg, std::string sPrivateKey) -{ - CKey key; - std::vector vchPrivKey = ParseHex(sPrivateKey); - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - return SignMessage(sMsg, key); -} -std::string SendMessage( - bool bAdd, - std::string sType, - std::string sPrimaryKey, - std::string sValue, - std::string sMasterKey, - int64_t MinimumBalance, - double dFees, - std::string strPublicKey) -{ - std::string sAddress = GetBurnAddress(); - CBitcoinAddress address(sAddress); - if (!address.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Gridcoin address"); - int64_t nAmount = AmountFromValue(dFees); - // Wallet comments - CWalletTx wtx; - if (pwalletMain->IsLocked()) throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); - std::string sMessageType = "" + sType + ""; //Project or Smart Contract - std::string sMessageKey = "" + sPrimaryKey + ""; - std::string sMessageValue = "" + sValue + ""; - std::string sMessagePublicKey = ""+ strPublicKey + ""; - std::string sMessageAction = bAdd ? "A" : "D"; //Add or Delete - //Sign Message - std::string sSig = SignMessage(sType+sPrimaryKey+sValue,sMasterKey); - std::string sMessageSignature = "" + sSig + ""; - wtx.hashBoinc = sMessageType+sMessageKey+sMessageValue+sMessageAction+sMessagePublicKey+sMessageSignature; - std::string strError = pwalletMain->SendMoneyToDestinationWithMinimumBalance(address.Get(), nAmount, MinimumBalance, wtx); - if (!strError.empty()) throw JSONRPCError(RPC_WALLET_ERROR, strError); - return wtx.GetHash().GetHex().c_str(); -} + std::pair result = SendContract(contract, 0.00001, 1); -std::string SendContract(std::string sType, std::string sName, std::string sContract) -{ - std::string sPass = (sType=="project" || sType=="projectmapping" || sType=="smart_contract") ? GetArgument("masterprojectkey", msMasterMessagePrivateKey) : msMasterMessagePrivateKey; - std::string result = SendMessage(true,sType,sName,sContract,sPass,AmountFromValue(1),.00001,""); - return result; + return std::get<1>(result); } diff --git a/src/contract/message.h b/src/contract/message.h index 5c9c313469..a5351df424 100644 --- a/src/contract/message.h +++ b/src/contract/message.h @@ -2,27 +2,10 @@ #include -bool CheckMessageSignature( - std::string sAction, - std::string messagetype, - std::string sMsg, - std::string sSig, - std::string opt_pubkey); +class CWalletTx; -std::string SendContract(std::string sType, std::string sName, std::string sContract); +namespace NN { class Contract; } -std::string SendMessage( - bool bAdd, - std::string sType, - std::string sPrimaryKey, - std::string sValue, - std::string sMasterKey, - int64_t MinimumBalance, - double dFees, - std::string strPublicKey); +std::pair SendContract(NN::Contract contract); -std::string GetBurnAddress(); - -std::string SignMessage(std::string sMsg, std::string sPrivateKey); - -std::string SignMessage(const std::string& sMsg, CKey& key); +std::string SendPublicContract(NN::Contract contract); diff --git a/src/contract/polls.cpp b/src/contract/polls.cpp index 42a818b035..d32b4eb3dd 100644 --- a/src/contract/polls.cpp +++ b/src/contract/polls.cpp @@ -10,6 +10,7 @@ #include "appcache.h" #include "init.h" // for pwalletMain #include "block.h" +#include "neuralnet/contract.h" #include "neuralnet/quorum.h" #include "neuralnet/tally.h" @@ -59,10 +60,25 @@ std::pair CreatePollContract(std::string sTitle, int d return std::make_pair("Error", "You must specify a value of 1, 2, 3, 4 or 5 for the sSharetype."); else { - std::string expiration = RoundToString(GetAdjustedTime() + (days*86400), 0); - std::string contract = "" + sTitle + "" + std::to_string(days) + "" + sQuestion + "" + sAnswers + "" + std::to_string(iSharetype) + "" + sURL + "" + expiration + ""; - std::string result = SendContract("poll", sTitle, contract); - return std::make_pair("Success",result); + NN::Contract contract( + NN::ContractType::POLL, + NN::ContractAction::ADD, + sTitle, + "" + sTitle + "" + + "" + std::to_string(days) + "" + + "" + sQuestion + "" + + "" + sAnswers + "" + + "" + std::to_string(iSharetype) + "" + + "" + sURL + "" + + "" + RoundToString(GetAdjustedTime() + (days * 86400), 0) + ""); + + std::string error = SendPublicContract(contract); + + if (!error.empty()) { + return std::make_pair("Error", error); + } + + return std::make_pair("Success", ""); } } } @@ -124,17 +140,29 @@ std::pair CreateVoteContract(std::string sTitle, std:: return std::make_pair("Error", "Sorry, When voting in a Both Share Type poll, your stake age Or your CPID age must be older than the poll duration."); else { - std::string voter = "" - + primary_cpid + "" + GRCAddress + "" - + hashRand.GetHex() + "" + RoundToString(nBalance,2) - + "" + RoundToString(dmag,0) + ""; - // Add the provable balance and the provable magnitude - this goes into effect July 1 2017 - voter += GetProvableVotingWeightXML(); - std::string pk = sTitle + ";" + GRCAddress + ";" + primary_cpid; - std::string contract = "" + sTitle + "" + sAnswer + "" + voter; - std::string result = SendContract("vote",pk,contract); + NN::Contract contract( + NN::ContractType::VOTE, + NN::ContractAction::ADD, + sTitle + ";" + GRCAddress + ";" + primary_cpid, + "" + sTitle + "" + + "" + sAnswer + "" + + "" + primary_cpid + "" + + "" + GRCAddress + "" + + "" + hashRand.GetHex() + "" + + "" + RoundToString(nBalance,2) + "" + + "" + RoundToString(dmag,0) + "" + // Add the provable balance and the provable magnitude - this + // goes into effect July 1 2017: + + GetProvableVotingWeightXML()); + + std::string error = SendPublicContract(contract); + + if (!error.empty()) { + return std::make_pair("Error", error); + } + std::string narr = "Your CPID weight is " + RoundToString(dmag,0) + " and your Balance weight is " + RoundToString(nBalance,0) + "."; - return std::make_pair("Success", narr + " " + "Your vote has been cast for topic " + sTitle + ": With an Answer of " + sAnswer + ": " + result.c_str()); + return std::make_pair("Success", narr + " " + "Your vote has been cast for topic " + sTitle + ": With an Answer of " + sAnswer); } } diff --git a/src/init.cpp b/src/init.cpp index 10b7dcfd9c..ba87c63876 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -31,7 +31,7 @@ #include "global_objects_noui.hpp" -bool LoadAdminMessages(bool bFullTableScan,std::string& out_errors); +bool LoadAdminMessages(bool bFullTableScan); static boost::thread_group threadGroup; static CScheduler scheduler; @@ -1122,10 +1122,9 @@ bool AppInit2(ThreadHandlerPtr threads) // ********************************************************* Step 11: start node uiInterface.InitMessage(_("Loading Persisted Data Cache...")); - // - std::string sOut = ""; + LogPrint(BCLog::LogFlags::NET, "Loading admin Messages"); - LoadAdminMessages(true,sOut); + LoadAdminMessages(true); LogPrintf("Done loading Admin messages"); uiInterface.InitMessage(_("Finding first applicable Research Project...")); diff --git a/src/main.cpp b/src/main.cpp index 527516319b..52ea04d534 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,8 +41,7 @@ extern bool GridcoinServices(); extern bool IsContract(CBlockIndex* pIndex); extern bool BlockNeedsChecked(int64_t BlockTime); int64_t GetEarliestWalletTransaction(); -bool MemorizeMessage(const CTransaction &tx, double dAmount, std::string sRecipient); -extern bool LoadAdminMessages(bool bFullTableScan,std::string& out_errors); +extern bool LoadAdminMessages(bool bFullTableScan); extern bool GetEarliestStakeTime(std::string grcaddress, std::string cpid); extern double GetTotalBalance(); extern std::string PubKeyToAddress(const CScript& scriptPubKey); @@ -72,10 +71,6 @@ int64_t nLastCleaned = 0; extern double CoinToDouble(double surrogate); ///////////////////////MINOR VERSION//////////////////////////////// -std::string msMasterProjectPublicKey = "049ac003b3318d9fe28b2830f6a95a2624ce2a69fb0c0c7ac0b513efcc1e93a6a6e8eba84481155dd82f2f1104e0ff62c69d662b0094639b7106abc5d84f948c0a"; -// The Private Key is revealed by design, for public messages only: -std::string msMasterMessagePrivateKey = "308201130201010420fbd45ffb02ff05a3322c0d77e1e7aea264866c24e81e5ab6a8e150666b4dc6d8a081a53081a2020101302c06072a8648ce3d0101022100fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141020101a144034200044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a252467f692ec9c5ba7e5eaa898ab99cbd9949496f7e3cafbf56304b1cc2e5bdf06e"; -std::string msMasterMessagePublicKey = "044b2938fbc38071f24bede21e838a0758a52a0085f2e034e7f971df445436a252467f692ec9c5ba7e5eaa898ab99cbd9949496f7e3cafbf56304b1cc2e5bdf06e"; extern int64_t GetCoinYearReward(int64_t nTime); @@ -175,35 +170,6 @@ std::map mvTimers; // Contains event timers that reset after m // End of Gridcoin Global vars -// TODO: replace these with upcoming contract handler implementation: -namespace { -bool AddContract( - const std::string& type, - const std::string& key, - const std::string& value, - const int64_t& timestamp) -{ - if (type == "project" || type == "projectmapping") { - NN::GetWhitelist().Add(key, value, timestamp); - } else { - return false; - } - - return true; -} - -bool DeleteContract(const std::string& type, const std::string& key) -{ - if (type == "project" || type == "projectmapping") { - NN::GetWhitelist().Delete(key); - } else { - return false; - } - - return true; -} -} // anonymous namespace - ////////////////////////////////////////////////////////////////////////////// // // dispatching functions @@ -2164,7 +2130,6 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, MapPrevTx inputs, map= 0; i--) @@ -2174,35 +2139,20 @@ bool CBlock::DisconnectBlock(CTxDB& txdb, CBlockIndex* pindex) bDiscTxFailed = true; } - /* Delete the contract. - * Previous version will be reloaded in reoranize. */ - { - std::string sMType = ExtractXML(vtx[i].hashBoinc, "", ""); - if(!sMType.empty()) - { - std::string sMKey = ExtractXML(vtx[i].hashBoinc, "", ""); + if (!NN::Contract::Detect(vtx[i].hashBoinc)) { + continue; + } - try - { - if (!DeleteContract(sMType, sMKey)) { - DeleteCache(StringToSection(sMType), sMKey); - } - if(fDebug) - LogPrintf("DisconnectBlock: Delete contract %s %s", sMType, sMKey); - } - catch(const std::runtime_error& e) - { - error("Attempting to delete from unknown cache: %s", sMType); - } + // Delete the contract. Previous version will be reloaded in reorganize. + NN::Contract contract = NN::Contract::Parse(vtx[i].hashBoinc, vtx[i].nTime); - if("beacon"==sMType) - { - sMKey=sMKey+"A"; - DeleteCache(Section::BEACONALT, sMKey+"."+ToString(vtx[i].nTime)); - } + if (contract.VerifySignature()) { + if(fDebug) { + LogPrintf("DisconnectBlock: Delete contract %s %s", contract.m_type.ToString(), contract.m_key); } - } + NN::RevertContract(std::move(contract)); + } } // Update block index on disk without changing it in memory. @@ -2955,8 +2905,8 @@ bool DisconnectBlocksBatch(CTxDB& txdb, list& vResurrect, unsigned // Need to reload all contracts if (fDebug10) LogPrintf("DisconnectBlocksBatch: LoadAdminMessages"); - std::string admin_messages; - LoadAdminMessages(true, admin_messages); + LoadAdminMessages(true); + NN::Quorum::LoadSuperblockIndex(pindexBest); // Tally research averages. @@ -5593,118 +5543,42 @@ bool SendMessages(CNode* pto, bool fSendTrickle) return true; } -bool MemorizeMessage(const CTransaction &tx, double dAmount, std::string sRecipient) +bool MemorizeMessage(const CTransaction &tx) { - const std::string &msg = tx.hashBoinc; - const int64_t &nTime = tx.nTime; - if (msg.empty()) return false; - bool fMessageLoaded = false; - - if (Contains(msg,"")) - { - std::string sMessageType = ExtractXML(msg,"",""); - std::string sMessageKey = ExtractXML(msg,"",""); - std::string sMessageValue = ExtractXML(msg,"",""); - std::string sMessageAction = ExtractXML(msg,"",""); - std::string sSignature = ExtractXML(msg,"",""); - std::string sMessagePublicKey = ExtractXML(msg,"",""); - if (sMessageType=="beacon" && Contains(sMessageValue,"INVESTOR")) - { - sMessageValue=""; - } - - if (sMessageType=="superblock") - { - // Deny access to superblock processing runtime data - sMessageValue=""; - } - - if (!sMessageType.empty() && !sMessageKey.empty() && !sMessageValue.empty() && !sMessageAction.empty() && !sSignature.empty()) - { - //Verify sig first - bool Verified = CheckMessageSignature(sMessageAction,sMessageType,sMessageType+sMessageKey+sMessageValue, - sSignature,sMessagePublicKey); - - if (Verified) - { - if (sMessageAction=="A") - { - /* With this we allow verifying blocks with stupid beacon */ - if("beacon"==sMessageType) - { - std::string out_cpid = ""; - std::string out_address = ""; - std::string out_publickey = ""; - GetBeaconElements(sMessageValue, out_cpid, out_address, out_publickey); - WriteCache(Section::BEACONALT, sMessageKey+"."+ToString(nTime),out_publickey,nTime); - } - - try - { - if (!AddContract(sMessageType, sMessageKey, sMessageValue, nTime)) { - WriteCache(StringToSection(sMessageType), sMessageKey,sMessageValue,nTime); - - if(fDebug10 && sMessageType=="beacon" ) - LogPrintf("BEACON add %s %s %s", sMessageKey, DecodeBase64(sMessageValue), TimestampToHRDate(nTime)); - } - } - catch(const std::runtime_error& e) - { - error("Attempting to add to unknown cache: %s", sMessageType); - } + if (!NN::Contract::Detect(tx.hashBoinc)) { + return false; + } - fMessageLoaded = true; - if (sMessageType=="poll") - { - msPoll = msg; - } - } - else if(sMessageAction=="D") - { - if (fDebug10) LogPrintf("Deleting key type %s Key %s Value %s", sMessageType, sMessageKey, sMessageValue); - if(fDebug10 && sMessageType=="beacon" ){ - LogPrintf("BEACON DEL %s - %s", sMessageKey, TimestampToHRDate(nTime)); - } + NN::Contract contract = NN::Contract::Parse(tx.hashBoinc, tx.nTime); - try - { - if (!DeleteContract(sMessageType, sMessageKey)) { - DeleteCache(StringToSection(sMessageType), sMessageKey); - } - fMessageLoaded = true; - } - catch(const std::runtime_error& e) - { - error("Attempting to add to unknown cache: %s", sMessageType); - } - } - // If this is a boinc project, load the projects into the coin: - if (sMessageType=="project" || sMessageType=="projectmapping") - { - //Reserved - fMessageLoaded = true; - } + if (!contract.VerifySignature()) { + return false; + } - // Support dynamic team requirement or whitelist configuration: - // - // TODO: move this into the appropriate contract handler. - // - if (sMessageType == "protocol" - && (sMessageKey == "REQUIRE_TEAM_WHITELIST_MEMBERSHIP" - || sMessageKey == "TEAM_WHITELIST")) - { - // Rescan in-memory project CPIDs to resolve a primary CPID - // that fits the now active team requirement settings: - NN::Researcher::Refresh(); - } + // Support dynamic team requirement or whitelist configuration: + // + // TODO: move this into the appropriate contract handler. + // + if (contract.m_type == NN::ContractType::PROTOCOL + && (contract.m_key == "REQUIRE_TEAM_WHITELIST_MEMBERSHIP" + || contract.m_key == "TEAM_WHITELIST")) + { + // Rescan in-memory project CPIDs to resolve a primary CPID + // that fits the now active team requirement settings: + NN::Researcher::Refresh(); + } - if(fDebug) - WriteCache(Section::TRXID, sMessageType + ";" + sMessageKey,tx.GetHash().GetHex(),nTime); - } - } + if (fDebug) { + WriteCache( + Section::TRXID, + contract.m_type.ToString() + ";" + contract.m_key, + tx.GetHash().GetHex(), + tx.nTime); } - return fMessageLoaded; + NN::ProcessContract(contract); + + return true; } const CBlockIndex* GetHistoricalMagnitude(const NN::MiningId mining_id) @@ -5745,7 +5619,7 @@ const CBlockIndex* GetHistoricalMagnitude(const NN::MiningId mining_id) return pindexGenesisBlock; } -bool LoadAdminMessages(bool bFullTableScan, std::string& out_errors) +bool LoadAdminMessages(bool bFullTableScan) { // Find starting block. On full table scan we want to scan 6 months back. // On a shallow scan we can limit to 6 blocks back. @@ -5768,19 +5642,10 @@ bool LoadAdminMessages(bool bFullTableScan, std::string& out_errors) int iPos = 0; for (auto const &tx : block.vtx) { - if (iPos > 0) - { - // Retrieve the Burn Amount for Contracts - double dAmount = 0; - std::string sRecipient = ""; - for (unsigned int i = 1; i < tx.vout.size(); i++) - { - sRecipient = PubKeyToAddress(tx.vout[i].scriptPubKey); - dAmount += CoinToDouble(tx.vout[i].nValue); - } - MemorizeMessage(tx,dAmount,sRecipient); - } - iPos++; + if (iPos > 0) { + MemorizeMessage(tx); + } + iPos++; } } } diff --git a/src/main.h b/src/main.h index ba59dc186a..3ec1cb4a3a 100644 --- a/src/main.h +++ b/src/main.h @@ -35,10 +35,6 @@ static const int BLOCK_GRANULARITY = 10; //Consensus block divisor static const int TALLY_GRANULARITY = BLOCK_GRANULARITY; static const int64_t DEFAULT_CBR = 10 * COIN; -extern std::string msMasterProjectPublicKey; -extern std::string msMasterMessagePublicKey; -extern std::string msMasterMessagePrivateKey; - /** The maximum allowed size for a serialized block, in bytes (network rule) */ static const unsigned int MAX_BLOCK_SIZE = 1000000; /** Target Blocks Per day */ diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 3c761c1e44..41aba41eb4 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -11,6 +11,7 @@ #include "checkpoints.h" #include "txdb.h" #include "beacon.h" +#include "neuralnet/contract.h" #include "neuralnet/project.h" #include "neuralnet/quorum.h" #include "neuralnet/researcher.h" @@ -35,7 +36,7 @@ bool ForceReorganizeToHash(uint256 NewHash); extern UniValue MagnitudeReport(const NN::Cpid cpid); extern std::string ExtractValue(std::string data, std::string delimiter, int pos); extern UniValue SuperblockReport(int lookback = 14, bool displaycontract = false, std::string cpid = ""); -bool LoadAdminMessages(bool bFullTableScan,std::string& out_errors); +bool LoadAdminMessages(bool bFullTableScan); extern bool AdvertiseBeacon(std::string &sOutPrivKey, std::string &sOutPubKey, std::string &sError, std::string &sMessage); extern bool ScraperSynchronizeDPOR(); std::string ExplainMagnitude(std::string sCPID); @@ -445,14 +446,16 @@ bool AdvertiseBeacon(std::string &sOutPrivKey, std::string &sOutPubKey, std::str // Convert the new pubkey into legacy hex format sOutPubKey = HexStr(keyBeacon.GetPubKey().Raw()); - std::string GRCAddress = DefaultWalletAddress(); - // Public Signing Key is stored in Beacon - std::string contract = "UNUSED;" + hashRand.GetHex() + ";" + GRCAddress + ";" + sOutPubKey; - LogPrintf("Creating beacon for cpid %s, %s",primary_cpid, contract); - std::string sBase = EncodeBase64(contract); - std::string sAction = "add"; - std::string sType = "beacon"; - std::string sName = primary_cpid; + std::string value = "UNUSED;" + hashRand.GetHex() + ";" + DefaultWalletAddress() + ";" + sOutPubKey; + + LogPrintf("Creating beacon for cpid %s, %s", primary_cpid, value); + + NN::Contract contract( + NN::ContractType::BEACON, + NN::ContractAction::ADD, + primary_cpid, + EncodeBase64(std::move(value))); + try { // Backup config with old keys like a normal backup @@ -469,7 +472,7 @@ bool AdvertiseBeacon(std::string &sOutPrivKey, std::string &sOutPubKey, std::str } // Send the beacon transaction - sMessage = SendContract(sType,sName,sBase); + sMessage = SendPublicContract(contract); // This prevents repeated beacons nLastBeaconAdvertised = nBestHeight; @@ -1104,6 +1107,25 @@ UniValue superblocks(const UniValue& params, bool fHelp) return res; } +//! +//! \brief Send a transaction that contains an administrative contract. +//! +//! To whitelist a project: +//! +//! addkey add project projectname url +//! +//! To de-whitelist a project: +//! +//! addkey delete project projectname 1 +//! +//! Key examples: +//! +//! addkey add project milkyway@home http://milkyway.cs.rpi.edu/milkyway/@ +//! addkey delete project milkyway@home 1 +//! +//! GRC will only memorize the *last* value it finds for a key in the highest +//! block. +//! UniValue addkey(const UniValue& params, bool fHelp) { if (fHelp || params.size() != 4) @@ -1117,51 +1139,67 @@ UniValue addkey(const UniValue& params, bool fHelp) "\n" "Add a key to the network\n"); + if (pwalletMain->IsLocked()) { + throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); + } + + CKey key; + + if (!key.SetPrivKey(NN::Contract::MasterPrivateKey()) + || key.GetPubKey() != NN::Contract::MasterPublicKey() + ) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid master key."); + } + + NN::ContractAction action = NN::ContractAction::UNKNOWN; + + if (params[0].get_str() == "add") { + action = NN::ContractAction::ADD; + } else if (params[0].get_str() == "delete") { + action = NN::ContractAction::REMOVE; + } + + NN::Contract contract( + NN::Contract::Type::Parse(params[1].get_str()), + action, + params[2].get_str(), // key + params[3].get_str()); // value + + if (contract.m_type == NN::ContractType::UNKNOWN) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Unknown contract type."); + } + + if (contract.m_action == NN::ContractAction::UNKNOWN) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Action must be 'add' or 'delete'."); + } + + if (!contract.RequiresMasterKey()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Not an admin contract type."); + } + + if (!contract.Sign(key)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to sign."); + } + + if (!contract.VerifySignature()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to verify signature."); + } + + std::pair result = SendContract(contract, 5, 0.1); + std::string error = result.second; + + if (!error.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, error); + } + UniValue res(UniValue::VOBJ); - //To whitelist a project: - //execute addkey add project projectname 1 - //To blacklist a project: - //execute addkey delete project projectname 1 - //Key examples: - - //execute addkey add project milky 1 - //execute addkey delete project milky 1 - //execute addkey add project milky 2 - //execute addkey add price grc .0000046 - //execute addkey add price grc .0000045 - //execute addkey delete price grc .0000045 - //GRC will only memorize the *last* value it finds for a key in the highest block - //execute memorizekeys - //execute listdata project - //execute listdata price - //execute addkey add project grid20 - - std::string sAction = params[0].get_str(); - bool bAdd = (sAction == "add") ? true : false; - std::string sType = params[1].get_str(); - std::string sName = params[2].get_str(); - std::string sValue = params[3].get_str(); - - bool bProjectKey = (sType == "project" || sType == "projectmapping" - || (sType == "beacon" && sAction == "delete") - || sType == "protocol" - || sType == "scraper" - ); - - const std::string sPass = bProjectKey - ? GetArgument("masterprojectkey", msMasterMessagePrivateKey) - : msMasterMessagePrivateKey; - - res.pushKV("Action", sAction); - res.pushKV("Type", sType); - res.pushKV("Passphrase", sPass); - res.pushKV("Name", sName); - res.pushKV("Value", sValue); - - std::string result = SendMessage(bAdd, sType, sName, sValue, sPass, AmountFromValue(5), .1, ""); - - res.pushKV("Results", result); + res.pushKV("Action", contract.m_action.ToString()); + res.pushKV("Type", contract.m_type.ToString()); + res.pushKV("Passphrase", contract.m_public_key.ToString()); + res.pushKV("Name", contract.m_key); + res.pushKV("Value", contract.m_value); + res.pushKV("Results", result.first.GetHash().GetHex().c_str()); return res; } @@ -1341,13 +1379,11 @@ UniValue memorizekeys(const UniValue& params, bool fHelp) UniValue res(UniValue::VOBJ); - std::string sOut; - LOCK(cs_main); - LoadAdminMessages(true, sOut); + LoadAdminMessages(true); - res.pushKV("Results", sOut); + res.pushKV("Results", "done"); return res; } @@ -1559,9 +1595,7 @@ UniValue sendrawcontract(const UniValue& params, bool fHelp) if (pwalletMain->IsLocked()) throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); - std::string sAddress = GetBurnAddress(); - - CBitcoinAddress address(sAddress); + CBitcoinAddress address(NN::Contract::BurnAddress()); if (!address.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Gridcoin address"); @@ -1578,7 +1612,7 @@ UniValue sendrawcontract(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_WALLET_ERROR, strError); res.pushKV("Contract", sContract); - res.pushKV("Recipient", sAddress); + res.pushKV("Recipient", address.ToString()); res.pushKV("TrxID", wtx.GetHash().GetHex()); return res; diff --git a/src/scraper/scraper.cpp b/src/scraper/scraper.cpp index c44bd37742..d7ebe13bb1 100755 --- a/src/scraper/scraper.cpp +++ b/src/scraper/scraper.cpp @@ -32,7 +32,6 @@ fs::path pathScraper = {}; extern bool fShutdown; extern bool fDebug; -extern std::string msMasterMessagePrivateKey; extern CWallet* pwalletMain; bool fScraperActive = false; std::vector> vuserpass; diff --git a/src/test/beacon_tests.cpp b/src/test/beacon_tests.cpp index a94bf4ef8e..022197fa44 100644 --- a/src/test/beacon_tests.cpp +++ b/src/test/beacon_tests.cpp @@ -210,14 +210,6 @@ struct GridcoinBeaconSigningFixture } }; -BOOST_AUTO_TEST_CASE(SignMessage1_smoke) -{ - const std::string sKey ("308201130201010420bf98171763bb82f36e054e864077fd7aab8bad6bd9d35a03a5850ce974308628a081a53081a2020101302c06072a8648ce3d0101022100fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141020101a14403420004ec1804bda713f161c02181ac05d84d4bb3101a10b5381ee463db4f729132128ef8eba71a00c6e20401122fe321a7cdb69b682ac8b04d36cf4cb8dba65e407aec"); - std::string signature= SignMessage("Spooky Sample Text.",sKey); - BOOST_CHECK( !signature.empty() ); - BOOST_CHECK( signature.find(' ')==std::string::npos ); - /* there is no verify signature ... */ -} BOOST_FIXTURE_TEST_CASE(CPID_Sign_Verify_integ, GridcoinBeaconSigningFixture) { const std::string sBlockHash("Block Hash Sample Text"); From 4b43183e1f7757c6c7e680e1eb70a7c426e516e7 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:42 -0500 Subject: [PATCH 09/15] Remove the TRXID AppCache section --- src/appcache.cpp | 1 - src/appcache.h | 1 - src/main.cpp | 8 -------- 3 files changed, 10 deletions(-) diff --git a/src/appcache.cpp b/src/appcache.cpp index 8daf29bc34..d70c2bdb22 100644 --- a/src/appcache.cpp +++ b/src/appcache.cpp @@ -18,7 +18,6 @@ namespace { "beaconalt", Section::BEACONALT }, { "global", Section::GLOBAL }, { "protocol", Section::PROTOCOL }, - { "trxid", Section::TRXID }, { "poll", Section::POLL }, { "vote", Section::VOTE }, { "scraper", Section::SCRAPER } diff --git a/src/appcache.h b/src/appcache.h index e49151fdc4..0f0c3f8d86 100644 --- a/src/appcache.h +++ b/src/appcache.h @@ -10,7 +10,6 @@ enum class Section BEACONALT, GLOBAL, PROTOCOL, - TRXID, POLL, VOTE, SCRAPER, diff --git a/src/main.cpp b/src/main.cpp index 52ea04d534..5f6147f11e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5568,14 +5568,6 @@ bool MemorizeMessage(const CTransaction &tx) NN::Researcher::Refresh(); } - if (fDebug) { - WriteCache( - Section::TRXID, - contract.m_type.ToString() + ";" + contract.m_key, - tx.GetHash().GetHex(), - tx.nTime); - } - NN::ProcessContract(contract); return true; From 11d060745800cb322bf2498b4cb9d31d1ec327d1 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:43 -0500 Subject: [PATCH 10/15] Refactor application for transaction v2 (binary contracts) --- src/contract/message.cpp | 3 +- src/main.cpp | 213 ++++++++++++++++++++++++++------------ src/main.h | 37 ++++++- src/miner.cpp | 8 ++ src/rpcrawtransaction.cpp | 26 +++++ src/wallet.cpp | 37 ++++--- 6 files changed, 244 insertions(+), 80 deletions(-) diff --git a/src/contract/message.cpp b/src/contract/message.cpp index ce7459e41c..0d33e64e54 100644 --- a/src/contract/message.cpp +++ b/src/contract/message.cpp @@ -9,7 +9,8 @@ extern CWallet* pwalletMain; std::pair SendContract(NN::Contract contract, int64_t min_balance, double fee) { CWalletTx wtx; - wtx.hashBoinc = contract.ToString(); + + wtx.vContracts.push_back(contract); std::string error = pwalletMain->SendMoneyToDestinationWithMinimumBalance( CBitcoinAddress(NN::Contract::BurnAddress()).Get(), diff --git a/src/main.cpp b/src/main.cpp index 5f6147f11e..976133eb29 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -915,9 +915,6 @@ bool CTransaction::ReadFromDisk(COutPoint prevout) } - - - bool IsStandardTx(const CTransaction& tx) { std::string reason = ""; @@ -991,7 +988,6 @@ bool IsStandardTx(const CTransaction& tx) } } - // not more than one data txout per non-data txout is permitted // only one data txout is permitted too if (nDataOut > 1 && nDataOut > tx.vout.size()/2) @@ -1000,7 +996,6 @@ bool IsStandardTx(const CTransaction& tx) return false; } - return true; } @@ -1147,8 +1142,6 @@ int CMerkleTx::SetMerkleBranch(const CBlock* pblock) } - - bool CTransaction::CheckTransaction() const { // Basic checks that don't depend on any context @@ -1195,6 +1188,42 @@ bool CTransaction::CheckTransaction() const if (txin.prevout.IsNull()) return DoS(10, error("CTransaction::CheckTransaction() : prevout is null")); } + + return true; +} + +bool CTransaction::CheckContracts(bool check_replay) const +{ + if (nVersion < 2) { + return true; + } + + for (const auto& contract : vContracts) { + // Destructure contract.Validate() to perform the least expensive checks + // first and the most expensive last: + if (!contract.WellFormed()) { + return error("CTransaction::CheckContract(): Malformed contract"); + } + + // A contract must be signed before sending it in a transaction: + if (contract.m_timestamp > nTime) { + return error("CTransaction::CheckContract(): Contract signed after tx"); + } + + // A wallet has 30 seconds to add the contract to a transaction: + if (contract.m_timestamp < nTime - 30) { + return error("CTransaction::CheckContract(): Contract signed too early"); + } + + if (check_replay && !NN::CheckContractReplay(contract)) { + return error("CTransaction::CheckContract(): Replayed contract"); + } + + if (!contract.VerifySignature()) { + return error("CTransaction::CheckContract(): Invalid signature"); + } + } + return true; } @@ -1241,13 +1270,30 @@ int64_t CTransaction::GetMinFee(unsigned int nBlockSize, enum GetMinFee_mode mod return nMinFee; } - bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInputs) { AssertLockHeld(cs_main); if (pfMissingInputs) *pfMissingInputs = false; + // Mandatory switch to binary contracts (tx version 2): + if (IsV11Enabled(nBestHeight + 1) && tx.nVersion < 2) { + // Disallow tx version 1 after the mandatory block to prohibit the + // use of legacy string contracts: + return tx.DoS(100, error("AcceptToMemoryPool : legacy transaction")); + } + + // Reject version 2 transactions until mandatory threshold. + // + // CTransaction::CURRENT_VERSION is now 2, but we cannot send version 2 + // transactions with binary contracts until clients can handle them. + // + // TODO: remove this check in the next release after mandatory block. + // + if (!IsV11Enabled(nBestHeight + 1) && tx.nVersion > 1) { + return tx.DoS(100, error("AcceptToMemoryPool : v2 transaction too early")); + } + if (!tx.CheckTransaction()) return error("AcceptToMemoryPool : CheckTransaction failed"); @@ -1267,6 +1313,11 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInput if (!fTestNet && !IsStandardTx(tx)) return error("AcceptToMemoryPool : nonstandard transaction type"); + // Contracts are expensive to validate. Reject any transactions that contain + // malformed or replayed contracts to prevent propagation: + if (!tx.CheckContracts(true)) + return tx.DoS(100, error("AcceptToMemoryPool : invalid contract in tx")); + // is it already in the memory pool? uint256 hash = tx.GetHash(); if (pool.exists(hash)) @@ -1380,6 +1431,11 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInput } } + // Track any contracts for replay protection: + if (tx.nVersion > 1 && tx.vContracts.size() > 0) { + NN::TrackContracts(tx.vContracts); + } + // Store transaction in memory { LOCK(pool.cs); @@ -2139,19 +2195,14 @@ bool CBlock::DisconnectBlock(CTxDB& txdb, CBlockIndex* pindex) bDiscTxFailed = true; } - if (!NN::Contract::Detect(vtx[i].hashBoinc)) { - continue; - } - - // Delete the contract. Previous version will be reloaded in reorganize. - NN::Contract contract = NN::Contract::Parse(vtx[i].hashBoinc, vtx[i].nTime); - - if (contract.VerifySignature()) { - if(fDebug) { - LogPrintf("DisconnectBlock: Delete contract %s %s", contract.m_type.ToString(), contract.m_key); + // Reverse the contracts. Reorganize will load any previous versions: + for (const auto& contract : vtx[i].vContracts) { + // V2 contract signatures are checked upon receipt: + if (vtx[i].nVersion == 1 && !contract.VerifySignature()) { + continue; } - NN::RevertContract(std::move(contract)); + NN::RevertContract(contract); } } @@ -2595,9 +2646,28 @@ bool GridcoinConnectBlock( // Load contracts: for (auto iter = ++block.vtx.begin(), end = block.vtx.end(); iter != end; ++iter) { - if (iter->hashBoinc.length() > 3) { + for (const auto& contract : iter->vContracts) { + // V2 contract signatures are checked upon receipt: + if (iter->nVersion == 1 && !contract.Validate()) { + continue; + } + pindex->nIsContract = 1; - MemorizeMessage(*iter, 0, ""); // TODO: replace with contract handler + + // Support dynamic team requirement or whitelist configuration: + // + // TODO: move this into the appropriate contract handler. + // + if (contract.m_type == NN::ContractType::PROTOCOL + && (contract.m_key == "REQUIRE_TEAM_WHITELIST_MEMBERSHIP" + || contract.m_key == "TEAM_WHITELIST")) + { + // Rescan in-memory project CPIDs to resolve a primary CPID + // that fits the now active team requirement settings: + NN::Researcher::Refresh(); + } + + NN::ProcessContract(contract); } } @@ -3377,12 +3447,35 @@ bool CBlock::CheckBlock(std::string sCaller, int height1, int64_t Mint, bool fCh // Check transactions for (auto const& tx : vtx) { + // Mandatory switch to binary contracts (tx version 2): + if (IsV11Enabled(height1) && tx.nVersion < 2) { + // Disallow tx version 1 after the mandatory block to prohibit the + // use of legacy string contracts: + return tx.DoS(100, error("CheckBlock[] : legacy transaction")); + } + + // Reject version 2 transactions until mandatory threshold. + // + // CTransaction::CURRENT_VERSION is now 2, but we cannot send version 2 + // transactions with binary contracts until clients can handle them. + // + // TODO: remove this check in the next release after mandatory block. + // + if (!IsV11Enabled(height1) && tx.nVersion > 1) { + return tx.DoS(100, error("CheckBlock[] : v2 transaction too early")); + } + if (!tx.CheckTransaction()) return DoS(tx.nDoS, error("CheckBlock[] : CheckTransaction failed")); // ppcoin: check transaction timestamp if (GetBlockTime() < (int64_t)tx.nTime) return DoS(50, error("CheckBlock[] : block timestamp earlier than transaction timestamp")); + + // Validate any contracts. Check for contract replay if the block + // occurs within the replay checking window. + if (!tx.CheckContracts(nTime > NN::Contract::ReplayPeriod())) + return DoS(50, error("CheckBlock[] : CheckContracts failed")); } // Check for duplicate txids. This is caught by ConnectInputs(), @@ -3474,6 +3567,11 @@ bool CBlock::AcceptBlock(bool generated_by_me) // Current bad contracts in chain would cause a fork on sync, skip them if (nVersion>=9 && !VerifyBeaconContractTx(tx)) return DoS(25, error("CheckBlock[] : bad beacon contract found in tx %s contained within block; rejected", tx.GetHash().ToString().c_str())); + + // Track any contracts for replay protection: + if (tx.nVersion > 1 && tx.vContracts.size() > 0) { + NN::TrackContracts(tx.vContracts); + } } // Check that the block chain matches the known block chain up to a checkpoint @@ -3977,6 +4075,7 @@ bool LoadBlockIndex(bool fAllowNew) CTransaction txNew; //GENESIS TIME + txNew.nVersion = 1; txNew.nTime = 1413033777; txNew.vin.resize(1); txNew.vout.resize(1); @@ -5543,36 +5642,6 @@ bool SendMessages(CNode* pto, bool fSendTrickle) return true; } -bool MemorizeMessage(const CTransaction &tx) -{ - if (!NN::Contract::Detect(tx.hashBoinc)) { - return false; - } - - NN::Contract contract = NN::Contract::Parse(tx.hashBoinc, tx.nTime); - - if (!contract.VerifySignature()) { - return false; - } - - // Support dynamic team requirement or whitelist configuration: - // - // TODO: move this into the appropriate contract handler. - // - if (contract.m_type == NN::ContractType::PROTOCOL - && (contract.m_key == "REQUIRE_TEAM_WHITELIST_MEMBERSHIP" - || contract.m_key == "TEAM_WHITELIST")) - { - // Rescan in-memory project CPIDs to resolve a primary CPID - // that fits the now active team requirement settings: - NN::Researcher::Refresh(); - } - - NN::ProcessContract(contract); - - return true; -} - const CBlockIndex* GetHistoricalMagnitude(const NN::MiningId mining_id) { if (const NN::CpidOption cpid = mining_id.TryCpid()) @@ -5623,21 +5692,37 @@ bool LoadAdminMessages(bool bFullTableScan) return true; // These are memorized consecutively in order from oldest to newest. - for(; pindex; pindex = pindex->pnext) - { - if (!pindex->IsInMainChain()) + for (; pindex; pindex = pindex->pnext) { + CBlock block; + + if (!pindex->IsInMainChain() || !IsContract(pindex) || !block.ReadFromDisk(pindex)) { continue; - if (IsContract(pindex)) - { - CBlock block; - if (!block.ReadFromDisk(pindex)) continue; - int iPos = 0; - for (auto const &tx : block.vtx) - { - if (iPos > 0) { - MemorizeMessage(tx); + } + + auto tx_iter = block.vtx.begin(); + ++tx_iter; // skip the first transaction + + for (auto end = block.vtx.end(); tx_iter != end; ++tx_iter) { + for (const auto& contract : tx_iter->vContracts) { + // V2 contract signatures are checked upon receipt: + if (tx_iter->nVersion == 1 && !contract.Validate()) { + continue; } - iPos++; + + // Support dynamic team requirement or whitelist configuration: + // + // TODO: move this into the appropriate contract handler. + // + if (contract.m_type == NN::ContractType::PROTOCOL + && (contract.m_key == "REQUIRE_TEAM_WHITELIST_MEMBERSHIP" + || contract.m_key == "TEAM_WHITELIST")) + { + // Rescan in-memory project CPIDs to resolve a primary CPID + // that fits the now active team requirement settings: + NN::Researcher::Refresh(); + } + + NN::ProcessContract(contract); } } } diff --git a/src/main.h b/src/main.h index 3ec1cb4a3a..916b36ddf1 100644 --- a/src/main.h +++ b/src/main.h @@ -13,6 +13,7 @@ #include "sync.h" #include "script.h" #include "scrypt.h" +#include "neuralnet/contract.h" #include #include @@ -602,7 +603,7 @@ typedef std::map > MapPrevTx; class CTransaction { public: - static const int CURRENT_VERSION=1; + static const int CURRENT_VERSION = 2; int nVersion; unsigned int nTime; std::vector vin; @@ -613,6 +614,7 @@ class CTransaction mutable int nDoS; bool DoS(int nDoSIn, bool fIn) const { nDoS += nDoSIn; return fIn; } std::string hashBoinc; + std::vector vContracts; CTransaction() { @@ -629,8 +631,25 @@ class CTransaction READWRITE(vin); READWRITE(vout); READWRITE(nLockTime); - READWRITE(hashBoinc); + + // Version 1: If the hashBoinc field contains a legacy contract string, + // parse it into the contract vector when deserializing the transaction. + // + // Version 2+: Directly serialize and deserialize the binary contracts + // in vContracts. Ignore contract messages in hashBoinc. + // + if (nVersion == 1 && ser_action.ForRead() && NN::Contract::Detect(hashBoinc)) { + REF(vContracts).push_back(NN::Contract::Parse(hashBoinc, nTime)); + } else if (nVersion > 1) { + READWRITE(vContracts); + + if (ser_action.ForRead()) { + for (auto& contract : REF(vContracts)) { + contract.m_tx_timestamp = nTime; + } + } + } } void SetNull() @@ -641,7 +660,8 @@ class CTransaction vout.clear(); nLockTime = 0; nDoS = 0; // Denial-of-service prevention - hashBoinc=""; + hashBoinc = ""; + vContracts.clear(); } bool IsNull() const @@ -852,7 +872,18 @@ class CTransaction bool ConnectInputs(CTxDB& txdb, MapPrevTx inputs, std::map& mapTestPool, const CDiskTxPos& posThisTx, const CBlockIndex* pindexBlock, bool fBlock, bool fMiner); + bool CheckTransaction() const; + + //! + //! \brief Check the validity of any contracts contained in the transaction. + //! + //! \param check_replay If \c true, also check for contract replay attacks. + //! + //! \return \c True if all of the contracts in the transaction validate. + //! + bool CheckContracts(bool check_replay = false) const; + bool GetCoinAge(CTxDB& txdb, uint64_t& nCoinAge) const; // ppcoin: get transaction coin age protected: diff --git a/src/miner.cpp b/src/miner.cpp index 06a394a015..e9dcf15e7b 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -215,6 +215,10 @@ bool CreateRestOfTheBlock(CBlock &block, CBlockIndex* pindexPrev) assert(CoinBase.vin[0].scriptSig.size() <= 100); CoinBase.vout[0].SetEmpty(); + if (block.nVersion <= 10) { + CoinBase.nVersion = 1; // TODO: remove after mandatory + } + // Largest block you're willing to create: unsigned int nBlockMaxSize = GetArg("-blockmaxsize", MAX_BLOCK_SIZE_GEN/2); // Limit to betweeen 1K and MAX_BLOCK_SIZE-1K for sanity: @@ -498,6 +502,10 @@ bool CreateCoinStake( CBlock &blocknew, CKey &key, double StakeDiffMax = 0; CTransaction &txnew = blocknew.vtx[1]; // second tx is coinstake + if (blocknew.nVersion <= 10) { + txnew.nVersion = 1; // TODO: remove after mandatory + } + //initialize the transaction txnew.nTime = blocknew.nTime & (~STAKE_TIMESTAMP_MASK); txnew.vin.clear(); diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index d6ee52a93f..e3cf1b440d 100755 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -7,6 +7,7 @@ #include #include "base58.h" +#include "neuralnet/contract.h" #include "rpcserver.h" #include "rpcprotocol.h" #include "txdb.h" @@ -383,6 +384,23 @@ void ScriptPubKeyToJSON(const CScript& scriptPubKey, UniValue& out, bool fInclud out.pushKV("addresses", a); } +UniValue ContractToJSON(const NN::Contract& contract) +{ + UniValue out(UniValue::VOBJ); + + out.pushKV("version", (int)contract.m_version); + out.pushKV("type", contract.m_type.ToString()); + out.pushKV("action", contract.m_action.ToString()); + out.pushKV("key", contract.m_key); + out.pushKV("value", contract.m_value); + out.pushKV("public_key", contract.m_public_key.ToString()); + out.pushKV("signature", contract.m_signature.ToString()); + out.pushKV("signing_time", contract.m_timestamp); + out.pushKV("nonce", contract.m_nonce); + + return out; +} + void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry) { entry.pushKV("txid", tx.GetHash().GetHex()); @@ -391,6 +409,14 @@ void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry) entry.pushKV("locktime", (int)tx.nLockTime); entry.pushKV("hashboinc", tx.hashBoinc); + UniValue contracts(UniValue::VARR); + + for (const auto& contract : tx.vContracts) { + contracts.push_back(ContractToJSON(contract)); + } + + entry.pushKV("contracts", contracts); + UniValue vin(UniValue::VARR); for (auto const& txin : tx.vin) { diff --git a/src/wallet.cpp b/src/wallet.cpp index c58beb08dd..5197f82b8d 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -1601,6 +1601,27 @@ bool CWallet::CreateTransaction(const vector >& vecSend, { LOCK2(cs_main, cs_wallet); + + // Force version 1 transactions until mandatory threshold. + // + // CTransaction::CURRENT_VERSION is now 2, but we cannot send version 2 + // transactions until clients can handle them. + // + // TODO: remove this check in the next release after mandatory block. + // + if (!IsV11Enabled(nBestHeight + 1)) { + wtxNew.nVersion = 1; + + // Convert any binary contracts to the legacy string representation. + // + // V2 transactions support multiple contracts, but nothing uses + // this ability yet. Just check the first element: + // + if (wtxNew.vContracts.size() == 1) { + wtxNew.hashBoinc = wtxNew.vContracts[0].ToString(); + } + } + // txdb must be opened before the mapWallet lock CTxDB txdb("r"); { @@ -1617,23 +1638,15 @@ bool CWallet::CreateTransaction(const vector >& vecSend, for (auto const& s : vecSend) wtxNew.vout.push_back(CTxOut(s.second, s.first)); - - // Determine if transaction is a contract - bool contract = false; - - if (!wtxNew.hashBoinc.empty() && !coinControl) - { - string contracttype = ExtractXML(wtxNew.hashBoinc, "", ""); - - if (contracttype == "beacon" || contracttype == "vote" || contracttype == "poll" || contracttype == "project") - contract = true; - } - int64_t nValueIn = 0; // If provided coin set is empty, choose coins to use. if (!setCoins.size()) { + // If the transaction contains a contract, we want to select the + // smallest UTXOs available: + const bool contract = !coinControl && !wtxNew.vContracts.empty(); + if (!SelectCoins(nTotalValue, wtxNew.nTime, setCoins, nValueIn, coinControl, contract)) return false; } From c8f22bea18c9aa4ad358d161b3ecf11e4ec897e0 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:44 -0500 Subject: [PATCH 11/15] Replace contract replay pool with input scriptPubKey matching --- src/contract/message.cpp | 172 ++++++++++++++++++++++--- src/contract/message.h | 17 +++ src/contract/polls.cpp | 8 +- src/main.cpp | 91 ++++++++------ src/main.h | 20 ++- src/neuralnet/contract.cpp | 175 ++++---------------------- src/neuralnet/contract.h | 73 +++-------- src/rpcblockchain.cpp | 24 ++-- src/rpcrawtransaction.cpp | 2 - src/test/neuralnet/contract_tests.cpp | 135 +++----------------- src/test/neuralnet/project_tests.cpp | 5 +- src/wallet.cpp | 15 +-- 12 files changed, 334 insertions(+), 403 deletions(-) diff --git a/src/contract/message.cpp b/src/contract/message.cpp index 0d33e64e54..b385bc7add 100644 --- a/src/contract/message.cpp +++ b/src/contract/message.cpp @@ -1,33 +1,175 @@ -#include "base58.h" #include "message.h" #include "neuralnet/contract.h" -#include "rpcserver.h" +#include "script.h" #include "wallet.h" +#include "coincontrol.h" + +using namespace NN; + extern CWallet* pwalletMain; -std::pair SendContract(NN::Contract contract, int64_t min_balance, double fee) +namespace { +//! +//! \brief Configure coin control for a transaction to select inputs from the +//! set of UTXOs associated with the master key address and to send the change +//! back to the same address. +//! +//! \param coin_control Stores the selected input/output configuration. +//! +//! \return \c true if the wallet owns UTXOs for the master key address. +//! +bool SelectMasterInputOutput(CCoinControl& coin_control) +{ + const CTxDestination master_address = CWallet::MasterAddress().Get(); + + // Send change back to the master address: + coin_control.destChange = master_address; + + std::vector unspent_coins; + pwalletMain->AvailableCoins(unspent_coins, true, &coin_control, false); + + for (const auto& coin : unspent_coins) { + CTxDestination dest; + + if (!ExtractDestination(coin.tx->vout[coin.i].scriptPubKey, dest)) { + continue; + } + + // Select all UTXOs for the master address as inputs to consolidate the + // change back into one output: + if (dest == master_address) { + COutPoint out_point(coin.tx->GetHash(), coin.i); + coin_control.Select(out_point); + } + } + + return coin_control.HasSelected(); +} + +//! +//! \brief Configure a contract transaction and set up inputs and outputs. +//! +//! \param wtx_new A new transaction with a contract. +//! \param reserve_key Key reserved for any change. +//! \param admin \c true for an administrative contract. +//! +//! \return \c true if coin selection succeeded. +//! +bool CreateContractTx(CWalletTx& wtx_out, CReserveKey reserve_key, bool admin) +{ + CCoinControl coin_control_out; + int64_t applied_fee_out; // Unused + + // Configure inputs/outputs for the address associated with the master key. + // Nodes validate administrative contracts by checking that the containing + // transactions include an input signed by the master key, so select coins + // from the master address and send any change back to it: + // + if (admin && !SelectMasterInputOutput(coin_control_out)) { + return false; + } + + // Burn the output. For reference, the old burn addresses for contracts are: + // + // mainnet: S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB + // testnet: mk1e432zWKH1MW57ragKywuXaWAtHy1AHZ + // + CScript scriptPubKey; + scriptPubKey << OP_RETURN; + + return pwalletMain->CreateTransaction( + { std::make_pair(std::move(scriptPubKey), Contract::BURN_AMOUNT) }, + wtx_out, + reserve_key, + applied_fee_out, + &coin_control_out); +} + +//! +//! \brief Send a transaction that contains a contract. +//! +//! \param wtx_new A new transaction with a contract. +//! \param admin \c true for an administrative contract. +//! +//! \return An empty string when successful or a description of the error that +//! occurred. TODO: Refactor to remove string-based signaling. +//! +std::string SendContractTx(CWalletTx& wtx_new, const bool admin) +{ + CReserveKey reserve_key(pwalletMain); + + if (pwalletMain->IsLocked()) { + std::string strError = _("Error: Wallet locked, unable to create transaction."); + LogPrintf("%s: %s", __func__, strError); + return strError; + } + + if (fWalletUnlockStakingOnly) { + std::string strError = _("Error: Wallet unlocked for staking only, unable to create transaction."); + LogPrintf("%s: %s", __func__, strError); + return strError; + } + + int64_t balance = pwalletMain->GetBalance(); + + // Check that balance is greater than one coin and 0.00000001 + fee: + if (balance < COIN || balance < 1 + nTransactionFee) { + std::string strError = _("Balance too low to create a smart contract."); + LogPrintf("%s: %s", __func__, strError); + return strError; + } + + if (!CreateContractTx(wtx_new, reserve_key, admin)) { + std::string strError = _("Error: Transaction creation failed."); + LogPrintf("%s: %s", __func__, strError); + return strError; + } + + if (!pwalletMain->CommitTransaction(wtx_new, reserve_key)) { + std::string strError = _( + "Error: The transaction was rejected. This might happen if some of " + "the coins in your wallet were already spent, such as if you used " + "a copy of wallet.dat and coins were spent in the copy but not " + "marked as spent here."); + + LogPrintf("%s: %s", __func__, strError); + return strError; + } + + return ""; +} +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Functions +// ----------------------------------------------------------------------------- + +std::pair SendContract(Contract contract) { CWalletTx wtx; + bool admin = contract.RequiresMasterKey(); - wtx.vContracts.push_back(contract); + wtx.vContracts.push_back(std::move(contract)); - std::string error = pwalletMain->SendMoneyToDestinationWithMinimumBalance( - CBitcoinAddress(NN::Contract::BurnAddress()).Get(), - AmountFromValue(fee), - AmountFromValue(min_balance), - wtx); + std::string error = SendContractTx(wtx, admin); - return std::make_pair(wtx, error); + return std::make_pair(std::move(wtx), std::move(error)); } -std::string SendPublicContract(NN::Contract contract) +std::string SendPublicContract(Contract contract) { - if (!contract.SignWithMessageKey()) { - return "Failed to sign contract with shared message key."; + // TODO: remove this after the v11 mandatory block. We don't need to sign + // version 2 contracts: + if (!IsV11Enabled(nBestHeight + 1)) { + contract.m_version = 1; + + if (!contract.SignWithMessageKey()) { + return "Failed to sign contract with shared message key."; + } } - std::pair result = SendContract(contract, 0.00001, 1); + std::pair result = SendContract(std::move(contract)); - return std::get<1>(result); + return std::get<1>(std::move(result)); } diff --git a/src/contract/message.h b/src/contract/message.h index a5351df424..65f7113599 100644 --- a/src/contract/message.h +++ b/src/contract/message.h @@ -6,6 +6,23 @@ class CWalletTx; namespace NN { class Contract; } +//! +//! \brief Create and send a transaction that contains the provided contract. +//! +//! \param contract A new contract to publish in a transaction. +//! +//! \return Contains the finalized transaction and error message, if any. +//! TODO: refactor to remove string-based signaling. +//! std::pair SendContract(NN::Contract contract); +//! +//! \brief Create and send a transaction that contains the provided public, +//! non-administrative contract. +//! +//! \param contract A new contract to publish in a transaction. +//! +//! \return An empty string upon success or a description of the error. +//! TODO: refactor to remove string-based signaling. +//! std::string SendPublicContract(NN::Contract contract); diff --git a/src/contract/polls.cpp b/src/contract/polls.cpp index d32b4eb3dd..90a75609ec 100644 --- a/src/contract/polls.cpp +++ b/src/contract/polls.cpp @@ -72,10 +72,10 @@ std::pair CreatePollContract(std::string sTitle, int d + "" + sURL + "" + "" + RoundToString(GetAdjustedTime() + (days * 86400), 0) + ""); - std::string error = SendPublicContract(contract); + std::string error = SendPublicContract(std::move(contract)); if (!error.empty()) { - return std::make_pair("Error", error); + return std::make_pair("Error", std::move(error)); } return std::make_pair("Success", ""); @@ -155,10 +155,10 @@ std::pair CreateVoteContract(std::string sTitle, std:: // goes into effect July 1 2017: + GetProvableVotingWeightXML()); - std::string error = SendPublicContract(contract); + std::string error = SendPublicContract(std::move(contract)); if (!error.empty()) { - return std::make_pair("Error", error); + return std::make_pair("Error", std::move(error)); } std::string narr = "Your CPID weight is " + RoundToString(dmag,0) + " and your Balance weight is " + RoundToString(nBalance,0) + "."; diff --git a/src/main.cpp b/src/main.cpp index 976133eb29..5f8b6170b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1192,39 +1192,64 @@ bool CTransaction::CheckTransaction() const return true; } -bool CTransaction::CheckContracts(bool check_replay) const +bool CTransaction::CheckContracts(const MapPrevTx& inputs) const { - if (nVersion < 2) { + // Although v2 transactions support multiple contracts, we just allow one + // for now to mitigate spam: + if (vContracts.size() > 1) { + return DoS(100, error("%s: only one contract allowed in tx", __func__)); + } + + if ((IsCoinBase() || IsCoinStake())) { + return DoS(100, error("%s: contract in non-standard tx", __func__)); + } + + if (nVersion <= 1) { return true; } + const auto is_valid_burn_output = [](const CTxOut& output) { + return output.scriptPubKey[0] == OP_RETURN + && output.nValue >= NN::Contract::BURN_AMOUNT; + }; + + if (std::none_of(vout.begin(), vout.end(), is_valid_burn_output)) { + return DoS(100, error("%s: no sufficient burn output", __func__)); + } + for (const auto& contract : vContracts) { - // Destructure contract.Validate() to perform the least expensive checks - // first and the most expensive last: - if (!contract.WellFormed()) { - return error("CTransaction::CheckContract(): Malformed contract"); + if (!contract.Validate()) { + return DoS(100, error("%s: malformed contract", __func__)); } - // A contract must be signed before sending it in a transaction: - if (contract.m_timestamp > nTime) { - return error("CTransaction::CheckContract(): Contract signed after tx"); + // Reject any transactions with administrative contracts sent from a + // wallet that does not hold the master key: + if (contract.RequiresMasterKey() && !HasMasterKeyInput(inputs)) { + return DoS(100, error("%s: contract requires master key", __func__)); } + } - // A wallet has 30 seconds to add the contract to a transaction: - if (contract.m_timestamp < nTime - 30) { - return error("CTransaction::CheckContract(): Contract signed too early"); - } + return true; +} - if (check_replay && !NN::CheckContractReplay(contract)) { - return error("CTransaction::CheckContract(): Replayed contract"); +bool CTransaction::HasMasterKeyInput(const MapPrevTx& inputs) const +{ + const CTxDestination master_address = NN::Contract::MasterAddress().Get(); + + for (const auto& input : vin) { + const CTxOut& prev_out = GetOutputFor(input, inputs); + CTxDestination dest; + + if (!ExtractDestination(prev_out.scriptPubKey, dest)) { + continue; } - if (!contract.VerifySignature()) { - return error("CTransaction::CheckContract(): Invalid signature"); + if (dest == master_address) { + return true; } } - return true; + return false; } int64_t CTransaction::GetBaseFee(enum GetMinFee_mode mode) const @@ -1313,11 +1338,6 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInput if (!fTestNet && !IsStandardTx(tx)) return error("AcceptToMemoryPool : nonstandard transaction type"); - // Contracts are expensive to validate. Reject any transactions that contain - // malformed or replayed contracts to prevent propagation: - if (!tx.CheckContracts(true)) - return tx.DoS(100, error("AcceptToMemoryPool : invalid contract in tx")); - // is it already in the memory pool? uint256 hash = tx.GetHash(); if (pool.exists(hash)) @@ -1416,6 +1436,11 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInput } } + // Validate any contracts published in the transaction: + if (!tx.vContracts.empty() && !tx.CheckContracts(mapInputs)) { + return false; + } + // Check against previous transactions // This is done last to help prevent CPU exhaustion denial-of-service attacks. if (!tx.ConnectInputs(txdb, mapInputs, mapUnused, CDiskTxPos(1,1,1), pindexBest, false, false)) @@ -1431,11 +1456,6 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CTransaction &tx, bool* pfMissingInput } } - // Track any contracts for replay protection: - if (tx.nVersion > 1 && tx.vContracts.size() > 0) { - NN::TrackContracts(tx.vContracts); - } - // Store transaction in memory { LOCK(pool.cs); @@ -2811,6 +2831,11 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck) } } + // Validate any contracts published in the transaction: + if (!tx.vContracts.empty() && !tx.CheckContracts(mapInputs)) { + return false; + } + if (!tx.ConnectInputs(txdb, mapInputs, mapQueuedChanges, posThisTx, pindex, true, false)) return false; } @@ -3471,11 +3496,6 @@ bool CBlock::CheckBlock(std::string sCaller, int height1, int64_t Mint, bool fCh // ppcoin: check transaction timestamp if (GetBlockTime() < (int64_t)tx.nTime) return DoS(50, error("CheckBlock[] : block timestamp earlier than transaction timestamp")); - - // Validate any contracts. Check for contract replay if the block - // occurs within the replay checking window. - if (!tx.CheckContracts(nTime > NN::Contract::ReplayPeriod())) - return DoS(50, error("CheckBlock[] : CheckContracts failed")); } // Check for duplicate txids. This is caught by ConnectInputs(), @@ -3567,11 +3587,6 @@ bool CBlock::AcceptBlock(bool generated_by_me) // Current bad contracts in chain would cause a fork on sync, skip them if (nVersion>=9 && !VerifyBeaconContractTx(tx)) return DoS(25, error("CheckBlock[] : bad beacon contract found in tx %s contained within block; rejected", tx.GetHash().ToString().c_str())); - - // Track any contracts for replay protection: - if (tx.nVersion > 1 && tx.vContracts.size() > 0) { - NN::TrackContracts(tx.vContracts); - } } // Check that the block chain matches the known block chain up to a checkpoint diff --git a/src/main.h b/src/main.h index 916b36ddf1..9f7f4dced7 100644 --- a/src/main.h +++ b/src/main.h @@ -721,6 +721,18 @@ class CTransaction */ bool AreInputsStandard(const MapPrevTx& mapInputs) const; + //! + //! \brief Determine whether the transaction contains an input spent by the + //! master key holder. + //! + //! \param inputs Map of the previous transactions with outputs spent by + //! this transaction to search for the master key address. + //! + //! \return \c true if at least one of the inputs from one of the previous + //! transactions comes from the master key address. + //! + bool HasMasterKeyInput(const MapPrevTx& inputs) const; + /** Count ECDSA signature operations the old-fashioned (pre-0.6) way @return number of sigops this transaction's outputs will produce when spent @see CTransaction::FetchInputs @@ -878,11 +890,13 @@ class CTransaction //! //! \brief Check the validity of any contracts contained in the transaction. //! - //! \param check_replay If \c true, also check for contract replay attacks. + //! \param inputs Map of the previous transactions with outputs spent by + //! this transaction to search for the master key address for validating + //! administrative contracts. //! - //! \return \c True if all of the contracts in the transaction validate. + //! \return \c true if all of the contracts in the transaction validate. //! - bool CheckContracts(bool check_replay = false) const; + bool CheckContracts(const MapPrevTx& inputs) const; bool GetCoinAge(CTxDB& txdb, uint64_t& nCoinAge) const; // ppcoin: get transaction coin age diff --git a/src/neuralnet/contract.cpp b/src/neuralnet/contract.cpp index 91d9485502..1945a0e1d9 100644 --- a/src/neuralnet/contract.cpp +++ b/src/neuralnet/contract.cpp @@ -127,100 +127,7 @@ class Dispatcher GetHandler(contract.m_type.Value()).Revert(contract); } - //! - //! \brief Check that the provided contract does not match an existing - //! contract to protect against replay attacks. - //! - //! The application calls this method when it receives a new transaction - //! from another node. The return value determines whether we should keep - //! or discard the transaction. If the received contract is too old (as - //! defined by \c REPLAY_RETENTION_PERIOD), or if the contract matches an - //! existing contract in the cache, this method returns \c false, and the - //! calling code shall reject the transaction containing the contract. - //! - //! Version 2+ contracts can be checked for replay. Version 1 contracts do - //! not contain the data necessary to determine uniqueness. - //! - //! Replay protection relies on the contract's signing timestamp and nonce - //! values captured in the contract hash. A contract received with a signing - //! time earlier than the configured number of seconds from now shall be - //! considered invalid. The addition of a nonce prevents replay of recent - //! contracts received within this window. - //! - //! \param contract Contract to check. Received in a transaction. - //! - //! \return \c true if the check deteremines that the contract is unique. - //! - bool CheckReplay(const Contract& contract) - { - int64_t valid_after_time = Contract::ReplayPeriod(); - - // Reject any contracts timestamped earlier than a reasonable window. - // By invalidating contracts older than a cut-off threshold, we only - // need to store contracts newer than REPLAY_RETENTION_PERIOD in memory - // for replay detection: - if (contract.m_timestamp < valid_after_time) { - return false; - } - - std::list::iterator item = m_replay_pool.begin(); - - while (item != m_replay_pool.end()) { - // If a contract hash matches an entry in the pool, we can assume - // that it was replayed: - if (item->m_hash == contract.GetHash()) { - return false; - } - - // Lazily purge old entries from the pool whenever we check a new - // contract. Any expired entries eventualy pass out when we receive - // a valid contract: - if (item->m_timestamp < valid_after_time) { - item = m_replay_pool.erase(item); - continue; - } - - ++item; - } - - return true; - } - - //! - //! \brief Add a contract to the replay tracking pool. - //! - //! \param contract A newly-received contract from a transaction. - //! - void TrackForReplay(const Contract& contract) - { - ReplayPoolItem new_item; - - new_item.m_timestamp = contract.m_timestamp; - new_item.m_hash = contract.GetHash(); - - m_replay_pool.push_back(std::move(new_item)); - } - private: - //! - //! \brief Contains a hash of contract data for replay checks. - //! - struct ReplayPoolItem - { - int64_t m_timestamp; //!< To cull entries older than retention period. - uint256 m_hash; //!< Hash of a contract compared to new contracts. - }; - - //! - //! \brief Contains a rolling cache of recently-received contract hashes - //! used to compare with contracts received in transaction messages for - //! replay protection. - //! - //! Calling \c CheckReplay() will purge the old entries from the cache as - //! it checks a valid contract. - //! - std::list m_replay_pool; - AppCacheContractHandler m_appcache_handler; //& contracts) -{ - for (const auto& contract : contracts) { - gateway.TrackForReplay(contract); - } -} - // ----------------------------------------------------------------------------- // Class: Contract // ----------------------------------------------------------------------------- +constexpr int64_t Contract::BURN_AMOUNT; // for clang + Contract::Contract() : m_version(Contract::CURRENT_VERSION) , m_type(Contract::Type(ContractType::UNKNOWN)) @@ -293,8 +190,6 @@ Contract::Contract() , m_value(std::string()) , m_signature(Contract::Signature()) , m_public_key(Contract::PublicKey()) - , m_nonce(0) - , m_timestamp(0) , m_tx_timestamp(0) { } @@ -311,8 +206,6 @@ Contract::Contract( , m_value(std::move(value)) , m_signature(Contract::Signature()) , m_public_key(Contract::PublicKey()) - , m_nonce(0) - , m_timestamp(0) , m_tx_timestamp(0) { } @@ -325,8 +218,6 @@ Contract::Contract( std::string value, Contract::Signature signature, Contract::PublicKey public_key, - unsigned int nonce, - long timestamp, int64_t tx_timestamp) : m_version(version) , m_type(std::move(type)) @@ -335,8 +226,6 @@ Contract::Contract( , m_value(std::move(value)) , m_signature(std::move(signature)) , m_public_key(std::move(public_key)) - , m_nonce(std::move(nonce)) - , m_timestamp(std::move(timestamp)) , m_tx_timestamp(std::move(tx_timestamp)) { } @@ -367,6 +256,14 @@ const CPrivKey Contract::MasterPrivateKey() return CPrivKey(key.begin(), key.end()); } +const CBitcoinAddress Contract::MasterAddress() +{ + CBitcoinAddress master_address; + master_address.Set(NN::Contract::MasterPublicKey().GetID()); + + return master_address; +} + const CPubKey& Contract::MessagePublicKey() { // If the message key changes, add a conditional entry to this method that @@ -439,11 +336,6 @@ const std::string Contract::BurnAddress() : "S67nL4vELWwdDVzjgtEP4MxryarTZ9a8GB"; } -int64_t Contract::ReplayPeriod() -{ - return GetAdjustedTime() - REPLAY_RETENTION_PERIOD; -} - bool Contract::Detect(const std::string& message) { return !message.empty() @@ -470,8 +362,6 @@ Contract Contract::Parse(const std::string& message, const int64_t timestamp) // altogether. We verify contracts with the master and message keys: //Contract::PublicKey::Parse(ExtractXML(message, "", "")), Contract::PublicKey(), - 0, // Nonce unused in v1 contracts. - 0, // Signing timestamp unused in v1 contracts. timestamp); } @@ -523,26 +413,24 @@ bool Contract::WellFormed() const && m_action != ContractAction::UNKNOWN && !m_key.empty() && !m_value.empty() - && m_signature.Viable() - && (RequiresSpecialKey() || m_public_key.Viable()) - && m_tx_timestamp > 0 - && (m_version == 1 || (m_timestamp > 0 && m_nonce > 0)); + // Version 2+ contracts rely on the signatures in the transactions + // instead of embedding another signature in the contract: + && (m_version > 1 || m_signature.Viable()) + && (m_version > 1 || (RequiresSpecialKey() || m_public_key.Viable())) + && m_tx_timestamp > 0; } bool Contract::Validate() const { - return WellFormed() && VerifySignature(); + return WellFormed() + // Version 2+ contracts rely on the signatures in the transactions + // instead of embedding another signature in the contract: + && (m_version > 1 || VerifySignature()); } bool Contract::Sign(CKey& private_key) { std::vector output; - m_hash_cache = 0; // Invalidate the cached hash, if any. - - if (m_version > 1) { - m_timestamp = GetAdjustedTime(); - RAND_bytes(reinterpret_cast(&m_nonce), sizeof(m_nonce)); - } if (!private_key.Sign(GetHash(), output)) { Log("ERROR: Failed to sign contract"); @@ -581,34 +469,19 @@ bool Contract::VerifySignature() const uint256 Contract::GetHash() const { - // Nodes use the hash at least twice when validating a received contract - // (once to verify the signature, once to track the contract for replay - // protection, and one or more times to compare the contract to any other - // contracts in the replay protection cache), so we cache the value to - // avoid re-computing it. Once cached, the hash is only invalidated when - // re-signing a contract, so avoid calling this method on a contract in - // an intermediate state. - - if (m_hash_cache > 0) { - return m_hash_cache; - } - if (m_version > 1) { - m_hash_cache = SerializeHash(*this); - return m_hash_cache; + return SerializeHash(*this); } const std::string type_string = m_type.ToString(); - m_hash_cache = Hash( + return Hash( type_string.begin(), type_string.end(), m_key.begin(), m_key.end(), m_value.begin(), m_value.end()); - - return m_hash_cache; } std::string Contract::ToString() const @@ -629,18 +502,16 @@ void Contract::Log(const std::string& prefix) const } LogPrintf( - ": %s: v%d, %d, %d, %s, %s, %s, %s, %s, %s, %d", + ": %s: v%d, %d, %s, %s, %s, %s, %s, %s", prefix, m_version, m_tx_timestamp, - m_timestamp, m_type.ToString(), m_action.ToString(), m_key, m_value, m_public_key.ToString(), - m_signature.ToString(), - m_nonce); + m_signature.ToString()); } // ----------------------------------------------------------------------------- diff --git a/src/neuralnet/contract.h b/src/neuralnet/contract.h index 5d58ac2ff7..b4faba1213 100644 --- a/src/neuralnet/contract.h +++ b/src/neuralnet/contract.h @@ -1,5 +1,6 @@ #pragma once +#include "base58.h" #include "key.h" #include "serialize.h" #include "uint256.h" @@ -201,14 +202,6 @@ class Contract OptionalString m_other; //!< Holds invalid or non-standard types. }; // Contract::EnumVariant - //! - //! \brief Caches a contract's hash value. - //! - //! Prevents repeated, expensive hash calculations. However, once cached, - //! this value is only invalidated when re-signing a contract. - //! - mutable uint256 m_hash_cache = 0; - public: //! //! \brief Version number of the current format for a serialized contract. @@ -220,11 +213,12 @@ class Contract static constexpr int CURRENT_VERSION = 2; // TODO: int32_t //! - //! \brief The amount of time in seconds to store received contracts in - //! memory for replay detection. Reject contracts timestamped earlier than - //! this window. + //! \brief The amount of coin set for a burn output in a transaction that + //! broadcasts a contract in units of 1/100000000 GRC. + //! + //! Currently, we burn the smallest amount possible (0.00000001). //! - static constexpr int64_t REPLAY_RETENTION_PERIOD = 60 * 60; + static constexpr int64_t BURN_AMOUNT = 1; //! //! \brief A contract type from a transaction message. @@ -466,13 +460,13 @@ class Contract //! //! Defaults to the most recent version for a new contract instance. //! - //! Version 1: Legacy string XML-like contract message parsed from the - //! \c hashBoinc field of a transaction. Contains no nonce or signing - //! timestamp. + //! Version 1: Legacy string XML-like contract messages parsed from the + //! \c hashBoinc field of a transaction object. It contains a signature + //! and public key. //! //! Version 2: Contract data serializable in binary format. Stored in a - //! transaction's \c vContracts field. Must contain a nonce and signing - //! timestamp which protect against contract replay. + //! transaction's \c vContracts field. It excludes the legacy signature + //! and public key from version 1. //! int m_version = CURRENT_VERSION; @@ -482,8 +476,6 @@ class Contract std::string m_value; //!< Body containing any specialized data. Signature m_signature; //!< Proves authenticity of the contract. PublicKey m_public_key; //!< Verifies the contract signature. - unsigned int m_nonce; //!< Random. Prevents contract replay. - long m_timestamp; //!< Signing time. Prevents contract replay. int64_t m_tx_timestamp; //!< Timestamp of the contract's transaction. //! @@ -515,8 +507,6 @@ class Contract //! \param value The contract value as it exists in the transaction. //! \param signature Proves authenticity of the contract message. //! \param public_key Optional for some types. Verifies the signature. - //! \param nonce Random. Prevents contract replay. Version 2+. - //! \param timestamp Contract signing time. Prevents replay. Version 2+. //! \param tx_timestamp Timestamp of the transaction containing the contract. //! Contract( @@ -527,8 +517,6 @@ class Contract std::string value, Signature signature, PublicKey public_key, - unsigned int nonce, - long timestamp, int64_t tx_timestamp); //! @@ -546,6 +534,13 @@ class Contract //! static const CPrivKey MasterPrivateKey(); + //! + //! \brief Get the output address controlled by the master private key. + //! + //! \return Address as calculated from the master public key. + //! + static const CBitcoinAddress MasterAddress(); + //! //! \brief Get the message public key used to verify public contracts. //! @@ -570,14 +565,6 @@ class Contract //! static const std::string BurnAddress(); - //! - //! \brief Get the time after which newly-received contracts must pass - //! replay checking. - //! - //! \return Number of seconds before the current time. - //! - static int64_t ReplayPeriod(); - //! //! \brief Determine whether the supplied message might contain a contract. //! @@ -680,7 +667,7 @@ class Contract //! verify the contract signature. //! //! \return Hash of the contract type, key, and value. Versions 2+ also - //! include the action, nonce, and timestamp in the hash. + //! include the action. //! uint256 GetHash() const; @@ -719,13 +706,6 @@ class Contract READWRITE(m_action); READWRITE(m_key); READWRITE(m_value); - READWRITE(m_nonce); - READWRITE(m_timestamp); - - if (!(s.GetType() & SER_GETHASH)) { - READWRITE(m_public_key); - READWRITE(m_signature); - } } }; // Contract @@ -792,19 +772,4 @@ void ProcessContract(const Contract& contract); //! \param contract Received in a transaction message. //! void RevertContract(const Contract& contract); - -//! -//! \brief Check that the provided contract does not match an existing contract -//! to protect against replay attacks. -//! -//! \param contract Contract to check. Received in a transaction. -//! -bool CheckContractReplay(const Contract& contract); - -//! -//! \brief Add contracts from a transaction to the replay tracking pool. -//! -//! \param contracts A set of newly-received contracts from a transaction. -//! -void TrackContracts(const std::vector& contracts); } diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 41aba41eb4..637ef8d57b 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -472,7 +472,7 @@ bool AdvertiseBeacon(std::string &sOutPrivKey, std::string &sOutPubKey, std::str } // Send the beacon transaction - sMessage = SendPublicContract(contract); + sMessage = SendPublicContract(std::move(contract)); // This prevents repeated beacons nLastBeaconAdvertised = nBestHeight; @@ -1145,6 +1145,8 @@ UniValue addkey(const UniValue& params, bool fHelp) CKey key; + // TODO: remove this after Elizabeth mandatory block. We don't need to sign + // version 2 contracts (the signature is discarded after the threshold): if (!key.SetPrivKey(NN::Contract::MasterPrivateKey()) || key.GetPubKey() != NN::Contract::MasterPublicKey() ) { @@ -1177,19 +1179,25 @@ UniValue addkey(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_INVALID_PARAMETER, "Not an admin contract type."); } - if (!contract.Sign(key)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to sign."); - } + // TODO: remove this after the v11 mandatory block. We don't need to sign + // version 2 contracts (the signature is discarded after the threshold): + if (!IsV11Enabled(nBestHeight + 1)) { + contract.m_version = 1; + + if (!contract.Sign(key)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to sign."); + } - if (!contract.VerifySignature()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to verify signature."); + if (!contract.VerifySignature()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to verify signature."); + } } - std::pair result = SendContract(contract, 5, 0.1); + std::pair result = SendContract(contract); std::string error = result.second; if (!error.empty()) { - throw JSONRPCError(RPC_WALLET_ERROR, error); + throw JSONRPCError(RPC_WALLET_ERROR, std::move(error)); } UniValue res(UniValue::VOBJ); diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index e3cf1b440d..b79783d905 100755 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -395,8 +395,6 @@ UniValue ContractToJSON(const NN::Contract& contract) out.pushKV("value", contract.m_value); out.pushKV("public_key", contract.m_public_key.ToString()); out.pushKV("signature", contract.m_signature.ToString()); - out.pushKV("signing_time", contract.m_timestamp); - out.pushKV("nonce", contract.m_nonce); return out; } diff --git a/src/test/neuralnet/contract_tests.cpp b/src/test/neuralnet/contract_tests.cpp index 6b1b61725d..254b76462b 100644 --- a/src/test/neuralnet/contract_tests.cpp +++ b/src/test/neuralnet/contract_tests.cpp @@ -177,8 +177,8 @@ struct TestSig //! static std::string V2String() { - return "MEUCIQCJAVz+8CfEmbeHsx7mbBXP0LEISnsuHjC8UVD/rLEx" - "SAIgXmouutnnad8B+4+MYfHc/WXysckwvwheR7Y0+7i2yCM="; + return "MEQCIAeQb8PvjU4wN1wZNCXbtykkiW+hvle/OtqzUDsiQmfj" + "AiBh9uUbvaVDljrK4EC4aFfnAdubfP3cEaqruINfxdDTjg=="; } //! @@ -190,13 +190,13 @@ struct TestSig static std::vector V2Bytes() { return std::vector { - 0x30, 0x45, 0x02, 0x21, 0x00, 0x89, 0x01, 0x5c, 0xfe, 0xf0, 0x27, - 0xc4, 0x99, 0xb7, 0x87, 0xb3, 0x1e, 0xe6, 0x6c, 0x15, 0xcf, 0xd0, - 0xb1, 0x08, 0x4a, 0x7b, 0x2e, 0x1e, 0x30, 0xbc, 0x51, 0x50, 0xff, - 0xac, 0xb1, 0x31, 0x48, 0x02, 0x20, 0x5e, 0x6a, 0x2e, 0xba, 0xd9, - 0xe7, 0x69, 0xdf, 0x01, 0xfb, 0x8f, 0x8c, 0x61, 0xf1, 0xdc, 0xfd, - 0x65, 0xf2, 0xb1, 0xc9, 0x30, 0xbf, 0x08, 0x5e, 0x47, 0xb6, 0x34, - 0xfb, 0xb8, 0xb6, 0xc8, 0x23 + 0x30, 0x44, 0x02, 0x20, 0x07, 0x90, 0x6f, 0xc3, 0xef, 0x8d, 0x4e, + 0x30, 0x37, 0x5c, 0x19, 0x34, 0x25, 0xdb, 0xb7, 0x29, 0x24, 0x89, + 0x6f, 0xa1, 0xbe, 0x57, 0xbf, 0x3a, 0xda, 0xb3, 0x50, 0x3b, 0x22, + 0x42, 0x67, 0xe3, 0x02, 0x20, 0x61, 0xf6, 0xe5, 0x1b, 0xbd, 0xa5, + 0x43, 0x96, 0x3a, 0xca, 0xe0, 0x40, 0xb8, 0x68, 0x57, 0xe7, 0x01, + 0xdb, 0x9b, 0x7c, 0xfd, 0xdc, 0x11, 0xaa, 0xab, 0xb8, 0x83, 0x5f, + 0xc5, 0xd0, 0xd3, 0x8e }; } @@ -226,7 +226,7 @@ struct TestSig static uint256 V2Hash() { return uint256S( - "bb434a253430c3b829ff8497ea7d497cf1f053a7af000b6c46ecf8b98dbe4046"); + "df3c70b21961973ac24bdd259283186d690134a1b446e4fde9e853d55ae89e32"); } //! @@ -306,8 +306,6 @@ struct TestMessage "test", NN::Contract::Signature(TestSig::V2Bytes()), NN::Contract::PublicKey(), - 123, // Nonce - 234, // Signing timestamp 123); // Tx timestamp is never part of a message } @@ -327,8 +325,6 @@ struct TestMessage "test", NN::Contract::Signature(TestSig::V1Bytes()), NN::Contract::PublicKey(), - 0, // Unused in v1 (nonce) - 0, // Unused in v1 (signing timestamp) 123); // Tx timestamp is never part of a message } @@ -367,15 +363,8 @@ struct TestMessage 0x74, 0x65, 0x73, 0x74, // Key: "test" as ASCII bytes 0x04, // Length of the contract value 0x74, 0x65, 0x73, 0x74, // Value: "test" as ASCII bytes - 0x7b, 0x00, 0x00, 0x00, // Nonce: 123 as 32-bit int (little-endian) - 0xEA, 0x00, 0x00, 0x00, // Signing timestamp: 234 as 64-bit int... - 0x00, 0x00, 0x00, 0x00, // ...(little-endian) - 0x00, // Length of the empty public key }; - std::vector signature = TestSig::V2Serialized(); - serialized.insert(serialized.end(), signature.begin(), signature.end()); - return serialized; } @@ -845,8 +834,6 @@ BOOST_AUTO_TEST_CASE(it_initializes_to_an_invalid_contract_by_default) BOOST_CHECK(contract.m_value.empty() == true); BOOST_CHECK(contract.m_signature.Raw().empty() == true); BOOST_CHECK(contract.m_public_key.Key() == CPubKey()); - BOOST_CHECK(contract.m_nonce == 0); - BOOST_CHECK(contract.m_timestamp == 0); BOOST_CHECK(contract.m_tx_timestamp == 0); } @@ -865,8 +852,6 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_components_for_a_new_contract) BOOST_CHECK(contract.m_value == "bar"); BOOST_CHECK(contract.m_signature.Raw().empty() == true); BOOST_CHECK(contract.m_public_key.Key() == CPubKey()); - BOOST_CHECK(contract.m_nonce == 0); - BOOST_CHECK(contract.m_timestamp == 0); BOOST_CHECK(contract.m_tx_timestamp == 0); } @@ -880,8 +865,6 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_components_from_a_contract_message) "bar", NN::Contract::Signature(TestSig::V2Bytes()), NN::Contract::PublicKey(TestKey::Public()), - 123, // Nonce - 456, // Signing timestamp 789); // Tx timestamp BOOST_CHECK(contract.m_version == NN::Contract::CURRENT_VERSION); @@ -891,8 +874,6 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_components_from_a_contract_message) BOOST_CHECK(contract.m_value == "bar"); BOOST_CHECK(contract.m_signature.Raw() == TestSig::V2Bytes()); BOOST_CHECK(contract.m_public_key.Key() == TestKey::Public()); - BOOST_CHECK(contract.m_nonce == 123); - BOOST_CHECK(contract.m_timestamp == 456); BOOST_CHECK(contract.m_tx_timestamp == 789); } @@ -993,16 +974,6 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_contract_is_complete) contract = TestMessage::Current(); contract.m_signature = NN::Contract::Signature(TestSig::InvalidBytes()); BOOST_CHECK(contract.WellFormed() == true); - - // In addition to v1, a contract must contain a nonce: - contract = TestMessage::Current(); - contract.m_nonce = 0; - BOOST_CHECK(contract.WellFormed() == false); - - // In addition to v1, a contract must contain a signing timestamp: - contract = TestMessage::Current(); - contract.m_timestamp = 0; - BOOST_CHECK(contract.WellFormed() == false); } BOOST_AUTO_TEST_CASE(it_determines_whether_a_legacy_v1_contract_is_complete) @@ -1029,20 +1000,11 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_contract_is_valid) NN::Contract contract = TestMessage::Current(); BOOST_CHECK(contract.Validate() == true); - // Valid() DOES verify the signature: + // Version 2+ contracts rely on the signatures in the transactions instead + // of embedding another signature in the contract: contract = TestMessage::Current(); contract.m_signature = NN::Contract::Signature(TestSig::InvalidBytes()); - BOOST_CHECK(contract.Validate() == false); - - // In addition to v1, a contract must contain a nonce: - contract = TestMessage::Current(); - contract.m_nonce = 0; - BOOST_CHECK(contract.Validate() == false); - - // In addition to v1, a contract must contain a signing timestamp: - contract = TestMessage::Current(); - contract.m_timestamp = 0; - BOOST_CHECK(contract.Validate() == false); + BOOST_CHECK(contract.Validate() == true); } BOOST_AUTO_TEST_CASE(it_determines_whether_a_legacy_v1_contract_is_valid) @@ -1114,10 +1076,6 @@ BOOST_AUTO_TEST_CASE(it_signs_a_message_with_a_supplied_private_key) CKey private_key = TestKey::Private(); BOOST_CHECK(contract.Sign(private_key) == true); - BOOST_CHECK(contract.m_nonce > 0); - - // This assertion will fail if it takes longer than 1 second to sign: - BOOST_CHECK(contract.m_timestamp > GetAdjustedTime() - 1); // Build the message body to hash to verify the new signature: std::vector body { @@ -1127,22 +1085,6 @@ BOOST_AUTO_TEST_CASE(it_signs_a_message_with_a_supplied_private_key) 0x04, 0x74, 0x65, 0x73, 0x74, // Value: "test" preceeded by length }; - // Add contract nonce bytes for hash (32-bit int, little-endian) - body.push_back(contract.m_nonce & 0xFF); - body.push_back((contract.m_nonce >> 8) & 0xFF); - body.push_back((contract.m_nonce >> 16) & 0xFF); - body.push_back((contract.m_nonce >> 24) & 0xFF); - - // Add contract timestamp bytes for hash (64-bit int, little-endian) - body.push_back(contract.m_timestamp & 0xFF); - body.push_back((contract.m_timestamp >> 8) & 0xFF); - body.push_back((contract.m_timestamp >> 16) & 0xFF); - body.push_back((contract.m_timestamp >> 24) & 0xFF); - body.push_back((contract.m_timestamp >> 32) & 0xFF); - body.push_back((contract.m_timestamp >> 40) & 0xFF); - body.push_back((contract.m_timestamp >> 48) & 0xFF); - body.push_back((contract.m_timestamp >> 56) & 0xFF); - uint256 hashed = Hash(body.begin(), body.end()); BOOST_CHECK(contract.m_signature.Viable() == true); @@ -1161,10 +1103,6 @@ BOOST_AUTO_TEST_CASE(it_signs_a_legacy_v1_message_with_a_supplied_private_key) BOOST_CHECK(contract.Sign(private_key) == true); - // Nonce and signing timestamp not generated for legacy v1 contracts: - BOOST_CHECK(contract.m_nonce == 0); - BOOST_CHECK(contract.m_timestamp == 0); - // Build the message body to hash to verify the new signature: std::string body = "beacontesttest"; uint256 hashed = Hash(body.begin(), body.end()); @@ -1190,22 +1128,6 @@ BOOST_AUTO_TEST_CASE(it_signs_a_message_with_the_shared_message_private_key) 0x04, 0x74, 0x65, 0x73, 0x74, // Value: "test" preceeded by length }; - // Add contract nonce bytes for hash (32-bit int, little-endian) - body.push_back(contract.m_nonce & 0xff); - body.push_back((contract.m_nonce >> 8) & 0xff); - body.push_back((contract.m_nonce >> 16) & 0xff); - body.push_back((contract.m_nonce >> 24) & 0xff); - - // Add contract timestamp bytes for hash (64-bit int, little-endian) - body.push_back(contract.m_timestamp & 0xff); - body.push_back((contract.m_timestamp >> 8) & 0xff); - body.push_back((contract.m_timestamp >> 16) & 0xff); - body.push_back((contract.m_timestamp >> 24) & 0xff); - body.push_back((contract.m_timestamp >> 32) & 0xff); - body.push_back((contract.m_timestamp >> 40) & 0xff); - body.push_back((contract.m_timestamp >> 48) & 0xff); - body.push_back((contract.m_timestamp >> 56) & 0xff); - uint256 hashed = Hash(body.begin(), body.end()); CKey key; key.SetPrivKey(NN::Contract::MessagePrivateKey()); @@ -1246,16 +1168,6 @@ BOOST_AUTO_TEST_CASE(it_verifies_a_contract_signature) contract = TestMessage::Current(); contract.m_action = NN::ContractAction::REMOVE; BOOST_CHECK(contract.VerifySignature() == false); - - // V2 contracts must additionally sign the nonce: - contract = TestMessage::Current(); - contract.m_nonce = 999; - BOOST_CHECK(contract.VerifySignature() == false); - - // V2 contracts must additionally sign the signing timestamp: - contract = TestMessage::Current(); - contract.m_timestamp = 999; - BOOST_CHECK(contract.VerifySignature() == false); } BOOST_AUTO_TEST_CASE(it_verifies_a_legacy_v1_contract_signature) @@ -1271,13 +1183,6 @@ BOOST_AUTO_TEST_CASE(it_verifies_a_legacy_v1_contract_signature) // Test a message with an invalid signature: contract = NN::Contract::Parse(TestMessage::InvalidV1String(), 123); BOOST_CHECK(contract.VerifySignature() == false); - - // V1 contracts must NOT sign the action, nonce, or signing timestamp - // (but we can't test action without the master private key): - contract = NN::Contract::Parse(TestMessage::V1String(), 123); - contract.m_nonce = 999; - contract.m_timestamp = 999; - BOOST_CHECK(contract.VerifySignature() == true); } BOOST_AUTO_TEST_CASE(it_generates_a_hash_of_a_contract_body) @@ -1305,14 +1210,12 @@ BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) { NN::Contract contract = TestMessage::Current(); - // 101 bytes = 4 bytes for the serialization format version + // 16 bytes = 4 bytes for the serialization protocol version // + 1 byte each for the type and action - // + 4 bytes for the nonce (32-bit int) - // + 8 bytes for the signing timestamp (64-bit int) // + 5 bytes each for key and value (4 for string, 1 for size) // + 1 byte for the empty public key size - // + 72 bytes for the signature (71 + 1 for the size) - BOOST_CHECK(GetSerializeSize(contract, SER_NETWORK, 1) == 101); + // + BOOST_CHECK(GetSerializeSize(contract, SER_NETWORK, 1) == 16); CDataStream stream(SER_NETWORK, 1); @@ -1337,10 +1240,10 @@ BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) BOOST_CHECK(contract.m_action == NN::ContractAction::ADD); BOOST_CHECK(contract.m_key == "test"); BOOST_CHECK(contract.m_value == "test"); + // Version 2+ contracts rely on the signatures in the transactions instead + // of embedding another signature in the contract: BOOST_CHECK(contract.m_public_key == CPubKey()); - BOOST_CHECK(contract.m_signature.Raw() == TestSig::V2Bytes()); - BOOST_CHECK(contract.m_nonce == 123); - BOOST_CHECK(contract.m_timestamp == 234); + BOOST_CHECK(contract.m_signature.Raw().empty() == true); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/neuralnet/project_tests.cpp b/src/test/neuralnet/project_tests.cpp index 8b5c3298a8..2b11662f81 100644 --- a/src/test/neuralnet/project_tests.cpp +++ b/src/test/neuralnet/project_tests.cpp @@ -22,12 +22,9 @@ NN::Contract contract(std::string key, std::string value) key, value, // Signature checked before passing to handler, so we don't need to - // give specific signature, public key, nonce, or signing timestamp - // values here: + // give specific signature or public key here: NN::Contract::Signature(), NN::Contract::PublicKey(), - 0, - 0, 1234567); } } // anonymous namespace diff --git a/src/wallet.cpp b/src/wallet.cpp index 5197f82b8d..893ce7eab4 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -32,11 +32,7 @@ unsigned int nDerivationMethodIndex; namespace NN { std::string GetPrimaryCpid(); } -////////////////////////////////////////////////////////////////////////////// -// -// mapWallet -// - +namespace { struct CompareValueOnly { bool operator()(const pair >& t1, @@ -45,6 +41,12 @@ struct CompareValueOnly return t1.first < t2.first; } }; +} // anonymous namespace + +////////////////////////////////////////////////////////////////////////////// +// +// mapWallet +// CPubKey CWallet::GenerateNewKey() { @@ -1767,6 +1769,7 @@ bool CWallet::CreateTransaction(CScript scriptPubKey, int64_t nValue, CWalletTx& return CreateTransaction(vecSend, wtxNew, reservekey, nFeeRet, coinControl); } + bool CWallet::GetStakeWeight(uint64_t& nWeight) { // Choose coins to use @@ -1963,8 +1966,6 @@ string CWallet::SendMoneyToDestination(const CTxDestination& address, int64_t nV } - - DBErrors CWallet::LoadWallet(bool& fFirstRunRet) { if (!fFileBacked) From 44313a409b4b8c37345893da5ca8848d6a281be0 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 28 Apr 2020 20:41:45 -0500 Subject: [PATCH 12/15] Move storage of contract master keys into wallet --- src/main.cpp | 2 +- src/neuralnet/contract.cpp | 37 ++-------------------- src/neuralnet/contract.h | 22 -------------- src/rpcblockchain.cpp | 23 ++++++++++---- src/test/neuralnet/contract_tests.cpp | 13 ++------ src/test/wallet_tests.cpp | 18 +++++++++++ src/wallet.cpp | 43 +++++++++++++++++++++++--- src/wallet.h | 44 +++++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 78 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5f8b6170b9..ea5df2cd72 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1234,7 +1234,7 @@ bool CTransaction::CheckContracts(const MapPrevTx& inputs) const bool CTransaction::HasMasterKeyInput(const MapPrevTx& inputs) const { - const CTxDestination master_address = NN::Contract::MasterAddress().Get(); + const CTxDestination master_address = CWallet::MasterAddress().Get(); for (const auto& input : vin) { const CTxOut& prev_out = GetOutputFor(input, inputs); diff --git a/src/neuralnet/contract.cpp b/src/neuralnet/contract.cpp index 1945a0e1d9..39aec18a20 100644 --- a/src/neuralnet/contract.cpp +++ b/src/neuralnet/contract.cpp @@ -4,6 +4,7 @@ #include "neuralnet/beacon.h" #include "neuralnet/project.h" #include "util.h" +#include "wallet.h" using namespace NN; @@ -230,40 +231,6 @@ Contract::Contract( { } -const CPubKey& Contract::MasterPublicKey() -{ - // If the master key changes, add a conditional entry to this method that - // returns the new key for the appropriate height. - - // 049ac003b3318d9fe28b2830f6a95a2624ce2a69fb0c0c7ac0b513efcc1e93a6a - // 6e8eba84481155dd82f2f1104e0ff62c69d662b0094639b7106abc5d84f948c0a - static const CPubKey since_block_0(std::vector { - 0x04, 0x9a, 0xc0, 0x03, 0xb3, 0x31, 0x8d, 0x9f, 0xe2, 0x8b, 0x28, - 0x30, 0xf6, 0xa9, 0x5a, 0x26, 0x24, 0xce, 0x2a, 0x69, 0xfb, 0x0c, - 0x0c, 0x7a, 0xc0, 0xb5, 0x13, 0xef, 0xcc, 0x1e, 0x93, 0xa6, 0xa6, - 0xe8, 0xeb, 0xa8, 0x44, 0x81, 0x15, 0x5d, 0xd8, 0x2f, 0x2f, 0x11, - 0x04, 0xe0, 0xff, 0x62, 0xc6, 0x9d, 0x66, 0x2b, 0x00, 0x94, 0x63, - 0x9b, 0x71, 0x06, 0xab, 0xc5, 0xd8, 0x4f, 0x94, 0x8c, 0x0a - }); - - return since_block_0; -} - -const CPrivKey Contract::MasterPrivateKey() -{ - std::vector key = ParseHex(GetArgument("masterprojectkey", "")); - - return CPrivKey(key.begin(), key.end()); -} - -const CBitcoinAddress Contract::MasterAddress() -{ - CBitcoinAddress master_address; - master_address.Set(NN::Contract::MasterPublicKey().GetID()); - - return master_address; -} - const CPubKey& Contract::MessagePublicKey() { // If the message key changes, add a conditional entry to this method that @@ -400,7 +367,7 @@ const CPubKey& Contract::ResolvePublicKey() const } if (RequiresMasterKey()) { - return MasterPublicKey(); + return CWallet::MasterPublicKey(); } return m_public_key.Key(); diff --git a/src/neuralnet/contract.h b/src/neuralnet/contract.h index b4faba1213..a2bca0c18f 100644 --- a/src/neuralnet/contract.h +++ b/src/neuralnet/contract.h @@ -519,28 +519,6 @@ class Contract PublicKey public_key, int64_t tx_timestamp); - //! - //! \brief Get the master public key used to verify administrative contracts. - //! - //! \return A \c CPubKey object containing the master public key. - //! - static const CPubKey& MasterPublicKey(); - - //! - //! \brief Get the master private key provided by configuration or command- - //! line option. - //! - //! \return An empty key (vector) when no master key configured. - //! - static const CPrivKey MasterPrivateKey(); - - //! - //! \brief Get the output address controlled by the master private key. - //! - //! \return Address as calculated from the master public key. - //! - static const CBitcoinAddress MasterAddress(); - //! //! \brief Get the message public key used to verify public contracts. //! diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 637ef8d57b..27c19dc04c 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -1110,6 +1110,15 @@ UniValue superblocks(const UniValue& params, bool fHelp) //! //! \brief Send a transaction that contains an administrative contract. //! +//! Before invoking this command, import the master key used to sign and verify +//! transactions that contain administrative contracts. The label is optional: +//! +//! importprivkey master +//! +//! Send some coins to the master key address if necessary: +//! +//! sendtoaddress
+//! //! To whitelist a project: //! //! addkey add project projectname url @@ -1143,14 +1152,16 @@ UniValue addkey(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); } - CKey key; - // TODO: remove this after Elizabeth mandatory block. We don't need to sign // version 2 contracts (the signature is discarded after the threshold): - if (!key.SetPrivKey(NN::Contract::MasterPrivateKey()) - || key.GetPubKey() != NN::Contract::MasterPublicKey() - ) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid master key."); + CKey key = pwalletMain->MasterPrivateKey(); + + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Missing or invalid master key."); + } + + if (key.GetPubKey() != CWallet::MasterPublicKey()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Master private key mismatch."); } NN::ContractAction action = NN::ContractAction::UNKNOWN; diff --git a/src/test/neuralnet/contract_tests.cpp b/src/test/neuralnet/contract_tests.cpp index 254b76462b..f2f0be455e 100644 --- a/src/test/neuralnet/contract_tests.cpp +++ b/src/test/neuralnet/contract_tests.cpp @@ -1,4 +1,5 @@ #include "neuralnet/contract.h" +#include "wallet.h" #include #include @@ -877,18 +878,10 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_components_from_a_contract_message) BOOST_CHECK(contract.m_tx_timestamp == 789); } -BOOST_AUTO_TEST_CASE(it_provides_the_master_and_message_keys) +BOOST_AUTO_TEST_CASE(it_provides_the_message_keys) { - // Master private key read from configuration or command-line option: - SetArgument("masterprojectkey", "1234"); - - BOOST_CHECK(NN::Contract::MasterPrivateKey().size() == 2); - BOOST_CHECK(NN::Contract::MasterPublicKey().Raw().size() == 65); BOOST_CHECK(NN::Contract::MessagePrivateKey().size() == 279); BOOST_CHECK(NN::Contract::MessagePublicKey().Raw().size() == 65); - - SetArgument("masterprojectkey", ""); - BOOST_CHECK(NN::Contract::MasterPrivateKey().empty() == true); } BOOST_AUTO_TEST_CASE(it_provides_the_contract_burn_address) @@ -1062,7 +1055,7 @@ BOOST_AUTO_TEST_CASE(it_resolves_the_appropriate_public_key_for_a_contract) contract.m_type = NN::ContractType::PROJECT; - BOOST_CHECK(contract.ResolvePublicKey() == NN::Contract::MasterPublicKey()); + BOOST_CHECK(contract.ResolvePublicKey() == CWallet::MasterPublicKey()); } BOOST_AUTO_TEST_CASE(it_signs_a_message_with_a_supplied_private_key) diff --git a/src/test/wallet_tests.cpp b/src/test/wallet_tests.cpp index 76b9cb2e95..2c4a8f24c2 100755 --- a/src/test/wallet_tests.cpp +++ b/src/test/wallet_tests.cpp @@ -54,6 +54,24 @@ static bool equal_sets(CoinSet a, CoinSet b) return ret.first == a.end() && ret.second == b.end(); } +BOOST_AUTO_TEST_CASE(it_embeds_the_contract_master_public_key) +{ + BOOST_CHECK(CWallet::MasterPublicKey().Raw().size() == 65); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_contract_master_address) +{ + BOOST_CHECK(CWallet::MasterAddress().IsValid() == true); +} + +BOOST_AUTO_TEST_CASE(it_provides_the_master_private_key) +{ + // We can't actually test that the wallet contains the master private key + // without importing the key itself, but we can check that the wallet tries + // to fetch it unsuccessfully: + BOOST_CHECK(wallet.MasterPrivateKey().IsValid() == false); +} + BOOST_AUTO_TEST_CASE(coin_selection_tests) { static CoinSet setCoinsRet, setCoinsRet2; diff --git a/src/wallet.cpp b/src/wallet.cpp index 893ce7eab4..608e723eaf 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -43,10 +43,45 @@ struct CompareValueOnly }; } // anonymous namespace -////////////////////////////////////////////////////////////////////////////// -// -// mapWallet -// +// ----------------------------------------------------------------------------- +// Class: CWallet +// ----------------------------------------------------------------------------- + +const CPubKey& CWallet::MasterPublicKey() +{ + // If the master key changes, add a conditional entry to this method that + // returns the new key for the appropriate height. + + // 049ac003b3318d9fe28b2830f6a95a2624ce2a69fb0c0c7ac0b513efcc1e93a6a + // 6e8eba84481155dd82f2f1104e0ff62c69d662b0094639b7106abc5d84f948c0a + static const CPubKey since_block_0({ + 0x04, 0x9a, 0xc0, 0x03, 0xb3, 0x31, 0x8d, 0x9f, 0xe2, 0x8b, 0x28, + 0x30, 0xf6, 0xa9, 0x5a, 0x26, 0x24, 0xce, 0x2a, 0x69, 0xfb, 0x0c, + 0x0c, 0x7a, 0xc0, 0xb5, 0x13, 0xef, 0xcc, 0x1e, 0x93, 0xa6, 0xa6, + 0xe8, 0xeb, 0xa8, 0x44, 0x81, 0x15, 0x5d, 0xd8, 0x2f, 0x2f, 0x11, + 0x04, 0xe0, 0xff, 0x62, 0xc6, 0x9d, 0x66, 0x2b, 0x00, 0x94, 0x63, + 0x9b, 0x71, 0x06, 0xab, 0xc5, 0xd8, 0x4f, 0x94, 0x8c, 0x0a + }); + + return since_block_0; +} + +const CBitcoinAddress CWallet::MasterAddress() +{ + CBitcoinAddress master_address; + master_address.Set(MasterPublicKey().GetID()); + + return master_address; +} + +CKey CWallet::MasterPrivateKey() const +{ + CKey key_out; + + GetKey(MasterPublicKey().GetID(), key_out); + + return key_out; +} CPubKey CWallet::GenerateNewKey() { diff --git a/src/wallet.h b/src/wallet.h index 666f892cab..d44a8e29ee 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -170,6 +170,7 @@ class CWallet : public CCryptoKeyStore { SetNull(); } + CWallet(std::string strWalletFileIn) { SetNull(); @@ -177,6 +178,49 @@ class CWallet : public CCryptoKeyStore strWalletFile = strWalletFileIn; fFileBacked = true; } + + //! + //! \brief Get the public key used to verify administrative contracts. + //! + //! The wallet embeds the master public key so that every node can verify + //! the authenticity of administrative contracts like a project whitelist + //! addition or removal. The master key holders must import a private key + //! that corresponds to this public key before they can sign contracts. + //! + //! \return A \c CPubKey object containing the master public key. + //! + static const CPubKey& MasterPublicKey(); + + //! + //! \brief Get the output address controlled by the master private key used + //! to verify administrative contracts. + //! + //! \return Address as calculated from the master public key. + //! + static const CBitcoinAddress MasterAddress(); + + //! + //! \brief Get the imported master private key used to sign administrative + //! contracts. + //! + //! A master key holder uses the master private key to sign administrative + //! contracts, such as project whitelist additions and removals. All nodes + //! verify the contracts using the embedded public key that corresponds to + //! this private key. + //! + //! Key holders need to import the private key into the wallet before they + //! can sign administrative contracts. The following RPC command imports a + //! private key and assigns to it the optional label of "master": + //! + //! importprivkey master + //! + //! Note that this private key differs from the wallet keystore's "master" + //! key which the wallet uses to encrypt the private keys in storage. + //! + //! \return An empty key when no master key imported. + //! + CKey MasterPrivateKey() const; + void SetNull() { nWalletVersion = FEATURE_BASE; From d1085ff2f6e602ec8230f7a189316e8f2cf1b2d7 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Thu, 30 Apr 2020 19:29:43 -0500 Subject: [PATCH 13/15] Add CONTRACT logging category --- src/init.cpp | 4 ++-- src/logging.cpp | 1 + src/logging.h | 1 + src/main.cpp | 2 +- src/neuralnet/contract.cpp | 6 +----- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index ba87c63876..a6813674a8 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -1123,9 +1123,9 @@ bool AppInit2(ThreadHandlerPtr threads) // ********************************************************* Step 11: start node uiInterface.InitMessage(_("Loading Persisted Data Cache...")); - LogPrint(BCLog::LogFlags::NET, "Loading admin Messages"); + LogPrint(BCLog::LogFlags::CONTRACT, "Loading admin Messages"); LoadAdminMessages(true); - LogPrintf("Done loading Admin messages"); + LogPrint(BCLog::LogFlags::CONTRACT, "Done loading Admin messages"); uiInterface.InitMessage(_("Finding first applicable Research Project...")); NN::Researcher::Reload(); diff --git a/src/logging.cpp b/src/logging.cpp index f9a1f14b12..880d4d6bf6 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -181,6 +181,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::ALERT, "alert"}, {BCLog::TALLY, "tally"}, {BCLog::ACCRUAL, "accrual"}, + {BCLog::CONTRACT, "contract"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, }; diff --git a/src/logging.h b/src/logging.h index 67812ab898..0493b05fcf 100644 --- a/src/logging.h +++ b/src/logging.h @@ -82,6 +82,7 @@ namespace BCLog { ALERT = (1 << 24), TALLY = (1 << 25), ACCRUAL = (1 << 26), + CONTRACT = (1 << 27), ALL = ~(uint32_t)0, }; diff --git a/src/main.cpp b/src/main.cpp index ea5df2cd72..8546abb070 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2999,7 +2999,7 @@ bool DisconnectBlocksBatch(CTxDB& txdb, list& vResurrect, unsigned return error("DisconnectBlocksBatch: TxnCommit failed"); /*fatal*/ // Need to reload all contracts - if (fDebug10) LogPrintf("DisconnectBlocksBatch: LoadAdminMessages"); + LogPrint(BCLog::LogFlags::CONTRACT, "%s: LoadAdminMessages", __func__); LoadAdminMessages(true); NN::Quorum::LoadSuperblockIndex(pindexBest); diff --git a/src/neuralnet/contract.cpp b/src/neuralnet/contract.cpp index 39aec18a20..6e43ab3afc 100644 --- a/src/neuralnet/contract.cpp +++ b/src/neuralnet/contract.cpp @@ -464,11 +464,7 @@ std::string Contract::ToString() const void Contract::Log(const std::string& prefix) const { // TODO: temporary... needs better logging - if (!fDebug) { - return; - } - - LogPrintf( + LogPrint(BCLog::LogFlags::CONTRACT, ": %s: v%d, %d, %s, %s, %s, %s, %s, %s", prefix, m_version, From e5a4454e3d90d26c7c487cb6839479ddf453a5fe Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Fri, 1 May 2020 00:19:30 -0500 Subject: [PATCH 14/15] Add some logging for CreateTransaction() failures CWallet::CreateTransaction() fails silently. This adds some messages that help to identify what caused a failure. --- src/wallet.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/wallet.cpp b/src/wallet.cpp index 608e723eaf..be19d348e1 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -1628,11 +1628,11 @@ bool CWallet::CreateTransaction(const vector >& vecSend, for (auto const& s : vecSend) { if (nValueOut < 0) - return false; + return error("%s: invalid output value: %" PRId64, __func__, nValueOut); nValueOut += s.second; } if (vecSend.empty() || nValueOut < 0) - return false; + return error("%s: invalid output value: %" PRId64, __func__, nValueOut); wtxNew.BindWallet(this); @@ -1684,8 +1684,9 @@ bool CWallet::CreateTransaction(const vector >& vecSend, // smallest UTXOs available: const bool contract = !coinControl && !wtxNew.vContracts.empty(); - if (!SelectCoins(nTotalValue, wtxNew.nTime, setCoins, nValueIn, coinControl, contract)) - return false; + if (!SelectCoins(nTotalValue, wtxNew.nTime, setCoins, nValueIn, coinControl, contract)) { + return error("%s: Failed to select coins", __func__); + } } else { @@ -1755,13 +1756,16 @@ bool CWallet::CreateTransaction(const vector >& vecSend, // Sign int nIn = 0; for (auto const& coin : setCoins) - if (!SignSignature(*this, *coin.first, wtxNew, nIn++)) - return false; + if (!SignSignature(*this, *coin.first, wtxNew, nIn++)) { + return error("%s: Failed to sign tx", __func__); + } // Limit size unsigned int nBytes = ::GetSerializeSize(*(CTransaction*)&wtxNew, SER_NETWORK, PROTOCOL_VERSION); - if (nBytes >= MAX_STANDARD_TX_SIZE) - return false; + if (nBytes >= MAX_STANDARD_TX_SIZE) { + return error("%s: tx size %d greater than standard %d", __func__, nBytes, MAX_STANDARD_TX_SIZE); + } + dPriority /= nBytes; // Check that enough fee is included From a77bb7082b50a1260ddb2273c07a1cce95b6d2b2 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Sat, 2 May 2020 17:43:09 -0500 Subject: [PATCH 15/15] Set 0.5 GRC burn value for contract transactions --- src/neuralnet/contract.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/neuralnet/contract.h b/src/neuralnet/contract.h index a2bca0c18f..c2c962e8d2 100644 --- a/src/neuralnet/contract.h +++ b/src/neuralnet/contract.h @@ -216,9 +216,7 @@ class Contract //! \brief The amount of coin set for a burn output in a transaction that //! broadcasts a contract in units of 1/100000000 GRC. //! - //! Currently, we burn the smallest amount possible (0.00000001). - //! - static constexpr int64_t BURN_AMOUNT = 1; + static constexpr int64_t BURN_AMOUNT = 0.5 * COIN; //! //! \brief A contract type from a transaction message.