WIP: #12 - Replay attack prevention

Conflicts:
	src/Makefile.am
This commit is contained in:
joshuayabut 2017-05-02 10:46:54 -04:00
parent 8430716115
commit f766ab3146
11 changed files with 236 additions and 10 deletions

View File

@ -170,7 +170,7 @@ bool CBloomFilter::IsRelevantAndUpdate(const CTransaction& tx)
txnouttype type;
vector<vector<unsigned char> > vSolutions;
if (Solver(txout.scriptPubKey, type, vSolutions) &&
(type == TX_PUBKEY || type == TX_MULTISIG))
(type == TX_PUBKEY || type == TX_PUBKEY_REPLAY || type == TX_MULTISIG || type == TX_MULTISIG_REPLAY))
insert(COutPoint(hash, i));
}
break;

View File

@ -14,6 +14,10 @@
#include "script/script.h"
#include "uint256.h"
#include "main.h"
#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string.hpp>
using namespace std;
typedef vector<unsigned char> valtype;
@ -188,6 +192,40 @@ bool static IsDefinedHashtypeSignature(const valtype &vchSig) {
return true;
}
// Generic anti-replay protection using Script
bool CheckBlockIndex(int &txBlockIndex, int blockIndex)
{
// checks for relative txBlockIndex
if (txBlockIndex < 0)
return false;
// checks for absolute blockIndex
else if (txBlockIndex >= 0) {
ssize_t blockDelta = txBlockIndex - blockIndex;
// blockDelta must be negative
if (blockDelta > 0)
return false;
// blockDelta must be greater than 100 but lesser than 262144
if ((blockDelta > -101) || (blockDelta < -262144))
return false;
// check if txBlockIndex refers to an actual block
if (txBlockIndex > blockIndex)
return false;
return true;
}
return false;
}
// Generic anti-replay protection using Script
bool CheckBlockHash(uint256 &txBlockHash, uint256 &blockHash)
{
// OP_CHECKBLOCKATHEIGHT matches active chain
if (txBlockHash == blockHash)
return true;
return false;
}
bool static CheckSignatureEncoding(const valtype &vchSig, unsigned int flags, ScriptError* serror) {
// Empty signature. Not strictly DER encoded, but allowed to provide a
// compact way to provide an invalid signature for use with CHECK(MULTI)SIG
@ -379,7 +417,83 @@ bool EvalScript(vector<vector<unsigned char> >& stack, const CScript& script, un
break;
}
case OP_NOP1: case OP_NOP3: case OP_NOP4: case OP_NOP5:
#ifdef BITCOIN_TX // zen-tx can't process OP_CHECKBLOCKATHEIGHT because it requires an active chain
case OP_CHECKBLOCKATHEIGHT:
{
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT_UNVERIFIED);
}
break;
#else
// Generic anti-replay protection using Script
// https://github.com/luke-jr/bips/blob/bip-noreplay/bip-noreplay.mediawiki
// Author: Luke Dashjr <luke+bip@dashjr.org>
// Implemented by: @movrcx; See: https://github.com/zencashio/zen/issues/12
case OP_CHECKBLOCKATHEIGHT:
{
// we need two objects on the stack
if (stack.size() < 2)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
CBlockIndex *currentBlock = chainActive.Tip();
valtype vchBlockHash(stacktop(-2));
valtype vchBlockIndex(stacktop(-1));
int txBlockIndex;
uint256 blockHash;
uint256 txBlockHash;
bool fSuccess = false;
// check for overflow before casting
if ((vchBlockIndex.size() > 0x7ffffff) || (vchBlockHash.size() > 32))
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT);
else
{
// get txBlockIndex from stack vch
txBlockIndex = *reinterpret_cast<const uint16_t*>(&vchBlockIndex[0]);
// get txBlockHash from stack vch and convert to hex string
std::string str;
boost::algorithm::hex(vchBlockHash.begin(), vchBlockHash.end(), back_inserter(str));
boost::algorithm::to_lower(str);
txBlockHash.SetHex(str);
}
if (currentBlock == NULL)
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT_UNVERIFIED);
else
{
// relative blockIndex lookups are not permitted
if (txBlockIndex < 0)
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT);
else if (txBlockIndex >= 0)
blockHash.SetHex(chainActive[txBlockIndex]->GetBlockHash().GetHex());
// ensure we are not doing any null comparisons
if (blockHash.IsNull() || txBlockHash.IsNull())
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT_UNVERIFIED);
// check tx against blockchain
fSuccess = ((CheckBlockIndex(txBlockIndex, currentBlock->nHeight)) && (CheckBlockHash(txBlockHash, blockHash)));
}
// pop OP_CHECKBLOCKATHEIGHT related vars off the stack
popstack(stack);
popstack(stack);
stack.push_back(fSuccess ? vchTrue : vchFalse);
if (opcode == OP_CHECKBLOCKATHEIGHT)
{
if (fSuccess)
{
popstack(stack);
}
else
return set_error(serror, SCRIPT_ERR_CHECKBLOCKATHEIGHT);
}
}
break;
#endif
case OP_NOP1: case OP_NOP3: case OP_NOP4:
case OP_NOP6: case OP_NOP7: case OP_NOP8: case OP_NOP9: case OP_NOP10:
{
if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS)

View File

@ -85,6 +85,7 @@ enum
//
// See BIP65 for details.
SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY = (1U << 9),
};
uint256 SignatureHash(const CScript &scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType);

View File

@ -138,13 +138,14 @@ const char* GetOpName(opcodetype opcode)
case OP_CHECKSIGVERIFY : return "OP_CHECKSIGVERIFY";
case OP_CHECKMULTISIG : return "OP_CHECKMULTISIG";
case OP_CHECKMULTISIGVERIFY : return "OP_CHECKMULTISIGVERIFY";
case OP_CHECKBLOCKATHEIGHT : return "OP_CHECKBLOCKATHEIGHT";
// expanson
case OP_NOP1 : return "OP_NOP1";
case OP_NOP2 : return "OP_NOP2";
case OP_NOP3 : return "OP_NOP3";
case OP_NOP4 : return "OP_NOP4";
case OP_NOP5 : return "OP_NOP5";
//case OP_NOP5 : return "OP_NOP5"; Generic anti-replay protection using Script
case OP_NOP6 : return "OP_NOP6";
case OP_NOP7 : return "OP_NOP7";
case OP_NOP8 : return "OP_NOP8";

View File

@ -162,6 +162,7 @@ enum opcodetype
OP_NOP3 = 0xb2,
OP_NOP4 = 0xb3,
OP_NOP5 = 0xb4,
OP_CHECKBLOCKATHEIGHT = OP_NOP5,
OP_NOP6 = 0xb5,
OP_NOP7 = 0xb6,
OP_NOP8 = 0xb7,

View File

@ -67,6 +67,10 @@ const char* ScriptErrorString(const ScriptError serror)
return "NOPx reserved for soft-fork upgrades";
case SCRIPT_ERR_PUBKEYTYPE:
return "Public key is neither compressed or uncompressed";
case SCRIPT_ERR_CHECKBLOCKATHEIGHT:
return "Transaction failed to pass replay prevention checks";
case SCRIPT_ERR_CHECKBLOCKATHEIGHT_UNVERIFIED:
return "OP_CHECKBLOCKATHEIGHT cannot be verified with zen-tx";
case SCRIPT_ERR_UNKNOWN_ERROR:
case SCRIPT_ERR_ERROR_COUNT:
default: break;

View File

@ -51,8 +51,12 @@ typedef enum ScriptError_t
/* softfork safeness */
SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS,
SCRIPT_ERR_ERROR_COUNT,
/* tx replay prevention */
SCRIPT_ERR_CHECKBLOCKATHEIGHT,
SCRIPT_ERR_CHECKBLOCKATHEIGHT_UNVERIFIED
SCRIPT_ERR_ERROR_COUNT
} ScriptError;
#define SCRIPT_ERR_LAST SCRIPT_ERR_ERROR_COUNT

View File

@ -82,9 +82,14 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP
case TX_NONSTANDARD:
case TX_NULL_DATA:
return false;
case TX_NULL_DATA_REPLAY:
return false;
case TX_PUBKEY:
keyID = CPubKey(vSolutions[0]).GetID();
return Sign1(keyID, creator, scriptPubKey, scriptSigRet);
case TX_PUBKEY_REPLAY:
keyID = CPubKey(vSolutions[0]).GetID();
return Sign1(keyID, creator, scriptPubKey, scriptSigRet);
case TX_PUBKEYHASH:
keyID = CKeyID(uint160(vSolutions[0]));
if (!Sign1(keyID, creator, scriptPubKey, scriptSigRet))
@ -96,12 +101,26 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP
scriptSigRet << ToByteVector(vch);
}
return true;
case TX_PUBKEYHASH_REPLAY:
keyID = CKeyID(uint160(vSolutions[0]));
if (!Sign1(keyID, creator, scriptPubKey, scriptSigRet))
return false;
else
{
CPubKey vch;
creator.KeyStore().GetPubKey(keyID, vch);
scriptSigRet << ToByteVector(vch);
}
return true;
case TX_SCRIPTHASH:
return creator.KeyStore().GetCScript(uint160(vSolutions[0]), scriptSigRet);
case TX_MULTISIG:
scriptSigRet << OP_0; // workaround CHECKMULTISIG bug
return (SignN(vSolutions, creator, scriptPubKey, scriptSigRet));
case TX_MULTISIG_REPLAY:
scriptSigRet << OP_0; // workaround CHECKMULTISIG bug
return (SignN(vSolutions, creator, scriptPubKey, scriptSigRet));
}
return false;
}
@ -227,12 +246,23 @@ static CScript CombineSignatures(const CScript& scriptPubKey, const BaseSignatur
if (sigs1.size() >= sigs2.size())
return PushAll(sigs1);
return PushAll(sigs2);
case TX_NULL_DATA_REPLAY:
// Don't know anything about this, assume bigger one is correct:
if (sigs1.size() >= sigs2.size())
return PushAll(sigs1);
return PushAll(sigs2);
case TX_PUBKEY:
case TX_PUBKEY_REPLAY:
case TX_PUBKEYHASH:
// Signatures are bigger than placeholders or empty scripts:
if (sigs1.empty() || sigs1[0].empty())
return PushAll(sigs2);
return PushAll(sigs1);
case TX_PUBKEYHASH_REPLAY:
// Signatures are bigger than placeholders or empty scripts:
if (sigs1.empty() || sigs1[0].empty())
return PushAll(sigs2);
return PushAll(sigs1);
case TX_SCRIPTHASH:
if (sigs1.empty() || sigs1.back().empty())
return PushAll(sigs2);
@ -255,6 +285,8 @@ static CScript CombineSignatures(const CScript& scriptPubKey, const BaseSignatur
}
case TX_MULTISIG:
return CombineMultisig(scriptPubKey, checker, vSolutions, sigs1, sigs2);
case TX_MULTISIG_REPLAY:
return CombineMultisig(scriptPubKey, checker, vSolutions, sigs1, sigs2);
}
return CScript();

View File

@ -12,6 +12,8 @@
#include <boost/foreach.hpp>
#include "main.h"
using namespace std;
typedef vector<unsigned char> valtype;
@ -26,10 +28,14 @@ const char* GetTxnOutputType(txnouttype t)
{
case TX_NONSTANDARD: return "nonstandard";
case TX_PUBKEY: return "pubkey";
case TX_PUBKEY_REPLAY: return "pubkeyreplay";
case TX_PUBKEYHASH: return "pubkeyhash";
case TX_PUBKEYHASH_REPLAY: return "pubkeyhashreplay";
case TX_SCRIPTHASH: return "scripthash";
case TX_MULTISIG: return "multisig";
case TX_MULTISIG_REPLAY: return "multisigreplay";
case TX_NULL_DATA: return "nulldata";
case TX_NULL_DATA_REPLAY: return "nulldatareplay";
}
return NULL;
}
@ -45,17 +51,24 @@ bool Solver(const CScript& scriptPubKey, txnouttype& typeRet, vector<vector<unsi
{
// Standard tx, sender provides pubkey, receiver adds signature
mTemplates.insert(make_pair(TX_PUBKEY, CScript() << OP_PUBKEY << OP_CHECKSIG));
mTemplates.insert(make_pair(TX_PUBKEY_REPLAY, CScript() << OP_PUBKEY << OP_CHECKSIG << OP_SMALLDATA << OP_SMALLDATA << OP_CHECKBLOCKATHEIGHT));
// Bitcoin address tx, sender provides hash of pubkey, receiver provides signature and pubkey
mTemplates.insert(make_pair(TX_PUBKEYHASH, CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG));
mTemplates.insert(make_pair(TX_PUBKEYHASH_REPLAY, CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG << OP_SMALLDATA << OP_SMALLDATA << OP_CHECKBLOCKATHEIGHT));
// Sender provides N pubkeys, receivers provides M signatures
mTemplates.insert(make_pair(TX_MULTISIG, CScript() << OP_SMALLINTEGER << OP_PUBKEYS << OP_SMALLINTEGER << OP_CHECKMULTISIG));
mTemplates.insert(make_pair(TX_MULTISIG_REPLAY, CScript() << OP_SMALLINTEGER << OP_PUBKEYS << OP_SMALLINTEGER << OP_CHECKMULTISIG << OP_SMALLDATA << OP_SMALLDATA << OP_CHECKBLOCKATHEIGHT));
// Empty, provably prunable, data-carrying output
if (GetBoolArg("-datacarrier", true))
{
mTemplates.insert(make_pair(TX_NULL_DATA, CScript() << OP_RETURN << OP_SMALLDATA));
mTemplates.insert(make_pair(TX_NULL_DATA_REPLAY, CScript() << OP_RETURN << OP_SMALLDATA << OP_SMALLDATA << OP_SMALLDATA << OP_CHECKBLOCKATHEIGHT));
}
mTemplates.insert(make_pair(TX_NULL_DATA, CScript() << OP_RETURN));
}
// Shortcut for pay-to-script-hash, which are more constrained than the other types:
@ -87,7 +100,7 @@ bool Solver(const CScript& scriptPubKey, txnouttype& typeRet, vector<vector<unsi
{
// Found a match
typeRet = tplate.first;
if (typeRet == TX_MULTISIG)
if (typeRet == TX_MULTISIG || typeRet == TX_MULTISIG_REPLAY)
{
// Additional checks for TX_MULTISIG:
unsigned char m = vSolutionsRet.front()[0];
@ -166,14 +179,24 @@ int ScriptSigArgsExpected(txnouttype t, const std::vector<std::vector<unsigned c
case TX_NONSTANDARD:
case TX_NULL_DATA:
return -1;
case TX_NULL_DATA_REPLAY:
return -1;
case TX_PUBKEY:
return 1;
case TX_PUBKEY_REPLAY:
return 1;
case TX_PUBKEYHASH:
return 2;
case TX_PUBKEYHASH_REPLAY:
return 2;
case TX_MULTISIG:
if (vSolutions.size() < 1 || vSolutions[0].size() < 1)
return -1;
return vSolutions[0][0] + 1;
case TX_MULTISIG_REPLAY:
if (vSolutions.size() < 1 || vSolutions[0].size() < 1)
return -1;
return vSolutions[0][0] + 1;
case TX_SCRIPTHASH:
return 1; // doesn't include args needed by the script
}
@ -186,7 +209,7 @@ bool IsStandard(const CScript& scriptPubKey, txnouttype& whichType)
if (!Solver(scriptPubKey, whichType, vSolutions))
return false;
if (whichType == TX_MULTISIG)
if (whichType == TX_MULTISIG || whichType == TX_MULTISIG_REPLAY)
{
unsigned char m = vSolutions.front()[0];
unsigned char n = vSolutions.back()[0];
@ -207,7 +230,7 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
if (!Solver(scriptPubKey, whichType, vSolutions))
return false;
if (whichType == TX_PUBKEY)
if (whichType == TX_PUBKEY || whichType == TX_PUBKEY_REPLAY)
{
CPubKey pubKey(vSolutions[0]);
if (!pubKey.IsValid())
@ -216,7 +239,7 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
addressRet = pubKey.GetID();
return true;
}
else if (whichType == TX_PUBKEYHASH)
else if (whichType == TX_PUBKEYHASH || whichType == TX_PUBKEYHASH_REPLAY)
{
addressRet = CKeyID(uint160(vSolutions[0]));
return true;
@ -237,12 +260,12 @@ bool ExtractDestinations(const CScript& scriptPubKey, txnouttype& typeRet, vecto
vector<valtype> vSolutions;
if (!Solver(scriptPubKey, typeRet, vSolutions))
return false;
if (typeRet == TX_NULL_DATA){
if (typeRet == TX_NULL_DATA || typeRet == TX_NULL_DATA_REPLAY){
// This is data, not addresses
return false;
}
if (typeRet == TX_MULTISIG)
if (typeRet == TX_MULTISIG || typeRet == TX_MULTISIG_REPLAY)
{
nRequiredRet = vSolutions.front()[0];
for (unsigned int i = 1; i < vSolutions.size()-1; i++)
@ -284,6 +307,7 @@ public:
return false;
}
#ifdef BITCOIN_TX // zen-tx does not have access to chain state so no replay protection is possible
bool operator()(const CKeyID &keyID) const {
script->clear();
*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG;
@ -293,8 +317,23 @@ public:
bool operator()(const CScriptID &scriptID) const {
script->clear();
*script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL;
}
#else
bool operator()(const CKeyID &keyID) const {
script->clear();
CBlockIndex *currentBlock = chainActive.Tip();
*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG << ToByteVector(chainActive[currentBlock->nHeight - 300]->GetBlockHash()) << chainActive[currentBlock->nHeight - 300]->nHeight << OP_CHECKBLOCKATHEIGHT;
return true;
}
bool operator()(const CScriptID &scriptID) const {
script->clear();
CBlockIndex *currentBlock = chainActive.Tip();
*script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL << ToByteVector(chainActive[currentBlock->nHeight - 300]->GetBlockHash()) << chainActive[currentBlock->nHeight - 300]->nHeight << OP_CHECKBLOCKATHEIGHT;
return true;
}
#endif
};
}

View File

@ -62,10 +62,14 @@ enum txnouttype
TX_NONSTANDARD,
// 'standard' transaction types:
TX_PUBKEY,
TX_PUBKEY_REPLAY,
TX_PUBKEYHASH,
TX_PUBKEYHASH_REPLAY,
TX_SCRIPTHASH,
TX_MULTISIG,
TX_MULTISIG_REPLAY,
TX_NULL_DATA,
TX_NULL_DATA_REPLAY,
};
class CNoDestination {

View File

@ -50,16 +50,29 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey)
case TX_NONSTANDARD:
case TX_NULL_DATA:
break;
case TX_NULL_DATA_REPLAY:
break;
case TX_PUBKEY:
keyID = CPubKey(vSolutions[0]).GetID();
if (keystore.HaveKey(keyID))
return ISMINE_SPENDABLE;
break;
case TX_PUBKEY_REPLAY:
keyID = CPubKey(vSolutions[0]).GetID();
if (keystore.HaveKey(keyID))
return ISMINE_SPENDABLE;
break;
case TX_PUBKEYHASH:
keyID = CKeyID(uint160(vSolutions[0]));
if (keystore.HaveKey(keyID))
return ISMINE_SPENDABLE;
break;
case TX_PUBKEYHASH_REPLAY:
keyID = CKeyID(uint160(vSolutions[0]));
if (keystore.HaveKey(keyID))
return ISMINE_SPENDABLE;
break;
case TX_SCRIPTHASH:
{
CScriptID scriptID = CScriptID(uint160(vSolutions[0]));
@ -83,6 +96,19 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey)
return ISMINE_SPENDABLE;
break;
}
case TX_MULTISIG_REPLAY:
{
// Only consider transactions "mine" if we own ALL the
// keys involved. Multi-signature transactions that are
// partially owned (somebody else has a key that can spend
// them) enable spend-out-from-under-you attacks, especially
// in shared-wallet situations.
vector<valtype> keys(vSolutions.begin()+1, vSolutions.begin()+vSolutions.size()-1);
if (HaveKeys(keys, keystore) == keys.size())
return ISMINE_SPENDABLE;
break;
}
}
if (keystore.HaveWatchOnly(scriptPubKey))