diff --git a/doc/release-notes/release-notes-5.6.0.md b/doc/release-notes/release-notes-5.6.0.md new file mode 100644 index 0000000000000..f268531f6e677 --- /dev/null +++ b/doc/release-notes/release-notes-5.6.0.md @@ -0,0 +1,85 @@ +PIVX Core version *v5.6.0* is now available from: + +This is a new major version release, including various bug fixes and performance improvements, as well as updated translations. + +Please report bugs using the issue tracker at github: + + +How to Upgrade +============== + +If you are running an older version, shut it down. Wait until it has completely shut down (which might take a few minutes for older versions), then run the installer (on Windows) or just copy over /Applications/PIVX-Qt (on Mac) or pivxd/pivx-qt (on Linux). + +Notable Changes +============== + +(Developers: add your notes here as part of your pull requests whenever possible) + + +### Deprecated autocombinerewards RPC Command + +The `autocombinerewards` RPC command was soft-deprecated in v5.3.0 and replaced with explicit setter/getter commands `setautocombinethreshold`/`getautocombinethreshold`. PIVX Core, by default, will no longer accept the `autocombinerewards` command, returning a deprecation error, unless the `pivxd`/`pivx-qt` is started with the `-deprecatedrpc=autocombinerewards` option. + +This command will be fully removed in v6.0.0. + +### Shield address support for RPC label commands + +The `setlabel` RPC command now supports a shield address input argument to allow users to set labels for shield addresses. Additionally, the `getaddressesbylabel` RPC command will also now return shield addresses with a matching label. + +### Specify optional label for getnewshieldaddress + +The `getnewshieldaddress` RPC command now takes an optional argument `label (string)` to denote the desired label for the generated address. + +### New getnewexchangeaddress RPC Command + +The `getnewexchangeaddress` RPC command has been introduced to create an essentially fully Transparent address to disallow deshielding to in compliance for exchanges. It takes an optional `label (string)` to set a label for the address if necessary. Functionality is the same comparitively to `getnewaddress`, `getnewstakingaddress` and `getnewshieldaddress` + +| Command Name | Purpose | Requires Unlocked Wallet? | +| ------------ | ------- | ------------------------- | +| `getnewexchangeaddress` | Creates a new exchange address | Yes | + +Command is detailed below: + +* `getnewexchangeaddress` + ``` + Returns a new exchange address for receiving payments. + + Result: + "address" (string) The new exchange address. + ``` + +P2P connection management +-------------------------- + +- Peers manually added through the addnode option or addnode RPC now have their own + limit of sixteen connections which does not compete with other inbound or outbound + connection usage and is not subject to the maxconnections limitation. + +- New connections to manually added peers are much faster. + +*version* Change log +============== + +Detailed release notes follow. This overview includes changes that affect behavior, not code moves, refactors and string updates. For convenience in locating the code changes and accompanying discussion, both the pull request and git merge commit are mentioned. + +### Core Features + +### Build System + +### P2P Protocol and Network Code + +### GUI + +### RPC/REST +- #2895 Exchange Address `getnewexchangeaddress` (Liquid369) + +### Wallet + +### Miscellaneous + +## Credits + +Thanks to everyone who directly contributed to this release: + + +As well as everyone that helped translating on [Transifex](https://www.transifex.com/projects/p/pivx-project-translations/), the QA team during Testing and the Node hosts supporting our Testnet. diff --git a/src/addressbook.cpp b/src/addressbook.cpp index 6b590a7ad2335..2f8c93b9e4120 100644 --- a/src/addressbook.cpp +++ b/src/addressbook.cpp @@ -16,6 +16,7 @@ namespace AddressBook { const std::string COLD_STAKING_SEND{"coldstaking_send"}; const std::string SHIELDED_RECEIVE{"shielded_receive"}; const std::string SHIELDED_SEND{"shielded_spend"}; + const std::string EXCHANGE_ADDRESS{"exchange_address"}; } bool IsColdStakingPurpose(const std::string& purpose) { @@ -28,6 +29,10 @@ namespace AddressBook { || purpose == AddressBookPurpose::SHIELDED_SEND; } + bool IsExchangePurpose(const std::string& purpose) { + return purpose == AddressBookPurpose::EXCHANGE_ADDRESS; + } + bool CAddressBookData::isSendColdStakingPurpose() const { return purpose == AddressBookPurpose::COLD_STAKING_SEND; } diff --git a/src/addressbook.h b/src/addressbook.h index 09a4f454443b2..37f15b3018568 100644 --- a/src/addressbook.h +++ b/src/addressbook.h @@ -20,10 +20,12 @@ namespace AddressBook { extern const std::string COLD_STAKING_SEND; extern const std::string SHIELDED_RECEIVE; extern const std::string SHIELDED_SEND; + extern const std::string EXCHANGE_ADDRESS; } bool IsColdStakingPurpose(const std::string& purpose); bool IsShieldedPurpose(const std::string& purpose); + bool IsExchangePurpose(const std::string& purpose); /** Address book data */ class CAddressBookData { diff --git a/src/blocksignature.cpp b/src/blocksignature.cpp index a8f21904c6f04..6fb9b6058a9c1 100644 --- a/src/blocksignature.cpp +++ b/src/blocksignature.cpp @@ -21,6 +21,10 @@ static bool GetKeyIDFromUTXO(const CTxOut& utxo, CKeyID& keyIDRet) keyIDRet = CKeyID(uint160(vSolutions[0])); return true; } + if (whichType == TX_EXCHANGEADDR) { + keyIDRet = CExchangeKeyID(uint160(vSolutions[0])); + return true; + } return false; } diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e07477288c4b8..de55381dad583 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -293,6 +293,7 @@ class CMainParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_2].nActivationHeight = 2927000; consensus.vUpgrades[Consensus::UPGRADE_V5_3].nActivationHeight = 3014000; consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 3715200; + consensus.vUpgrades[Consensus::UPGRADE_V5_6].nActivationHeight = 4281680; // Estimate Feb 29th 12:00 UTC consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; @@ -328,6 +329,7 @@ class CMainParams : public CChainParams base58Prefixes[PUBKEY_ADDRESS] = std::vector(1, 30); base58Prefixes[SCRIPT_ADDRESS] = std::vector(1, 13); base58Prefixes[STAKING_ADDRESS] = std::vector(1, 63); // starting with 'S' + base58Prefixes[EXCHANGE_ADDRESS] = {0x01, 0xb9}; // starts with EX base58Prefixes[SECRET_KEY] = std::vector(1, 212); base58Prefixes[EXT_PUBLIC_KEY] = {0x02, 0x2D, 0x25, 0x33}; base58Prefixes[EXT_SECRET_KEY] = {0x02, 0x21, 0x31, 0x2B}; @@ -451,6 +453,7 @@ class CTestNetParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_2].nActivationHeight = 262525; consensus.vUpgrades[Consensus::UPGRADE_V5_3].nActivationHeight = 332300; consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 925056; + consensus.vUpgrades[Consensus::UPGRADE_V5_6].nActivationHeight = 1624280; // Estimate Feb 23 Midnight UTC consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; @@ -472,6 +475,7 @@ class CTestNetParams : public CChainParams base58Prefixes[PUBKEY_ADDRESS] = std::vector(1, 139); // Testnet pivx addresses start with 'x' or 'y' base58Prefixes[SCRIPT_ADDRESS] = std::vector(1, 19); // Testnet pivx script addresses start with '8' or '9' base58Prefixes[STAKING_ADDRESS] = std::vector(1, 73); // starting with 'W' + base58Prefixes[EXCHANGE_ADDRESS] = {0x01, 0xb9, 0xb1}; // EXT prefix for the address base58Prefixes[SECRET_KEY] = std::vector(1, 239); // Testnet private keys start with '9' or 'c' (Bitcoin defaults) // Testnet pivx BIP32 pubkeys start with 'DRKV' base58Prefixes[EXT_PUBLIC_KEY] = {0x3a, 0x80, 0x61, 0xa0}; @@ -601,6 +605,7 @@ class CRegTestParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_2].nActivationHeight = 300; consensus.vUpgrades[Consensus::UPGRADE_V5_3].nActivationHeight = 251; consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 576; + consensus.vUpgrades[Consensus::UPGRADE_V5_6].nActivationHeight = 1000; consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; @@ -618,6 +623,7 @@ class CRegTestParams : public CChainParams base58Prefixes[PUBKEY_ADDRESS] = std::vector(1, 139); // Testnet pivx addresses start with 'x' or 'y' base58Prefixes[SCRIPT_ADDRESS] = std::vector(1, 19); // Testnet pivx script addresses start with '8' or '9' base58Prefixes[STAKING_ADDRESS] = std::vector(1, 73); // starting with 'W' + base58Prefixes[EXCHANGE_ADDRESS] = {0x01, 0xb9, 0xb1}; // EXT prefix for the address base58Prefixes[SECRET_KEY] = std::vector(1, 239); // Testnet private keys start with '9' or 'c' (Bitcoin defaults) // Testnet pivx BIP32 pubkeys start with 'DRKV' base58Prefixes[EXT_PUBLIC_KEY] = {0x3a, 0x80, 0x61, 0xa0}; diff --git a/src/chainparams.h b/src/chainparams.h index 1b95c3281afed..0fd25459c7162 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -51,6 +51,7 @@ class CChainParams EXT_SECRET_KEY, // BIP32 EXT_COIN_TYPE, // BIP44 STAKING_ADDRESS, + EXCHANGE_ADDRESS, MAX_BASE58_TYPES }; diff --git a/src/consensus/params.h b/src/consensus/params.h index b8523b6b14ead..7e89ada6ae197 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -37,6 +37,7 @@ enum UpgradeIndex : uint32_t { UPGRADE_V5_2, UPGRADE_V5_3, UPGRADE_V5_5, + UPGRADE_V5_6, UPGRADE_V6_0, UPGRADE_TESTDUMMY, // NOTE: Also add new upgrades to NetworkUpgradeInfo in upgrades.cpp diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 027a0eb3ae849..66776216ad5c4 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -115,9 +115,16 @@ bool CheckTransaction(const CTransaction& tx, CValidationState& state, bool fCol } } + bool hasExchangeUTXOs = tx.HasExchangeAddr(); + int nTxHeight = chainActive.Height(); + if (hasExchangeUTXOs && !Params().GetConsensus().NetworkUpgradeActive(nTxHeight, Consensus::UPGRADE_V5_6)) + return state.DoS(100, false, REJECT_INVALID, "bad-exchange-address-not-started"); + if (tx.IsCoinBase()) { if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > 150) return state.DoS(100, false, REJECT_INVALID, "bad-cb-length"); + if (hasExchangeUTXOs) + return state.DoS(100, false, REJECT_INVALID, "bad-exchange-address-in-cb"); } else { for (const CTxIn& txin : tx.vin) if (txin.prevout.IsNull() && !txin.IsZerocoinSpend()) diff --git a/src/consensus/upgrades.cpp b/src/consensus/upgrades.cpp index e313d831c9478..17490d592af3b 100644 --- a/src/consensus/upgrades.cpp +++ b/src/consensus/upgrades.cpp @@ -65,6 +65,10 @@ const struct NUInfo NetworkUpgradeInfo[Consensus::MAX_NETWORK_UPGRADES] = { /*.strName =*/ "PIVX_v5.5", /*.strInfo =*/ "New rewards structure", }, + { + /*.strName =*/ "PIVX_v5.6", + /*.strInfo =*/ "Exchange address", + }, { /*.strName =*/ "v6_evo", /*.strInfo =*/ "Deterministic Masternodes", diff --git a/src/destination_io.cpp b/src/destination_io.cpp index 11f9a4ac2d578..f7c0f5a1302aa 100644 --- a/src/destination_io.cpp +++ b/src/destination_io.cpp @@ -19,20 +19,21 @@ namespace Standard { CWDestination DecodeDestination(const std::string& strAddress) { bool isStaking = false; - return DecodeDestination(strAddress, isStaking); + bool isExchange = false; + return DecodeDestination(strAddress, isStaking, isExchange); } - CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking) + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isExchange) { bool isShielded = false; - return DecodeDestination(strAddress, isStaking, isShielded); + return DecodeDestination(strAddress, isStaking, isExchange, isShielded); } // agregar isShielded - CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isShielded) + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isExchange, bool& isShielded) { CWDestination dest; - CTxDestination regDest = ::DecodeDestination(strAddress, isStaking); + CTxDestination regDest = ::DecodeDestination(strAddress, isStaking, isExchange); if (!IsValidDestination(regDest)) { const auto sapDest = KeyIO::DecodeSaplingPaymentAddress(strAddress); if (sapDest) { @@ -70,6 +71,7 @@ Destination& Destination::operator=(const Destination& from) { this->dest = from.dest; this->isP2CS = from.isP2CS; + this->isExchange = from.isExchange; return *this; } @@ -86,6 +88,9 @@ std::string Destination::ToString() const // Invalid address return ""; } - return Standard::EncodeDestination(dest, isP2CS ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS); + CChainParams::Base58Type addrType = isP2CS ? CChainParams::STAKING_ADDRESS + : (isExchange ? CChainParams::EXCHANGE_ADDRESS + : CChainParams::PUBKEY_ADDRESS); + return Standard::EncodeDestination(dest, addrType); } diff --git a/src/destination_io.h b/src/destination_io.h index 0697f6b9fabb3..63a95d6862203 100644 --- a/src/destination_io.h +++ b/src/destination_io.h @@ -16,8 +16,8 @@ namespace Standard { std::string EncodeDestination(const CWDestination &address, const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); CWDestination DecodeDestination(const std::string& strAddress); - CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking); - CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isShielded); + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isExchange); + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isExchange, bool& isShielded); bool IsValidDestination(const CWDestination& dest); @@ -34,10 +34,12 @@ class Destination { public: explicit Destination() {} explicit Destination(const CTxDestination& _dest, bool _isP2CS) : dest(_dest), isP2CS(_isP2CS) {} + explicit Destination(const CTxDestination& _dest, bool _isP2CS, bool _isEXCHANGE = false) : dest(_dest), isP2CS(_isP2CS), isExchange(_isEXCHANGE) {} explicit Destination(const libzcash::SaplingPaymentAddress& _dest) : dest(_dest) {} CWDestination dest{CNoDestination()}; bool isP2CS{false}; + bool isExchange{false}; Destination& operator=(const Destination& from); // Returns the key ID if Destination is a transparent "regular" destination diff --git a/src/key_io.cpp b/src/key_io.cpp index e0b881743bf51..664e52f63fcda 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -31,6 +31,13 @@ namespace return EncodeBase58Check(data); } + std::string operator()(const CExchangeKeyID& id) const + { + std::vector data = m_params.Base58Prefix(CChainParams::EXCHANGE_ADDRESS); + data.insert(data.end(), id.begin(), id.end()); + return EncodeBase58Check(data); + } + std::string operator()(const CScriptID& id) const { std::vector data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS); @@ -41,11 +48,11 @@ namespace std::string operator()(const CNoDestination& no) const { return ""; } }; - CTxDestination DecodeDestination(const std::string& str, const CChainParams& params, bool& isStaking) + CTxDestination DecodeDestination(const std::string& str, const CChainParams& params, bool& isStaking, bool& isExchange) { std::vector data; uint160 hash; - if (DecodeBase58Check(str, data, 21)) { + if (DecodeBase58Check(str, data, 23)) { // base58-encoded PIVX addresses. // Public-key-hash-addresses have version 30 (or 139 testnet). // The data vector contains RIPEMD160(SHA256(pubkey)), where pubkey is the serialized public key. @@ -54,6 +61,13 @@ namespace std::copy(data.begin() + pubkey_prefix.size(), data.end(), hash.begin()); return CKeyID(hash); } + // Exchange Transparent addresses have version 31 + const std::vector& exchange_pubkey_prefix = params.Base58Prefix(CChainParams::EXCHANGE_ADDRESS); + if (data.size() == hash.size() + exchange_pubkey_prefix.size() && std::equal(exchange_pubkey_prefix.begin(), exchange_pubkey_prefix.end(), data.begin())) { + isExchange = true; + std::copy(data.begin() + exchange_pubkey_prefix.size(), data.end(), hash.begin()); + return CExchangeKeyID(hash); + } // Public-key-hash-coldstaking-addresses have version 63 (or 73 testnet). const std::vector& staking_prefix = params.Base58Prefix(CChainParams::STAKING_ADDRESS); if (data.size() == hash.size() + staking_prefix.size() && std::equal(staking_prefix.begin(), staking_prefix.end(), data.begin())) { @@ -74,9 +88,9 @@ namespace } // anon namespace -std::string EncodeDestination(const CTxDestination& dest, bool isStaking) +std::string EncodeDestination(const CTxDestination& dest, bool isStaking, bool isExchange) { - return EncodeDestination(dest, isStaking ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS); + return isExchange ? EncodeDestination(dest, CChainParams::EXCHANGE_ADDRESS) : (isStaking ? EncodeDestination(dest, CChainParams::STAKING_ADDRESS) : EncodeDestination(dest, CChainParams::PUBKEY_ADDRESS)); } std::string EncodeDestination(const CTxDestination& dest, const CChainParams::Base58Type addrType) @@ -87,18 +101,20 @@ std::string EncodeDestination(const CTxDestination& dest, const CChainParams::Ba CTxDestination DecodeDestination(const std::string& str) { bool isStaking; - return DecodeDestination(str, Params(), isStaking); + bool isExchange; + return DecodeDestination(str, Params(), isStaking, isExchange); } -CTxDestination DecodeDestination(const std::string& str, bool& isStaking) +CTxDestination DecodeDestination(const std::string& str, bool& isStaking, bool& isExchange) { - return DecodeDestination(str, Params(), isStaking); + return DecodeDestination(str, Params(), isStaking, isExchange); } bool IsValidDestinationString(const std::string& str, bool fStaking, const CChainParams& params) { bool isStaking = false; - return IsValidDestination(DecodeDestination(str, params, isStaking)) && (isStaking == fStaking); + bool isExchange = false; + return IsValidDestination(DecodeDestination(str, params, isStaking, isExchange)) && (isStaking == fStaking); } bool IsValidDestinationString(const std::string& str, bool isStaking) diff --git a/src/key_io.h b/src/key_io.h index b6cfb58621ccb..8eeb807eeb13c 100644 --- a/src/key_io.h +++ b/src/key_io.h @@ -13,10 +13,10 @@ #include -std::string EncodeDestination(const CTxDestination& dest, bool isStaking); +std::string EncodeDestination(const CTxDestination& dest, bool isStaking, bool isExchange); std::string EncodeDestination(const CTxDestination& dest, const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); -// DecodeDestinationisStaking flag is set to true when the string arg is from an staking address -CTxDestination DecodeDestination(const std::string& str, bool& isStaking); +// DecodeDestination isStaking flag is set to true when the string arg is from an staking address +CTxDestination DecodeDestination(const std::string& str, bool& isStaking, bool& isExchange); CTxDestination DecodeDestination(const std::string& str); // Return true if the address is valid and is following the fStaking flag type (true means that the destination must be a staking address, false the opposite). diff --git a/src/keystore.h b/src/keystore.h index 40eb633a03956..5ecba8c3aa50d 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -113,6 +113,7 @@ class CBasicKeyStore : public CKeyStore bool AddKeyPubKey(const CKey& key, const CPubKey& pubkey); bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const; bool HaveKey(const CKeyID& address) const; + bool HaveKey(const CExchangeKeyID& address) const; std::set GetKeys() const; bool GetKey(const CKeyID& address, CKey& keyOut) const; diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index e84af1dbf8dd2..96e73f61e8281 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -143,6 +143,11 @@ bool CTransaction::IsCoinStake() const return (vout.size() >= 2 && vout[0].IsEmpty()); } +bool CTransaction::HasExchangeAddr() const +{ + return std::any_of(vout.begin(), vout.end(), [](const auto& txout) { return txout.scriptPubKey.IsPayToExchangeAddress(); }); +} + bool CTransaction::HasP2CSOutputs() const { for(const CTxOut& txout : vout) { diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 7147fe966d27b..bbe4e453b16d1 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -380,6 +380,7 @@ class CTransaction bool IsCoinStake() const; bool HasP2CSOutputs() const; + bool HasExchangeAddr() const; friend bool operator==(const CTransaction& a, const CTransaction& b) { diff --git a/src/pubkey.h b/src/pubkey.h index c3916875be4c5..418078ff6bc94 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -24,6 +24,19 @@ class CKeyID : public uint160 explicit CKeyID(const uint160& in) : uint160(in) {} }; +/** A reference to a CKey: the Hash160 of its serialized public key, special case for exchange key */ + +class CExchangeKeyID : public uint160 +{ +public: + CExchangeKeyID() : uint160() {} + explicit CExchangeKeyID(const uint160& in) : uint160(in) {} + + operator CKeyID() const { + return CKeyID(static_cast(*this)); + } +}; + typedef uint256 ChainCode; /** An encapsulated public key. */ diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 9aba9ea64e956..b872925054588 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -150,9 +150,14 @@ class AddressTablePriv LOCK(wallet->cs_wallet); for (auto it = wallet->NewAddressBookIterator(); it.IsValid(); it.Next()) { auto addrBookData = it.GetValue(); - const CChainParams::Base58Type addrType = - AddressBook::IsColdStakingPurpose(addrBookData.purpose) ? - CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS; + CChainParams::Base58Type addrType; + if (AddressBook::IsColdStakingPurpose(addrBookData.purpose)) { + addrType = CChainParams::STAKING_ADDRESS; + } else if (AddressBook::IsExchangePurpose(addrBookData.purpose)) { + addrType = CChainParams::EXCHANGE_ADDRESS; + } else { + addrType = CChainParams::PUBKEY_ADDRESS; + } const CWDestination& dest = *it.GetDestKey(); bool fMine = IsMine(*wallet, dest); diff --git a/src/qt/pivx/addresseswidget.cpp b/src/qt/pivx/addresseswidget.cpp index d30f794a88770..6057302cff5e9 100644 --- a/src/qt/pivx/addresseswidget.cpp +++ b/src/qt/pivx/addresseswidget.cpp @@ -184,8 +184,8 @@ void AddressesWidget::onStoreContactClicked() QString label = ui->lineEditName->text(); QString address = ui->lineEditAddress->text(); - bool isStakingAddress = false; - auto pivAdd = Standard::DecodeDestination(address.toUtf8().constData(), isStakingAddress); + bool isStaking, isExchange, isShield = false; + auto pivAdd = Standard::DecodeDestination(address.toUtf8().constData(), isStaking, isExchange, isShield); if (!Standard::IsValidDestination(pivAdd)) { setCssEditLine(ui->lineEditAddress, false, true); @@ -209,7 +209,7 @@ void AddressesWidget::onStoreContactClicked() bool isShielded = walletModel->IsShieldedDestination(pivAdd); if (walletModel->updateAddressBookLabels(pivAdd, label.toUtf8().constData(), isShielded ? AddressBook::AddressBookPurpose::SHIELDED_SEND : - isStakingAddress ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND) + isStaking ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND) ) { ui->lineEditAddress->setText(""); ui->lineEditName->setText(""); diff --git a/src/qt/pivx/send.cpp b/src/qt/pivx/send.cpp index ac5f9318eae7f..ed13d6a053170 100644 --- a/src/qt/pivx/send.cpp +++ b/src/qt/pivx/send.cpp @@ -906,10 +906,10 @@ void SendWidget::onContactMultiClicked() return; } - bool isStakingAddr = false; - auto pivAdd = Standard::DecodeDestination(address.toStdString(), isStakingAddr); + bool isStaking, isExchange, isShielded = false; + auto pivAdd = Standard::DecodeDestination(address.toStdString(), isStaking, isExchange, isShielded); - if (!Standard::IsValidDestination(pivAdd) || isStakingAddr) { + if (!Standard::IsValidDestination(pivAdd) || isStaking) { inform(tr("Invalid address")); return; } diff --git a/src/qt/pivx/sendchangeaddressdialog.cpp b/src/qt/pivx/sendchangeaddressdialog.cpp index 6ab1e151fc867..7225e159e3abb 100644 --- a/src/qt/pivx/sendchangeaddressdialog.cpp +++ b/src/qt/pivx/sendchangeaddressdialog.cpp @@ -73,15 +73,14 @@ void SendChangeAddressDialog::accept() QDialog::accept(); } else { // validate address - bool isStakingAddr = false; - bool isShield = false; - dest = Standard::DecodeDestination(ui->lineEditAddress->text().toStdString(), isStakingAddr, isShield); + bool isStaking, isExchange, isShielded = false; + dest = Standard::DecodeDestination(ui->lineEditAddress->text().toStdString(), isStaking, isExchange, isShielded); if (!Standard::IsValidDestination(dest)) { inform(tr("Invalid address")); - } else if (isStakingAddr) { + } else if (isStaking) { inform(tr("Cannot use cold staking addresses for change")); - } else if (!isShield && !isTransparent) { + } else if (!isShielded && !isTransparent) { inform(tr("Cannot use a transparent change for a shield transaction")); } else { QDialog::accept(); diff --git a/src/qt/pivx/sendconfirmdialog.cpp b/src/qt/pivx/sendconfirmdialog.cpp index ef1d745f10d80..87ef97e87ab63 100644 --- a/src/qt/pivx/sendconfirmdialog.cpp +++ b/src/qt/pivx/sendconfirmdialog.cpp @@ -349,8 +349,9 @@ void TxDetailDialog::onOutputsClicked() QString labelRes; CTxDestination dest; bool isCsAddress = out.scriptPubKey.IsPayToColdStaking(); + bool isExchange = out.scriptPubKey.IsPayToExchangeAddress(); if (ExtractDestination(out.scriptPubKey, dest, isCsAddress)) { - std::string address = EncodeDestination(dest, isCsAddress); + std::string address = EncodeDestination(dest, isCsAddress, isExchange); labelRes = QString::fromStdString(address); labelRes = labelRes.left(16) + "..." + labelRes.right(16); } else { diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 88f685bb23964..3c77b23ef5cf0 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -399,8 +399,8 @@ void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) bool WalletModel::validateAddress(const QString& address) { // Only regular base58 addresses and shielded addresses accepted here - bool isStaking = false; - CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking); + bool isStaking, isExchange = false; + CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking, isExchange); const auto regDest = boost::get(&dest); if (regDest && IsValidDestination(*regDest) && isStaking) return false; return Standard::IsValidDestination(dest); @@ -413,8 +413,8 @@ bool WalletModel::validateAddress(const QString& address, bool fStaking) bool WalletModel::validateAddress(const QString& address, bool fStaking, bool& isShielded) { - bool isStaking = false; - CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking); + bool isStaking, isExchange = false; + CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking, isExchange); if (IsShieldedDestination(dest)) { isShielded = true; return true; @@ -492,7 +492,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact if (!res) return CannotCreateInternalAddress; ownerAdd = *res.getObjResult(); } else { - ownerAdd = Destination(DecodeDestination(rcp.ownerAddress.toStdString()), false); + ownerAdd = Destination(DecodeDestination(rcp.ownerAddress.toStdString()), false, false); } const CKeyID* stakerId = boost::get(&out); @@ -596,9 +596,8 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran for (const SendCoinsRecipient& rcp : transaction.getRecipients()) { // Don't touch the address book when we have a payment request { - bool isStaking = false; - bool isShielded = false; - auto address = Standard::DecodeDestination(rcp.address.toStdString(), isStaking, isShielded); + bool isStaking, isExchange, isShielded = false; + auto address = Standard::DecodeDestination(rcp.address.toStdString(), isStaking, isExchange, isShielded); std::string purpose = isShielded ? AddressBook::AddressBookPurpose::SHIELDED_SEND : isStaking ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND; std::string strLabel = rcp.label.toStdString(); @@ -943,7 +942,8 @@ int64_t WalletModel::getKeyCreationTime(const CTxDestination& address) int64_t WalletModel::getKeyCreationTime(const std::string& address) { - return wallet->GetKeyCreationTime(Standard::DecodeDestination(address)); + bool isStaking, isExchange, isShielded = false; + return wallet->GetKeyCreationTime(Standard::DecodeDestination(address, isStaking, isExchange, isShielded)); } int64_t WalletModel::getKeyCreationTime(const libzcash::SaplingPaymentAddress& address) @@ -957,14 +957,14 @@ int64_t WalletModel::getKeyCreationTime(const libzcash::SaplingPaymentAddress& a CallResult WalletModel::getNewAddress(const std::string& label) const { auto res = wallet->getNewAddress(label); - return res ? CallResult(Destination(*res.getObjResult(), false)) : + return res ? CallResult(Destination(*res.getObjResult(), false, false)) : CallResult(res.getError()); } CallResult WalletModel::getNewStakingAddress(const std::string& label) const { auto res = wallet->getNewStakingAddress(label); - return res ? CallResult(Destination(*res.getObjResult(), true)) : + return res ? CallResult(Destination(*res.getObjResult(), true, false)) : CallResult(res.getError()); } @@ -985,8 +985,8 @@ bool WalletModel::blacklistAddressFromColdStaking(const QString &addressStr) bool WalletModel::updateAddressBookPurpose(const QString &addressStr, const std::string& purpose) { - bool isStaking = false; - CTxDestination address = DecodeDestination(addressStr.toStdString(), isStaking); + bool isStaking, isExchange = false; + CTxDestination address = DecodeDestination(addressStr.toStdString(), isStaking, isExchange); if (isStaking) return error("Invalid PIVX address, cold staking address"); CKeyID keyID; diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 50689f79a1f7a..a84634b276e41 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -246,6 +246,17 @@ class DescribeAddressVisitor : public boost::static_visitor return obj; } + UniValue operator()(const CExchangeKeyID &keyID) const { + UniValue obj(UniValue::VOBJ); + CPubKey vchPubKey; + obj.pushKV("isscript", false); + if (pwallet && pwallet->GetPubKey(keyID, vchPubKey)) { + obj.pushKV("exchangepubkey", HexStr(vchPubKey)); + obj.pushKV("iscompressed", vchPubKey.IsCompressed()); + } + return obj; + } + UniValue operator()(const CScriptID &scriptID) const { UniValue obj(UniValue::VOBJ); obj.pushKV("isscript", true); @@ -420,8 +431,9 @@ UniValue validateaddress(const JSONRPCRequest& request) std::string strAddress = request.params[0].get_str(); // First check if it's a regular address - bool isStakingAddress = false; - CTxDestination dest = DecodeDestination(strAddress, isStakingAddress); + bool isStaking = false; + bool isExchange = false; + CTxDestination dest = DecodeDestination(strAddress, isStaking, isExchange); bool isValid = IsValidDestination(dest); PPaymentAddress finalAddress; @@ -436,7 +448,7 @@ UniValue validateaddress(const JSONRPCRequest& request) ret.pushKV("isvalid", isValid); if (isValid) { ret.pushKV("address", strAddress); - UniValue detail = boost::apply_visitor(DescribePaymentAddressVisitor(pwallet, isStakingAddress), finalAddress); + UniValue detail = boost::apply_visitor(DescribePaymentAddressVisitor(pwallet, isStaking), finalAddress); ret.pushKVs(detail); } diff --git a/src/rpc/rpcevo.cpp b/src/rpc/rpcevo.cpp index df6890e723182..7676ffee93c0e 100644 --- a/src/rpc/rpcevo.cpp +++ b/src/rpc/rpcevo.cpp @@ -168,8 +168,8 @@ static void CheckEvoUpgradeEnforcement() // Allows to specify PIVX address or priv key (as strings). In case of PIVX address, the priv key is taken from the wallet static CKey ParsePrivKey(CWallet* pwallet, const std::string &strKeyOrAddress, bool allowAddresses = true) { - bool isStaking{false}, isShield{false}; - const CWDestination& cwdest = Standard::DecodeDestination(strKeyOrAddress, isStaking, isShield); + bool isStaking{false}, isShield{false}, isExchange{false}; + const CWDestination& cwdest = Standard::DecodeDestination(strKeyOrAddress, isStaking, isExchange, isShield); if (isStaking) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "cold staking addresses not supported"); } @@ -198,8 +198,8 @@ static CKey ParsePrivKey(CWallet* pwallet, const std::string &strKeyOrAddress, b static CKeyID ParsePubKeyIDFromAddress(const std::string& strAddress) { - bool isStaking{false}, isShield{false}; - const CWDestination& cwdest = Standard::DecodeDestination(strAddress, isStaking, isShield); + bool isStaking{false}, isShield{false}, isExchange{false}; + const CWDestination& cwdest = Standard::DecodeDestination(strAddress, isStaking, isExchange, isShield); if (isStaking) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "cold staking addresses not supported"); } diff --git a/src/sapling/sapling_validation.cpp b/src/sapling/sapling_validation.cpp index 2216003a357cb..383e61394f429 100644 --- a/src/sapling/sapling_validation.cpp +++ b/src/sapling/sapling_validation.cpp @@ -33,8 +33,8 @@ bool CheckTransaction(const CTransaction& tx, CValidationState& state, CAmount& // From here, all of the checks are done in v3+ transactions. - // if the tx has shielded data, cannot be a coinstake, coinbase, zcspend and zcmint - if (tx.IsCoinStake() || tx.IsCoinBase() || tx.HasZerocoinSpendInputs() || tx.HasZerocoinMintOutputs()) + // if the tx has shielded data, cannot be a coinstake, coinbase, zcspend and zcmint or exchange address + if (tx.IsCoinStake() || tx.IsCoinBase() || tx.HasZerocoinSpendInputs() || tx.HasZerocoinMintOutputs() || tx.HasExchangeAddr()) return state.DoS(100, error("%s: Sapling version with invalid data", __func__), REJECT_INVALID, "bad-txns-invalid-sapling"); diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 2b260474fd21c..633638b992fd7 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -333,6 +333,11 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_NOP: break; + case OP_EXCHANGEADDR: + if (!script.IsPayToExchangeAddress()) + return set_error(serror, SCRIPT_ERR_EXCHANGEADDRVERIFY); + break; + case OP_CHECKLOCKTIMEVERIFY: { if (!(flags & SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY)) { diff --git a/src/script/ismine.cpp b/src/script/ismine.cpp index 9236258385525..3c9700e09b3f6 100644 --- a/src/script/ismine.cpp +++ b/src/script/ismine.cpp @@ -94,6 +94,7 @@ isminetype IsMine(const CKeyStore& keystore, const CScript& scriptPubKey) return ISMINE_SPENDABLE; break; case TX_PUBKEYHASH: + case TX_EXCHANGEADDR: keyID = CKeyID(uint160(vSolutions[0])); if(keystore.HaveKey(keyID)) return ISMINE_SPENDABLE; diff --git a/src/script/script.cpp b/src/script/script.cpp index 7a8723d89e547..f9c1a578746a6 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -151,6 +151,9 @@ const char* GetOpName(opcodetype opcode) case OP_CHECKCOLDSTAKEVERIFY_LOF : return "OP_CHECKCOLDSTAKEVERIFY_LOF"; case OP_CHECKCOLDSTAKEVERIFY : return "OP_CHECKCOLDSTAKEVERIFY"; + // exchange address + case OP_EXCHANGEADDR : return "OP_EXCHANGEADDR"; + case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE"; default: @@ -248,6 +251,17 @@ bool CScript::IsPayToColdStakingLOF() const return IsPayToColdStaking() && (*this)[4] == OP_CHECKCOLDSTAKEVERIFY_LOF; } +bool CScript::IsPayToExchangeAddress() const +{ + return (this->size() == 26 && + (*this)[0] == OP_EXCHANGEADDR && + (*this)[1] == OP_DUP && + (*this)[2] == OP_HASH160 && + (*this)[3] == 0x14 && + (*this)[24] == OP_EQUALVERIFY && + (*this)[25] == OP_CHECKSIG); +} + bool CScript::StartsWithOpcode(const opcodetype opcode) const { return (!this->empty() && (*this)[0] == opcode); diff --git a/src/script/script.h b/src/script/script.h index 85d1b32ad0f39..35d4c855be6b2 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -187,6 +187,9 @@ enum opcodetype OP_CHECKCOLDSTAKEVERIFY_LOF = 0xd1, // last output free for masternode/budget payments OP_CHECKCOLDSTAKEVERIFY = 0xd2, + // exchange address, NOP but identifies as an address not allowing private outputs + OP_EXCHANGEADDR = 0xe0, + OP_INVALIDOPCODE = 0xff, }; @@ -630,6 +633,7 @@ class CScript : public CScriptBase bool IsPayToScriptHash() const; bool IsPayToColdStaking() const; bool IsPayToColdStakingLOF() const; + bool IsPayToExchangeAddress() const; bool StartsWithOpcode(const opcodetype opcode) const; bool IsZerocoinMint() const; bool IsZerocoinSpend() const; diff --git a/src/script/script_error.cpp b/src/script/script_error.cpp index a7a4a64e9067f..e3e33209bfde9 100644 --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -18,6 +18,8 @@ const char* ScriptErrorString(const ScriptError serror) return "Script failed an OP_VERIFY operation"; case SCRIPT_ERR_EQUALVERIFY: return "Script failed an OP_EQUALVERIFY operation"; + case SCRIPT_ERR_EXCHANGEADDRVERIFY: + return "Script failed OP_EXCHANGEADDR operation"; case SCRIPT_ERR_CHECKCOLDSTAKEVERIFY: return "Script failed an OP_CHECKCOLDSTAKEVERIFY operation"; case SCRIPT_ERR_CHECKMULTISIGVERIFY: diff --git a/src/script/script_error.h b/src/script/script_error.h index b06874874dc1e..a9a76d3de3824 100644 --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -25,6 +25,7 @@ typedef enum ScriptError_t /* Failed verify operations */ SCRIPT_ERR_VERIFY, SCRIPT_ERR_EQUALVERIFY, + SCRIPT_ERR_EXCHANGEADDRVERIFY, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, SCRIPT_ERR_CHECKMULTISIGVERIFY, SCRIPT_ERR_CHECKSIGVERIFY, diff --git a/src/script/sign.cpp b/src/script/sign.cpp index fe79d0df06ebb..98e351376dbfd 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -87,6 +87,7 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP keyID = CPubKey(vSolutions[0]).GetID(); return Sign1(keyID, creator, scriptPubKey, ret, sigversion); case TX_PUBKEYHASH: + case TX_EXCHANGEADDR: keyID = CKeyID(uint160(vSolutions[0])); if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) return false; @@ -300,6 +301,7 @@ static Stacks CombineSignatures(const CScript& scriptPubKey, const BaseSignature return sigs2; case TX_PUBKEY: case TX_PUBKEYHASH: + case TX_EXCHANGEADDR: case TX_COLDSTAKE: // Signatures are bigger than placeholders or empty scripts: if (sigs1.script.empty() || sigs1.script[0].empty()) diff --git a/src/script/standard.cpp b/src/script/standard.cpp index 6352ee5258e33..559ca76009646 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -22,6 +22,7 @@ const char* GetTxnOutputType(txnouttype t) case TX_NONSTANDARD: return "nonstandard"; case TX_PUBKEY: return "pubkey"; case TX_PUBKEYHASH: return "pubkeyhash"; + case TX_EXCHANGEADDR: return "exchangeaddress"; case TX_SCRIPTHASH: return "scripthash"; case TX_MULTISIG: return "multisig"; case TX_COLDSTAKE: return "coldstake"; @@ -126,6 +127,15 @@ bool Solver(const CScript& scriptPubKey, txnouttype& typeRet, std::vector hashBytes(scriptPubKey.begin()+4, scriptPubKey.begin()+24); + vSolutionsRet.push_back(hashBytes); + return true; + } + + std::vector data1; if (MatchPayToColdStaking(scriptPubKey, data, data1)) { typeRet = TX_COLDSTAKE; @@ -171,6 +181,9 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet, } else if (whichType == TX_SCRIPTHASH) { addressRet = CScriptID(uint160(vSolutions[0])); return true; + } else if (whichType == TX_EXCHANGEADDR) { + addressRet = CExchangeKeyID(uint160(vSolutions[0])); + return true; } else if (whichType == TX_COLDSTAKE) { addressRet = CKeyID(uint160(vSolutions[!fColdStake])); return true; @@ -248,6 +261,12 @@ class CScriptVisitor : public boost::static_visitor return true; } + bool operator()(const CExchangeKeyID &keyID) const { + script->clear(); + *script << OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG; + return true; + } + bool operator()(const CScriptID &scriptID) const { script->clear(); *script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL; diff --git a/src/script/standard.h b/src/script/standard.h index 4ea3aefc2c469..b8dd5d713de0f 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -51,7 +51,8 @@ enum txnouttype TX_SCRIPTHASH, TX_MULTISIG, TX_NULL_DATA, - TX_COLDSTAKE + TX_COLDSTAKE, + TX_EXCHANGEADDR }; class CNoDestination { @@ -64,10 +65,11 @@ class CNoDestination { * A txout script template with a specific destination. It is either: * * CNoDestination: no destination set * * CKeyID: TX_PUBKEYHASH destination + * * CExchangeKeyID: CKeyID for exchange key * * CScriptID: TX_SCRIPTHASH destination * A CTxDestination is the internal data type encoded in a PIVX address */ -typedef boost::variant CTxDestination; +typedef boost::variant CTxDestination; /** Check whether a CTxDestination is a CNoDestination. */ bool IsValidDestination(const CTxDestination& dest); diff --git a/src/test/base58_tests.cpp b/src/test/base58_tests.cpp index 31bcab0a6244e..631032e09460d 100644 --- a/src/test/base58_tests.cpp +++ b/src/test/base58_tests.cpp @@ -96,6 +96,10 @@ class TestAddrTypeVisitor : public boost::static_visitor { return (exp_addrType == "pubkey"); } + bool operator()(const CExchangeKeyID &id) const + { + return (exp_addrType == "exchangepubkey"); + } bool operator()(const CScriptID &id) const { return (exp_addrType == "script"); diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index 846980c4739f6..1b984c350a79a 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -276,7 +276,6 @@ ["0", "IF 0xdd ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xde ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xdf ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], -["0", "IF 0xe0 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xe1 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xe2 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xe3 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], @@ -910,7 +909,6 @@ ["1", "IF 0xdd ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xde ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xdf ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], -["1", "IF 0xe0 ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xe1 ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xe2 ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xe3 ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], diff --git a/src/test/script_standard_tests.cpp b/src/test/script_standard_tests.cpp index a25fe99908daf..2875242a423d2 100644 --- a/src/test/script_standard_tests.cpp +++ b/src/test/script_standard_tests.cpp @@ -43,6 +43,14 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success) BOOST_CHECK_EQUAL(solutions.size(), 1); BOOST_CHECK(solutions[0] == ToByteVector(pubkeys[0].GetID())); + // TX_EXCHANGEADDR + s.clear(); + s << OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(pubkeys[0].GetID()) << OP_EQUALVERIFY << OP_CHECKSIG; + BOOST_CHECK(Solver(s, whichType, solutions)); + BOOST_CHECK_EQUAL(whichType, TX_EXCHANGEADDR); + BOOST_CHECK_EQUAL(solutions.size(), 1); + BOOST_CHECK(solutions[0] == ToByteVector(pubkeys[0].GetID())); + // TX_SCRIPTHASH CScript redeemScript(s); // initialize with leftover P2PKH script s.clear(); @@ -119,6 +127,11 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_failure) s << OP_DUP << OP_HASH160 << ToByteVector(pubkey) << OP_EQUALVERIFY << OP_CHECKSIG; BOOST_CHECK(!Solver(s, whichType, solutions)); + // TX_EXCHANGEADDR with incorrectly sized key hash + s.clear(); + s << OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(pubkey) << OP_EQUALVERIFY << OP_CHECKSIG; + BOOST_CHECK(!Solver(s, whichType, solutions)); + // TX_SCRIPTHASH with incorrectly sized script hash s.clear(); s << OP_HASH160 << std::vector(21, 0x01) << OP_EQUAL; @@ -179,6 +192,13 @@ BOOST_AUTO_TEST_CASE(script_standard_ExtractDestination) BOOST_CHECK(boost::get(&address) && *boost::get(&address) == pubkey.GetID()); + // TX_EXCHANGEADDR + s.clear(); + s << OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(pubkey.GetID()) << OP_EQUALVERIFY << OP_CHECKSIG; + BOOST_CHECK(ExtractDestination(s, address)); + BOOST_CHECK(boost::get(&address) && + *boost::get(&address) == pubkey.GetID()); + // TX_SCRIPTHASH CScript redeemScript(s); // initialize with leftover P2PKH script s.clear(); @@ -232,6 +252,16 @@ BOOST_AUTO_TEST_CASE(script_standard_ExtractDestinations) BOOST_CHECK(boost::get(&addresses[0]) && *boost::get(&addresses[0]) == pubkeys[0].GetID()); + // TX_EXCHANGEADDR + s.clear(); + s << OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(pubkeys[0].GetID()) << OP_EQUALVERIFY << OP_CHECKSIG; + BOOST_CHECK(ExtractDestinations(s, whichType, addresses, nRequired)); + BOOST_CHECK_EQUAL(whichType, TX_EXCHANGEADDR); + BOOST_CHECK_EQUAL(addresses.size(), 1); + BOOST_CHECK_EQUAL(nRequired, 1); + BOOST_CHECK(boost::get(&addresses[0]) && + *boost::get(&addresses[0]) == pubkeys[0].GetID()); + // TX_SCRIPTHASH CScript redeemScript(s); // initialize with leftover P2PKH script s.clear(); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 37dfa5f86319f..b94430614eee2 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -6,6 +6,7 @@ #include "bip38.h" #include "key_io.h" +#include "destination_io.h" #include "rpc/server.h" #include "sapling/key_io_sapling.h" #include "script/script.h" @@ -243,13 +244,14 @@ UniValue importaddress(const JSONRPCRequest& request) { LOCK2(cs_main, pwallet->cs_wallet); - bool isStakingAddress = false; - CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStakingAddress); + bool isStaking = false; + bool isExchange = false; + CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStaking, isExchange); if (IsValidDestination(dest)) { if (fP2SH) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot use the p2sh flag with an address - use a script instead"); - ImportAddress(pwallet, dest, strLabel, isStakingAddress ? + ImportAddress(pwallet, dest, strLabel, isStaking ? AddressBook::AddressBookPurpose::COLD_STAKING : AddressBook::AddressBookPurpose::RECEIVE); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 85904299e88ca..c14d06046a7be 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -308,7 +308,7 @@ UniValue getaddressesbylabel(const JSONRPCRequest& request) auto addrBook = it.GetValue(); if (addrBook.name == label) { if (!addrBook.isShielded()) { - ret.pushKV(EncodeDestination(*it.GetCTxDestKey(), AddressBook::IsColdStakingPurpose(addrBook.purpose)), AddressBookDataToJSON(addrBook, false)); + ret.pushKV(EncodeDestination(*it.GetCTxDestKey(), AddressBook::IsColdStakingPurpose(addrBook.purpose), AddressBook::IsExchangePurpose(addrBook.purpose)), AddressBookDataToJSON(addrBook, false)); } else { ret.pushKV(Standard::EncodeDestination(*it.GetShieldedDestKey()), AddressBookDataToJSON(addrBook, false)); } @@ -528,6 +528,34 @@ UniValue getnewaddress(const JSONRPCRequest& request) return EncodeDestination(GetNewAddressFromLabel(pwallet, AddressBook::AddressBookPurpose::RECEIVE, request.params)); } +UniValue getnewexchangeaddress(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() > 1) + throw std::runtime_error( + "getnewexchangeaddress ( \"label\" )\n" + "\nReturns a new PIVX exchange address for receiving transparent funds only.\n" + + "\nArguments:\n" + "1. \"label\" (string, optional) The label name for the address to be linked to. if not provided, the default label \"\" is used. It can also be set to the empty string \"\" to represent the default label. The label does not need to exist, it will be created if there is no label by the given name.\n" + + + "\nResult:\n" + "\"pivxaddress\" (string) The new pivx exchange address\n" + + "\nExamples:\n" + + HelpExampleCli("getnewexchangeaddress", "") + HelpExampleRpc("getnewexchangeaddress", "")); + + LOCK2(cs_main, pwallet->cs_wallet); + + return EncodeDestination(GetNewAddressFromLabel(pwallet, "exchange", request.params, CChainParams::EXCHANGE_ADDRESS), CChainParams::EXCHANGE_ADDRESS); +} + UniValue getnewstakingaddress(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -753,10 +781,10 @@ UniValue delegatoradd(const JSONRPCRequest& request) HelpExampleRpc("delegatoradd", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"") + HelpExampleRpc("delegatoradd", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"myPaperWallet\"")); - - bool isStakingAddress = false; - CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStakingAddress); - if (!IsValidDestination(dest) || isStakingAddress) + bool isStaking = false; + bool isExchange = false; + CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStaking, isExchange); + if (!IsValidDestination(dest) || isStaking) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid PIVX address"); const std::string strLabel = (request.params.size() > 1 ? request.params[1].get_str() : ""); @@ -791,9 +819,10 @@ UniValue delegatorremove(const JSONRPCRequest& request) HelpExampleCli("delegatorremove", "DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6") + HelpExampleRpc("delegatorremove", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"")); - bool isStakingAddress = false; - CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStakingAddress); - if (!IsValidDestination(dest) || isStakingAddress) + bool isStaking = false; + bool isExchange = false; + CTxDestination dest = DecodeDestination(request.params[0].get_str(), isStaking, isExchange); + if (!IsValidDestination(dest) || isStaking) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid PIVX address"); const CKeyID* keyID = boost::get(&dest); @@ -814,10 +843,14 @@ UniValue delegatorremove(const JSONRPCRequest& request) static UniValue ListaddressesForPurpose(CWallet* const pwallet, const std::string strPurpose) { - const CChainParams::Base58Type addrType = ( - AddressBook::IsColdStakingPurpose(strPurpose) ? - CChainParams::STAKING_ADDRESS : - CChainParams::PUBKEY_ADDRESS); + CChainParams::Base58Type addrType; + if (AddressBook::IsColdStakingPurpose(strPurpose)) { + addrType = CChainParams::STAKING_ADDRESS; + } else if (AddressBook::IsExchangePurpose(strPurpose)) { + addrType = CChainParams::EXCHANGE_ADDRESS; + } else { + addrType = CChainParams::PUBKEY_ADDRESS; + } UniValue ret(UniValue::VARR); { LOCK(pwallet->cs_wallet); @@ -1004,7 +1037,8 @@ UniValue setlabel(const JSONRPCRequest& request) LOCK2(cs_main, pwallet->cs_wallet); - const CWDestination& dest = Standard::DecodeDestination(request.params[0].get_str()); + bool isStaking = false, isExchange = false, isShielded = false; + const CWDestination& dest = Standard::DecodeDestination(request.params[0].get_str(), isStaking, isExchange, isShielded); // Make sure the destination is valid if (!Standard::IsValidDestination(dest)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); @@ -1147,9 +1181,9 @@ UniValue sendtoaddress(const JSONRPCRequest& request) // the user could have gotten from another RPC command prior to now pwallet->BlockUntilSyncedToCurrentChain(); - bool isStaking = false, isShielded = false; + bool isStaking = false, isExchange = false, isShielded = false; const std::string addrStr = request.params[0].get_str(); - const CWDestination& destination = Standard::DecodeDestination(addrStr, isStaking, isShielded); + const CWDestination& destination = Standard::DecodeDestination(addrStr, isStaking, isExchange, isShielded); if (!Standard::IsValidDestination(destination) || isStaking) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid PIVX address"); const std::string commentStr = (request.params.size() > 2 && !request.params[2].isNull()) ? @@ -1203,9 +1237,9 @@ static UniValue CreateColdStakeDelegation(CWallet* const pwallet, const UniValue } // Get Staking Address - bool isStaking = false; - CTxDestination stakeAddr = DecodeDestination(params[0].get_str(), isStaking); - if (!IsValidDestination(stakeAddr) || !isStaking) + bool isStaking{false}; + CTxDestination stakeAddr = DecodeDestination(params[0].get_str()); + if (!IsValidDestination(stakeAddr) || isStaking) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid PIVX staking address"); CKeyID* stakeKey = boost::get(&stakeAddr); @@ -1233,11 +1267,12 @@ static UniValue CreateColdStakeDelegation(CWallet* const pwallet, const UniValue // Get Owner Address std::string ownerAddressStr; CKeyID ownerKey; + bool isStakingAddress = false; + bool isExchange = false; if (params.size() > 2 && !params[2].isNull() && !params[2].get_str().empty()) { // Address provided - bool isStaking = false; - CTxDestination dest = DecodeDestination(params[2].get_str(), isStaking); - if (!IsValidDestination(dest) || isStaking) + CTxDestination dest = DecodeDestination(params[2].get_str(), isStakingAddress, isExchange); + if (!IsValidDestination(dest) || isStakingAddress) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid PIVX spending address"); ownerKey = *boost::get(&dest); // Check that the owner address belongs to this wallet, or fForceExternalAddr is true @@ -1293,7 +1328,7 @@ static UniValue CreateColdStakeDelegation(CWallet* const pwallet, const UniValue UniValue result(UniValue::VOBJ); result.pushKV("owner_address", ownerAddressStr); - result.pushKV("staker_address", EncodeDestination(stakeAddr, true)); + result.pushKV("staker_address", EncodeDestination(stakeAddr, true, false)); return result; } @@ -2325,7 +2360,8 @@ static UniValue legacy_sendmany(CWallet* const pwallet, const UniValue& sendTo, std::vector keys = sendTo.getKeys(); for (const std::string& name_ : keys) { bool isStaking = false; - CTxDestination dest = DecodeDestination(name_,isStaking); + bool isExchange = false; + CTxDestination dest = DecodeDestination(name_, isStaking, isExchange); if (!IsValidDestination(dest) || isStaking) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid PIVX address: ")+name_); @@ -2458,8 +2494,8 @@ UniValue sendmany(const JSONRPCRequest& request) // Check if any recipient address is shield bool fShieldSend = false; for (const std::string& key : sendTo.getKeys()) { - bool isStaking = false, isShielded = false; - Standard::DecodeDestination(key, isStaking, isShielded); + bool isStaking = false, isExchange = false, isShielded = false; + Standard::DecodeDestination(key, isStaking, isExchange, isShielded); if (isShielded) { fShieldSend = true; break; @@ -2646,7 +2682,7 @@ static UniValue ListReceived(CWallet* const pwallet, const UniValue& params, boo UniValue obj(UniValue::VOBJ); if (fIsWatchonly) obj.pushKV("involvesWatchonly", true); - obj.pushKV("address", EncodeDestination(address, AddressBook::IsColdStakingPurpose(label))); + obj.pushKV("address", EncodeDestination(address, AddressBook::IsColdStakingPurpose(label), AddressBook::IsExchangePurpose(label))); obj.pushKV("amount", ValueFromAmount(nAmount)); if (nConf == std::numeric_limits::max()) nConf = 0; obj.pushKV("confirmations", nConf); @@ -4749,6 +4785,7 @@ static const CRPCCommand commands[] = { "wallet", "upgradewallet", &upgradewallet, true, {} }, { "wallet", "sethdseed", &sethdseed, true, {"newkeypool","seed"} }, { "wallet", "getnewaddress", &getnewaddress, true, {"label"} }, + { "wallet", "getnewexchangeaddress", &getnewexchangeaddress, true, {"label"} }, { "wallet", "getnewstakingaddress", &getnewstakingaddress, true, {"label"} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, true, {} }, { "wallet", "getreceivedbyaddress", &getreceivedbyaddress, false, {"address","minconf"} }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index b51a1f9794942..10f583ffdb4c8 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -100,6 +100,11 @@ class CAffectedKeysVisitor : public boost::static_visitor vKeys.push_back(keyId); } + void operator()(const CExchangeKeyID& keyId) { + if (keystore.HaveKey(keyId)) + vKeys.push_back(keyId); + } + void operator()(const CScriptID& scriptId) { CScript script; @@ -3601,9 +3606,14 @@ DBErrors CWallet::ZapWalletTx(std::vector& vWtx) } std::string CWallet::ParseIntoAddress(const CWDestination& dest, const std::string& purpose) { - const CChainParams::Base58Type addrType = - AddressBook::IsColdStakingPurpose(purpose) ? - CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS; + CChainParams::Base58Type addrType; + if (AddressBook::IsColdStakingPurpose(purpose)) { + addrType = CChainParams::STAKING_ADDRESS; + } else if (AddressBook::IsExchangePurpose(purpose)) { + addrType = CChainParams::EXCHANGE_ADDRESS; + } else { + addrType = CChainParams::PUBKEY_ADDRESS; + } return Standard::EncodeDestination(dest, addrType); } diff --git a/test/functional/feature_exchangeaddr.py b/test/functional/feature_exchangeaddr.py new file mode 100755 index 0000000000000..7699474e4fc4d --- /dev/null +++ b/test/functional/feature_exchangeaddr.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test OP_EXCHANGEADDR. + +Test that the OP_EXCHANGEADDR soft-fork activates at (regtest) block height +1001, and that the following is capable + t > e + e > t + e > s +and not capable + s > e +""" + +from decimal import Decimal + +from test_framework.test_framework import PivxTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.script import CScript, OP_NOP, OP_CHECKSIG +from test_framework.messages import CTransaction, CTxIn, CTxOut, COutPoint, ToHex + +FEATURE_PRE_SPLIT_KEYPOOL = 169900 + +class ExchangeAddrTest(PivxTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.extra_args = [['-whitelist=127.0.0.1','-regtest'], ['-regtest']] + self.setup_clean_chain = True + + def run_test(self): + # Mine and test Pre v5.6 bad OP_EXCHANGEADDR + self.nodes[0].generate(800) + address = self.nodes[0].getnewaddress() + self.nodes[0].sendtoaddress(address, 10) + self.nodes[0].generate(6) + utxo = self.nodes[0].listunspent()[0] + # Create a raw transaction that attempts to spend the UTXO with a custom opcode + tx = CTransaction() + tx.vin.append(CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), b"")) + tx.vout.append(CTxOut(int(utxo["amount"] * 100000000) - 10000, CScript([OP_NOP, OP_CHECKSIG]))) + tx.vin[0].scriptSig = CScript([b'\xe0', b'\x51']) # OP_EXCHANGEADDR (0xe0) followed by OP_1 (0x51) + + # Sign the transaction + signed_tx = self.nodes[0].signrawtransaction(ToHex(tx)) + + # Send the raw transaction + # Before the upgrade, this should fail if OP_EXCHANGEADDR is disallowed + error_code = -26 + error_message = "scriptpubkey" + assert_raises_rpc_error(error_code, error_message, self.nodes[0].sendrawtransaction, signed_tx["hex"]) + + # Test that proper Exchange Address is not allowed before upgrade + ex_addr = self.nodes[1].getnewexchangeaddress() + + # Attempt to send funds from transparent address to exchange address + ex_addr_validation_result = self.nodes[0].validateaddress(ex_addr) + assert_equal(ex_addr_validation_result['isvalid'], True) + # This should fail to be sent + error_code = -4 + error_message = "bad-exchange-address-not-started" + assert_raises_rpc_error( + error_code, + error_message, + self.nodes[0].sendtoaddress, + ex_addr, 1.0 + ) + + # Mine and activate exchange addresses + self.nodes[0].generate(194) + assert_equal(self.nodes[0].getblockcount(), 1000) + self.nodes[0].generate(1) + + # Get addresses for testing + ex_addr_2 = self.nodes[1].getnewexchangeaddress() + t_addr_2 = self.nodes[0].getnewaddress() + + # Attempt to send funds from transparent address to exchange address pre-upgrade activation + self.nodes[0].sendtoaddress(ex_addr_2, 2.0) + self.nodes[0].generate(6) + self.sync_all() + + + # Verify balance + node_bal = self.nodes[1].getbalance() + assert_equal(node_bal, 2) + + # Attempt to send funds from exchange address back to transparent address + tx2 = self.nodes[0].sendtoaddress(t_addr_2, 1.0) + self.nodes[0].generate(6) + self.sync_all() + ex_result = self.nodes[0].gettransaction(tx2) + + # Assert that the transaction ID in the result matches tx2 + assert_equal(ex_result['txid'], tx2) + + # Transparent to Shield to Exchange should fail + # Check wallet version + wallet_info = self.nodes[0].getwalletinfo() + if wallet_info['walletversion'] < FEATURE_PRE_SPLIT_KEYPOOL: + self.log.info("Pre-HD wallet version detected. Skipping Shield tests.") + return + sapling_addr = self.nodes[0].getnewshieldaddress() + self.nodes[0].sendtoaddress(sapling_addr, 2.0) + self.nodes[0].generate(6) + sap_to_ex = [{"address": ex_addr, "amount": Decimal('1')}] + + # Expect shieldsendmany to fail with bad-txns-invalid-sapling + expected_error_code = -4 + expected_error_message = "Failed to accept tx in the memory pool (reason: bad-txns-invalid-sapling)" + assert_raises_rpc_error( + expected_error_code, + expected_error_message, + self.nodes[0].shieldsendmany, + sapling_addr, sap_to_ex, 1, 0, sapling_addr + ) + + # Generate a new shielded address + new_shielded_addr = self.nodes[1].getnewshieldaddress() + + # Send to Shield from Exchange Addr + tx3 = self.nodes[1].sendtoaddress(new_shielded_addr, 1.0) + ex_result2 = self.nodes[1].gettransaction(tx3) + assert_equal(ex_result2['txid'], tx3) + + +if __name__ == "__main__": + ExchangeAddrTest().main() + diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 8935e99c6867a..3459541a4d923 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -229,6 +229,7 @@ def __new__(cls, n): OP_SMALLINTEGER = CScriptOp(0xfa) OP_PUBKEYS = CScriptOp(0xfb) OP_PUBKEYHASH = CScriptOp(0xfd) +OP_EXCHANGEADDR = CScriptOp(0xe0) OP_PUBKEY = CScriptOp(0xfe) OP_INVALIDOPCODE = CScriptOp(0xff) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index bcec57836aecb..5b8bfac082351 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -122,6 +122,7 @@ 'feature_filelock.py', # ~ 71 sec 'mempool_packages.py', # ~ 63 sec 'sapling_wallet_encryption.py', + 'feature_exchangeaddr.py', # vv Tests less than 60s vv 'rpc_users.py',