Support 3 new multisignature IsStandard transactions
Initial support for (a and b), (a or b), and 2-of-3 escrow transactions (where a, b, and c are keys).
This commit is contained in:
parent
1466b8b78a
commit
bf798734db
5 changed files with 578 additions and 91 deletions
|
@ -936,6 +936,101 @@ Value sendmany(const Array& params, bool fHelp)
|
||||||
return wtx.GetHash().GetHex();
|
return wtx.GetHash().GetHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Value sendmultisig(const Array& params, bool fHelp)
|
||||||
|
{
|
||||||
|
if (fHelp || params.size() < 4 || params.size() > 7)
|
||||||
|
{
|
||||||
|
string msg = "sendmultisig <fromaccount> <type> <[\"key\",\"key\"]> <amount> [minconf=1] [comment] [comment-to]\n"
|
||||||
|
"<type> is one of: \"and\", \"or\", \"escrow\"\n"
|
||||||
|
"<keys> is an array of strings (in JSON array format); each key is a bitcoin address, hex or base58 public key\n"
|
||||||
|
"<amount> is a real and is rounded to the nearest 0.00000001";
|
||||||
|
if (pwalletMain->IsCrypted())
|
||||||
|
msg += "\nrequires wallet passphrase to be set with walletpassphrase first";
|
||||||
|
throw runtime_error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
string strAccount = AccountFromValue(params[0]);
|
||||||
|
string strType = params[1].get_str();
|
||||||
|
const Array& keys = params[2].get_array();
|
||||||
|
int64 nAmount = AmountFromValue(params[3]);
|
||||||
|
int nMinDepth = 1;
|
||||||
|
if (params.size() > 4)
|
||||||
|
nMinDepth = params[4].get_int();
|
||||||
|
|
||||||
|
CWalletTx wtx;
|
||||||
|
wtx.strFromAccount = strAccount;
|
||||||
|
if (params.size() > 5 && params[5].type() != null_type && !params[5].get_str().empty())
|
||||||
|
wtx.mapValue["comment"] = params[5].get_str();
|
||||||
|
if (params.size() > 6 && params[6].type() != null_type && !params[6].get_str().empty())
|
||||||
|
wtx.mapValue["to"] = params[6].get_str();
|
||||||
|
|
||||||
|
if (pwalletMain->IsLocked())
|
||||||
|
throw JSONRPCError(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.");
|
||||||
|
|
||||||
|
// Check funds
|
||||||
|
int64 nBalance = GetAccountBalance(strAccount, nMinDepth);
|
||||||
|
if (nAmount > nBalance)
|
||||||
|
throw JSONRPCError(-6, "Account has insufficient funds");
|
||||||
|
|
||||||
|
// Gather public keys
|
||||||
|
int nKeysNeeded = 0;
|
||||||
|
if (strType == "and" || strType == "or")
|
||||||
|
nKeysNeeded = 2;
|
||||||
|
else if (strType == "escrow")
|
||||||
|
nKeysNeeded = 3;
|
||||||
|
else
|
||||||
|
throw runtime_error("sendmultisig: <type> must be one of: and or and_or");
|
||||||
|
if (keys.size() != nKeysNeeded)
|
||||||
|
throw runtime_error(
|
||||||
|
strprintf("sendmultisig: wrong number of keys (got %d, need %d)", keys.size(), nKeysNeeded));
|
||||||
|
std::vector<CKey> pubkeys;
|
||||||
|
pubkeys.resize(nKeysNeeded);
|
||||||
|
for (int i = 0; i < nKeysNeeded; i++)
|
||||||
|
{
|
||||||
|
const std::string& ks = keys[i].get_str();
|
||||||
|
if (ks.size() == 130) // hex public key
|
||||||
|
pubkeys[i].SetPubKey(ParseHex(ks));
|
||||||
|
else if (ks.size() > 34) // base58-encoded
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> vchPubKey;
|
||||||
|
if (DecodeBase58(ks, vchPubKey))
|
||||||
|
pubkeys[i].SetPubKey(vchPubKey);
|
||||||
|
else
|
||||||
|
throw runtime_error("Error base58 decoding key: "+ks);
|
||||||
|
}
|
||||||
|
else // bitcoin address for key in this wallet
|
||||||
|
{
|
||||||
|
CBitcoinAddress address(ks);
|
||||||
|
if (!pwalletMain->GetKey(address, pubkeys[i]))
|
||||||
|
throw runtime_error(
|
||||||
|
strprintf("sendmultisig: unknown address: %s",ks.c_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send
|
||||||
|
CScript scriptPubKey;
|
||||||
|
if (strType == "and")
|
||||||
|
scriptPubKey.SetMultisigAnd(pubkeys);
|
||||||
|
else if (strType == "or")
|
||||||
|
scriptPubKey.SetMultisigOr(pubkeys);
|
||||||
|
else
|
||||||
|
scriptPubKey.SetMultisigEscrow(pubkeys);
|
||||||
|
|
||||||
|
CReserveKey keyChange(pwalletMain);
|
||||||
|
int64 nFeeRequired = 0;
|
||||||
|
bool fCreated = pwalletMain->CreateTransaction(scriptPubKey, nAmount, wtx, keyChange, nFeeRequired);
|
||||||
|
if (!fCreated)
|
||||||
|
{
|
||||||
|
if (nAmount + nFeeRequired > pwalletMain->GetBalance())
|
||||||
|
throw JSONRPCError(-6, "Insufficient funds");
|
||||||
|
throw JSONRPCError(-4, "Transaction creation failed");
|
||||||
|
}
|
||||||
|
if (!pwalletMain->CommitTransaction(wtx, keyChange))
|
||||||
|
throw JSONRPCError(-4, "Transaction commit failed");
|
||||||
|
|
||||||
|
return wtx.GetHash().GetHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct tallyitem
|
struct tallyitem
|
||||||
{
|
{
|
||||||
|
@ -1596,7 +1691,17 @@ Value validateaddress(const Array& params, bool fHelp)
|
||||||
// version of the address:
|
// version of the address:
|
||||||
string currentAddress = address.ToString();
|
string currentAddress = address.ToString();
|
||||||
ret.push_back(Pair("address", currentAddress));
|
ret.push_back(Pair("address", currentAddress));
|
||||||
ret.push_back(Pair("ismine", (pwalletMain->HaveKey(address) > 0)));
|
if (pwalletMain->HaveKey(address))
|
||||||
|
{
|
||||||
|
ret.push_back(Pair("ismine", true));
|
||||||
|
std::vector<unsigned char> vchPubKey;
|
||||||
|
pwalletMain->GetPubKey(address, vchPubKey);
|
||||||
|
ret.push_back(Pair("pubkey", HexStr(vchPubKey)));
|
||||||
|
std::string strPubKey(vchPubKey.begin(), vchPubKey.end());
|
||||||
|
ret.push_back(Pair("pubkey58", EncodeBase58(vchPubKey)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ret.push_back(Pair("ismine", false));
|
||||||
if (pwalletMain->mapAddressBook.count(address))
|
if (pwalletMain->mapAddressBook.count(address))
|
||||||
ret.push_back(Pair("account", pwalletMain->mapAddressBook[address]));
|
ret.push_back(Pair("account", pwalletMain->mapAddressBook[address]));
|
||||||
}
|
}
|
||||||
|
@ -1841,6 +1946,7 @@ pair<string, rpcfn_type> pCallTable[] =
|
||||||
make_pair("move", &movecmd),
|
make_pair("move", &movecmd),
|
||||||
make_pair("sendfrom", &sendfrom),
|
make_pair("sendfrom", &sendfrom),
|
||||||
make_pair("sendmany", &sendmany),
|
make_pair("sendmany", &sendmany),
|
||||||
|
make_pair("sendmultisig", &sendmultisig),
|
||||||
make_pair("gettransaction", &gettransaction),
|
make_pair("gettransaction", &gettransaction),
|
||||||
make_pair("listtransactions", &listtransactions),
|
make_pair("listtransactions", &listtransactions),
|
||||||
make_pair("signmessage", &signmessage),
|
make_pair("signmessage", &signmessage),
|
||||||
|
@ -2484,6 +2590,16 @@ int CommandLineRPC(int argc, char *argv[])
|
||||||
params[1] = v.get_obj();
|
params[1] = v.get_obj();
|
||||||
}
|
}
|
||||||
if (strMethod == "sendmany" && n > 2) ConvertTo<boost::int64_t>(params[2]);
|
if (strMethod == "sendmany" && n > 2) ConvertTo<boost::int64_t>(params[2]);
|
||||||
|
if (strMethod == "sendmultisig" && n > 2)
|
||||||
|
{
|
||||||
|
string s = params[2].get_str();
|
||||||
|
Value v;
|
||||||
|
if (!read_string(s, v) || v.type() != array_type)
|
||||||
|
throw runtime_error("sendmultisig: type mismatch "+s);
|
||||||
|
params[2] = v.get_array();
|
||||||
|
}
|
||||||
|
if (strMethod == "sendmultisig" && n > 3) ConvertTo<double>(params[3]);
|
||||||
|
if (strMethod == "sendmultisig" && n > 4) ConvertTo<boost::int64_t>(params[4]);
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
Object reply = CallRPC(strMethod, params);
|
Object reply = CallRPC(strMethod, params);
|
||||||
|
|
229
src/script.cpp
229
src/script.cpp
|
@ -963,8 +963,11 @@ bool CheckSig(vector<unsigned char> vchSig, vector<unsigned char> vchPubKey, CSc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSolutionRet)
|
// Returns lists of public keys (or public key hashes), any one of which can
|
||||||
|
// satisfy scriptPubKey
|
||||||
|
//
|
||||||
|
bool Solver(const CScript& scriptPubKey, vector<vector<pair<opcodetype, valtype> > >& vSolutionsRet)
|
||||||
{
|
{
|
||||||
// Templates
|
// Templates
|
||||||
static vector<CScript> vTemplates;
|
static vector<CScript> vTemplates;
|
||||||
|
@ -975,13 +978,24 @@ bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSo
|
||||||
|
|
||||||
// Bitcoin address tx, sender provides hash of pubkey, receiver provides signature and pubkey
|
// Bitcoin address tx, sender provides hash of pubkey, receiver provides signature and pubkey
|
||||||
vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG);
|
vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG);
|
||||||
|
|
||||||
|
// Sender provides two pubkeys, receivers provides two signatures
|
||||||
|
vTemplates.push_back(CScript() << OP_2 << OP_PUBKEY << OP_PUBKEY << OP_2 << OP_CHECKMULTISIG);
|
||||||
|
|
||||||
|
// Sender provides two pubkeys, receivers provides one of two signatures
|
||||||
|
vTemplates.push_back(CScript() << OP_1 << OP_PUBKEY << OP_PUBKEY << OP_2 << OP_CHECKMULTISIG);
|
||||||
|
|
||||||
|
// Sender provides three pubkeys, receiver provides 2 of 3 signatures.
|
||||||
|
vTemplates.push_back(CScript() << OP_2 << OP_PUBKEY << OP_PUBKEY << OP_PUBKEY << OP_3 << OP_CHECKMULTISIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan templates
|
// Scan templates
|
||||||
const CScript& script1 = scriptPubKey;
|
const CScript& script1 = scriptPubKey;
|
||||||
BOOST_FOREACH(const CScript& script2, vTemplates)
|
BOOST_FOREACH(const CScript& script2, vTemplates)
|
||||||
{
|
{
|
||||||
vSolutionRet.clear();
|
vSolutionsRet.clear();
|
||||||
|
|
||||||
|
vector<pair<opcodetype, valtype> > currentSolution;
|
||||||
opcodetype opcode1, opcode2;
|
opcodetype opcode1, opcode2;
|
||||||
vector<unsigned char> vch1, vch2;
|
vector<unsigned char> vch1, vch2;
|
||||||
|
|
||||||
|
@ -992,9 +1006,7 @@ bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSo
|
||||||
{
|
{
|
||||||
if (pc1 == script1.end() && pc2 == script2.end())
|
if (pc1 == script1.end() && pc2 == script2.end())
|
||||||
{
|
{
|
||||||
// Found a match
|
return !vSolutionsRet.empty();
|
||||||
reverse(vSolutionRet.begin(), vSolutionRet.end());
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (!script1.GetOp(pc1, opcode1, vch1))
|
if (!script1.GetOp(pc1, opcode1, vch1))
|
||||||
break;
|
break;
|
||||||
|
@ -1004,13 +1016,54 @@ bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSo
|
||||||
{
|
{
|
||||||
if (vch1.size() < 33 || vch1.size() > 120)
|
if (vch1.size() < 33 || vch1.size() > 120)
|
||||||
break;
|
break;
|
||||||
vSolutionRet.push_back(make_pair(opcode2, vch1));
|
currentSolution.push_back(make_pair(opcode2, vch1));
|
||||||
}
|
}
|
||||||
else if (opcode2 == OP_PUBKEYHASH)
|
else if (opcode2 == OP_PUBKEYHASH)
|
||||||
{
|
{
|
||||||
if (vch1.size() != sizeof(uint160))
|
if (vch1.size() != sizeof(uint160))
|
||||||
break;
|
break;
|
||||||
vSolutionRet.push_back(make_pair(opcode2, vch1));
|
currentSolution.push_back(make_pair(opcode2, vch1));
|
||||||
|
}
|
||||||
|
else if (opcode2 == OP_CHECKSIG)
|
||||||
|
{
|
||||||
|
vSolutionsRet.push_back(currentSolution);
|
||||||
|
currentSolution.clear();
|
||||||
|
}
|
||||||
|
else if (opcode2 == OP_CHECKMULTISIG)
|
||||||
|
{ // Dig out the "m" from before the pubkeys:
|
||||||
|
CScript::const_iterator it = script2.begin();
|
||||||
|
opcodetype op_m;
|
||||||
|
script2.GetOp(it, op_m, vch1);
|
||||||
|
int m = CScript::DecodeOP_N(op_m);
|
||||||
|
int n = currentSolution.size();
|
||||||
|
|
||||||
|
if (m == 2 && n == 2)
|
||||||
|
{
|
||||||
|
vSolutionsRet.push_back(currentSolution);
|
||||||
|
currentSolution.clear();
|
||||||
|
}
|
||||||
|
else if (m == 1 && n == 2)
|
||||||
|
{ // 2 solutions: either first key or second
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
vector<pair<opcodetype, valtype> > s;
|
||||||
|
s.push_back(currentSolution[i]);
|
||||||
|
vSolutionsRet.push_back(s);
|
||||||
|
}
|
||||||
|
currentSolution.clear();
|
||||||
|
}
|
||||||
|
else if (m == 2 && n == 3)
|
||||||
|
{ // 3 solutions: any pair
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
for (int j = i+1; j < 3; j++)
|
||||||
|
{
|
||||||
|
vector<pair<opcodetype, valtype> > s;
|
||||||
|
s.push_back(currentSolution[i]);
|
||||||
|
s.push_back(currentSolution[j]);
|
||||||
|
vSolutionsRet.push_back(s);
|
||||||
|
}
|
||||||
|
currentSolution.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (opcode1 != opcode2 || vch1 != vch2)
|
else if (opcode1 != opcode2 || vch1 != vch2)
|
||||||
{
|
{
|
||||||
|
@ -1019,7 +1072,7 @@ bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vSolutionRet.clear();
|
vSolutionsRet.clear();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1028,51 +1081,61 @@ bool Solver(const CKeyStore& keystore, const CScript& scriptPubKey, uint256 hash
|
||||||
{
|
{
|
||||||
scriptSigRet.clear();
|
scriptSigRet.clear();
|
||||||
|
|
||||||
vector<pair<opcodetype, valtype> > vSolution;
|
vector<vector<pair<opcodetype, valtype> > > vSolutions;
|
||||||
if (!Solver(scriptPubKey, vSolution))
|
if (!Solver(scriptPubKey, vSolutions))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Compile solution
|
// See if we have all the keys for any of the solutions:
|
||||||
BOOST_FOREACH(PAIRTYPE(opcodetype, valtype)& item, vSolution)
|
int whichSolution = -1;
|
||||||
|
for (int i = 0; i < vSolutions.size(); i++)
|
||||||
{
|
{
|
||||||
if (item.first == OP_PUBKEY)
|
int keysFound = 0;
|
||||||
|
CScript scriptSig;
|
||||||
|
|
||||||
|
BOOST_FOREACH(PAIRTYPE(opcodetype, valtype)& item, vSolutions[i])
|
||||||
{
|
{
|
||||||
// Sign
|
if (item.first == OP_PUBKEY)
|
||||||
const valtype& vchPubKey = item.second;
|
|
||||||
CKey key;
|
|
||||||
if (!keystore.GetKey(Hash160(vchPubKey), key))
|
|
||||||
return false;
|
|
||||||
if (key.GetPubKey() != vchPubKey)
|
|
||||||
return false;
|
|
||||||
if (hash != 0)
|
|
||||||
{
|
{
|
||||||
|
const valtype& vchPubKey = item.second;
|
||||||
|
CKey key;
|
||||||
vector<unsigned char> vchSig;
|
vector<unsigned char> vchSig;
|
||||||
if (!key.Sign(hash, vchSig))
|
if (keystore.GetKey(Hash160(vchPubKey), key) && key.GetPubKey() == vchPubKey
|
||||||
return false;
|
&& hash != 0 && key.Sign(hash, vchSig))
|
||||||
vchSig.push_back((unsigned char)nHashType);
|
{
|
||||||
scriptSigRet << vchSig;
|
vchSig.push_back((unsigned char)nHashType);
|
||||||
|
scriptSig << vchSig;
|
||||||
|
++keysFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item.first == OP_PUBKEYHASH)
|
||||||
|
{
|
||||||
|
CKey key;
|
||||||
|
vector<unsigned char> vchSig;
|
||||||
|
if (keystore.GetKey(uint160(item.second), key)
|
||||||
|
&& hash != 0 && key.Sign(hash, vchSig))
|
||||||
|
{
|
||||||
|
vchSig.push_back((unsigned char)nHashType);
|
||||||
|
scriptSig << vchSig << key.GetPubKey();
|
||||||
|
++keysFound;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (item.first == OP_PUBKEYHASH)
|
if (keysFound == vSolutions[i].size())
|
||||||
{
|
{
|
||||||
// Sign and give pubkey
|
whichSolution = i;
|
||||||
CKey key;
|
scriptSigRet = scriptSig;
|
||||||
if (!keystore.GetKey(uint160(item.second), key))
|
break;
|
||||||
return false;
|
|
||||||
if (hash != 0)
|
|
||||||
{
|
|
||||||
vector<unsigned char> vchSig;
|
|
||||||
if (!key.Sign(hash, vchSig))
|
|
||||||
return false;
|
|
||||||
vchSig.push_back((unsigned char)nHashType);
|
|
||||||
scriptSigRet << vchSig << key.GetPubKey();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (whichSolution == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// CHECKMULTISIG bug workaround:
|
||||||
|
if (vSolutions.size() != 1 ||
|
||||||
|
vSolutions[0].size() != 1)
|
||||||
|
{
|
||||||
|
scriptSigRet.insert(scriptSigRet.begin(), OP_0);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1080,51 +1143,59 @@ bool Solver(const CKeyStore& keystore, const CScript& scriptPubKey, uint256 hash
|
||||||
|
|
||||||
bool IsStandard(const CScript& scriptPubKey)
|
bool IsStandard(const CScript& scriptPubKey)
|
||||||
{
|
{
|
||||||
vector<pair<opcodetype, valtype> > vSolution;
|
vector<vector<pair<opcodetype, valtype> > > vSolutions;
|
||||||
return Solver(scriptPubKey, vSolution);
|
return Solver(scriptPubKey, vSolutions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool IsMine(const CKeyStore &keystore, const CScript& scriptPubKey)
|
bool IsMine(const CKeyStore &keystore, const CScript& scriptPubKey)
|
||||||
{
|
{
|
||||||
vector<pair<opcodetype, valtype> > vSolution;
|
vector<vector<pair<opcodetype, valtype> > > vSolutions;
|
||||||
if (!Solver(scriptPubKey, vSolution))
|
if (!Solver(scriptPubKey, vSolutions))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Compile solution
|
int keysFound = 0;
|
||||||
BOOST_FOREACH(PAIRTYPE(opcodetype, valtype)& item, vSolution)
|
int keysRequired = 0;
|
||||||
|
for (int i = 0; i < vSolutions.size(); i++)
|
||||||
{
|
{
|
||||||
if (item.first == OP_PUBKEY)
|
BOOST_FOREACH(PAIRTYPE(opcodetype, valtype)& item, vSolutions[i])
|
||||||
{
|
{
|
||||||
const valtype& vchPubKey = item.second;
|
++keysRequired;
|
||||||
vector<unsigned char> vchPubKeyFound;
|
if (item.first == OP_PUBKEY)
|
||||||
if (!keystore.GetPubKey(Hash160(vchPubKey), vchPubKeyFound))
|
{
|
||||||
return false;
|
const valtype& vchPubKey = item.second;
|
||||||
if (vchPubKeyFound != vchPubKey)
|
vector<unsigned char> vchPubKeyFound;
|
||||||
return false;
|
if (keystore.GetPubKey(Hash160(vchPubKey), vchPubKeyFound) && vchPubKeyFound == vchPubKey)
|
||||||
}
|
++keysFound;
|
||||||
else if (item.first == OP_PUBKEYHASH)
|
}
|
||||||
{
|
else if (item.first == OP_PUBKEYHASH)
|
||||||
if (!keystore.HaveKey(uint160(item.second)))
|
{
|
||||||
return false;
|
if (keystore.HaveKey(uint160(item.second)))
|
||||||
}
|
++keysFound;
|
||||||
else
|
}
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// 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
|
||||||
|
// for shared-wallet situations.
|
||||||
|
return (keysFound == keysRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExtractAddress(const CScript& scriptPubKey, const CKeyStore* keystore, CBitcoinAddress& addressRet)
|
bool ExtractAddress(const CScript& scriptPubKey, const CKeyStore* keystore, CBitcoinAddress& addressRet)
|
||||||
{
|
{
|
||||||
vector<pair<opcodetype, valtype> > vSolution;
|
vector<vector<pair<opcodetype, valtype> > > vSolutions;
|
||||||
if (!Solver(scriptPubKey, vSolution))
|
if (!Solver(scriptPubKey, vSolutions))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
BOOST_FOREACH(PAIRTYPE(opcodetype, valtype)& item, vSolution)
|
for (int i = 0; i < vSolutions.size(); i++)
|
||||||
{
|
{
|
||||||
|
if (vSolutions[i].size() != 1)
|
||||||
|
continue; // Can't return more than one address...
|
||||||
|
|
||||||
|
PAIRTYPE(opcodetype, valtype)& item = vSolutions[i][0];
|
||||||
if (item.first == OP_PUBKEY)
|
if (item.first == OP_PUBKEY)
|
||||||
addressRet.SetPubKey(item.second);
|
addressRet.SetPubKey(item.second);
|
||||||
else if (item.first == OP_PUBKEYHASH)
|
else if (item.first == OP_PUBKEYHASH)
|
||||||
|
@ -1132,7 +1203,6 @@ bool ExtractAddress(const CScript& scriptPubKey, const CKeyStore* keystore, CBit
|
||||||
if (keystore == NULL || keystore->HaveKey(addressRet))
|
if (keystore == NULL || keystore->HaveKey(addressRet))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1192,3 +1262,22 @@ bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsig
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CScript::SetMultisigAnd(const std::vector<CKey>& keys)
|
||||||
|
{
|
||||||
|
assert(keys.size() >= 2);
|
||||||
|
this->clear();
|
||||||
|
*this << OP_2 << keys[0].GetPubKey() << keys[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
}
|
||||||
|
void CScript::SetMultisigOr(const std::vector<CKey>& keys)
|
||||||
|
{
|
||||||
|
assert(keys.size() >= 2);
|
||||||
|
this->clear();
|
||||||
|
*this << OP_1 << keys[0].GetPubKey() << keys[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
}
|
||||||
|
void CScript::SetMultisigEscrow(const std::vector<CKey>& keys)
|
||||||
|
{
|
||||||
|
assert(keys.size() >= 3);
|
||||||
|
this->clear();
|
||||||
|
*this << OP_2 << keys[0].GetPubKey() << keys[1].GetPubKey() << keys[1].GetPubKey() << OP_3 << OP_CHECKMULTISIG;
|
||||||
|
}
|
||||||
|
|
25
src/script.h
25
src/script.h
|
@ -574,6 +574,13 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int DecodeOP_N(opcodetype opcode)
|
||||||
|
{
|
||||||
|
if (opcode == OP_0)
|
||||||
|
return 0;
|
||||||
|
assert(opcode >= OP_1 && opcode <= OP_16);
|
||||||
|
return (int)opcode - (int)(OP_1 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
void FindAndDelete(const CScript& b)
|
void FindAndDelete(const CScript& b)
|
||||||
{
|
{
|
||||||
|
@ -625,21 +632,6 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
CBitcoinAddress GetBitcoinAddress() const
|
|
||||||
{
|
|
||||||
opcodetype opcode;
|
|
||||||
std::vector<unsigned char> vch;
|
|
||||||
CScript::const_iterator pc = begin();
|
|
||||||
if (!GetOp(pc, opcode, vch) || opcode != OP_DUP) return 0;
|
|
||||||
if (!GetOp(pc, opcode, vch) || opcode != OP_HASH160) return 0;
|
|
||||||
if (!GetOp(pc, opcode, vch) || vch.size() != sizeof(uint160)) return 0;
|
|
||||||
uint160 hash160 = uint160(vch);
|
|
||||||
if (!GetOp(pc, opcode, vch) || opcode != OP_EQUALVERIFY) return 0;
|
|
||||||
if (!GetOp(pc, opcode, vch) || opcode != OP_CHECKSIG) return 0;
|
|
||||||
if (pc != end()) return 0;
|
|
||||||
return CBitcoinAddress(hash160);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetBitcoinAddress(const CBitcoinAddress& address)
|
void SetBitcoinAddress(const CBitcoinAddress& address)
|
||||||
{
|
{
|
||||||
this->clear();
|
this->clear();
|
||||||
|
@ -650,6 +642,9 @@ public:
|
||||||
{
|
{
|
||||||
SetBitcoinAddress(CBitcoinAddress(vchPubKey));
|
SetBitcoinAddress(CBitcoinAddress(vchPubKey));
|
||||||
}
|
}
|
||||||
|
void SetMultisigAnd(const std::vector<CKey>& keys);
|
||||||
|
void SetMultisigOr(const std::vector<CKey>& keys);
|
||||||
|
void SetMultisigEscrow(const std::vector<CKey>& keys);
|
||||||
|
|
||||||
|
|
||||||
void PrintHex() const
|
void PrintHex() const
|
||||||
|
|
288
src/test/multisig_tests.cpp
Normal file
288
src/test/multisig_tests.cpp
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
#include <boost/assert.hpp>
|
||||||
|
#include <boost/assign/list_of.hpp>
|
||||||
|
#include <boost/assign/list_inserter.hpp>
|
||||||
|
#include <boost/assign/std/vector.hpp>
|
||||||
|
#include <boost/test/unit_test.hpp>
|
||||||
|
#include <boost/foreach.hpp>
|
||||||
|
#include <boost/tuple/tuple.hpp>
|
||||||
|
|
||||||
|
#include <openssl/ec.h>
|
||||||
|
#include <openssl/err.h>
|
||||||
|
|
||||||
|
#include "keystore.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "script.h"
|
||||||
|
#include "wallet.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace boost::assign;
|
||||||
|
|
||||||
|
typedef vector<unsigned char> valtype;
|
||||||
|
|
||||||
|
extern uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType);
|
||||||
|
extern bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CTransaction& txTo, unsigned int nIn, int nHashType);
|
||||||
|
extern bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType);
|
||||||
|
extern bool Solver(const CScript& scriptPubKey, vector<vector<pair<opcodetype, valtype> > >& vSolutionsRet);
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE(multisig_tests)
|
||||||
|
|
||||||
|
CScript
|
||||||
|
sign_multisig(CScript scriptPubKey, vector<CKey> keys, CTransaction transaction, int whichIn)
|
||||||
|
{
|
||||||
|
uint256 hash = SignatureHash(scriptPubKey, transaction, whichIn, SIGHASH_ALL);
|
||||||
|
|
||||||
|
CScript result;
|
||||||
|
result << OP_0; // CHECKMULTISIG bug workaround
|
||||||
|
BOOST_FOREACH(CKey key, keys)
|
||||||
|
{
|
||||||
|
vector<unsigned char> vchSig;
|
||||||
|
BOOST_CHECK(key.Sign(hash, vchSig));
|
||||||
|
vchSig.push_back((unsigned char)SIGHASH_ALL);
|
||||||
|
result << vchSig;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(multisig_verify)
|
||||||
|
{
|
||||||
|
CKey key[4];
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
key[i].MakeNewKey();
|
||||||
|
|
||||||
|
CScript a_and_b;
|
||||||
|
a_and_b << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CScript a_or_b;
|
||||||
|
a_or_b << OP_1 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CScript escrow;
|
||||||
|
escrow << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << key[2].GetPubKey() << OP_3 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CTransaction txFrom; // Funding transaction
|
||||||
|
txFrom.vout.resize(3);
|
||||||
|
txFrom.vout[0].scriptPubKey = a_and_b;
|
||||||
|
txFrom.vout[1].scriptPubKey = a_or_b;
|
||||||
|
txFrom.vout[2].scriptPubKey = escrow;
|
||||||
|
|
||||||
|
CTransaction txTo[3]; // Spending transaction
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
txTo[i].vin.resize(1);
|
||||||
|
txTo[i].vout.resize(1);
|
||||||
|
txTo[i].vin[0].prevout.n = i;
|
||||||
|
txTo[i].vin[0].prevout.hash = txFrom.GetHash();
|
||||||
|
txTo[i].vout[0].nValue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<CKey> keys;
|
||||||
|
CScript s;
|
||||||
|
|
||||||
|
// Test a AND b:
|
||||||
|
keys.clear();
|
||||||
|
keys += key[0],key[1]; // magic operator+= from boost.assign
|
||||||
|
s = sign_multisig(a_and_b, keys, txTo[0], 0);
|
||||||
|
BOOST_CHECK(VerifyScript(s, a_and_b, txTo[0], 0, 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
keys.clear();
|
||||||
|
keys += key[i];
|
||||||
|
s = sign_multisig(a_and_b, keys, txTo[0], 0);
|
||||||
|
BOOST_CHECK_MESSAGE(!VerifyScript(s, a_and_b, txTo[0], 0, 0), strprintf("a&b 1: %d", i));
|
||||||
|
|
||||||
|
keys.clear();
|
||||||
|
keys += key[1],key[i];
|
||||||
|
s = sign_multisig(a_and_b, keys, txTo[0], 0);
|
||||||
|
BOOST_CHECK_MESSAGE(!VerifyScript(s, a_and_b, txTo[0], 0, 0), strprintf("a&b 2: %d", i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a OR b:
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
keys.clear();
|
||||||
|
keys += key[i];
|
||||||
|
s = sign_multisig(a_or_b, keys, txTo[1], 0);
|
||||||
|
if (i == 0 || i == 1)
|
||||||
|
BOOST_CHECK_MESSAGE(VerifyScript(s, a_or_b, txTo[1], 0, 0), strprintf("a|b: %d", i));
|
||||||
|
else
|
||||||
|
BOOST_CHECK_MESSAGE(!VerifyScript(s, a_or_b, txTo[1], 0, 0), strprintf("a|b: %d", i));
|
||||||
|
}
|
||||||
|
s.clear();
|
||||||
|
s << OP_0 << OP_0;
|
||||||
|
BOOST_CHECK(!VerifyScript(s, a_or_b, txTo[1], 0, 0));
|
||||||
|
s.clear();
|
||||||
|
s << OP_0 << OP_1;
|
||||||
|
BOOST_CHECK(!VerifyScript(s, a_or_b, txTo[1], 0, 0));
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
keys.clear();
|
||||||
|
keys += key[i],key[j];
|
||||||
|
s = sign_multisig(escrow, keys, txTo[2], 0);
|
||||||
|
if (i < j && i < 3 && j < 3)
|
||||||
|
BOOST_CHECK_MESSAGE(VerifyScript(s, escrow, txTo[2], 0, 0), strprintf("escrow 1: %d %d", i, j));
|
||||||
|
else
|
||||||
|
BOOST_CHECK_MESSAGE(!VerifyScript(s, escrow, txTo[2], 0, 0), strprintf("escrow 2: %d %d", i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(multisig_IsStandard)
|
||||||
|
{
|
||||||
|
CKey key[3];
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
key[i].MakeNewKey();
|
||||||
|
|
||||||
|
CScript a_and_b;
|
||||||
|
a_and_b << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(::IsStandard(a_and_b));
|
||||||
|
|
||||||
|
CScript a_or_b;
|
||||||
|
a_or_b << OP_1 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(::IsStandard(a_or_b));
|
||||||
|
|
||||||
|
CScript escrow;
|
||||||
|
escrow << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << key[2].GetPubKey() << OP_3 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(::IsStandard(escrow));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(multisig_Solver1)
|
||||||
|
{
|
||||||
|
// Tests Solver() that returns lists of keys that are
|
||||||
|
// required to satisfy a ScriptPubKey
|
||||||
|
//
|
||||||
|
// Also tests IsMine() and ExtractAddress()
|
||||||
|
//
|
||||||
|
// Note: ExtractAddress for the multisignature transactions
|
||||||
|
// always returns false for this release, even if you have
|
||||||
|
// one key that would satisfy an (a|b) or 2-of-3 keys needed
|
||||||
|
// to spend an escrow transaction.
|
||||||
|
//
|
||||||
|
CBasicKeyStore keystore, emptykeystore;
|
||||||
|
CKey key[3];
|
||||||
|
CBitcoinAddress keyaddr[3];
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
key[i].MakeNewKey();
|
||||||
|
keystore.AddKey(key[i]);
|
||||||
|
keyaddr[i].SetPubKey(key[i].GetPubKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
vector<vector<pair<opcodetype, valtype> > > solutions;
|
||||||
|
CScript s;
|
||||||
|
s << key[0].GetPubKey() << OP_CHECKSIG;
|
||||||
|
BOOST_CHECK(Solver(s, solutions));
|
||||||
|
BOOST_CHECK(solutions.size() == 1);
|
||||||
|
if (solutions.size() == 1)
|
||||||
|
BOOST_CHECK(solutions[0].size() == 1);
|
||||||
|
CBitcoinAddress addr;
|
||||||
|
BOOST_CHECK(ExtractAddress(s, &keystore, addr));
|
||||||
|
BOOST_CHECK(addr == keyaddr[0]);
|
||||||
|
BOOST_CHECK(IsMine(keystore, s));
|
||||||
|
BOOST_CHECK(!IsMine(emptykeystore, s));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
vector<vector<pair<opcodetype, valtype> > > solutions;
|
||||||
|
CScript s;
|
||||||
|
s << OP_DUP << OP_HASH160 << Hash160(key[0].GetPubKey()) << OP_EQUALVERIFY << OP_CHECKSIG;
|
||||||
|
BOOST_CHECK(Solver(s, solutions));
|
||||||
|
BOOST_CHECK(solutions.size() == 1);
|
||||||
|
if (solutions.size() == 1)
|
||||||
|
BOOST_CHECK(solutions[0].size() == 1);
|
||||||
|
CBitcoinAddress addr;
|
||||||
|
BOOST_CHECK(ExtractAddress(s, &keystore, addr));
|
||||||
|
BOOST_CHECK(addr == keyaddr[0]);
|
||||||
|
BOOST_CHECK(IsMine(keystore, s));
|
||||||
|
BOOST_CHECK(!IsMine(emptykeystore, s));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
vector<vector<pair<opcodetype, valtype> > > solutions;
|
||||||
|
CScript s;
|
||||||
|
s << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(Solver(s, solutions));
|
||||||
|
BOOST_CHECK(solutions.size() == 1);
|
||||||
|
if (solutions.size() == 1)
|
||||||
|
BOOST_CHECK(solutions[0].size() == 2);
|
||||||
|
CBitcoinAddress addr;
|
||||||
|
BOOST_CHECK(!ExtractAddress(s, &keystore, addr));
|
||||||
|
BOOST_CHECK(IsMine(keystore, s));
|
||||||
|
BOOST_CHECK(!IsMine(emptykeystore, s));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
vector<vector<pair<opcodetype, valtype> > > solutions;
|
||||||
|
CScript s;
|
||||||
|
s << OP_1 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(Solver(s, solutions));
|
||||||
|
BOOST_CHECK(solutions.size() == 2);
|
||||||
|
if (solutions.size() == 2)
|
||||||
|
{
|
||||||
|
BOOST_CHECK(solutions[0].size() == 1);
|
||||||
|
BOOST_CHECK(solutions[1].size() == 1);
|
||||||
|
}
|
||||||
|
CBitcoinAddress addr;
|
||||||
|
BOOST_CHECK(ExtractAddress(s, &keystore, addr));
|
||||||
|
BOOST_CHECK(addr == keyaddr[0]);
|
||||||
|
BOOST_CHECK(IsMine(keystore, s));
|
||||||
|
BOOST_CHECK(!IsMine(emptykeystore, s));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
vector<vector<pair<opcodetype, valtype> > > solutions;
|
||||||
|
CScript s;
|
||||||
|
s << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << key[2].GetPubKey() << OP_3 << OP_CHECKMULTISIG;
|
||||||
|
BOOST_CHECK(Solver(s, solutions));
|
||||||
|
BOOST_CHECK(solutions.size() == 3);
|
||||||
|
if (solutions.size() == 3)
|
||||||
|
{
|
||||||
|
BOOST_CHECK(solutions[0].size() == 2);
|
||||||
|
BOOST_CHECK(solutions[1].size() == 2);
|
||||||
|
BOOST_CHECK(solutions[2].size() == 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(multisig_Sign)
|
||||||
|
{
|
||||||
|
// Test SignSignature() (and therefore the version of Solver() that signs transactions)
|
||||||
|
CBasicKeyStore keystore;
|
||||||
|
CKey key[4];
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
key[i].MakeNewKey();
|
||||||
|
keystore.AddKey(key[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CScript a_and_b;
|
||||||
|
a_and_b << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CScript a_or_b;
|
||||||
|
a_or_b << OP_1 << key[0].GetPubKey() << key[1].GetPubKey() << OP_2 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CScript escrow;
|
||||||
|
escrow << OP_2 << key[0].GetPubKey() << key[1].GetPubKey() << key[2].GetPubKey() << OP_3 << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
CTransaction txFrom; // Funding transaction
|
||||||
|
txFrom.vout.resize(3);
|
||||||
|
txFrom.vout[0].scriptPubKey = a_and_b;
|
||||||
|
txFrom.vout[1].scriptPubKey = a_or_b;
|
||||||
|
txFrom.vout[2].scriptPubKey = escrow;
|
||||||
|
|
||||||
|
CTransaction txTo[3]; // Spending transaction
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
txTo[i].vin.resize(1);
|
||||||
|
txTo[i].vout.resize(1);
|
||||||
|
txTo[i].vin[0].prevout.n = i;
|
||||||
|
txTo[i].vin[0].prevout.hash = txFrom.GetHash();
|
||||||
|
txTo[i].vout[0].nValue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
BOOST_CHECK_MESSAGE(SignSignature(keystore, txFrom, txTo[i], 0), strprintf("SignSignature %d", i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
|
@ -997,12 +997,11 @@ bool CWallet::CreateTransaction(const vector<pair<CScript, int64> >& vecSend, CW
|
||||||
vector<unsigned char> vchPubKey = reservekey.GetReservedKey();
|
vector<unsigned char> vchPubKey = reservekey.GetReservedKey();
|
||||||
// assert(mapKeys.count(vchPubKey));
|
// assert(mapKeys.count(vchPubKey));
|
||||||
|
|
||||||
// Fill a vout to ourself, using same address type as the payment
|
// Fill a vout to ourself
|
||||||
|
// TODO: pass in scriptChange instead of reservekey so
|
||||||
|
// change transaction isn't always pay-to-bitcoin-address
|
||||||
CScript scriptChange;
|
CScript scriptChange;
|
||||||
if (vecSend[0].first.GetBitcoinAddress().IsValid())
|
scriptChange.SetBitcoinAddress(vchPubKey);
|
||||||
scriptChange.SetBitcoinAddress(vchPubKey);
|
|
||||||
else
|
|
||||||
scriptChange << vchPubKey << OP_CHECKSIG;
|
|
||||||
|
|
||||||
// Insert change txn at random position:
|
// Insert change txn at random position:
|
||||||
vector<CTxOut>::iterator position = wtxNew.vout.begin()+GetRandInt(wtxNew.vout.size());
|
vector<CTxOut>::iterator position = wtxNew.vout.begin()+GetRandInt(wtxNew.vout.size());
|
||||||
|
|
Loading…
Reference in a new issue