test suite for replacementPool
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -119,3 +119,4 @@ src/rpcmisc~.cpp
|
||||
src/komodo-cli
|
||||
src/komodod
|
||||
src/komodo-tx
|
||||
src/komodo-test
|
||||
|
||||
@@ -562,9 +562,10 @@ endif
|
||||
@test -f $(PROTOC)
|
||||
$(AM_V_GEN) $(PROTOC) --cpp_out=$(@D) --proto_path=$(abspath $(<D) $<)
|
||||
|
||||
#if ENABLE_TESTS
|
||||
if ENABLE_TESTS
|
||||
include Makefile.ktest.include
|
||||
#include Makefile.test.include
|
||||
#include Makefile.gtest.include
|
||||
#endif
|
||||
endif
|
||||
|
||||
include Makefile.zcash.include
|
||||
|
||||
16
src/Makefile.ktest.include
Normal file
16
src/Makefile.ktest.include
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
TESTS += komodo-test
|
||||
bin_PROGRAMS += komodo-test
|
||||
|
||||
# tool for generating our public parameters
|
||||
komodo_test_SOURCES = \
|
||||
test-komodo/main.cpp \
|
||||
test-komodo/test_replacementpool.cpp
|
||||
|
||||
komodo_test_CPPFLAGS = $(komodod_CPPFLAGS)
|
||||
|
||||
komodo_test_LDADD = -lgtest -lgmock $(komodod_LDADD)
|
||||
|
||||
|
||||
|
||||
komodo_test_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) -static
|
||||
Submodule src/cryptoconditions updated: f606567ce3...3200bc78e8
@@ -4,7 +4,7 @@
|
||||
#include "cryptoconditions/include/cryptoconditions.h"
|
||||
|
||||
|
||||
#define REPLACEMENT_WINDOW_BLOCKS 2
|
||||
bool ASSETCHAINS_CC_TEST = false;
|
||||
|
||||
|
||||
/*
|
||||
@@ -12,12 +12,16 @@
|
||||
*/
|
||||
bool EvalConditionValidity(const CC *cond, const CTransaction *txTo)
|
||||
{
|
||||
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) {
|
||||
return true;
|
||||
std::vector<unsigned char> 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,18 +35,24 @@ 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 (ASSETCHAINS_CC_TEST) {
|
||||
if (strcmp((const char*)cond->method, "testReplace") == 0) {
|
||||
std::vector<unsigned char> data;
|
||||
if (GetOpReturnData(rep->tx.vout[0].scriptPubKey, data)) {
|
||||
rep->priority = (uint64_t) data[0];
|
||||
rep->replacementWindow = REPLACEMENT_WINDOW_BLOCKS;
|
||||
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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
extern int32_t ASSETCHAINS_CC;
|
||||
extern bool ASSETCHAINS_CC_TEST;
|
||||
|
||||
static bool IsCryptoConditionsEnabled() {
|
||||
return 0 != ASSETCHAINS_CC;
|
||||
|
||||
74
src/main.cpp
74
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
int startBlock = item.startBlock;
|
||||
|
||||
auto it = replaceMap.find(item.tx.vin[0].prevout);
|
||||
|
||||
if (it != replaceMap.end()) {
|
||||
if (it->second.priority >= item.priority) {
|
||||
return RP_HaveBetter; // (ThanksThough)
|
||||
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<CTransaction> &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<CTransaction> &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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CBlockTemplate> pblocktemplate(CreateNewBlockWithKey(reservekey));
|
||||
#else
|
||||
|
||||
9
src/test-komodo/main.cpp
Normal file
9
src/test-komodo/main.cpp
Normal file
@@ -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();
|
||||
}
|
||||
461
src/test-komodo/test_replacementpool.cpp
Normal file
461
src/test-komodo/test_replacementpool.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#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<unsigned char>(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<unsigned char> 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<unsigned char> 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<uint256, COrphanTx> 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());
|
||||
}
|
||||
Reference in New Issue
Block a user