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/komodo-cli
|
||||||
src/komodod
|
src/komodod
|
||||||
src/komodo-tx
|
src/komodo-tx
|
||||||
|
src/komodo-test
|
||||||
|
|||||||
@@ -562,9 +562,10 @@ endif
|
|||||||
@test -f $(PROTOC)
|
@test -f $(PROTOC)
|
||||||
$(AM_V_GEN) $(PROTOC) --cpp_out=$(@D) --proto_path=$(abspath $(<D) $<)
|
$(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.test.include
|
||||||
#include Makefile.gtest.include
|
#include Makefile.gtest.include
|
||||||
#endif
|
endif
|
||||||
|
|
||||||
include Makefile.zcash.include
|
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"
|
#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)
|
bool EvalConditionValidity(const CC *cond, const CTransaction *txTo)
|
||||||
{
|
{
|
||||||
|
if (ASSETCHAINS_CC_TEST) {
|
||||||
if (strcmp(cond->method, "testEval") == 0) {
|
if (strcmp(cond->method, "testEval") == 0) {
|
||||||
return cond->paramsBinLength == 8 &&
|
return cond->paramsBinLength == 8 &&
|
||||||
memcmp(cond->paramsBin, "testEval", 8) == 0;
|
memcmp(cond->paramsBin, "testEval", 8) == 0;
|
||||||
}
|
}
|
||||||
if (strcmp(cond->method, "testReplace") == 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);
|
fprintf(stderr, "no defined behaviour for method: %s\n", cond->method);
|
||||||
return 0;
|
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
|
* of the provided replacement pool item. Priority is a 64 bit unsigned int and
|
||||||
* 0 is invalid.
|
* 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)
|
bool EvalConditionPriority(const CC *cond, CTxReplacementPoolItem *rep)
|
||||||
{
|
{
|
||||||
|
if (ASSETCHAINS_CC_TEST) {
|
||||||
if (strcmp((const char*)cond->method, "testReplace") == 0) {
|
if (strcmp((const char*)cond->method, "testReplace") == 0) {
|
||||||
std::vector<unsigned char> data;
|
std::vector<unsigned char> data;
|
||||||
if (GetOpReturnData(rep->tx.vout[0].scriptPubKey, data)) {
|
auto out = rep->tx.vout[rep->tx.vout.size()-1]; // Last output is data
|
||||||
rep->priority = (uint64_t) data[0];
|
if (GetOpReturnData(out.scriptPubKey, data)) {
|
||||||
rep->replacementWindow = REPLACEMENT_WINDOW_BLOCKS;
|
rep->replacementWindow = (int) data[0];
|
||||||
|
rep->priority = (uint64_t) data[1];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +68,7 @@ int visitConditionPriority(CC *cond, struct CCVisitor visitor)
|
|||||||
/*
|
/*
|
||||||
* Try to get replacement parameters from a transaction in &rep.
|
* 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
|
// first, see if we have a cryptocondition
|
||||||
const CScript &sig = rep.tx.vin[0].scriptSig;
|
const CScript &sig = rep.tx.vin[0].scriptSig;
|
||||||
@@ -68,7 +78,6 @@ bool PutReplacementParams(CTxReplacementPoolItem &rep)
|
|||||||
opcodetype opcode;
|
opcodetype opcode;
|
||||||
if (!sig.GetOp(pc, opcode, data)) return false;
|
if (!sig.GetOp(pc, opcode, data)) return false;
|
||||||
CC *cond = cc_readFulfillmentBinary((unsigned char*)data.data(), data.size());
|
CC *cond = cc_readFulfillmentBinary((unsigned char*)data.data(), data.size());
|
||||||
auto wat = {1, ""};
|
|
||||||
if (!cond) return false;
|
if (!cond) return false;
|
||||||
|
|
||||||
// now, see if it has a replacement node
|
// now, see if it has a replacement node
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
|
|
||||||
extern int32_t ASSETCHAINS_CC;
|
extern int32_t ASSETCHAINS_CC;
|
||||||
|
extern bool ASSETCHAINS_CC_TEST;
|
||||||
|
|
||||||
static bool IsCryptoConditionsEnabled() {
|
static bool IsCryptoConditionsEnabled() {
|
||||||
return 0 != ASSETCHAINS_CC;
|
return 0 != ASSETCHAINS_CC;
|
||||||
|
|||||||
72
src/main.cpp
72
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)
|
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree,bool* pfMissingInputs, bool fRejectAbsurdFee, bool fAcceptReplacement)
|
||||||
{
|
{
|
||||||
AssertLockHeld(cs_main);
|
AssertLockHeld(cs_main);
|
||||||
@@ -1181,8 +1220,6 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CTxReplacementPoolResult rpr = RP_NotReplaceable;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
CCoinsView dummy;
|
CCoinsView dummy;
|
||||||
CCoinsViewCache view(&dummy);
|
CCoinsViewCache view(&dummy);
|
||||||
@@ -1341,40 +1378,15 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
|
|||||||
|
|
||||||
if (fAcceptReplacement)
|
if (fAcceptReplacement)
|
||||||
{
|
{
|
||||||
CTxReplacementPoolItem item(tx, GetHeight());
|
if (AcceptToReplacementPool(tx, state)) return true;
|
||||||
if (SetReplacementParams(item)) {
|
if (state.IsInvalid()) return false;
|
||||||
rpr = replacementPool.replace(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Store transaction in memory
|
||||||
pool.addUnchecked(hash, entry, !IsInitialBlockDownload());
|
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);
|
SyncWithWallets(tx, NULL);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2826,7 +2838,7 @@ bool static ConnectTip(CValidationState &state, CBlockIndex *pindexNew, CBlock *
|
|||||||
// Update chainActive & related variables.
|
// Update chainActive & related variables.
|
||||||
UpdateTip(pindexNew);
|
UpdateTip(pindexNew);
|
||||||
// Process pending replacements
|
// Process pending replacements
|
||||||
ProcessReplacementPool(pindexNew->nHeight-1);
|
ProcessReplacementPool(pindexNew->nHeight);
|
||||||
// Tell wallet about transactions that went from mempool
|
// Tell wallet about transactions that went from mempool
|
||||||
// to conflicted:
|
// to conflicted:
|
||||||
BOOST_FOREACH(const CTransaction &tx, txConflicted) {
|
BOOST_FOREACH(const CTransaction &tx, txConflicted) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "replacementpool.h"
|
#include "replacementpool.h"
|
||||||
|
#include "sync.h"
|
||||||
|
|
||||||
|
|
||||||
CTxReplacementPool replacementPool;
|
CTxReplacementPool replacementPool;
|
||||||
|
CCriticalSection cs_replacementPool;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -17,33 +19,39 @@ CTxReplacementPool replacementPool;
|
|||||||
*/
|
*/
|
||||||
CTxReplacementPoolResult CTxReplacementPool::replace(CTxReplacementPoolItem &item)
|
CTxReplacementPoolResult CTxReplacementPool::replace(CTxReplacementPoolItem &item)
|
||||||
{
|
{
|
||||||
AssertLockHeld(cs_main);
|
LOCK(cs_replacementPool);
|
||||||
|
|
||||||
// Perform some validations.
|
// Replaceable transactions with multiple inputs are disabled
|
||||||
if (item.tx.vin.size() > 1) {
|
// until someone figures out how they would work.
|
||||||
// Replaceable transactions with multiple inputs are disabled.
|
if (item.tx.vin.size() > 1) return RP_InvalidStructure;
|
||||||
// It seems like quite alot of additional complexity.
|
|
||||||
return RP_Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A transaction with 0 priority is not valid.
|
// A transaction with 0 priority is not valid.
|
||||||
if (item.priority == 0) {
|
if (item.priority == 0) return RP_InvalidZeroPriority;
|
||||||
return RP_Invalid;
|
|
||||||
|
// 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);
|
auto it = replaceMap.find(item.tx.vin[0].prevout);
|
||||||
|
if (it != replaceMap.end())
|
||||||
if (it != replaceMap.end()) {
|
{
|
||||||
if (it->second.priority >= item.priority) {
|
if (it->second.replacementWindow <= item.replacementWindow &&
|
||||||
return RP_HaveBetter; // (ThanksThough)
|
it->second.priority >= item.priority) {
|
||||||
|
return RP_HaveBetter;
|
||||||
}
|
}
|
||||||
|
startBlock = it->second.startBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This transaction has higher priority
|
// This transaction has higher priority
|
||||||
replaceMap[item.tx.vin[0].prevout] = item;
|
replaceMap[item.tx.vin[0].prevout] = item;
|
||||||
replaceMap[item.tx.vin[0].prevout].startBlock = it->second.startBlock;
|
replaceMap[item.tx.vin[0].prevout].startBlock = startBlock;
|
||||||
|
return RP_Accept;
|
||||||
return RP_Accepted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -52,10 +60,11 @@ CTxReplacementPoolResult CTxReplacementPool::replace(CTxReplacementPoolItem &ite
|
|||||||
*/
|
*/
|
||||||
void CTxReplacementPool::removePending(int height, std::vector<CTransaction> &txs)
|
void CTxReplacementPool::removePending(int height, std::vector<CTransaction> &txs)
|
||||||
{
|
{
|
||||||
AssertLockHeld(cs_main);
|
LOCK(cs_replacementPool);
|
||||||
|
|
||||||
for (auto it = replaceMap.begin(); it != replaceMap.end(); /**/) {
|
for (auto it = replaceMap.begin(); it != replaceMap.end(); /**/) {
|
||||||
CTxReplacementPoolItem &rep = it->second;
|
CTxReplacementPoolItem &rep = it->second;
|
||||||
|
|
||||||
if (rep.GetTargetBlock() <= height) {
|
if (rep.GetTargetBlock() <= height) {
|
||||||
txs.push_back(rep.tx);
|
txs.push_back(rep.tx);
|
||||||
replaceMap.erase(it++);
|
replaceMap.erase(it++);
|
||||||
@@ -71,6 +80,7 @@ void CTxReplacementPool::removePending(int height, std::vector<CTransaction> &tx
|
|||||||
*/
|
*/
|
||||||
bool CTxReplacementPool::lookup(uint256 txHash, CTransaction &tx)
|
bool CTxReplacementPool::lookup(uint256 txHash, CTransaction &tx)
|
||||||
{
|
{
|
||||||
|
LOCK(cs_replacementPool);
|
||||||
for (auto it = replaceMap.begin(); it != replaceMap.end(); it++) {
|
for (auto it = replaceMap.begin(); it != replaceMap.end(); it++) {
|
||||||
if (it->second.tx.GetHash() == txHash) {
|
if (it->second.tx.GetHash() == txHash) {
|
||||||
tx = it->second.tx;
|
tx = it->second.tx;
|
||||||
|
|||||||
@@ -8,7 +8,14 @@
|
|||||||
#include "primitives/transaction.h"
|
#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
|
class CTxReplacementPoolItem
|
||||||
|
|||||||
@@ -206,8 +206,13 @@ UniValue generate(const UniValue& params, bool fHelp)
|
|||||||
UniValue blockHashes(UniValue::VARR);
|
UniValue blockHashes(UniValue::VARR);
|
||||||
unsigned int n = Params().EquihashN();
|
unsigned int n = Params().EquihashN();
|
||||||
unsigned int k = Params().EquihashK();
|
unsigned int k = Params().EquihashK();
|
||||||
|
uint64_t lastTime = 0;
|
||||||
while (nHeight < nHeightEnd)
|
while (nHeight < nHeightEnd)
|
||||||
{
|
{
|
||||||
|
// Validation may fail if block generation is too fast
|
||||||
|
if (GetTime() == lastTime) MilliSleep(1001);
|
||||||
|
lastTime = GetTime();
|
||||||
|
|
||||||
#ifdef ENABLE_WALLET
|
#ifdef ENABLE_WALLET
|
||||||
std::unique_ptr<CBlockTemplate> pblocktemplate(CreateNewBlockWithKey(reservekey));
|
std::unique_ptr<CBlockTemplate> pblocktemplate(CreateNewBlockWithKey(reservekey));
|
||||||
#else
|
#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