diff --git a/.gitignore b/.gitignore index 6f82717db..bebcef932 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ src/rpcmisc~.cpp src/komodo-cli src/komodod src/komodo-tx +src/komodo-test diff --git a/src/Makefile.am b/src/Makefile.am index 865960ec6..7c0200623 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -562,9 +562,10 @@ endif @test -f $(PROTOC) $(AM_V_GEN) $(PROTOC) --cpp_out=$(@D) --proto_path=$(abspath $(method, "testEval") == 0) { - return cond->paramsBinLength == 8 && - memcmp(cond->paramsBin, "testEval", 8) == 0; - } - if (strcmp(cond->method, "testReplace") == 0) { - return true; + if (ASSETCHAINS_CC_TEST) { + if (strcmp(cond->method, "testEval") == 0) { + return cond->paramsBinLength == 8 && + memcmp(cond->paramsBin, "testEval", 8) == 0; + } + if (strcmp(cond->method, "testReplace") == 0) { + std::vector data; + auto out = txTo->vout[txTo->vout.size()-1]; // Last output is data + return GetOpReturnData(out.scriptPubKey, data) && data.size() == 2; + } } fprintf(stderr, "no defined behaviour for method: %s\n", cond->method); return 0; @@ -31,16 +35,22 @@ bool EvalConditionValidity(const CC *cond, const CTransaction *txTo) * of the provided replacement pool item. Priority is a 64 bit unsigned int and * 0 is invalid. * - * This method does not need to validate, that is done separately. + * This method does not need to validate, that is done separately. Actually, + * this method will nearly always be called with the same condition and transaction + * in sequence after EvalConditionValidity. If performance became an issue, a very + * small LRU cache could be used to cache a result. */ bool EvalConditionPriority(const CC *cond, CTxReplacementPoolItem *rep) { - if (strcmp((const char*)cond->method, "testReplace") == 0) { - std::vector data; - if (GetOpReturnData(rep->tx.vout[0].scriptPubKey, data)) { - rep->priority = (uint64_t) data[0]; - rep->replacementWindow = REPLACEMENT_WINDOW_BLOCKS; - return true; + if (ASSETCHAINS_CC_TEST) { + if (strcmp((const char*)cond->method, "testReplace") == 0) { + std::vector data; + auto out = rep->tx.vout[rep->tx.vout.size()-1]; // Last output is data + if (GetOpReturnData(out.scriptPubKey, data)) { + rep->replacementWindow = (int) data[0]; + rep->priority = (uint64_t) data[1]; + return true; + } } } return false; @@ -58,7 +68,7 @@ int visitConditionPriority(CC *cond, struct CCVisitor visitor) /* * Try to get replacement parameters from a transaction in &rep. */ -bool PutReplacementParams(CTxReplacementPoolItem &rep) +bool SetReplacementParams(CTxReplacementPoolItem &rep) { // first, see if we have a cryptocondition const CScript &sig = rep.tx.vin[0].scriptSig; @@ -68,7 +78,6 @@ bool PutReplacementParams(CTxReplacementPoolItem &rep) opcodetype opcode; if (!sig.GetOp(pc, opcode, data)) return false; CC *cond = cc_readFulfillmentBinary((unsigned char*)data.data(), data.size()); - auto wat = {1, ""}; if (!cond) return false; // now, see if it has a replacement node diff --git a/src/komodo_cryptoconditions.h b/src/komodo_cryptoconditions.h index 926cd0be9..de12140ef 100644 --- a/src/komodo_cryptoconditions.h +++ b/src/komodo_cryptoconditions.h @@ -7,6 +7,7 @@ extern int32_t ASSETCHAINS_CC; +extern bool ASSETCHAINS_CC_TEST; static bool IsCryptoConditionsEnabled() { return 0 != ASSETCHAINS_CC; diff --git a/src/main.cpp b/src/main.cpp index ecf0e4f30..6c25f1fe8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1110,6 +1110,45 @@ CAmount GetMinRelayFee(const CTransaction& tx, unsigned int nBytes, bool fAllowF } +/* + * This should be called from AcceptToMemoryPool in order + * to perform all validations. + */ +bool AcceptToReplacementPool(const CTransaction &tx, CValidationState &state) +{ + CTxReplacementPoolItem item(tx, GetHeight()); + if (!SetReplacementParams(item)) return false; + + switch (replacementPool.replace(item)) { + + case RP_Accept: + return true; + + case RP_HaveBetter: + // already have a better one + fprintf(stderr,"accept failure.20\n"); + return state.Invalid(error("AcceptToMemoryPool: Replacement is worse"), + REJECT_HAVEBETTER, "replacement-is-worse"); + + case RP_InvalidZeroPriority: + // Not valid according to replaceability rules + fprintf(stderr,"accept failure.21\n"); + return state.Invalid(error("AcceptToMemoryPool: Replacement has 0 priority"), + REJECT_INVALID, "replacement-invalid-zero-priority"); + + case RP_InvalidStructure: + // Not valid according to replaceability rules + fprintf(stderr,"accept failure.22\n"); + return state.Invalid(error("AcceptToMemoryPool: Replacement has multiple inputs"), + REJECT_INVALID, "replacement-has-invalid-structure"); + + default: + return false; + } +} + + + bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree,bool* pfMissingInputs, bool fRejectAbsurdFee, bool fAcceptReplacement) { AssertLockHeld(cs_main); @@ -1181,8 +1220,6 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa } } - CTxReplacementPoolResult rpr = RP_NotReplaceable; - { CCoinsView dummy; CCoinsViewCache view(&dummy); @@ -1341,40 +1378,15 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa if (fAcceptReplacement) { - CTxReplacementPoolItem item(tx, GetHeight()); - if (SetReplacementParams(item)) { - rpr = replacementPool.replace(item); - } + if (AcceptToReplacementPool(tx, state)) return true; + if (state.IsInvalid()) return false; } - if (rpr == RP_HaveBetter) - { - // already have a better one - fprintf(stderr,"accept failure.20\n"); - return state.DoS(0, error("AcceptToMemoryPool: Replacement is worse"), REJECT_HAVEBETTER, "replacement-is-worse"); - } - - if (rpr == RP_Invalid) - { - // Not valid according to replaceability rules - fprintf(stderr,"accept failure.21\n"); - return state.DoS(0, error("AcceptToMemoryPool: Replacement is invalid"), REJECT_INVALID, "replacement-is-worse"); - } - - // If there is no replacement action happening... - if (rpr == RP_NotReplaceable) - { - // Store transaction in memory - pool.addUnchecked(hash, entry, !IsInitialBlockDownload()); - } - } - - // in order for replaceable transactions to sync with wallet, replacementpool should advise - // wallet of transaction eviction - if (rpr == RP_NotReplaceable) { - SyncWithWallets(tx, NULL); + // Store transaction in memory + pool.addUnchecked(hash, entry, !IsInitialBlockDownload()); } + SyncWithWallets(tx, NULL); return true; } @@ -1989,7 +2001,7 @@ bool ContextualCheckInputs(const CTransaction& tx, CValidationState &state, cons // as to the correct behavior - we may want to continue // peering with non-upgraded nodes even after a soft-fork // super-majority vote has passed. - return state.DoS(100,false, REJECT_INVALID, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError()))); + return state.DoS(100, false, REJECT_INVALID, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError()))); } } } @@ -2826,7 +2838,7 @@ bool static ConnectTip(CValidationState &state, CBlockIndex *pindexNew, CBlock * // Update chainActive & related variables. UpdateTip(pindexNew); // Process pending replacements - ProcessReplacementPool(pindexNew->nHeight-1); + ProcessReplacementPool(pindexNew->nHeight); // Tell wallet about transactions that went from mempool // to conflicted: BOOST_FOREACH(const CTransaction &tx, txConflicted) { diff --git a/src/replacementpool.cpp b/src/replacementpool.cpp index 42da4fa7c..306fa5460 100644 --- a/src/replacementpool.cpp +++ b/src/replacementpool.cpp @@ -1,9 +1,11 @@ #include "main.h" #include "replacementpool.h" +#include "sync.h" CTxReplacementPool replacementPool; +CCriticalSection cs_replacementPool; /* @@ -17,33 +19,39 @@ CTxReplacementPool replacementPool; */ CTxReplacementPoolResult CTxReplacementPool::replace(CTxReplacementPoolItem &item) { - AssertLockHeld(cs_main); + LOCK(cs_replacementPool); - // Perform some validations. - if (item.tx.vin.size() > 1) { - // Replaceable transactions with multiple inputs are disabled. - // It seems like quite alot of additional complexity. - return RP_Invalid; - } + // Replaceable transactions with multiple inputs are disabled + // until someone figures out how they would work. + if (item.tx.vin.size() > 1) return RP_InvalidStructure; // A transaction with 0 priority is not valid. - if (item.priority == 0) { - return RP_Invalid; + if (item.priority == 0) return RP_InvalidZeroPriority; + + // replacementWindow of 0 goes direct to mempool + if (item.replacementWindow == 0) + { + // But we also need to remove replacement candidates + replaceMap.erase(item.tx.vin[0].prevout); + return RP_NoReplace; } - auto it = replaceMap.find(item.tx.vin[0].prevout); + int startBlock = item.startBlock; - if (it != replaceMap.end()) { - if (it->second.priority >= item.priority) { - return RP_HaveBetter; // (ThanksThough) + auto it = replaceMap.find(item.tx.vin[0].prevout); + if (it != replaceMap.end()) + { + if (it->second.replacementWindow <= item.replacementWindow && + it->second.priority >= item.priority) { + return RP_HaveBetter; } + startBlock = it->second.startBlock; } // This transaction has higher priority replaceMap[item.tx.vin[0].prevout] = item; - replaceMap[item.tx.vin[0].prevout].startBlock = it->second.startBlock; - - return RP_Accepted; + replaceMap[item.tx.vin[0].prevout].startBlock = startBlock; + return RP_Accept; } @@ -52,10 +60,11 @@ CTxReplacementPoolResult CTxReplacementPool::replace(CTxReplacementPoolItem &ite */ void CTxReplacementPool::removePending(int height, std::vector &txs) { - AssertLockHeld(cs_main); + LOCK(cs_replacementPool); for (auto it = replaceMap.begin(); it != replaceMap.end(); /**/) { CTxReplacementPoolItem &rep = it->second; + if (rep.GetTargetBlock() <= height) { txs.push_back(rep.tx); replaceMap.erase(it++); @@ -71,6 +80,7 @@ void CTxReplacementPool::removePending(int height, std::vector &tx */ bool CTxReplacementPool::lookup(uint256 txHash, CTransaction &tx) { + LOCK(cs_replacementPool); for (auto it = replaceMap.begin(); it != replaceMap.end(); it++) { if (it->second.tx.GetHash() == txHash) { tx = it->second.tx; diff --git a/src/replacementpool.h b/src/replacementpool.h index 6bf577999..4642f28c5 100644 --- a/src/replacementpool.h +++ b/src/replacementpool.h @@ -8,7 +8,14 @@ #include "primitives/transaction.h" -enum CTxReplacementPoolResult { RP_Accepted, RP_HaveBetter, RP_Invalid, RP_NotReplaceable }; +// My kingdom for a proper sum type... +enum CTxReplacementPoolResult { + RP_Accept, + RP_HaveBetter, + RP_InvalidZeroPriority, + RP_InvalidStructure, + RP_NoReplace +}; class CTxReplacementPoolItem diff --git a/src/rpcmining.cpp b/src/rpcmining.cpp index f2fdc1415..da1d5207f 100644 --- a/src/rpcmining.cpp +++ b/src/rpcmining.cpp @@ -206,8 +206,13 @@ UniValue generate(const UniValue& params, bool fHelp) UniValue blockHashes(UniValue::VARR); unsigned int n = Params().EquihashN(); unsigned int k = Params().EquihashK(); + uint64_t lastTime = 0; while (nHeight < nHeightEnd) { + // Validation may fail if block generation is too fast + if (GetTime() == lastTime) MilliSleep(1001); + lastTime = GetTime(); + #ifdef ENABLE_WALLET std::unique_ptr pblocktemplate(CreateNewBlockWithKey(reservekey)); #else diff --git a/src/test-komodo/main.cpp b/src/test-komodo/main.cpp new file mode 100644 index 000000000..7811d5f9a --- /dev/null +++ b/src/test-komodo/main.cpp @@ -0,0 +1,9 @@ +#include "gtest/gtest.h" +#include "crypto/common.h" + +int main(int argc, char **argv) { + assert(init_and_check_sodium() != -1); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/test-komodo/test_replacementpool.cpp b/src/test-komodo/test_replacementpool.cpp new file mode 100644 index 000000000..fa081204a --- /dev/null +++ b/src/test-komodo/test_replacementpool.cpp @@ -0,0 +1,461 @@ +#include +#include + +#include "base58.h" +#include "core_io.h" +#include "key.h" +#include "komodo_cryptoconditions.h" +#include "main.h" +#include "miner.h" +#include "random.h" +#include "rpcserver.h" +#include "rpcprotocol.h" +#include "replacementpool.h" +#include "txdb.h" +#include "util.h" +#include "utilstrencodings.h" +#include "utiltime.h" +#include "consensus/validation.h" +#include "primitives/transaction.h" +#include "script/interpreter.h" +#include "cryptoconditions/include/cryptoconditions.h" + + +extern int32_t USE_EXTERNAL_PUBKEY; +extern std::string NOTARY_PUBKEY; +std::string _NOTARY_PUBKEY = "0205a8ad0c1dbc515f149af377981aab58b836af008d4d7ab21bd76faf80550b47"; +std::string NOTARY_SECRET = "UxFWWxsf1d7w7K5TvAWSkeX4H95XQKwdwGv49DXwWUTzPTTjHBbU"; +CKey notaryKey; + +#define VCH(a,b) std::vector(a, a + b) + + +/* + * We need to have control of clock, + * otherwise block production can fail. + */ +int64_t nMockTime; + + +void testSetup() +{ + SelectParams(CBaseChainParams::REGTEST); + + // enable CC + ASSETCHAINS_CC = 1; + ASSETCHAINS_CC_TEST = true; + + // Settings to get block reward + NOTARY_PUBKEY = _NOTARY_PUBKEY; + USE_EXTERNAL_PUBKEY = 1; + mapArgs["-mineraddress"] = "bogus"; + COINBASE_MATURITY = 1; + // Global mock time + nMockTime = GetTime(); + + // Init blockchain + ClearDatadirCache(); + auto pathTemp = GetTempPath() / strprintf("test_komodo_%li_%i", GetTime(), GetRand(100000)); + boost::filesystem::create_directories(pathTemp); + mapArgs["-datadir"] = pathTemp.string(); + pblocktree = new CBlockTreeDB(1 << 20, true); + CCoinsViewDB *pcoinsdbview = new CCoinsViewDB(1 << 23, true); + pcoinsTip = new CCoinsViewCache(pcoinsdbview); + InitBlockIndex(); + + // Set address + ECC_Start(); + + // Notary key + CBitcoinSecret vchSecret; + // this returns false due to network prefix mismatch but works anyway + vchSecret.SetString(NOTARY_SECRET); + notaryKey = vchSecret.GetKey(); +} + + +void generateBlock(CBlock *block=NULL) +{ + UniValue params; + params.setArray(); + params.push_back(1); + uint256 blockId; + + SetMockTime(nMockTime++); // CreateNewBlock can fail if not enough time passes + + try { + UniValue out = generate(params, false); + blockId.SetHex(out[0].getValStr()); + } catch (const UniValue& e) { + FAIL() << "failed to create block: " << e.write().data(); + } + if (block) ASSERT_TRUE(ReadBlockFromDisk(*block, mapBlockIndex[blockId])); +} + + +void acceptTx(const CTransaction tx) +{ + CValidationState state; + LOCK(cs_main); + if (!AcceptToMemoryPool(mempool, state, tx, false, NULL)) + FAIL() << state.GetRejectReason(); +} + + +static CMutableTransaction spendTx(const CTransaction &txIn, int nOut=0) +{ + CMutableTransaction mtx; + mtx.vin.resize(1); + mtx.vin[0].prevout.hash = txIn.GetHash(); + mtx.vin[0].prevout.n = nOut; + mtx.vout.resize(1); + mtx.vout[0].nValue = txIn.vout[nOut].nValue - 1000; + return mtx; +} + + +/* + * In order to do tests there needs to be inputs to spend. + * This method creates a block and returns a transaction that spends the coinbase. + */ +void getInputTx(CScript scriptPubKey, CTransaction &txIn) +{ + // Get coinbase + CBlock block; + generateBlock(&block); + CTransaction coinbase = block.vtx[0]; + + // Create tx + auto mtx = spendTx(coinbase); + mtx.vout[0].scriptPubKey = scriptPubKey; + uint256 hash = SignatureHash(coinbase.vout[0].scriptPubKey, mtx, 0, SIGHASH_ALL); + std::vector vchSig; + notaryKey.Sign(hash, vchSig); + vchSig.push_back((unsigned char)SIGHASH_ALL); + mtx.vin[0].scriptSig << vchSig; + + // Accept + acceptTx(mtx); + txIn = CTransaction(mtx); +} + + +std::string HexToB64(std::string hexStr) +{ + auto d = ParseHex(hexStr); + return EncodeBase64(d.data(), d.size()); +} + + +CC *getReplaceCond() +{ + const char *condJsonTpl = R"V0G0N( + { "type": "threshold-sha-256", + "threshold": 2, + "subfulfillments": [ + { "type": "secp256k1-sha-256", "publicKey": "%s"}, + { "type": "eval-sha-256", "method": "testReplace", "params": "" } + ] })V0G0N"; + char condJson[1000]; + sprintf(condJson, condJsonTpl, (char*)HexToB64(NOTARY_PUBKEY).data()); + + unsigned char err[1000] = "\0"; + return cc_conditionFromJSONString((const unsigned char*)condJson, err); + // above could fail +} + +CScript condPK(CC *cond) +{ + unsigned char buf[1000]; + size_t bufLen = cc_conditionBinary(cond, buf); + return CScript() << VCH(buf, bufLen) << OP_CHECKCRYPTOCONDITION; +} + +void setFulfillment(CMutableTransaction &mtx, CC *cond, const CScript &spk, int nIn=0) +{ + uint256 hash = SignatureHash(spk, mtx, nIn, SIGHASH_ALL); + int nSigned = cc_signTreeSecp256k1Msg32(cond, notaryKey.begin(), hash.begin()); + unsigned char buf[1000]; + size_t bufLen = cc_fulfillmentBinary(cond, buf, 1000); + mtx.vin[nIn].scriptSig = CScript() << VCH(buf, bufLen); +} + + +CScript getReplaceOut(unsigned char replacementWindow, unsigned char priority) +{ + std::vector v = {replacementWindow, priority}; + return CScript() << OP_RETURN << v; +} + + +CTransaction _txout; +#define ASSERT_REPLACEMENT_POOL(hash) ASSERT_TRUE(replacementPool.lookup(hash, _txout)); \ + ASSERT_FALSE(mempool.lookup(hash, _txout)); +#define ASSERT_MEM_POOL(hash) ASSERT_FALSE(replacementPool.lookup(hash, _txout)); \ + ASSERT_TRUE(mempool.lookup(hash, _txout)); + + + +// Setup environment and perform basic spend as test +TEST(replacementpool, 0_setup) +{ + testSetup(); // Only call this method here + + CTransaction txIn; + getInputTx(CScript() << OP_RETURN << VCH("1", 1), txIn); +} + + +// Perform replaceable spend +TEST(replacementpool, 1_basic) +{ + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + // Spend output to replaceable + auto mtx = spendTx(txIn); + mtx.vout[0].scriptPubKey = getReplaceOut(2, 100); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + + acceptTx(mtx); + + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + generateBlock(); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + generateBlock(); + ASSERT_MEM_POOL(mtx.GetHash()); +} + + +/* + * replacementWindow is 0, transaction should go direct to mempool + */ +TEST(replacementpool, 2_noWindow) +{ + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + // First set a tx with a 1 block wait. It should stay in the replacement pool. + auto mtx = spendTx(txIn); + mtx.vout[0].scriptPubKey = getReplaceOut(1, 100); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + + // Now set a transaction with a 0 block wait and higher priority. + // It should go direct to the mem pool. + auto mtx2 = spendTx(txIn); + mtx2.vout[0].scriptPubKey = getReplaceOut(0, 101); + setFulfillment(mtx2, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx2); + ASSERT_MEM_POOL(mtx2.GetHash()); + + // Additionally, there should be no replacement remaining for txIn in the mempool + ASSERT_FALSE(replacementPool.lookup(mtx.GetHash(), _txout)); +} + + +/* + * Multiple replaceable transactions dont interfere + */ +TEST(replacementpool, 3_noInterfere) +{ + CTransaction txIn1, txIn2; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn1); + getInputTx(condPK(cond), txIn2); + + // First set a transaction with a low window + auto mtx = spendTx(txIn1); + mtx.vout[0].scriptPubKey = getReplaceOut(1, 100); + setFulfillment(mtx, cond, txIn1.vout[0].scriptPubKey); + acceptTx(mtx); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + + // Now, a different spend with a higher window + auto mtx2 = spendTx(txIn2); + mtx2.vout[0].scriptPubKey = getReplaceOut(10, 100); + setFulfillment(mtx2, cond, txIn2.vout[0].scriptPubKey); + acceptTx(mtx2); + ASSERT_REPLACEMENT_POOL(mtx2.GetHash()); + + generateBlock(); + + // mtx has gone to mempool + ASSERT_MEM_POOL(mtx.GetHash()); + + // mtx2 still in replacementpool + ASSERT_REPLACEMENT_POOL(mtx2.GetHash()); + + // But 9 blocks later... + for (int i=0; i<9; i++) + { + generateBlock(); + ASSERT_EQ(i == 8, mempool.lookup(mtx2.GetHash(), _txout)); + } +} + + +/* + * 0 priority is invalid + */ +TEST(replacementpool, 4_invalidZeroPriority) +{ + LOCK(cs_main); + + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + auto mtx = spendTx(txIn); + mtx.vout[0].scriptPubKey = getReplaceOut(1, 0); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + + CValidationState state; + ASSERT_FALSE(AcceptToMemoryPool(mempool, state, mtx, false, NULL)); + ASSERT_EQ("replacement-invalid-zero-priority", state.GetRejectReason()); +} + + +/* + * Multiple inputs is invalid + */ +TEST(replacementpool, 5_invalidMultipleInputs) +{ + LOCK(cs_main); + + CTransaction txIn, txIn2; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + getInputTx(condPK(cond), txIn2); + + CMutableTransaction mtx; + mtx.vout.resize(1); + mtx.vout[0].nValue = txIn.vout[0].nValue * 2 - 1000; + mtx.vout[0].scriptPubKey = getReplaceOut(1, 100); + mtx.vin.resize(2); + mtx.vin[0].prevout.hash = txIn.GetHash(); + mtx.vin[0].prevout.n = 0; + mtx.vin[1].prevout.hash = txIn2.GetHash(); + mtx.vin[1].prevout.n = 0; + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + setFulfillment(mtx, cond, txIn2.vout[0].scriptPubKey, 1); + + CValidationState state; + ASSERT_FALSE(AcceptToMemoryPool(mempool, state, mtx, false, NULL)); + ASSERT_EQ("replacement-has-invalid-structure", state.GetRejectReason()); +} + + +extern bool AddOrphanTx(const CTransaction& tx, NodeId peer); +extern void ProcessOrphanTransactions(uint256 parentHash); +struct COrphanTx { CTransaction tx; NodeId fromPeer; }; +extern std::map mapOrphanTransactions; + +/* + * Orphans are processed + */ +TEST(replacementpool, 6_orphansAreProcessed) +{ + LOCK(cs_main); + + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + // Make basic replaceable spend and dont submit + auto mtx = spendTx(txIn); + mtx.vout.resize(2); + mtx.vout[0].nValue = txIn.vout[0].nValue - 1000; + mtx.vout[0].scriptPubKey = condPK(cond); + mtx.vout[1].nValue = 1; + mtx.vout[1].scriptPubKey = getReplaceOut(1, 100); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + + // Make an orphan and add it + auto orphan = spendTx(mtx); + orphan.vout[0].scriptPubKey = getReplaceOut(0, 100); + setFulfillment(orphan, cond, mtx.vout[0].scriptPubKey); + ASSERT_TRUE(AddOrphanTx(orphan, 1001)); + ASSERT_EQ(1, mapOrphanTransactions.count(orphan.GetHash())); + + // parent goes into replacement pool + acceptTx(mtx); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + + // this should not result in the movement of any orphans + ProcessOrphanTransactions(mtx.GetHash()); + ASSERT_EQ(1, mapOrphanTransactions.count(orphan.GetHash())); + + // Processing of parent transaction also un-orphanises orphan + generateBlock(); + ASSERT_MEM_POOL(mtx.GetHash()); + ASSERT_MEM_POOL(orphan.GetHash()); + ASSERT_EQ(0, mapOrphanTransactions.count(orphan.GetHash())); +} + + +/* + * Add transaction with lower priority, already have better + */ +TEST(replacementpool, 7_haveBetter) +{ + LOCK(cs_main); + + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + // A replaceable tx. + auto mtx = spendTx(txIn); + mtx.vout[0].scriptPubKey = getReplaceOut(2, 100); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + + // Another one, but not as good. + auto mtx2 = spendTx(txIn); + mtx2.vout[0].scriptPubKey = getReplaceOut(2, 99); + setFulfillment(mtx2, cond, txIn.vout[0].scriptPubKey); + CValidationState state; + ASSERT_FALSE(AcceptToMemoryPool(mempool, state, mtx, false, NULL)); + ASSERT_EQ("replacement-is-worse", state.GetRejectReason()); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); +} + + +/* + * Add transaction with lower priority, but shorter replacementWindow + */ +TEST(replacementpool, 8_shorterReplacementWindow) +{ + LOCK(cs_main); + + CTransaction txIn; + CC *cond = getReplaceCond(); + getInputTx(condPK(cond), txIn); + + // A replaceable tx. + auto mtx = spendTx(txIn); + mtx.vout[0].scriptPubKey = getReplaceOut(2, 100); + setFulfillment(mtx, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx); + ASSERT_REPLACEMENT_POOL(mtx.GetHash()); + + // Another one, lower priority but shorter replacementWindow so wins. + auto mtx2 = spendTx(txIn); + mtx2.vout[0].scriptPubKey = getReplaceOut(1, 99); + setFulfillment(mtx2, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx2); + ASSERT_REPLACEMENT_POOL(mtx2.GetHash()); + ASSERT_FALSE(replacementPool.lookup(mtx.GetHash(), _txout)); + + // Shorter still, in fact direct to mem pool + auto mtx3 = spendTx(txIn); + mtx3.vout[0].scriptPubKey = getReplaceOut(0, 98); + setFulfillment(mtx3, cond, txIn.vout[0].scriptPubKey); + acceptTx(mtx3); + ASSERT_MEM_POOL(mtx3.GetHash()); +}