diff --git a/src/masternodes/mn_rpc.cpp b/src/masternodes/mn_rpc.cpp index d928409758a..6658fd8b909 100644 --- a/src/masternodes/mn_rpc.cpp +++ b/src/masternodes/mn_rpc.cpp @@ -3,6 +3,8 @@ // file LICENSE or http://www.opensource.org/licenses/mit-license.php. #include + +#include #include extern bool EnsureWalletIsAvailable(bool avoidException); // in rpcwallet.cpp @@ -378,6 +380,39 @@ CWallet* GetWallet(const JSONRPCRequest& request) { return pwallet; } +CPubKey PublickeyFromString(const std::string &pubkey) +{ + if (!IsHex(pubkey) || (pubkey.length() != 66 && pubkey.length() != 130)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key: " + pubkey); + } + + return HexToPubKey(pubkey); +} + +CScript CreateScriptForHTLC(const JSONRPCRequest& request, uint32_t& blocks, std::vector& image) +{ + CPubKey seller_key = PublickeyFromString(request.params[0].get_str()); + CPubKey refund_key = PublickeyFromString(request.params[1].get_str()); + + { + UniValue timeout; + if (!timeout.read(std::string("[") + request.params[2].get_str() + std::string("]")) || !timeout.isArray() || timeout.size() != 1) + { + throw JSONRPCError(RPC_TYPE_ERROR, "Error parsing JSON: " + request.params[3].get_str()); + } + + blocks = timeout[0].get_int(); + } + + if (blocks >= CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) + { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid block denominated relative timeout"); + } + + return GetScriptForHTLC(seller_key, refund_key, image, blocks); +} + UniValue setgov(const JSONRPCRequest& request) { CWallet* const pwallet = GetWallet(request); diff --git a/src/masternodes/mn_rpc.h b/src/masternodes/mn_rpc.h index 4a3c1db8048..305d4416da8 100644 --- a/src/masternodes/mn_rpc.h +++ b/src/masternodes/mn_rpc.h @@ -73,5 +73,6 @@ std::vector GetAuthInputsSmart(CWallet* const pwallet, int32_t txVersion, std::string ScriptToString(CScript const& script); CAccounts GetAllMineAccounts(CWallet* const pwallet); CAccounts SelectAccountsByTargetBalances(const CAccounts& accounts, const CBalances& targetBalances, AccountSelectionMode selectionMode); +CScript CreateScriptForHTLC(const JSONRPCRequest& request, uint32_t &blocks, std::vector& image); #endif // DEFI_MASTERNODES_MN_RPC_H diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index b9bf765c899..7c669edbeff 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -220,14 +220,13 @@ static const CRPCConvertParam vRPCConvertParams[] = { "spv_estimateanchorcost", 0, "feerate" }, { "spv_rescan", 0, "height" }, { "spv_gettxconfirmations", 0, "txhash" }, - { "spv_splitutxo", 0, "parts" }, - { "spv_splitutxo", 1, "amount" }, { "spv_setlastheight", 0, "height" }, { "spv_listanchors", 0, "minBtcHeight" }, { "spv_listanchors", 1, "maxBtcHeight" }, { "spv_listanchors", 2, "minConfs" }, { "spv_listanchors", 3, "maxConfs" }, { "spv_sendtoaddress", 1, "amount" }, + { "spv_sendtoaddress", 2, "feerate" }, { "createpoolpair", 0, "metadata" }, { "createpoolpair", 1, "inputs" }, diff --git a/src/script/standard.cpp b/src/script/standard.cpp index a79d0efe59c..6c0dacd21a3 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -322,6 +322,30 @@ CScript GetScriptForWitness(const CScript& redeemscript) return GetScriptForDestination(WitnessV0ScriptHash(redeemscript)); } +CScript GetScriptForHTLC(const CPubKey& seller, const CPubKey& refund, const std::vector image, uint32_t timeout) +{ + CScript script; + + script << OP_IF; + script << OP_SHA256 << image << OP_EQUALVERIFY << ToByteVector(seller); + script << OP_ELSE; + + if (timeout <= 16) + { + script << CScript::EncodeOP_N(timeout); + } + else + { + script << CScriptNum(timeout); + } + + script << OP_CHECKSEQUENCEVERIFY << OP_DROP << ToByteVector(refund); + script << OP_ENDIF; + script << OP_CHECKSIG; + + return script; +} + bool IsValidDestination(const CTxDestination& dest) { return dest.which() != 0; } diff --git a/src/script/standard.h b/src/script/standard.h index 12e59a31634..71e76a11f95 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -199,6 +199,9 @@ CScript GetScriptForRawPubKey(const CPubKey& pubkey); /** Generate a multisig script. */ CScript GetScriptForMultisig(int nRequired, const std::vector& keys); +/** Generate a Hash-Timelock Script script. */ +CScript GetScriptForHTLC(const CPubKey& seller, const CPubKey& refund, const std::vector image, uint32_t timeout); + /** * Generate a pay-to-witness script for the given redeem script. If the redeem * script is P2PK or P2PKH, this returns a P2WPKH script, otherwise it returns a diff --git a/src/spv/bitcoin/BRTransaction.cpp b/src/spv/bitcoin/BRTransaction.cpp index d0eaa120a96..2c9d70644d5 100644 --- a/src/spv/bitcoin/BRTransaction.cpp +++ b/src/spv/bitcoin/BRTransaction.cpp @@ -24,14 +24,14 @@ #include "BRTransaction.h" #include "BRKey.h" -#include "BRAddress.h" #include "BRArray.h" #include #include #include #include -#define TX_VERSION 0x00000001 +#include + #define TX_LOCKTIME 0x00000000 #define SIGHASH_ALL 0x01 // default, sign all of the outputs #define SIGHASH_NONE 0x02 // sign none of the outputs, I don't care where the bitcoins go @@ -351,12 +351,12 @@ static size_t _BRTransactionData(const BRTransaction *tx, uint8_t *data, size_t } // returns a newly allocated empty transaction that must be freed by calling BRTransactionFree() -BRTransaction *BRTransactionNew(void) +BRTransaction *BRTransactionNew(uint32_t version) { BRTransaction *tx = (BRTransaction *)calloc(1, sizeof(*tx)); assert(tx != NULL); - tx->version = TX_VERSION; + tx->version = version; array_new(tx->inputs, 1); array_new(tx->outputs, 2); tx->lockTime = TX_LOCKTIME; @@ -585,6 +585,28 @@ size_t BRTransactionSize(const BRTransaction *tx) return size + witSize; } +size_t BRTransactionHTLCSize(const BRTransaction *tx, const size_t sigSize) +{ + BRTxInput *input; + size_t size; + + size = (tx) ? 8 + BRVarIntSize(tx->inCount) + BRVarIntSize(tx->outCount) : 0; + + for (size_t i = 0; i < tx->inCount; i++) + { + input = &tx->inputs[i]; + + size += sigSize + TX_HTLC_INPUT_NOSIG; + } + + for (size_t i = 0; i < tx->outCount; i++) + { + size += sizeof(uint64_t) + BRVarIntSize(tx->outputs[i].scriptLen) + tx->outputs[i].scriptLen; + } + + return size; +} + // virtual transaction size as defined by BIP141: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki size_t BRTransactionVSize(const BRTransaction *tx) { @@ -637,7 +659,7 @@ int BRTransactionIsSigned(const BRTransaction *tx) // adds signatures to any inputs with NULL signatures that can be signed with any keys // forkId is 0 for bitcoin, 0x40 for b-cash, 0x4f for b-gold // returns true if tx is signed -int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount) +int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType, const uint8_t* seed, const uint8_t *redeemScript) { UInt160 pkh[keysCount]; size_t i, j; @@ -651,17 +673,27 @@ int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCo for (i = 0; tx && i < tx->inCount; i++) { BRTxInput *input = &tx->inputs[i]; - const uint8_t *hash = BRScriptPKH(input->script, input->scriptLen); + const uint8_t *hash; + + if (htlcType == ScriptTypeNone) + { + hash = BRScriptPKH(input->script, input->scriptLen); + } + else + { + UInt160 hash160 = BRHTLCScriptPKH(input->script, input->scriptLen, htlcType); + hash = &hash160.u8[0]; + } j = 0; while (j < keysCount && (! hash || ! UInt160Eq(pkh[j], UInt160Get(hash)))) j++; if (j >= keysCount) continue; - + const uint8_t *elems[BRScriptElements(NULL, 0, input->script, input->scriptLen)]; size_t elemsCount = BRScriptElements(elems, sizeof(elems)/sizeof(*elems), input->script, input->scriptLen); uint8_t pubKey[BRKeyPubKey(&keys[j], NULL, 0)]; size_t pkLen = BRKeyPubKey(&keys[j], pubKey, sizeof(pubKey)); - uint8_t sig[73], script[1 + sizeof(sig) + 1 + sizeof(pubKey)]; + uint8_t sig[73], script[1 + sizeof(sig) + 1 + (seed ? seed[0] + (htlcType == ScriptTypeSeller ? 1 /* OP_1 */ : 0) + 1 /* pushdata */ + redeemScript[0]: sizeof(pubKey))]; size_t sigLen, scriptLen; UInt256 md = UINT256_ZERO; @@ -689,6 +721,36 @@ int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCo BRTxInputSetSignature(input, script, scriptLen); BRTxInputSetWitness(input, script, 0); } + else if (elemsCount == 12 && *elems[0] == OP_IF && *elems[1] == OP_SHA256 && *elems[3] == OP_EQUALVERIFY && // HTLC + *elems[5] == OP_ELSE && *elems[7] == OP_CHECKSEQUENCEVERIFY && *elems[8] == OP_DROP && + *elems[10] == OP_ENDIF && *elems[11] == OP_CHECKSIG) + { + uint8_t data[_BRTransactionData(tx, NULL, 0, i, forkId | SIGHASH_ALL)]; + size_t dataLen = _BRTransactionData(tx, data, sizeof(data), i, forkId | SIGHASH_ALL); + + BRSHA256_2(&md, data, dataLen); + sigLen = BRKeySign(&keys[j], sig, sizeof(sig) - 1, md); + sig[sigLen++] = forkId | SIGHASH_ALL; + + // Add signature + scriptLen = BRScriptPushData(script, sizeof(script), sig, sigLen); + + // Add seed + scriptLen += BRScriptPushData(&script[scriptLen], sizeof(script) - scriptLen, &seed[1], seed[0]); + + if (htlcType == ScriptTypeSeller) + { + // Add OP_1 after seed + script[scriptLen] = OP_1; + ++scriptLen; + } + + // Add redeemscript + scriptLen += BRScriptPushData(&script[scriptLen], sizeof(script) - scriptLen, &redeemScript[1], redeemScript[0]); + + BRTxInputSetSignature(input, script, scriptLen); + BRTxInputSetWitness(input, script, 0); + } else { // pay-to-pubkey uint8_t data[_BRTransactionData(tx, NULL, 0, i, forkId | SIGHASH_ALL)]; size_t dataLen = _BRTransactionData(tx, data, sizeof(data), i, forkId | SIGHASH_ALL); @@ -706,7 +768,7 @@ int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCo uint8_t data[BRTransactionSerialize(tx, NULL, 0)]; size_t len = BRTransactionSerialize(tx, data, sizeof(data)); BRTransaction *t = BRTransactionParse(data, len); - + if (t) tx->txHash = t->txHash, tx->wtxHash = t->wtxHash; if (t) BRTransactionFree(t); return 1; diff --git a/src/spv/bitcoin/BRTransaction.h b/src/spv/bitcoin/BRTransaction.h index f48ea514679..3768ca5ff73 100644 --- a/src/spv/bitcoin/BRTransaction.h +++ b/src/spv/bitcoin/BRTransaction.h @@ -26,6 +26,7 @@ #define BRTransaction_h #include "BRKey.h" +#include "BRAddress.h" #include "BRInt.h" #include @@ -36,9 +37,12 @@ extern "C" { #endif +#define TX_VERSION 0x00000001 +#define TX_VERSION_V2 0x00000002 #define TX_FEE_PER_KB 1000ULL // standard tx fee per kb of tx size (defid 0.12 default min-relay fee-rate) #define TX_OUTPUT_SIZE 34 // estimated size for a typical transaction output #define TX_INPUT_SIZE 148 // estimated size for a typical compact pubkey transaction input +#define TX_HTLC_INPUT_NOSIG 42 // approx. size of input without signature #define TX_MIN_OUTPUT_AMOUNT (TX_FEE_PER_KB*3*(TX_OUTPUT_SIZE + TX_INPUT_SIZE)/1000) //no txout can be below this amount #define TX_MAX_SIZE 100000 // no tx can be larger than this size in bytes #define TX_UNCONFIRMED INT32_MAX // block height indicating transaction is unconfirmed @@ -96,7 +100,7 @@ struct BRTransactionStruct { typedef struct BRTransactionStruct BRTransaction; // returns a newly allocated empty transaction that must be freed by calling BRTransactionFree() -BRTransaction *BRTransactionNew(void); +BRTransaction *BRTransactionNew(uint32_t version = TX_VERSION); // returns a deep copy of tx and that must be freed by calling BRTransactionFree() BRTransaction *BRTransactionCopy(const BRTransaction *tx); @@ -123,6 +127,9 @@ void BRTransactionShuffleOutputs(BRTransaction *tx); // size in bytes if signed, or estimated size assuming compact pubkey sigs size_t BRTransactionSize(const BRTransaction *tx); +// Calculate size of HTLC transaction +size_t BRTransactionHTLCSize(const BRTransaction *tx, const size_t sigSize); + // virtual transaction size as defined by BIP141: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki size_t BRTransactionVSize(const BRTransaction *tx); @@ -135,7 +142,7 @@ int BRTransactionIsSigned(const BRTransaction *tx); // adds signatures to any inputs with NULL signatures that can be signed with any keys // forkId is 0 for bitcoin, 0x40 for b-cash, 0x4f for b-gold // returns true if tx is signed -int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount); +int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType = ScriptTypeNone, const uint8_t* seed = nullptr, const uint8_t* redeemScript = nullptr); // true if tx meets IsStandard() rules: https://bitcoin.org/en/developer-guide#standard-transactions int BRTransactionIsStandard(const BRTransaction *tx); diff --git a/src/spv/bitcoin/BRWallet.cpp b/src/spv/bitcoin/BRWallet.cpp index f548148c1c4..96be3ac1e2c 100644 --- a/src/spv/bitcoin/BRWallet.cpp +++ b/src/spv/bitcoin/BRWallet.cpp @@ -89,7 +89,8 @@ struct BRWalletStruct { int forkId; UInt160 *internalChain, *externalChain; BRSet *allTx, *invalidTx, *pendingTx, *spentOutputs, *usedPKH, *allPKH; - std::set* userPKH; + BRUserAddresses* userPKH; + BRUserAddresses* htlcPKH; void *callbackInfo; void (*balanceChanged)(void *info, uint64_t balance); void (*txAdded)(void *info, BRTransaction *tx); @@ -180,7 +181,7 @@ static int _BRWalletContainsTx(BRWallet *wallet, const BRTransaction *tx) return r; } -static int _BRWalletContainsUserTx(BRWallet *wallet, const BRTransaction *tx) +static int _BRWalletContainsUserTx(BRWallet *wallet, const BRTransaction *tx, const bool htlc = false) { int r = 0; const uint8_t *pkh; @@ -191,6 +192,7 @@ static int _BRWalletContainsUserTx(BRWallet *wallet, const BRTransaction *tx) UInt160 hash160; UIntConvert(pkh, hash160); if (wallet->userPKH->count(hash160)) r = 1; + if (htlc && wallet->htlcPKH->count(hash160)) r = 1; } } @@ -203,12 +205,108 @@ static int _BRWalletContainsUserTx(BRWallet *wallet, const BRTransaction *tx) UInt160 hash160; UIntConvert(pkh, hash160); if (wallet->userPKH->count(hash160)) r = 1; + if (htlc && wallet->htlcPKH->count(hash160)) r = 1; } } return r; } +static bool _BRWalletContainsHTLCOutput(BRWallet *wallet, const BRTransaction *tx, size_t &output, const UInt160& addressFilter) +{ + bool r = false; + const uint8_t *pkh; + + for (size_t i = 0; ! r && i < tx->outCount; i++) + { + pkh = BRScriptPKH(tx->outputs[i].script, tx->outputs[i].scriptLen); + if (pkh) + { + UInt160 hash160; + UIntConvert(pkh, hash160); + if (wallet->htlcPKH->count(hash160)) + { + // Check if result matches address filter + if (!UInt160Eq(UINT160_ZERO, addressFilter) && !UInt160Eq(addressFilter, hash160)) + { + continue; + } + + output = i; + r = true; + } + } + } + + return r; +} + +std::string BRGetHTLCSeed(BRWallet *wallet, const uint8_t *md20) +{ + std::set htlcSends; + const uint8_t *pkh; + UInt160 htlcAddress; + UIntConvert(md20, htlcAddress); + + for (BRTransaction* previous = nullptr; (previous = static_cast(BRSetIterate(wallet->allTx, previous))); ) + { + for (size_t i = 0; i < previous->inCount; ++i) + { + BRTransaction *t = static_cast(BRSetGet(wallet->allTx, &previous->inputs[i].txHash)); + uint32_t n = previous->inputs[i].index; + + pkh = (t && n < t->outCount) ? BRScriptPKH(t->outputs[n].script, t->outputs[n].scriptLen) : nullptr; + if (pkh) + { + UInt160 hash160; + UIntConvert(pkh, hash160); + if (UInt160Eq(htlcAddress, hash160)) + { + size_t sigLen = previous->inputs[i].sigLen; + + // Check there is a signature + if (sigLen > 0) + { + size_t startOffset = 1 /* length byte */ + previous->inputs[i].signature[0]; + + if (sigLen > startOffset) + { + size_t secretLength = previous->inputs[i].signature[startOffset]; + size_t opCodeByte{startOffset + secretLength + 1}; + + // Check for OP_1 after secret, if missing then this is not the secret. + if (sigLen >= opCodeByte && previous->inputs[i].signature[opCodeByte] == OP_1) + { + return HexStr(previous->inputs[i].signature + startOffset + 1, previous->inputs[i].signature + opCodeByte); + } + } + } + } + } + } + } + + return {}; +} + + +bool BRWalletTxSpent(BRWallet *wallet, const BRTransaction *tx, const uint32_t output, uint256 &spent) +{ + for (BRTransaction* previous = nullptr; (previous = static_cast(BRSetIterate(wallet->allTx, previous))); ) + { + for (size_t i{0}; i < previous->inCount; ++i) + { + if (UInt256Eq(previous->inputs[i].txHash, tx->txHash) && previous->inputs[i].index == output) + { + spent = to_uint256(previous->txHash); + return true; + } + } + } + + return false; +} + std::set BRListUserTransactions(BRWallet *wallet) { std::set userTransactions; @@ -217,7 +315,7 @@ std::set BRListUserTransactions(BRWallet *wallet) for (size_t i = 0; i < array_count(wallet->transactions); ++i) { tx = wallet->transactions[i]; - if (_BRWalletContainsUserTx(wallet, tx)) { + if (_BRWalletContainsUserTx(wallet, tx, true /* HTLC */)) { userTransactions.insert(to_uint256(tx->txHash).ToString()); } } @@ -225,6 +323,25 @@ std::set BRListUserTransactions(BRWallet *wallet) return userTransactions; } +std::vector> BRListHTLCReceived(BRWallet *wallet, const UInt160& addr) +{ + std::vector> htlcTransactions; + BRTransaction *tx; + size_t output{0}; + + for (size_t i = 0; i < array_count(wallet->transactions); ++i) + { + tx = wallet->transactions[i]; + + if (_BRWalletContainsHTLCOutput(wallet, tx, output, addr)) + { + htlcTransactions.emplace_back(tx, output); + } + } + + return htlcTransactions; +} + std::string BRGetRawTransaction(BRWallet *wallet, UInt256 txHash) { auto tx = BRWalletTransactionForHash(wallet, txHash); @@ -317,8 +434,9 @@ static void _BRWalletUpdateBalance(BRWallet *wallet) for (j = 0; j < tx->outCount; j++) { if (tx->outputs[j].address[0] != '\0') { pkh = BRScriptPKH(tx->outputs[j].script, tx->outputs[j].scriptLen); - - if (pkh && BRSetContains(wallet->allPKH, pkh)) { + UInt160 hash160; + UIntConvert(pkh, hash160); + if (pkh && BRSetContains(wallet->allPKH, pkh) && wallet->htlcPKH->count(hash160) == 0) { BRSetAdd(wallet->usedPKH, (void *)pkh); array_add(wallet->utxos, ((const BRUTXO) { tx->txHash, (uint32_t)j })); balance += tx->outputs[j].amount; @@ -348,7 +466,7 @@ static void _BRWalletUpdateBalance(BRWallet *wallet) // allocates and populates a BRWallet struct which must be freed by calling BRWalletFree() // forkId is 0 for bitcoin, 0x40 for b-cash BRWallet *BRWalletNew(BRTransaction *transactions[], size_t txCount, BRMasterPubKey mpk, int forkId, - std::set* userAddresses) + BRUserAddresses* userAddresses, BRUserAddresses* htlcAddresses) { BRWallet *wallet = NULL; BRTransaction *tx; @@ -372,6 +490,7 @@ BRWallet *BRWalletNew(BRTransaction *transactions[], size_t txCount, BRMasterPub wallet->usedPKH = BRSetNew(_pkhHash, _pkhEq, txCount + 100); wallet->allPKH = BRSetNew(_pkhHash, _pkhEq, txCount + 100); wallet->userPKH = userAddresses; + wallet->htlcPKH = htlcAddresses; for (size_t i = 0; transactions && i < txCount; i++) { tx = transactions[i]; @@ -388,7 +507,7 @@ BRWallet *BRWalletNew(BRTransaction *transactions[], size_t txCount, BRMasterPub BRWalletUnusedAddrs(wallet, NULL, SEQUENCE_GAP_LIMIT_EXTERNAL, SEQUENCE_EXTERNAL_CHAIN); BRWalletUnusedAddrs(wallet, NULL, SEQUENCE_GAP_LIMIT_INTERNAL, SEQUENCE_INTERNAL_CHAIN); - if (!wallet->userPKH->empty()) { + if (!wallet->userPKH->empty() || !wallet->htlcPKH->empty()) { BRWalletAddUserAddresses(wallet); } @@ -530,7 +649,7 @@ bool BRWalletAddSingleAddress(BRWallet *wallet, const uint8_t& pubKey, const siz return true; } -void BRWalletImportAddress(BRWallet *wallet, const uint160& userHash) +void BRWalletImportAddress(BRWallet *wallet, const uint160& userHash, const bool htlc) { assert(wallet != nullptr); wallet->lock.lock(); @@ -544,7 +663,15 @@ void BRWalletImportAddress(BRWallet *wallet, const uint160& userHash) size_t count = array_count(chain); array_add(chain, hash); - wallet->userPKH->insert(hash); + + if (htlc) + { + wallet->htlcPKH->insert(hash); + } + else + { + wallet->userPKH->insert(hash); + } // was chain moved to a new memory location? if (chain == origChain) @@ -581,6 +708,10 @@ void BRWalletAddUserAddresses(BRWallet *wallet) { array_add(chain, address); } + for (const auto& address : *wallet->htlcPKH) { + array_add(chain, address); + } + size_t after_count = array_count(chain); // was chain moved to a new memory location? @@ -795,7 +926,7 @@ int BRWalletAddressIsUsed(BRWallet *wallet, const char *addr) // returns an unsigned transaction that sends the specified amount from the wallet to the given address // result must be freed by calling BRTransactionFree() -BRTransaction *BRWalletCreateTransaction(BRWallet *wallet, uint64_t amount, const char *addr, std::string changeAddress) +BRTransaction *BRWalletCreateTransaction(BRWallet *wallet, uint64_t amount, const char *addr, std::string changeAddress, int64_t feeRate) { BRTxOutput o = BR_TX_OUTPUT_NONE; @@ -804,12 +935,12 @@ BRTransaction *BRWalletCreateTransaction(BRWallet *wallet, uint64_t amount, cons assert(addr != NULL && BRAddressIsValid(addr)); o.amount = amount; BRTxOutputSetAddress(&o, addr); - return BRWalletCreateTxForOutputs(wallet, &o, 1, changeAddress); + return BRWalletCreateTxForOutputs(wallet, &o, 1, changeAddress, feeRate); } // returns an unsigned transaction that satisifes the given transaction outputs // result must be freed by calling BRTransactionFree() -BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput outputs[], size_t outCount, std::string changeAddress) +BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput outputs[], size_t outCount, std::string changeAddress, int64_t feeRate) { BRTransaction *tx, *transaction = BRTransactionNew(); uint64_t feeAmount, amount = 0, balance = 0, minAmount; @@ -829,7 +960,15 @@ BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput out minAmount = BRWalletMinOutputAmountWithFeePerKb(wallet, MIN_FEE_PER_KB); wallet->lock.lock(); - feeAmount = _txFee(wallet->feePerKb, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + if (feeRate > 1000) + { + feeAmount = _txFee(feeRate, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + } + else + { + feeAmount = _txFee(wallet->feePerKb, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + } + // TODO: use up all UTXOs for all used addresses to avoid leaving funds in addresses whose public key is revealed // TODO: avoid combining addresses in a single transaction when possible to reduce information leakage @@ -839,7 +978,13 @@ BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput out o = &wallet->utxos[i]; tx = (BRTransaction *)BRSetGet(wallet->allTx, o); - if (! tx || o->n >= tx->outCount) continue; + + // Exclude HTLC outputs + uint8_t* pkh = BRScriptPKH(tx->outputs[o->n].script, tx->outputs[o->n].scriptLen); + UInt160 hash160; + UIntConvert(pkh, hash160); + + if (! tx || o->n >= tx->outCount || wallet->htlcPKH->count(hash160)) continue; BRTransactionAddInput(transaction, tx->txHash, o->n, tx->outputs[o->n].amount, tx->outputs[o->n].script, tx->outputs[o->n].scriptLen, NULL, 0, NULL, 0, TXIN_SEQUENCE); @@ -875,7 +1020,14 @@ BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput out balance += tx->outputs[o->n].amount; // fee amount after adding a change output - feeAmount = _txFee(wallet->feePerKb, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + if (feeRate > 1000) + { + feeAmount = _txFee(feeRate, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + } + else + { + feeAmount = _txFee(wallet->feePerKb, BRTransactionVSize(transaction) + TX_OUTPUT_SIZE); + } // increase fee to round off remaining wallet balance to nearest 100 satoshi if (wallet->balance > amount + feeAmount) feeAmount += (wallet->balance - (amount + feeAmount)) % 100; @@ -1426,6 +1578,7 @@ void BRWalletFree(BRWallet *wallet) assert(wallet != NULL); wallet->lock.lock(); delete wallet->userPKH; + delete wallet->htlcPKH; BRSetFree(wallet->allPKH); BRSetFree(wallet->usedPKH); BRSetFree(wallet->invalidTx); diff --git a/src/spv/bitcoin/BRWallet.h b/src/spv/bitcoin/BRWallet.h index d41db76a6f3..f8ec5cf1ba9 100644 --- a/src/spv/bitcoin/BRWallet.h +++ b/src/spv/bitcoin/BRWallet.h @@ -32,6 +32,10 @@ #include #include +#include + +// Convert SPV UInt256 to Bitcoin uint256 +uint256 to_uint256(UInt256 const & i); #ifdef __cplusplus extern "C" { @@ -58,16 +62,16 @@ inline static int BRUTXOEq(const void *utxo, const void *otherUtxo) ((const BRUTXO *)utxo)->n == ((const BRUTXO *)otherUtxo)->n)); } - -// Convert SPV UInt256 to Bitcoin uint256 -uint256 to_uint256(UInt256 const & i); - +// Wallet struct typedef struct BRWalletStruct BRWallet; +// Type to hold user addresses added from the DeFi wallet store +typedef std::set BRUserAddresses; + // allocates and populates a BRWallet struct that must be freed by calling BRWalletFree() // forkId is 0 for bitcoin, 0x40 for b-cash BRWallet *BRWalletNew(BRTransaction *transactions[], size_t txCount, BRMasterPubKey mpk, int forkId, - std::set *userAddresses); + BRUserAddresses *userAddresses, BRUserAddresses *htlcAddresses); // not thread-safe, set callbacks once after BRWalletNew(), before calling other BRWallet functions // info is a void pointer that will be passed along with each callback call @@ -92,7 +96,7 @@ void BRWalletSetCallbacks(BRWallet *wallet, void *info, size_t BRWalletUnusedAddrs(BRWallet *wallet, BRAddress addrs[], uint32_t gapLimit, uint32_t internal); // Import single uint160 into wallet -void BRWalletImportAddress(BRWallet *wallet, const uint160& userHash); +void BRWalletImportAddress(BRWallet *wallet, const uint160& userHash, const bool htlc = false); // Add Bitcoin public key to SPV wallet from DeFi public key bool BRWalletAddSingleAddress(BRWallet *wallet, const uint8_t &pubKey, const size_t pkLen, BRAddress& addr); @@ -143,11 +147,11 @@ void BRWalletSetFeePerKb(BRWallet *wallet, uint64_t feePerKb); // returns an unsigned transaction that sends the specified amount from the wallet to the given address // result must be freed using BRTransactionFree() -BRTransaction *BRWalletCreateTransaction(BRWallet *wallet, uint64_t amount, const char *addr, std::string changeAddress); +BRTransaction *BRWalletCreateTransaction(BRWallet *wallet, uint64_t amount, const char *addr, std::string changeAddress, int64_t feeRate); // returns an unsigned transaction that satisifes the given transaction outputs // result must be freed using BRTransactionFree() -BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput outputs[], size_t outCount, std::string changeAddress); +BRTransaction *BRWalletCreateTxForOutputs(BRWallet *wallet, const BRTxOutput outputs[], size_t outCount, std::string changeAddress, int64_t feeRate = 0); // signs any inputs in tx that can be signed using private keys from the wallet // seed is the master private key (wallet seed) corresponding to the master public key given when the wallet was created @@ -207,6 +211,9 @@ uint64_t BRWalletMinOutputAmountWithFeePerKb(BRWallet *wallet, uint64_t feePerKb // maximum amount that can be sent from the wallet to a single address after fees uint64_t BRWalletMaxOutputAmount(BRWallet *wallet); +// Check if transaction is spent +bool BRWalletTxSpent(BRWallet *wallet, const BRTransaction *tx, const uint32_t output, uint256& spent); + // frees memory allocated for wallet, and calls BRTransactionFree() for all registered transactions void BRWalletFree(BRWallet *wallet); @@ -222,9 +229,15 @@ int64_t BRBitcoinAmount(int64_t localAmount, double price); } #endif -// Returns a set of all user related transactions. +// Get HTLC secret for contract address. +std::string BRGetHTLCSeed(BRWallet *wallet, const uint8_t *md20); + +// Returns a set of all user related TXIDs. std::set BRListUserTransactions(BRWallet *wallet); +// Returns a vector of all HTLC relates transactions +std::vector > BRListHTLCReceived(BRWallet *wallet, const UInt160 &addr); + // Returns the raw hex encoded transaction data if found std::string BRGetRawTransaction(BRWallet *wallet, UInt256 txHash); diff --git a/src/spv/spv_rpc.cpp b/src/spv/spv_rpc.cpp index 671ce2fa800..5ba28058ef4 100644 --- a/src/spv/spv_rpc.cpp +++ b/src/spv/spv_rpc.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -24,20 +25,10 @@ #include #include -static CWallet* GetWallet(const JSONRPCRequest& request) -{ - std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); - CWallet* const pwallet = wallet.get(); - - EnsureWalletIsAvailable(pwallet, false); - EnsureWalletIsUnlocked(pwallet); - return pwallet; -} - UniValue spv_sendrawtx(const JSONRPCRequest& request) { RPCHelpMan{"spv_sendrawtx", - "\nSending raw tx to DeFi Blockchain\n", + "\nSending raw tx to Bitcoin blockchain\n", { {"rawtx", RPCArg::Type::STR, RPCArg::Optional::NO, "The hex-encoded raw transaction with signature" }, }, @@ -66,53 +57,6 @@ UniValue spv_sendrawtx(const JSONRPCRequest& request) return UniValue(""); } -/* - * For tests|experiments only -*/ -UniValue spv_splitutxo(const JSONRPCRequest& request) -{ - RPCHelpMan{"spv_splitutxo", - "\nFor tests|experiments only\n", - { - {"parts", RPCArg::Type::NUM, RPCArg::Optional::NO, "Number of parts" }, - {"amount", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Amount of each part, optional"}, - }, - RPCResult{ - "\"txHex\" (string) The hex-encoded raw transaction with signature(s)\n" - "\"txHash\" (string) The hex-encoded transaction hash\n" - }, - RPCExamples{ - HelpExampleCli("spv_splitutxo", "5 10000") - + HelpExampleRpc("spv_splitutxo", "5 10000") - }, - }.Check(request); - - RPCTypeCheck(request.params, { UniValue::VNUM, UniValue::VNUM }, true); - - int parts = request.params[0].get_int(); - int amount = request.params[1].empty() ? 0 : request.params[1].get_int(); - - /// @todo temporary, tests - auto rawtx = spv::CreateSplitTx("1251d1fc46d104564ca8311696d561bf7de5c0e336039c7ccfe103f7cdfc026e", 2, 3071995, "cStbpreCo2P4nbehPXZAAM3gXXY1sAphRfEhj7ADaLx8i2BmxvEP", parts, amount); - - bool send = false; - if (send) { - if (!spv::pspv) - throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); - - spv::pspv->SendRawTx(rawtx); - } - - CMutableBtcTransaction mtx; - (void) DecodeHexBtcTx(mtx, std::string(rawtx.begin(), rawtx.end()), true); - - UniValue result(UniValue::VOBJ); - result.pushKV("txHex", HexStr(rawtx)); - result.pushKV("txHash", CBtcTransaction(mtx).GetHash().ToString()); - - return result; -} - extern CAmount GetAnchorSubsidy(int anchorHeight, int prevAnchorHeight, const Consensus::Params& consensusParams); /* @@ -540,8 +484,8 @@ UniValue spv_listanchorspending(const JSONRPCRequest& request) "\"array\" Returns array of pending anchors\n" }, RPCExamples{ - HelpExampleCli("spv_listanchors", "") // list completely confirmed anchors not older than 1500000 height - + HelpExampleRpc("spv_listanchors", "") // list anchors in mempool (or -1 -1 -1 0) + HelpExampleCli("spv_listanchorspending", "") // list completely confirmed anchors not older than 1500000 height + + HelpExampleRpc("spv_listanchorspending", "") // list anchors in mempool (or -1 -1 -1 0) }, }.Check(request); @@ -793,11 +737,11 @@ UniValue spv_listanchorsunrewarded(const JSONRPCRequest& request) { }, RPCResult{ - "\"array\" Returns array of anchor rewards\n" + "\"array\" Returns array of unrewarded anchors\n" }, RPCExamples{ - HelpExampleCli("spv_listanchorrewards", "") - + HelpExampleRpc("spv_listanchorrewards", "") + HelpExampleCli("spv_listanchorsunrewarded", "") + + HelpExampleRpc("spv_listanchorsunrewarded", "") }, }.Check(request); @@ -846,6 +790,281 @@ UniValue spv_setlastheight(const JSONRPCRequest& request) return UniValue(); } +UniValue spv_decodehtlcscript(const JSONRPCRequest& request) +{ + RPCHelpMan{"spv_decodehtlcscript", + "\nDecode and return value in a HTLC redeemscript\n", + { + {"redeemscript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The HTLC redeemscript"}, + }, + RPCResult{ + "{\n" + " \"sellerkey\" (string) Seller public key\n" + " \"buyerkey\" (string) Buyer public key\n" + " \"blocks\" (number) Locktime in number of blocks\n" + " \"hash\" (string) Hash of the seed\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("spv_decodehtlcscript", "\\\"redeemscript\\\"") + + HelpExampleRpc("spv_decodehtlcscript", "\\\"redeemscript\\\"") + }, + }.Check(request); + + if (!IsHex(request.params[0].get_str())) + { + throw JSONRPCError(RPC_TYPE_ERROR, "Redeemscript expected in hex format"); + } + + auto redeemBytes = ParseHex(request.params[0].get_str()); + CScript redeemScript(redeemBytes.begin(), redeemBytes.end()); + auto details = spv::GetHTLCDetails(redeemScript); + + UniValue result(UniValue::VOBJ); + result.pushKV("sellerkey", HexStr(details.sellerKey)); + result.pushKV("buyerkey", HexStr(details.buyerKey)); + result.pushKV("blocks", static_cast(details.locktime)); + result.pushKV("hash", HexStr(details.hash)); + + return result; +} + +UniValue spv_createhtlc(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWallet(request); + + RPCHelpMan{"spv_createhtlc", + "\nCreates a Bitcoin address whose funds can be unlocked with a seed or as a refund.\n" + "It returns a json object with the address and redeemScript.\n", + { + {"seller_key", RPCArg::Type::STR, RPCArg::Optional::NO, "The public key of the possessor of the seed"}, + {"refund_key", RPCArg::Type::STR, RPCArg::Optional::NO, "The public key of the recipient of the refund"}, + {"timeout", RPCArg::Type::STR, RPCArg::Optional::NO, "Timeout of the contract (denominated in blocks) relative to its placement in the blockchain"}, + {"seed", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "SHA256 hash of the seed. If none provided one will be generated"}, + }, + RPCResult{ + "{\n" + " \"address\":\"address\" (string) The value of the new Bitcoin address\n" + " \"redeemScript\":\"script\" (string) Hex-encoded redemption script\n" + " \"seed\":\"seed\" (string) Hex-encoded seed if no seed provided\n" + " \"seedhash\":\"seedhash\" (string) Hex-encoded seed hash if no seed provided\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("spv_createhtlc", "0333ffc4d18c7b2adbd1df49f5486030b0b70449c421189c2c0f8981d0da9669af 034201385acc094d24db4b53a05fc8991b10e3467e6e20a8551c49f89e7e4d0d3c 10 254e38932fdb9fc27f82aac2a5cc6d789664832383e3cf3298f8c120812712db") + + HelpExampleRpc("spv_createhtlc", "0333ffc4d18c7b2adbd1df49f5486030b0b70449c421189c2c0f8981d0da9669af, 034201385acc094d24db4b53a05fc8991b10e3467e6e20a8551c49f89e7e4d0d3c, 10, 254e38932fdb9fc27f82aac2a5cc6d789664832383e3cf3298f8c120812712db") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + std::vector hashBytes; + CKeyingMaterial seed; + + // Seed hash provided + if (!request.params[3].isNull()) + { + std::string hash = request.params[3].get_str(); + + if (IsHex(hash)) + { + hashBytes = ParseHex(hash); + + if (hashBytes.size() != 32) + { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid hash image length, 32 (SHA256) accepted"); + } + } + else + { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid hash image"); + } + } + else // No seed hash provided, generate seed + { + hashBytes.resize(32); + seed.resize(32); + GetStrongRandBytes(seed.data(), seed.size()); + + CSHA256 hash; + hash.Write(seed.data(), seed.size()); + hash.Finalize(hashBytes.data()); + } + + // Get HTLC script + uint32_t blocks; + CScript inner = CreateScriptForHTLC(request, blocks, hashBytes); + + // Get destination + CScriptID innerID(inner); + ScriptHash scriptHash(innerID); + + // Add address and script to DeFi wallet storage for persistance + pwallet->SetAddressBook(scriptHash, "htlc", "htlc"); + pwallet->AddCScript(inner); + + // Add to SPV to watch transactions to this script + spv::pspv->AddBitcoinHash(scriptHash, true); + spv::pspv->RebuildBloomFilter(); + + // Rescan negative blocks deep in case we are importing after Bitcoin send + spv::pspv->Rescan(-blocks); + + // Create Bitcoin address + std::vector data(21, spv::pspv->GetP2SHPrefix()); + memcpy(&data[1], &innerID, 20); + + UniValue result(UniValue::VOBJ); + result.pushKV("address", EncodeBase58Check(data)); + result.pushKV("redeemScript", HexStr(inner)); + + if (!seed.empty()) + { + result.pushKV("seed", HexStr(seed)); + result.pushKV("seedhash", HexStr(hashBytes)); + } + + return result; +} + +UniValue spv_listhtlcoutputs(const JSONRPCRequest& request) +{ + RPCHelpMan{"spv_listhtlcoutputs", + "\nList all outputs related to HTLC addresses in the wallet\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "HTLC address to filter results"}, + }, + RPCResult{ + "[ (JSON array of transaction details)\n" + "{\n" + " \"txid\" (string) The transaction id\n" + " \"vout\" (numeric) Output relating to the HTLC address\n" + " \"address\" (string) HTLC address\n" + " \"confirms\" (numeric) Number of confirmations\n" + " { \"spent\" (JSON object containing spent info)\n" + " \"txid\" (string) Transaction id spending this output\n" + " \"confirms\" (numeric) Number of spent confirmations\n" + " }\n" + "}, ...]\n" + }, + RPCExamples{ + HelpExampleCli("spv_listhtlcoutputs", "") + + HelpExampleRpc("spv_listhtlcoutputs", "") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + return spv::pspv->GetHTLCReceived(request.params[0].isNull() ? "" : request.params[0].get_str()); +} + +UniValue spv_gethtlcseed(const JSONRPCRequest& request) +{ + RPCHelpMan{"spv_gethtlcseed", + "\nReturns the HTLC secret if available\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "HTLC address"}, + }, + RPCResult{ + "\"secret\" (string) Returns HTLC seed\n" + }, + RPCExamples{ + HelpExampleCli("spv_gethtlcseed", "\\\"address\\\"") + + HelpExampleRpc("spv_gethtlcseed", "\\\"address\\\"") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + CKeyID key = spv::pspv->GetAddressKeyID(request.params[0].get_str().c_str()); + + return spv::pspv->GetHTLCSeed(key.begin()); +} + +UniValue spv_claimhtlc(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWallet(request); + + RPCHelpMan{"spv_claimhtlc", + "\nClaims all coins in HTLC address\n", + { + {"scriptaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "HTLC address"}, + {"destinationaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Destination for funds in the HTLC"}, + {"seed", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Seed that was used to generate the hash in the HTLC"}, + {"feerate", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Feerate (satoshis) per KB (Default: " + std::to_string(spv::DEFAULT_BTC_FEE_PER_KB) + ")"}, + }, + RPCResult{ + "{\n" + " \"txid\" (string) The transaction id\n" + " \"sendmessage\" (string) Send message result\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("spv_claimhtlc", "\"3QwKW5GKHc1eSbbwTozsVzB1UBVyAbZQpa\" \"bc1q28jh8l7a9m0x5ngq0ccld2glpn4ehzwmfczf0n\" \"696c6c756d696e617469\" 100000") + + HelpExampleRpc("spv_claimhtlc", "\"3QwKW5GKHc1eSbbwTozsVzB1UBVyAbZQpa\", \"bc1q28jh8l7a9m0x5ngq0ccld2glpn4ehzwmfczf0n\", \"696c6c756d696e617469\", 100000") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + if (!spv::pspv->IsConnected()) + { + throw JSONRPCError(RPC_MISC_ERROR, "spv not connected"); + } + + return spv::pspv->CreateHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), + request.params[2].get_str(), request.params[3].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[3].get_int64(), true); +} + +UniValue spv_refundhtlc(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWallet(request); + + RPCHelpMan{"spv_refundhtlc", + "\nRefunds all coins in HTLC address\n", + { + {"scriptaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "HTLC address"}, + {"destinationaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Destination for funds in the HTLC"}, + {"feerate", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Feerate (satoshis) per KB (Default: " + std::to_string(spv::DEFAULT_BTC_FEE_PER_KB) + ")"}, + }, + RPCResult{ + "{\n" + " \"txid\" (string) The transaction id\n" + " \"sendmessage\" (string) Send message result\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("spv_refundhtlc", "\"3QwKW5GKHc1eSbbwTozsVzB1UBVyAbZQpa\" \"bc1q28jh8l7a9m0x5ngq0ccld2glpn4ehzwmfczf0n\" 100000") + + HelpExampleRpc("spv_refundhtlc", "\"3QwKW5GKHc1eSbbwTozsVzB1UBVyAbZQpa\", \"bc1q28jh8l7a9m0x5ngq0ccld2glpn4ehzwmfczf0n\", 100000") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + if (!spv::pspv->IsConnected()) + { + throw JSONRPCError(RPC_MISC_ERROR, "spv not connected"); + } + + return spv::pspv->CreateHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), + "", request.params[3].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[3].get_int64(), false); +} + UniValue spv_fundaddress(const JSONRPCRequest& request) { CWallet* const pwallet = GetWallet(request); @@ -871,7 +1090,7 @@ UniValue spv_fundaddress(const JSONRPCRequest& request) std::string strAddress = request.params[0].get_str(); - return fake_spv->SendBitcoins(pwallet, strAddress, -1); + return fake_spv->SendBitcoins(pwallet, strAddress, -1, 1000); } static UniValue spv_getnewaddress(const JSONRPCRequest& request) @@ -914,12 +1133,41 @@ static UniValue spv_getnewaddress(const JSONRPCRequest& request) return newAddress; } +static UniValue spv_getaddresspubkey(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWallet(request); + + RPCHelpMan{"spv_getaddresspubkey", + "\nReturn raw pubkey for Bitcoin address if in SPV wallet\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "Bitcoin address"}, + }, + RPCResult{ + "\"pubkey\" (string) Raw pubkey hex\n" + }, + RPCExamples{ + HelpExampleCli("spv_getaddresspubkey", "") + + HelpExampleRpc("spv_getaddresspubkey", "") + }, + }.Check(request); + + if (!spv::pspv) { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + LOCK(pwallet->cs_wallet); + + auto address = request.params[0].get_str().c_str(); + + return spv::pspv->GetAddressPubkey(pwallet, address); +} + static UniValue spv_dumpprivkey(const JSONRPCRequest& request) { CWallet* const pwallet = GetWallet(request); RPCHelpMan{"spv_dumpprivkey", - "\nReveals the private key corresponding to 'address'.\n", + "\nReveals the private key corresponding to 'address'\n", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The BTC address for the private key"}, }, @@ -973,10 +1221,14 @@ static UniValue spv_sendtoaddress(const JSONRPCRequest& request) HelpRequiringPassphrase(pwallet) + "\n", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The Bitcoin address to send to."}, - {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in " + CURRENCY_UNIT + " to send. eg 0.1"}, + {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in BTC to send. eg 0.1"}, + {"feerate", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Feerate (satoshis) per KB (Default: " + std::to_string(spv::DEFAULT_BTC_FEE_PER_KB) + ")"}, }, RPCResult{ - "\"txid\" (string) The transaction id.\n" + "{\n" + " \"txid\" (string) The transaction id\n" + " \"sendmessage\" (string) Send message result\n" + "}\n" }, RPCExamples{ HelpExampleCli("spv_sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 0.1") @@ -1011,7 +1263,9 @@ static UniValue spv_sendtoaddress(const JSONRPCRequest& request) EnsureWalletIsUnlocked(pwallet); - return spv::pspv->SendBitcoins(pwallet, address, nAmount); + CAmount feerate = request.params[2].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[2].get_int64(); + + return spv::pspv->SendBitcoins(pwallet, address, nAmount, feerate); } static UniValue spv_listtransactions(const JSONRPCRequest& request) @@ -1074,7 +1328,6 @@ static const CRPCCommand commands[] = { "spv", "spv_rescan", &spv_rescan, { "height" } }, { "spv", "spv_syncstatus", &spv_syncstatus, { } }, { "spv", "spv_gettxconfirmations", &spv_gettxconfirmations, { "txhash" } }, - { "hidden", "spv_splitutxo", &spv_splitutxo, { "parts", "amount" } }, { "spv", "spv_listanchors", &spv_listanchors, { "minBtcHeight", "maxBtcHeight", "minConfs", "maxConfs" } }, { "spv", "spv_listanchorauths", &spv_listanchorauths, { } }, { "spv", "spv_listanchorrewardconfirms", &spv_listanchorrewardconfirms, { } }, @@ -1082,11 +1335,18 @@ static const CRPCCommand commands[] = { "spv", "spv_listanchorsunrewarded", &spv_listanchorsunrewarded, { } }, { "spv", "spv_listanchorspending", &spv_listanchorspending, { } }, { "spv", "spv_getnewaddress", &spv_getnewaddress, { } }, + { "spv", "spv_getaddresspubkey", &spv_getaddresspubkey, { "address" } }, { "spv", "spv_dumpprivkey", &spv_dumpprivkey, { } }, { "spv", "spv_getbalance", &spv_getbalance, { } }, - { "spv", "spv_sendtoaddress", &spv_sendtoaddress, { "address", "amount" } }, + { "spv", "spv_sendtoaddress", &spv_sendtoaddress, { "address", "amount", "feerate" } }, { "spv", "spv_listtransactions", &spv_listtransactions, { } }, { "spv", "spv_getrawtransaction", &spv_getrawtransaction, { "txid" } }, + { "spv", "spv_createhtlc", &spv_createhtlc, { "seller_key", "refund_key", "hash", "timeout" } }, + { "spv", "spv_claimhtlc", &spv_claimhtlc, { "scriptaddress", "destinationaddress", "seed", "feerate" } }, + { "spv", "spv_refundhtlc", &spv_refundhtlc, { "scriptaddress", "destinationaddress", "feerate" } }, + { "spv", "spv_listhtlcoutputs", &spv_listhtlcoutputs, { "address" } }, + { "spv", "spv_decodehtlcscript", &spv_decodehtlcscript, { "redeemscript" } }, + { "spv", "spv_gethtlcseed", &spv_gethtlcseed, { "address" } }, { "hidden", "spv_setlastheight", &spv_setlastheight, { "height" } }, { "hidden", "spv_fundaddress", &spv_fundaddress, { "address" } }, }; diff --git a/src/spv/spv_wrapper.cpp b/src/spv/spv_wrapper.cpp index 167b7a39bf3..707bb7b8342 100644 --- a/src/spv/spv_wrapper.cpp +++ b/src/spv/spv_wrapper.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -39,11 +40,11 @@ std::string DecodeSendResult(int result) { switch (result) { case ENOSPV: - return "spv module disabled"; + return "SPV module disabled"; case EPARSINGTX: - return "Can't parse transaction"; + return "Cannot parse transaction"; case ETXNOTSIGNED: - return "Tx not signed"; + return "Transaction not signed"; default: return strerror(result); } @@ -54,13 +55,12 @@ namespace spv std::unique_ptr pspv; -using namespace std; - // Prefixes to the masternodes database (masternodes/) static const char DB_SPVBLOCKS = 'B'; // spv "blocks" table static const char DB_SPVTXS = 'T'; // spv "tx2msg" table uint64_t const DEFAULT_BTC_FEERATE = TX_FEE_PER_KB; +uint64_t const DEFAULT_BTC_FEE_PER_KB = DEFAULT_FEE_PER_KB; /// spv wallet manager's callbacks wrappers: void balanceChanged(void *info, uint64_t balance) @@ -230,11 +230,14 @@ CSpvWrapper::CSpvWrapper(bool isMainnet, size_t nCacheSize, bool fMemory, bool f IterateTable(DB_SPVTXS, onLoadTx); } - auto userAddresses = new std::set; + auto userAddresses = new BRUserAddresses; + auto htlcAddresses = new BRUserAddresses; const auto wallets = GetWallets(); for (const auto& wallet : wallets) { - for (const auto& entry : wallet->mapAddressBook) { - if (entry.second.purpose == "spv") { + for (const auto& entry : wallet->mapAddressBook) + { + if (entry.second.purpose == "spv") + { uint160 userHash; if (entry.first.which() == PKHashType) { userHash = *boost::get(&entry.first); @@ -248,10 +251,17 @@ CSpvWrapper::CSpvWrapper(bool isMainnet, size_t nCacheSize, bool fMemory, bool f UIntConvert(userHash.begin(), spvHash); userAddresses->insert(spvHash); } + else if (entry.second.purpose == "htlc") + { + uint160 userHash = *boost::get(&entry.first); + UInt160 spvHash; + UIntConvert(userHash.begin(), spvHash); + htlcAddresses->insert(spvHash); + } } } - wallet = BRWalletNew(txs.data(), txs.size(), mpk, 0, userAddresses); + wallet = BRWalletNew(txs.data(), txs.size(), mpk, 0, userAddresses, htlcAddresses); BRWalletSetCallbacks(wallet, this, balanceChanged, txAdded, txUpdated, txDeleted); LogPrint(BCLog::SPV, "wallet created with first receive address: %s\n", BRWalletLegacyAddress(wallet).s); @@ -337,6 +347,11 @@ uint8_t CSpvWrapper::GetPKHashPrefix() const return BRPeerManagerChainParams(manager)->base58_p2pkh; } +uint8_t CSpvWrapper::GetP2SHPrefix() const +{ + return BRPeerManagerChainParams(manager)->base58_p2sh; +} + BRWallet * CSpvWrapper::GetWallet() { return wallet; @@ -492,7 +507,7 @@ void CSpvWrapper::WriteTx(const BRTransaction *tx) static TBytes buf; buf.resize(BRTransactionSerialize(tx, NULL, 0)); BRTransactionSerialize(tx, buf.data(), buf.size()); - db->Write(make_pair(DB_SPVTXS, to_uint256(tx->txHash)), std::make_pair(buf, std::make_pair(tx->blockHeight, tx->timestamp)) ); + db->Write(std::make_pair(DB_SPVTXS, to_uint256(tx->txHash)), std::make_pair(buf, std::make_pair(tx->blockHeight, tx->timestamp)) ); } void CSpvWrapper::UpdateTx(uint256 const & hash, uint32_t blockHeight, uint32_t timestamp) @@ -517,9 +532,21 @@ uint32_t CSpvWrapper::ReadTxTimestamp(uint256 const & hash) return 0; } +uint32_t CSpvWrapper::ReadTxBlockHeight(uint256 const & hash) +{ + std::pair const key{std::make_pair(DB_SPVTXS, hash)}; + db_tx_rec txrec; + if (db->Read(key, txrec)) { + return txrec.second.first; + } + + // If not found return the default value of an unconfirmed TX. + return std::numeric_limits::max(); +} + void CSpvWrapper::EraseTx(uint256 const & hash) { - db->Erase(make_pair(DB_SPVTXS, hash)); + db->Erase(std::make_pair(DB_SPVTXS, hash)); } void CSpvWrapper::WriteBlock(const BRMerkleBlock * block) @@ -529,7 +556,7 @@ void CSpvWrapper::WriteBlock(const BRMerkleBlock * block) buf.resize(blockSize); BRMerkleBlockSerialize(block, buf.data(), blockSize); - BatchWrite(make_pair(DB_SPVBLOCKS, to_uint256(block->blockHash)), make_pair(buf, block->height)); + BatchWrite(std::make_pair(DB_SPVBLOCKS, to_uint256(block->blockHash)), std::make_pair(buf, block->height)); } std::string CSpvWrapper::AddBitcoinAddress(const CPubKey& new_key) @@ -544,9 +571,14 @@ std::string CSpvWrapper::AddBitcoinAddress(const CPubKey& new_key) return addr.s; } -void CSpvWrapper::AddBitcoinHash(const uint160 &userHash) +void CSpvWrapper::AddBitcoinHash(const uint160 &userHash, const bool htlc) { - BRWalletImportAddress(wallet, userHash); + BRWalletImportAddress(wallet, userHash, htlc); +} + +void CSpvWrapper::RebuildBloomFilter() +{ + BRPeerManagerRebuildBloomFilter(manager); } std::string CSpvWrapper::DumpBitcoinPrivKey(const CWallet* pwallet, const std::string &strAddress) @@ -569,7 +601,22 @@ int64_t CSpvWrapper::GetBitcoinBalance() return BRWalletBalance(wallet); } -UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount) +// Helper function to convert key and populate vector +void ConvertPrivKeys(std::vector &inputKeys, const CKey &key) +{ + UInt256 rawKey; + memcpy(&rawKey, &(*key.begin()), key.size()); + + BRKey inputKey; + if (!BRKeySetSecret(&inputKey, &rawKey, key.IsCompressed())) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to create SPV private key"); + } + + inputKeys.push_back(inputKey); +} + +UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount, int64_t feeRate) { if (!BRAddressIsValid(address.c_str())) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); @@ -589,7 +636,7 @@ UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, auto dest = GetDestinationForKey(new_key, OutputType::BECH32); pwallet->SetAddressBook(dest, "spv", "spv"); - BRTransaction *tx = BRWalletCreateTransaction(wallet, static_cast(amount), address.c_str(), changeAddress); + BRTransaction *tx = BRWalletCreateTransaction(wallet, static_cast(amount), address.c_str(), changeAddress, feeRate); if (tx == nullptr) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); @@ -597,6 +644,7 @@ UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, std::vector inputKeys; for (size_t i{0}; i < tx->inCount; ++i) { + LogPrintf("INPUT TX %s vout %d\n", to_uint256(tx->inputs[i].txHash).ToString(), tx->inputs[i].index); CTxDestination dest; if (!ExtractDestination({tx->inputs[i].script, tx->inputs[i].script + tx->inputs[i].scriptLen}, dest)) { BRTransactionFree(tx); @@ -616,20 +664,10 @@ UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get address private key."); } - UInt256 rawKey; - memcpy(&rawKey, &(*vchSecret.begin()), vchSecret.size()); - - BRKey inputKey; - if (!BRKeySetSecret(&inputKey, &rawKey, vchSecret.IsCompressed())) { - BRTransactionFree(tx); - throw JSONRPCError(RPC_WALLET_ERROR, "Failed to create private key."); - } - - inputKeys.push_back(inputKey); + ConvertPrivKeys(inputKeys, vchSecret); } - BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size()); - if (!BRTransactionIsSigned(tx)) { + if (!BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size())) { BRTransactionFree(tx); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction."); } @@ -662,6 +700,61 @@ UniValue CSpvWrapper::ListTransactions() return result; } +UniValue CSpvWrapper::GetHTLCReceived(const std::string& addr) +{ + if (!addr.empty() && !BRAddressIsValid(addr.c_str())) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); + } + + UInt160 addressFilter = UINT160_ZERO; + if (!addr.empty()) + { + BRAddressHash160(&addressFilter, addr.c_str()); + } + + auto htlcTransactions = BRListHTLCReceived(wallet, addressFilter); + + UniValue result(UniValue::VARR); + for (const auto& txInfo : htlcTransactions) + { + uint256 txid = to_uint256(txInfo.first->txHash); + uint64_t confirmations{0}; + uint32_t blockHeight = ReadTxBlockHeight(txid); + + if (blockHeight != std::numeric_limits::max()) + { + confirmations = spv::pspv->GetLastBlockHeight() - blockHeight + 1; + } + + UniValue item(UniValue::VOBJ); + size_t output = txInfo.second; + item.pushKV("txid", txid.ToString()); + item.pushKV("vout", output); + item.pushKV("amount", ValueFromAmount(txInfo.first->outputs[output].amount)); + item.pushKV("address", txInfo.first->outputs[output].address); + item.pushKV("confirms", confirmations); + + uint256 spent; + if (BRWalletTxSpent(wallet, txInfo.first, output, spent)) + { + blockHeight = ReadTxBlockHeight(spent); + confirmations = 0; + if (blockHeight != std::numeric_limits::max()) + { + confirmations = spv::pspv->GetLastBlockHeight() - blockHeight + 1; + } + + UniValue spentItem(UniValue::VOBJ); + spentItem.pushKV("txid", spent.ToString()); + spentItem.pushKV("confirms", confirmations); + item.pushKV("spent", spentItem); + } + result.push_back(item); + } + return result; +} + std::string CSpvWrapper::GetRawTransactions(uint256& hash) { UInt256 spvHash; @@ -669,6 +762,156 @@ std::string CSpvWrapper::GetRawTransactions(uint256& hash) return BRGetRawTransaction(wallet, spvHash); } +std::string CSpvWrapper::GetHTLCSeed(uint8_t* md20) +{ + return BRGetHTLCSeed(wallet, md20); +} + +uint64_t CSpvWrapper::GetFeeRate() +{ + return BRWalletFeePerKb(wallet); +} + +HTLCDetails GetHTLCDetails(CScript& redeemScript) +{ + HTLCDetails script; + + if (redeemScript[2] != 32) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Incorrect seed hash length"); + } + script.hash = std::vector(&redeemScript[3], &redeemScript[35]); + + uint8_t sellerLength = redeemScript[36]; + if (sellerLength != CPubKey::PUBLIC_KEY_SIZE && sellerLength != CPubKey::COMPRESSED_PUBLIC_KEY_SIZE) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Seller pubkey incorrect pubkey length"); + } + + script.sellerKey.Set(&redeemScript[37], &redeemScript[37] + sellerLength); + if (!script.sellerKey.IsValid()) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid seller pubkey"); + } + + uint8_t timeoutLength = redeemScript[38 + sellerLength]; + if (timeoutLength > CScriptNum::nDefaultMaxNumSize) + { + if (timeoutLength >= OP_1) + { + // Time out length encoded into the opcode itself. + script.locktime = CScript::DecodeOP_N(static_cast(timeoutLength)); + timeoutLength = 0; + } + else + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Incorrect timeout length"); + } + } + else + { + memcpy(&script.locktime, &redeemScript[39 + sellerLength], timeoutLength); + + // If time more than expected reduce to max value + uint32_t maxLocktime{1 << 16}; + if (script.locktime > maxLocktime) + { + script.locktime = maxLocktime; + } + } + + uint8_t buyerLength = redeemScript[41 + timeoutLength + sellerLength]; + if (buyerLength != CPubKey::PUBLIC_KEY_SIZE && buyerLength != CPubKey::COMPRESSED_PUBLIC_KEY_SIZE) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Buyer pubkey incorrect pubkey length"); + } + + script.buyerKey.Set(&redeemScript[42 + timeoutLength + sellerLength], &redeemScript[42 + timeoutLength + sellerLength] + buyerLength); + if (!script.buyerKey.IsValid()) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid buyer pubkey"); + } + + return script; +} + +HTLCDetails HTLCScriptRequest(CWallet* const pwallet, const char* address, CScript &redeemScript) +{ + // Validate HTLC address + if (!BRAddressIsValid(address)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); + } + + // Decode address, bech32 will fail here. + std::vector data; + if (!DecodeBase58Check(address, data)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to decode address"); + } + + // Get hash160 + uint160 hash160; + memcpy(&hash160, &data[1], sizeof(uint160)); + + // Read redeem script from wallet DB + WalletBatch batch(pwallet->GetDBHandle()); + if (!batch.ReadCScript(hash160, redeemScript)) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Redeem script not found in wallet"); + } + + // Make sure stored script is at least expected length. + const unsigned int minScriptLength{110}; // With compressed public keys and small int block count + const unsigned int maxScriptLength{177}; // With uncompressed public keys and four bits for block count (1 size / 3 value) + if (redeemScript.size() < minScriptLength && redeemScript.size() > maxScriptLength) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Stored redeem script incorrect length, rerun spv_createhtlc"); + } + + // Get script details + return GetHTLCDetails(redeemScript); +} + +UniValue CSpvWrapper::GetAddressPubkey(const CWallet* pwallet, const char *addr) +{ + if (!BRAddressIsValid(addr)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Error: Invalid address"); + } + + CKeyID key; + if (!BRAddressHash160(key.begin(), addr)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("%s does not refer to a key", addr)); + } + + CPubKey vchPubKey; + if (!pwallet->GetPubKey(key, vchPubKey)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("no full public key for address %s", addr)); + } + + return HexStr(vchPubKey); +} + +CKeyID CSpvWrapper::GetAddressKeyID(const char *addr) +{ + if (!BRAddressIsValid(addr)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Error: Invalid address"); + } + + CKeyID key; + if (!BRAddressHash160(key.begin(), addr)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("%s does not refer to a key", addr)); + } + + return key; +} + + void publishedTxCallback(void *info, int error) { LogPrint(BCLog::SPV, "publishedTxCallback: %s\n", strerror(error)); @@ -689,15 +932,21 @@ struct TxOutput { TBytes script; }; -TBytes CreateRawTx(std::vector const & inputs, std::vector const & outputs) +BRTransaction* CreateTx(std::vector const & inputs, std::vector const & outputs, uint32_t version = TX_VERSION, uint32_t sequence = TXIN_SEQUENCE) { - BRTransaction *tx = BRTransactionNew(); + BRTransaction *tx = BRTransactionNew(version); for (auto input : inputs) { - BRTransactionAddInput(tx, input.txHash, input.index, input.amount, input.script.data(), input.script.size(), NULL, 0, NULL, 0, TXIN_SEQUENCE); + BRTransactionAddInput(tx, input.txHash, input.index, input.amount, input.script.data(), input.script.size(), NULL, 0, NULL, 0, sequence); } for (auto output : outputs) { BRTransactionAddOutput(tx, output.amount, output.script.data(), output.script.size()); } + return tx; +} + +TBytes CreateRawTx(std::vector const & inputs, std::vector const & outputs) +{ + BRTransaction *tx = CreateTx(inputs, outputs); size_t len = BRTransactionSerialize(tx, NULL, 0); TBytes buf(len); len = BRTransactionSerialize(tx, buf.data(), buf.size()); @@ -705,6 +954,125 @@ TBytes CreateRawTx(std::vector const & inputs, std::vector co return len ? buf : TBytes{}; } +UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller) +{ + // Validate HTLC address + if (!BRAddressIsValid(destinationAddress)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid destination address"); + } + + // Only interested in checking seed from seller + if (seller && !IsHex(seed)) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Provided seed is not in hex form"); + } + + // Get redeemscript and parsed details from script + CScript redeemScript; + auto scriptDetails = HTLCScriptRequest(pwallet, scriptAddress, redeemScript); + + // Calculate seed hash and make sure it matches hash in contract + auto seedBytes = ParseHex(seed); + if (seller) + { + std::vector calcSeedBytes(32); + CSHA256 hash; + hash.Write(seedBytes.data(), seedBytes.size()); + hash.Finalize(calcSeedBytes.data()); + + if (scriptDetails.hash != calcSeedBytes) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Seed provided does not match seed hash in contract"); + } + } + + // Get private key + CKey key; + if (!pwallet->GetKey(seller ? scriptDetails.sellerKey.GetID() : scriptDetails.buyerKey.GetID(), key)) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Private key for seller pubkey " + HexStr(scriptDetails.sellerKey) + " is not known"); + } + + // Convert private key to SPV key + std::vector inputKeys; + ConvertPrivKeys(inputKeys, key); + + // Get script address and any outputs related to it + CScriptID innerID(redeemScript); + UInt160 addressFilter; + UIntConvert(innerID.begin(), addressFilter); + auto htlcTransactions = BRListHTLCReceived(wallet, addressFilter); + + // Create inputs + std::vector inputs; + uint256 spent; + std::vector script(redeemScript.begin(), redeemScript.end()); + CAmount inputTotal{0}; + + for (const auto& output : htlcTransactions) + { + if (!BRWalletTxSpent(wallet, output.first, output.second, spent)) + { + inputs.push_back({output.first->txHash, static_cast(output.second), output.first->outputs[output.second].amount, script}); + inputTotal += output.first->outputs[output.second].amount; + } + } + + if (inputs.empty()) + { + throw JSONRPCError(RPC_WALLET_ERROR, "No unspent HTLC outputs found"); + } + + // Create output + std::vector outputs; + outputs.push_back({P2PKH_DUST, CreateScriptForAddress(destinationAddress)}); + + // Create transaction + auto tx = CreateTx(inputs, outputs, TX_VERSION_V2, seller ? TXIN_SEQUENCE : scriptDetails.locktime); + + // Calculate and set fee + int64_t sigSize = 73 /* sig */ + 1 /* sighash */ + seedBytes.size() + 1 /* OP_1 || size */ + 1 /* pushdata */ + script.size(); + CAmount const minFee = BRTransactionHTLCSize(tx, sigSize) * feerate / TX_FEE_PER_KB; + + if (inputTotal < minFee + static_cast(P2PKH_DUST)) + { + throw JSONRPCError(RPC_MISC_ERROR, "Not enough funds to cover fee"); + } + tx->outputs[0].amount = inputTotal - minFee; + + // Add seed length + seedBytes.insert(seedBytes.begin(), static_cast(seedBytes.size())); + + // Add redeemscript length + script.insert(script.begin(), static_cast(script.size())); + + // Sign transaction + if (!BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size(), seller ? ScriptTypeSeller : ScriptTypeBuyer, seedBytes.data(), script.data())) + { + BRTransactionFree(tx); + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction."); + } + + int sendResult = 0; + std::promise promise; + std::string txid = to_uint256(tx->txHash).ToString(); + OnSendRawTx(tx, &promise); + if (tx) + { + sendResult = promise.get_future().get(); + } + else + { + sendResult = EPARSINGTX; + } + + UniValue result(UniValue::VOBJ); + result.pushKV("txid", txid); + result.pushKV("sendmessage", DecodeSendResult(sendResult)); + return result; +} + /* * Encapsulates metadata into OP_RETURN + N * P2WSH scripts */ @@ -821,7 +1189,10 @@ std::tuple CreateAnchorTx(std::vector co } auto rawtx = CreateRawTx(inputs, outputs); + LogPrint(BCLog::SPV, "TXunsigned: %s\n", HexStr(rawtx)); + BRTransaction *tx = BRTransactionParse(rawtx.data(), rawtx.size()); + if (!tx) { LogPrint(BCLog::SPV, "***FAILED*** %s: BRTransactionParse()\n", __func__); throw std::runtime_error("spv: Can't parse created transaction"); @@ -876,81 +1247,6 @@ std::tuple CreateAnchorTx(std::vector co return std::make_tuple(txHash, signedTx, totalCost); } -// just for tests & experiments -TBytes CreateSplitTx(std::string const & hash, int32_t index, uint64_t inputAmount, std::string const & privkey_wif, int parts, int amount) -{ - /// @todo calculate minimum fee -// uint64_t fee = 10000; - uint64_t const p2pkh_dust = 546; - UInt256 inHash = UInt256Reverse(toUInt256(hash.c_str())); - - // creating key(priv/pub) from WIF priv - BRKey inputKey; - BRKeySetPrivKey(&inputKey, privkey_wif.c_str()); - BRKeyPubKey(&inputKey, NULL, 0); - - BRAddress address = BR_ADDRESS_NONE; - BRKeyLegacyAddr(&inputKey, address.s, sizeof(address)); - TBytes inputScript(CreateScriptForAddress(address.s)); - - std::vector inputs; - std::vector outputs; - - // create single input (current restriction) - inputs.push_back({ inHash, index, inputAmount, inputScript}); - - BRWallet * wallet = pspv->GetWallet(); - - uint64_t const valuePerOutput = (amount > 0) && (static_cast(parts*(amount + 34*3) + 148*3) < inputAmount) ? amount : (inputAmount - 148*3) / parts - 34*3; // 34*3 is min estimated fee per p2pkh output - uint64_t sum = 0; - for (int i = 0; i < parts; ++i) { - auto addr = BRWalletLegacyAddress(wallet); - - TBytes script(CreateScriptForAddress(addr.s)); - - // output[0] - anchor address with creation fee - outputs.push_back({ valuePerOutput, script }); - sum += valuePerOutput; - } - - auto rawtx = CreateRawTx(inputs, outputs); - LogPrint(BCLog::SPV, "TXunsigned: %s\n", HexStr(rawtx)); - - BRTransaction *tx = BRTransactionParse(rawtx.data(), rawtx.size()); - - if (! tx) - LogPrint(BCLog::SPV, "***FAILED*** %s: BRTransactionParse(): tx->inCount: %lu tx->outCount %lu\n", __func__, tx ? tx->inCount : 0, tx ? tx->outCount: 0); - else { - LogPrint(BCLog::SPV, "***OK*** %s: BRTransactionParse(): tx->inCount: %lu tx->outCount %lu\n", __func__, tx->inCount, tx->outCount); - } - - // output[n] (optional) - change - uint64_t const minFee = BRTransactionStandardFee(tx); - - auto change = inputAmount - sum - minFee - 3*34; // (3*34) is an estimated cost of change output itself - if (change > p2pkh_dust) { - BRTransactionAddOutput(tx, change, inputScript.data(), inputScript.size()); - } - - BRTransactionSign(tx, 0, &inputKey, 1); - { // just check - BRAddress addr; - BRAddressFromScriptSig(addr.s, sizeof(addr), tx->inputs[0].signature, tx->inputs[0].sigLen); - if (!BRTransactionIsSigned(tx) || !BRAddressEq(&address, &addr)) { - LogPrint(BCLog::SPV, "***FAILED*** %s: BRTransactionSign()\n", __func__); - BRTransactionFree(tx); - return {}; - } - } - TBytes signedTx(BRTransactionSerialize(tx, NULL, 0)); - BRTransactionSerialize(tx, signedTx.data(), signedTx.size()); - - LogPrint(BCLog::SPV, "TXsigned: %s\n", HexStr(signedTx)); - BRTransactionFree(tx); - - return signedTx; -} - bool CSpvWrapper::SendRawTx(TBytes rawtx, std::promise * promise) { BRTransaction *tx = BRTransactionParse(rawtx.data(), rawtx.size()); @@ -1001,11 +1297,11 @@ void CFakeSpvWrapper::OnSendRawTx(BRTransaction *tx, std::promise * promise } } -UniValue CFakeSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount) +UniValue CFakeSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount, int64_t feeRate) { // Normal TX, pass to parent. if (amount != -1) { - return CSpvWrapper::SendBitcoins(pwallet, address, amount); + return CSpvWrapper::SendBitcoins(pwallet, address, amount, feeRate); } // Fund Bitcoin wallet for testing with 1 Bitcoin. diff --git a/src/spv/spv_wrapper.h b/src/spv/spv_wrapper.h index 51e1b686030..075e44e8cdb 100644 --- a/src/spv/spv_wrapper.h +++ b/src/spv/spv_wrapper.h @@ -6,6 +6,7 @@ #define DEFI_SPV_SPV_WRAPPER_H #include +#include #include #include @@ -37,7 +38,7 @@ typedef struct BRTransactionStruct BRTransaction; typedef struct BRPeerStruct BRPeer; class CAnchor; -class CPubKey; +class CKeyID; class CScript; class CWallet; class UniValue; @@ -51,6 +52,15 @@ std::string DecodeSendResult(int result); namespace spv { +struct HTLCDetails { + CPubKey sellerKey; + CPubKey buyerKey; + uint32_t locktime{0}; + std::vector hash; +}; + +HTLCDetails GetHTLCDetails(CScript& redeemScript); + typedef std::vector TBytes; static const TBytes BtcAnchorMarker = { 'D', 'F', 'A'}; // 0x444641 @@ -60,6 +70,7 @@ uint64_t const P2WSH_DUST = 330; /// 546 p2pkh & 294 p2wpkh (330 p2wsh calc'ed m uint64_t const P2PKH_DUST = 546; extern uint64_t const DEFAULT_BTC_FEERATE; +extern uint64_t const DEFAULT_BTC_FEE_PER_KB; using namespace boost::multi_index; @@ -97,6 +108,7 @@ class CSpvWrapper virtual uint32_t GetLastBlockHeight() const; virtual uint32_t GetEstimatedBlockHeight() const; uint8_t GetPKHashPrefix() const; + uint8_t GetP2SHPrefix() const; std::vector GetWalletTxs() const; @@ -119,14 +131,24 @@ class CSpvWrapper // Get time stamp of Bitcoin TX uint32_t ReadTxTimestamp(uint256 const & hash); + // Get block height of Bitcoin TX + uint32_t ReadTxBlockHeight(uint256 const & hash); + // Bitcoin Address calls std::string AddBitcoinAddress(const CPubKey &new_key); - void AddBitcoinHash(const uint160 &userHash); + void AddBitcoinHash(const uint160 &userHash, const bool htlc = false); std::string DumpBitcoinPrivKey(const CWallet* pwallet, const std::string &strAddress); int64_t GetBitcoinBalance(); - virtual UniValue SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount); + virtual UniValue SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount, int64_t feeRate); UniValue ListTransactions(); + UniValue GetHTLCReceived(const std::string &addr); std::string GetRawTransactions(uint256& hash); + void RebuildBloomFilter(); + UniValue GetAddressPubkey(const CWallet *pwallet, const char *addr); // Used in HTLC creation + CKeyID GetAddressKeyID(const char *addr); + std::string GetHTLCSeed(uint8_t* md20); + UniValue CreateHTLCTransaction(CWallet* const pwallet, const char *scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller); + uint64_t GetFeeRate(); private: virtual void OnSendRawTx(BRTransaction * tx, std::promise * promise); @@ -223,7 +245,7 @@ class CFakeSpvWrapper : public CSpvWrapper uint32_t GetEstimatedBlockHeight() const override { return lastBlockHeight; } // dummy void OnSendRawTx(BRTransaction * tx, std::promise * promise) override; - UniValue SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount) override; + UniValue SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount, int64_t feeRate) override; uint32_t lastBlockHeight = 0; bool isConnected = false; @@ -244,7 +266,6 @@ struct TxInputData { uint64_t EstimateAnchorCost(TBytes const & meta, uint64_t feerate); std::vector EncapsulateMeta(TBytes const & meta); std::tuple CreateAnchorTx(std::vector const & inputs, TBytes const & meta, uint64_t feerate); -TBytes CreateSplitTx(std::string const & hash, int32_t index, uint64_t inputAmount, std::string const & privkey_wif, int parts, int amount); TBytes CreateScriptForAddress(char const * address); } diff --git a/src/spv/support/BRAddress.cpp b/src/spv/support/BRAddress.cpp index dad16d5f871..db8ce4aa15d 100644 --- a/src/spv/support/BRAddress.cpp +++ b/src/spv/support/BRAddress.cpp @@ -249,10 +249,40 @@ const uint8_t *BRScriptPKH(const uint8_t *script, size_t scriptLen) else if (count == 2 && (*elems[0] == OP_0 || (*elems[0] >= OP_1 && *elems[0] <= OP_16)) && *elems[1] == 20) { r = BRScriptData(elems[1], &l); // pay-to-witness } - + return r; } +// Returns a UInt160 of the seller's or buyer's address, UINT160_ZERO if not a HTLC +const UInt160 BRHTLCScriptPKH(const uint8_t *script, size_t scriptLen, HTLCScriptType htlcType) +{ + assert(script != NULL || scriptLen == 0); + if (! script || scriptLen == 0 || scriptLen > MAX_SCRIPT_LENGTH) return UINT160_ZERO; + + const uint8_t *elems[BRScriptElements(NULL, 0, script, scriptLen)], *r = NULL; + size_t l, count = BRScriptElements(elems, sizeof(elems)/sizeof(*elems), script, scriptLen); + + UInt160 hash160 = UINT160_ZERO; + + if (count == 12 && *elems[0] == OP_IF && *elems[1] == OP_SHA256 && *elems[3] == OP_EQUALVERIFY && // HTLC + *elems[5] == OP_ELSE && *elems[7] == OP_CHECKSEQUENCEVERIFY && *elems[8] == OP_DROP && + *elems[10] == OP_ENDIF && *elems[11] == OP_CHECKSIG) + { + if (htlcType == ScriptTypeSeller) + { + r = BRScriptData(elems[4], &l); + } + else if (htlcType == ScriptTypeBuyer) + { + r = BRScriptData(elems[9], &l); + } + + BRHash160(&hash160, r, l); + } + + return hash160; +} + // NOTE: It's important here to be permissive with scriptSig (spends) and strict with scriptPubKey (receives). If we // miss a receive transaction, only that transaction's funds are missed, however if we accept a receive transaction that // we are unable to correctly sign later, then the entire wallet balance after that point would become stuck with the diff --git a/src/spv/support/BRAddress.h b/src/spv/support/BRAddress.h index 74489acde75..34cbe7d3588 100644 --- a/src/spv/support/BRAddress.h +++ b/src/spv/support/BRAddress.h @@ -26,10 +26,17 @@ #define BRAddress_h #include "BRCrypto.h" +#include "BRInt.h" #include #include #include +enum HTLCScriptType { + ScriptTypeNone, + ScriptTypeSeller, + ScriptTypeBuyer, +}; + #ifdef __cplusplus extern "C" { #endif @@ -42,11 +49,17 @@ extern "C" { #define OP_1NEGATE 0x4f #define OP_1 0x51 #define OP_16 0x60 +#define OP_IF 0x63 +#define OP_ELSE 0x67 +#define OP_ENDIF 0x68 +#define OP_DROP 0x75 #define OP_DUP 0x76 #define OP_EQUAL 0x87 #define OP_EQUALVERIFY 0x88 +#define OP_SHA256 0xa8 #define OP_HASH160 0xa9 #define OP_CHECKSIG 0xac +#define OP_CHECKSEQUENCEVERIFY 0xb2 // reads a varint from buf and stores its length in intLen if intLen is non-NULL // returns the varint value @@ -71,6 +84,9 @@ size_t BRScriptPushData(uint8_t *script, size_t scriptLen, const uint8_t *data, // returns a pointer to the 20byte pubkey hash, or NULL if none const uint8_t *BRScriptPKH(const uint8_t *script, size_t scriptLen); + +// Returns a UInt160 of the seller's or buyer's address, UINT160_ZERO if not a HTLC +const UInt160 BRHTLCScriptPKH(const uint8_t *script, size_t scriptLen, HTLCScriptType htlcType); typedef struct { char s[75]; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 761258dee54..fab4d65ebf4 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2629,6 +2629,7 @@ static UniValue loadwallet(const JSONRPCRequest& request) } if (foundSPV) { + spv::pspv->RebuildBloomFilter(); spv::pspv->Rescan(std::numeric_limits::max()); } } diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 5d83b1f0f14..ee8d5a89fe7 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -128,6 +128,11 @@ bool WalletBatch::WriteCScript(const uint160& hash, const CScript& redeemScript) return WriteIC(std::make_pair(DBKeys::CSCRIPT, hash), redeemScript, false); } +bool WalletBatch::ReadCScript(const uint160& hash, CScript& redeemScript) +{ + return m_batch.Read(std::make_pair(DBKeys::CSCRIPT, hash), redeemScript); +} + bool WalletBatch::WriteWatchOnly(const CScript &dest, const CKeyMetadata& keyMeta) { if (!WriteIC(std::make_pair(DBKeys::WATCHMETA, dest), keyMeta)) { diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index ca155933381..69041acc226 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -228,6 +228,7 @@ class WalletBatch bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey); bool WriteCScript(const uint160& hash, const CScript& redeemScript); + bool ReadCScript(const uint160& hash, CScript& redeemScript); bool WriteWatchOnly(const CScript &script, const CKeyMetadata &keymeta); bool EraseWatchOnly(const CScript &script); diff --git a/test/functional/data/spv_addresses.txt b/test/functional/data/spv_addresses.txt new file mode 100644 index 00000000000..42b0a1d6c3a --- /dev/null +++ b/test/functional/data/spv_addresses.txt @@ -0,0 +1,2 @@ +cSkTV5jWJnKiqMUuuBWo6Sb99UMtddxXerDNQGfU1jJ8WiZoSTRh 2021-03-09T14:11:35Z label=spv # Bitcoin bcrt1qs2qsynezncmkzef3qsaaumf5r5uvyeh8ykrg37 +cSumkzL3QT3aGqeQNuswkLeC5n9BMuhBNvWcCST3VEsLpwVasuQR 2021-03-09T14:11:35Z label=spv # Bitcoin bcrt1q28ldz0kwh0ltfad95fzpdqmuxu5getf05jlqu7 diff --git a/test/functional/feature_bitcoin_htlc.py b/test/functional/feature_bitcoin_htlc.py new file mode 100755 index 00000000000..36a44c78092 --- /dev/null +++ b/test/functional/feature_bitcoin_htlc.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test Bitcoin SPV HTLC""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal +from test_framework.authproxy import JSONRPCException +from decimal import Decimal +import os + +class BitcoinHTLCTests(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [ + [ "-dummypos=1", "-spv=1"], + ] + self.setup_clean_chain = True + + def run_test(self): + # Set up wallet + address = self.nodes[0].spv_getnewaddress() + self.nodes[0].spv_fundaddress(address) + + # Should now have a balance of 1 Bitcoin + result = self.nodes[0].spv_getbalance() + assert_equal(result, Decimal("1.00000000")) + + # Import SPV addresses + self.nodes[0].importwallet(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/spv_addresses.txt')) + + seed = "aba5f7e9aecf6ec4372c8a1e49562680d066da4655ee8b4bb01640479fffeaa8" + seed_hash = "df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e" + + # Make sure addresses were imported and added to Bitcoin wallet + assert_equal(self.nodes[0].spv_dumpprivkey("bcrt1qs2qsynezncmkzef3qsaaumf5r5uvyeh8ykrg37"), "cSkTV5jWJnKiqMUuuBWo6Sb99UMtddxXerDNQGfU1jJ8WiZoSTRh") + assert_equal(self.nodes[0].spv_dumpprivkey("bcrt1q28ldz0kwh0ltfad95fzpdqmuxu5getf05jlqu7"), "cSumkzL3QT3aGqeQNuswkLeC5n9BMuhBNvWcCST3VEsLpwVasuQR") + + # Test getpubkey call + assert_equal(self.nodes[0].spv_getaddresspubkey("bcrt1qs2qsynezncmkzef3qsaaumf5r5uvyeh8ykrg37"), "0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86") + assert_equal(self.nodes[0].spv_getaddresspubkey("bcrt1q28ldz0kwh0ltfad95fzpdqmuxu5getf05jlqu7"), "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490") + + # Try annd create a HTLC script with relative time + try: + self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "4194304", seed_hash) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Invalid block denominated relative timeout" in errorString) + + # Try annd create a HTLC script with incorrect pubkey + try: + self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "10", seed_hash) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Invalid public key" in errorString) + + # Create and learn HTLC script + htlc_script = self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "10", seed_hash) + + # Make sure address and redeemscript are as expected. + assert_equal(htlc_script['address'], "2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") + assert_equal(htlc_script['redeemScript'], "63a820df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e88210224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86675ab27521035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be49068ac") + + # Test script decoding + result = self.nodes[0].spv_decodehtlcscript(htlc_script['redeemScript']) + assert_equal(result["sellerkey"], "0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86") + assert_equal(result["buyerkey"], "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490") + assert_equal(result["blocks"], 10) + assert_equal(result["hash"], "df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e") + + # Try and claim before output present + try: + self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, seed, 1000) + except JSONRPCException as e: + errorString = e.error['message'] + assert("No unspent HTLC outputs found" in errorString) + + # Send to contract for seller claim + result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) + assert_equal(result['sendmessage'], "Success") + + # Make sure output present in HTLC address + output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") + assert_equal(len(output), 1) + assert_equal(output[0]['amount'], Decimal("0.1")) + + # Try and claim with incorrect seed + try: + self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, "deadbeef", 1000) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Seed provided does not match seed hash in contract" in errorString) + + # Try and claim with unknown script address + try: + self.nodes[0].spv_claimhtlc("2NGT3gZvc75NX8DWGqfuEvniHGj5LiY33Ui", address, "deadbeef", 1000) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Redeem script not found in wallet" in errorString) + + # seller claim HTLC + result = self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, seed, 1000) + assert_equal(result['sendmessage'], "Success") + + # Check output spent + output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") + assert_equal(len(output[0]['spent']), 2) + + # Get raw TX and check secret and redeemscript part of unlock script + rawtx = self.nodes[0].spv_getrawtransaction(result["txid"]) + assert(rawtx.find("0120aba5f7e9aecf6ec4372c8a1e49562680d066da4655ee8b4bb01640479fffeaa8514c6e63a820df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e88210224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86675ab27521035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be49068ac") != -1) + + # Test getting seed from HTLC transaction + assert_equal(self.nodes[0].spv_gethtlcseed("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku"), "aba5f7e9aecf6ec4372c8a1e49562680d066da4655ee8b4bb01640479fffeaa8") + + # Send to contract for buyer refund + result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) + assert_equal(result['sendmessage'], "Success") + + # Make sure output present in HTLC address + output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") + assert_equal(len(output), 2) + assert_equal(output[0]['amount'], Decimal("0.1")) + assert_equal(output[1]['amount'], Decimal("0.1")) + + # seller claim HTLC + result = self.nodes[0].spv_refundhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, 1000) + assert_equal(result['sendmessage'], "Success") + + # Check outputs spent + output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") + assert_equal(len(output[0]['spent']), 2) + assert_equal(len(output[1]['spent']), 2) + + # Get raw TX and check redeemscript part of unlock script + rawtx = self.nodes[0].spv_getrawtransaction(result["txid"]) + assert(rawtx.find("01004c6e63a820df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e88210224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86675ab27521035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be49068ac") != -1) + + # Generate all new addresses and seeds and test multiple UTXOs claim in script + seller = self.nodes[0].spv_getnewaddress() + buyer = self.nodes[0].spv_getnewaddress() + seller_pubkey = self.nodes[0].spv_getaddresspubkey(seller) + buyer_pubkey = self.nodes[0].spv_getaddresspubkey(buyer) + + # Create and learn HTLC script, seed generated by RPC call. + htlc_script = self.nodes[0].spv_createhtlc(seller_pubkey, buyer_pubkey, "10") + + # Get seed and check lengths + seed = htlc_script['seed'] + assert_equal(len(seed), 64) + assert_equal(len(htlc_script['seedhash']), 64) + + # Send multiple TX to script address + for _ in range(3): + result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) + assert_equal(result['sendmessage'], "Success") + + # Make sure output present in HTLC address + output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) + assert_equal(len(output), 3) + assert_equal(output[0]['amount'], Decimal("0.1")) + assert_equal(output[1]['amount'], Decimal("0.1")) + assert_equal(output[2]['amount'], Decimal("0.1")) + + # seller claim HTLC + result = self.nodes[0].spv_claimhtlc(htlc_script['address'], address, seed, 1000) + assert_equal(result['sendmessage'], "Success") + + # Check output spent + output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) + assert_equal(len(output[0]['spent']), 2) + assert_equal(len(output[1]['spent']), 2) + assert_equal(len(output[2]['spent']), 2) + + # Generate all new addresses and seeds and test multiple UTXOs in script + seller = self.nodes[0].spv_getnewaddress() + buyer = self.nodes[0].spv_getnewaddress() + seller_pubkey = self.nodes[0].spv_getaddresspubkey(seller) + buyer_pubkey = self.nodes[0].spv_getaddresspubkey(buyer) + + # Create and learn HTLC script, seed generated by RPC call. + htlc_script = self.nodes[0].spv_createhtlc(seller_pubkey, buyer_pubkey, "10") + + # Send multiple TX to script address + for _ in range(3): + result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) + assert_equal(result['sendmessage'], "Success") + + # Make sure output present in HTLC address + output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) + assert_equal(len(output), 3) + assert_equal(output[0]['amount'], Decimal("0.1")) + assert_equal(output[1]['amount'], Decimal("0.1")) + assert_equal(output[2]['amount'], Decimal("0.1")) + + # seller claim HTLC + result = self.nodes[0].spv_refundhtlc(htlc_script['address'], address, 1000) + assert_equal(result['sendmessage'], "Success") + + # Check output spent + output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) + assert_equal(len(output[0]['spent']), 2) + assert_equal(len(output[1]['spent']), 2) + assert_equal(len(output[2]['spent']), 2) + +if __name__ == '__main__': + BitcoinHTLCTests().main() +#02000000015a8dfca4690787f986e6d8f76a229abb11f720798cbd55592c18f6f8fdebe01b01000000ba4830450221008b5499105b89fb01fab1b94e45a21b7de36e806d8fdd56f6ff8dfc74f0f0d8300220187b970a1bb34db51798bfc588032fdbfbd6ac9bde88f80a649266b74606c13601004c6e63a820df95183883789f237977543885e1f82ddc045a3ba90c8f25b43a5b797a35d20e88210224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86675ab27521035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be49068ac0a00000001fe8b980000000000160014aa55dd221e9d6947bc9c9536a2b4dce7b46d218900000000 \ No newline at end of file diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 22218d28736..96e7d839e82 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -121,6 +121,7 @@ 'feature_anchorauths_pruning.py', 'feature_autoauth.py', 'feature_bitcoin_wallet.py', + 'feature_bitcoin_htlc.py', 'feature_communitybalance_reorg.py', 'feature_auth_return_change.py', 'feature_criminals.py',