diff --git a/src/bitcoinrpc.cpp b/src/bitcoinrpc.cpp index 725037ad..21e37c75 100644 --- a/src/bitcoinrpc.cpp +++ b/src/bitcoinrpc.cpp @@ -234,6 +234,7 @@ static const CRPCCommand vRPCCommands[] = { "sendfrom", &sendfrom, false, false }, { "sendmany", &sendmany, false, false }, { "addmultisigaddress", &addmultisigaddress, false, false }, + { "createmultisig", &createmultisig, true, true }, { "getrawmempool", &getrawmempool, true, false }, { "getblock", &getblock, false, false }, { "getblockhash", &getblockhash, false, false }, @@ -1160,6 +1161,8 @@ Array RPCConvertValues(const std::string &strMethod, const std::vector 2) ConvertTo(params[2]); if (strMethod == "addmultisigaddress" && n > 0) ConvertTo(params[0]); if (strMethod == "addmultisigaddress" && n > 1) ConvertTo(params[1]); + if (strMethod == "createmultisig" && n > 0) ConvertTo(params[0]); + if (strMethod == "createmultisig" && n > 1) ConvertTo(params[1]); if (strMethod == "listunspent" && n > 0) ConvertTo(params[0]); if (strMethod == "listunspent" && n > 1) ConvertTo(params[1]); if (strMethod == "listunspent" && n > 2) ConvertTo(params[2]); diff --git a/src/bitcoinrpc.h b/src/bitcoinrpc.h index 92906976..dc4dc303 100644 --- a/src/bitcoinrpc.h +++ b/src/bitcoinrpc.h @@ -158,6 +158,7 @@ extern json_spirit::Value movecmd(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value sendfrom(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value sendmany(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value addmultisigaddress(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value createmultisig(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value listreceivedbyaddress(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value listreceivedbyaccount(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value listtransactions(const json_spirit::Array& params, bool fHelp); diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index 4714bfdd..e82f4ad9 100644 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -18,6 +18,39 @@ using namespace boost; using namespace boost::assign; using namespace json_spirit; +// +// Utilities: convert hex-encoded Values +// (throws error if not hex). +// +uint256 ParseHashV(const Value& v, string strName) +{ + string strHex; + if (v.type() == str_type) + strHex = v.get_str(); + if (!IsHex(strHex)) // Note: IsHex("") is false + throw JSONRPCError(RPC_INVALID_PARAMETER, strName+" must be hexadecimal string (not '"+strHex+"')"); + uint256 result; + result.SetHex(strHex); + return result; +} +uint256 ParseHashO(const Object& o, string strKey) +{ + return ParseHashV(find_value(o, strKey), strKey); +} +vector ParseHexV(const Value& v, string strName) +{ + string strHex; + if (v.type() == str_type) + strHex = v.get_str(); + if (!IsHex(strHex)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strName+" must be hexadecimal string (not '"+strHex+"')"); + return ParseHex(strHex); +} +vector ParseHexO(const Object& o, string strKey) +{ + return ParseHexV(find_value(o, strKey), strKey); +} + void ScriptPubKeyToJSON(const CScript& scriptPubKey, Object& out) { txnouttype type; @@ -109,8 +142,7 @@ Value getrawtransaction(const Array& params, bool fHelp) "If verbose is non-zero, returns an Object\n" "with information about ."); - uint256 hash; - hash.SetHex(params[0].get_str()); + uint256 hash = ParseHashV(params[0], "parameter 1"); bool fVerbose = false; if (params.size() > 1) @@ -178,10 +210,10 @@ Value listunspent(const Array& params, bool fHelp) if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth) continue; - if(setAddress.size()) + if (setAddress.size()) { CTxDestination address; - if(!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) + if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) continue; if (!setAddress.count(address)) @@ -194,6 +226,17 @@ Value listunspent(const Array& params, bool fHelp) entry.push_back(Pair("txid", out.tx->GetHash().GetHex())); entry.push_back(Pair("vout", out.i)); entry.push_back(Pair("scriptPubKey", HexStr(pk.begin(), pk.end()))); + if (pk.IsPayToScriptHash()) + { + CTxDestination address; + if (ExtractDestination(pk, address)) + { + const CScriptID& hash = boost::get(address); + CScript redeemScript; + if (pwalletMain->GetCScript(hash, redeemScript)) + entry.push_back(Pair("redeemScript", HexStr(redeemScript.begin(), redeemScript.end()))); + } + } entry.push_back(Pair("amount",ValueFromAmount(nValue))); entry.push_back(Pair("confirmations",out.nDepth)); results.push_back(entry); @@ -221,16 +264,11 @@ Value createrawtransaction(const Array& params, bool fHelp) CTransaction rawTx; - BOOST_FOREACH(Value& input, inputs) + BOOST_FOREACH(const Value& input, inputs) { const Object& o = input.get_obj(); - const Value& txid_v = find_value(o, "txid"); - if (txid_v.type() != str_type) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing txid key"); - string txid = txid_v.get_str(); - if (!IsHex(txid)) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected hex txid"); + uint256 txid = ParseHashO(o, "txid"); const Value& vout_v = find_value(o, "vout"); if (vout_v.type() != int_type) @@ -239,7 +277,7 @@ Value createrawtransaction(const Array& params, bool fHelp) if (nOutput < 0) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout must be positive"); - CTxIn in(COutPoint(uint256(txid), nOutput)); + CTxIn in(COutPoint(txid, nOutput)); rawTx.vin.push_back(in); } @@ -274,9 +312,7 @@ Value decoderawtransaction(const Array& params, bool fHelp) "decoderawtransaction \n" "Return a JSON object representing the serialized, hex-encoded transaction."); - RPCTypeCheck(params, list_of(str_type)); - - vector txData(ParseHex(params[0].get_str())); + vector txData(ParseHexV(params[0], "argument")); CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION); CTransaction tx; try { @@ -296,7 +332,7 @@ Value signrawtransaction(const Array& params, bool fHelp) { if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( - "signrawtransaction [{\"txid\":txid,\"vout\":n,\"scriptPubKey\":hex},...] [,...] [sighashtype=\"ALL\"]\n" + "signrawtransaction [{\"txid\":txid,\"vout\":n,\"scriptPubKey\":hex,\"redeemScript\":hex},...] [,...] [sighashtype=\"ALL\"]\n" "Sign inputs for raw transaction (serialized, hex-encoded).\n" "Second optional argument (may be null) is an array of previous transaction outputs that\n" "this transaction depends on but may not yet be in the block chain.\n" @@ -311,7 +347,7 @@ Value signrawtransaction(const Array& params, bool fHelp) RPCTypeCheck(params, list_of(str_type)(array_type)(array_type)(str_type), true); - vector txData(ParseHex(params[0].get_str())); + vector txData(ParseHexV(params[0], "argument 1")); CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION); vector txVariants; while (!ssData.empty()) @@ -352,6 +388,28 @@ Value signrawtransaction(const Array& params, bool fHelp) view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long } + bool fGivenKeys = false; + CBasicKeyStore tempKeystore; + if (params.size() > 2 && params[2].type() != null_type) + { + fGivenKeys = true; + Array keys = params[2].get_array(); + BOOST_FOREACH(Value k, keys) + { + CBitcoinSecret vchSecret; + bool fGood = vchSecret.SetString(k.get_str()); + if (!fGood) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); + CKey key; + bool fCompressed; + CSecret secret = vchSecret.GetSecret(fCompressed); + key.SetSecret(secret, fCompressed); + tempKeystore.AddKey(key); + } + } + else + EnsureWalletIsUnlocked(); + // Add previous txouts given in the RPC call: if (params.size() > 1 && params[1].type() != null_type) { @@ -363,22 +421,15 @@ Value signrawtransaction(const Array& params, bool fHelp) Object prevOut = p.get_obj(); - RPCTypeCheck(prevOut, map_list_of("txid", str_type)("vout", int_type)("scriptPubKey", str_type)); + RPCTypeCheck(prevOut, map_list_of("txid", str_type)("vout", int_type)("scriptPubKey", str_type)("redeemScript",str_type)); - string txidHex = find_value(prevOut, "txid").get_str(); - if (!IsHex(txidHex)) - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "txid must be hexadecimal"); - uint256 txid; - txid.SetHex(txidHex); + uint256 txid = ParseHashO(prevOut, "txid"); int nOut = find_value(prevOut, "vout").get_int(); if (nOut < 0) throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout must be positive"); - string pkHex = find_value(prevOut, "scriptPubKey").get_str(); - if (!IsHex(pkHex)) - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "scriptPubKey must be hexadecimal"); - vector pkData(ParseHex(pkHex)); + vector pkData(ParseHexO(prevOut, "scriptPubKey")); CScript scriptPubKey(pkData.begin(), pkData.end()); CCoins coins; @@ -391,33 +442,23 @@ Value signrawtransaction(const Array& params, bool fHelp) } // what todo if txid is known, but the actual output isn't? } + if ((unsigned int)nOut >= coins.vout.size()) + coins.vout.resize(nOut+1); coins.vout[nOut].scriptPubKey = scriptPubKey; coins.vout[nOut].nValue = 0; // we don't know the actual output value view.SetCoins(txid, coins); - } - } - bool fGivenKeys = false; - CBasicKeyStore tempKeystore; - if (params.size() > 2 && params[2].type() != null_type) - { - fGivenKeys = true; - Array keys = params[2].get_array(); - BOOST_FOREACH(Value k, keys) - { - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(k.get_str()); - if (!fGood) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,"Invalid private key"); - CKey key; - bool fCompressed; - CSecret secret = vchSecret.GetSecret(fCompressed); - key.SetSecret(secret, fCompressed); - tempKeystore.AddKey(key); + // if redeemScript given and not using the local wallet (private keys + // given), add redeemScript to the tempKeystore so it can be signed: + Value v = find_value(prevOut, "redeemScript"); + if (fGivenKeys && scriptPubKey.IsPayToScriptHash() && !(v == Value::null)) + { + vector rsData(ParseHexV(v, "redeemScript")); + CScript redeemScript(rsData.begin(), rsData.end()); + tempKeystore.AddCScript(redeemScript); + } } } - else - EnsureWalletIsUnlocked(); const CKeyStore& keystore = (fGivenKeys ? tempKeystore : *pwalletMain); @@ -484,10 +525,8 @@ Value sendrawtransaction(const Array& params, bool fHelp) "sendrawtransaction \n" "Submits raw transaction (serialized, hex-encoded) to local node and network."); - RPCTypeCheck(params, list_of(str_type)); - // parse hex string from parameter - vector txData(ParseHex(params[0].get_str())); + vector txData(ParseHexV(params[0], "parameter")); CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION); CTransaction tx; diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index 6f8728e9..cd91f650 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -702,22 +702,13 @@ Value sendmany(const Array& params, bool fHelp) return wtx.GetHash().GetHex(); } -Value addmultisigaddress(const Array& params, bool fHelp) +// +// Used by addmultisigaddress / createmultisig: +// +static CScript _createmultisig(const Array& params) { - if (fHelp || params.size() < 2 || params.size() > 3) - { - string msg = "addmultisigaddress <'[\"key\",\"key\"]'> [account]\n" - "Add a nrequired-to-sign multisignature address to the wallet\"\n" - "each key is a Bitcoin address or hex-encoded public key\n" - "If [account] is specified, assign address to [account]."; - throw runtime_error(msg); - } - int nRequired = params[0].get_int(); const Array& keys = params[1].get_array(); - string strAccount; - if (params.size() > 2) - strAccount = AccountFromValue(params[2]); // Gather public keys if (nRequired < 1) @@ -760,10 +751,28 @@ Value addmultisigaddress(const Array& params, bool fHelp) throw runtime_error(" Invalid public key: "+ks); } } + CScript result; + result.SetMultisig(nRequired, pubkeys); + return result; +} + +Value addmultisigaddress(const Array& params, bool fHelp) +{ + if (fHelp || params.size() < 2 || params.size() > 3) + { + string msg = "addmultisigaddress <'[\"key\",\"key\"]'> [account]\n" + "Add a nrequired-to-sign multisignature address to the wallet\"\n" + "each key is a Bitcoin address or hex-encoded public key\n" + "If [account] is specified, assign address to [account]."; + throw runtime_error(msg); + } + + string strAccount; + if (params.size() > 2) + strAccount = AccountFromValue(params[2]); // Construct using pay-to-script-hash: - CScript inner; - inner.SetMultisig(nRequired, pubkeys); + CScript inner = _createmultisig(params); CScriptID innerID = inner.GetID(); pwalletMain->AddCScript(inner); @@ -771,6 +780,30 @@ Value addmultisigaddress(const Array& params, bool fHelp) return CBitcoinAddress(innerID).ToString(); } +Value createmultisig(const Array& params, bool fHelp) +{ + if (fHelp || params.size() < 2 || params.size() > 2) + { + string msg = "createmultisig <'[\"key\",\"key\"]'>\n" + "Creates a multi-signature address and returns a json object\n" + "with keys:\n" + "address : bitcoin address\n" + "redeemScript : hex-encoded redemption script"; + throw runtime_error(msg); + } + + // Construct using pay-to-script-hash: + CScript inner = _createmultisig(params); + CScriptID innerID = inner.GetID(); + CBitcoinAddress address(innerID); + + Object result; + result.push_back(Pair("address", address.ToString())); + result.push_back(Pair("redeemScript", HexStr(inner.begin(), inner.end()))); + + return result; +} + struct tallyitem { diff --git a/src/test/rpc_tests.cpp b/src/test/rpc_tests.cpp index eb820ade..f8fe443b 100644 --- a/src/test/rpc_tests.cpp +++ b/src/test/rpc_tests.cpp @@ -1,5 +1,6 @@ -#include +#include #include +#include #include "base58.h" #include "util.h" @@ -22,14 +23,7 @@ createArgs(int nRequired, const char* address1=NULL, const char* address2=NULL) return result; } -// This can be removed this when addmultisigaddress is enabled on main net: -struct TestNetFixture -{ - TestNetFixture() { fTestNet = true; } - ~TestNetFixture() { fTestNet = false; } -}; - -BOOST_FIXTURE_TEST_CASE(rpc_addmultisig, TestNetFixture) +BOOST_AUTO_TEST_CASE(rpc_addmultisig) { rpcfn_type addmultisig = tableRPC["addmultisigaddress"]->actor; @@ -66,4 +60,91 @@ BOOST_FIXTURE_TEST_CASE(rpc_addmultisig, TestNetFixture) BOOST_CHECK_THROW(addmultisig(createArgs(2, short2.c_str()), false), runtime_error); } +static Value CallRPC(string args) +{ + vector vArgs; + boost::split(vArgs, args, boost::is_any_of(" \t")); + string strMethod = vArgs[0]; + vArgs.erase(vArgs.begin()); + Array params = RPCConvertValues(strMethod, vArgs); + + rpcfn_type method = tableRPC[strMethod]->actor; + try { + Value result = (*method)(params, false); + return result; + } + catch (Object& objError) + { + throw runtime_error(find_value(objError, "message").get_str()); + } +} + +BOOST_AUTO_TEST_CASE(rpc_rawparams) +{ + // Test raw transaction API argument handling + Value r; + + BOOST_CHECK_THROW(CallRPC("getrawtransaction"), runtime_error); + BOOST_CHECK_THROW(CallRPC("getrawtransaction not_hex"), runtime_error); + BOOST_CHECK_THROW(CallRPC("getrawtransaction a3b807410df0b60fcb9736768df5823938b2f838694939ba45f3c0a1bff150ed not_int"), runtime_error); + + BOOST_CHECK_NO_THROW(CallRPC("listunspent")); + BOOST_CHECK_THROW(CallRPC("listunspent string"), runtime_error); + BOOST_CHECK_THROW(CallRPC("listunspent 0 string"), runtime_error); + BOOST_CHECK_THROW(CallRPC("listunspent 0 1 not_array"), runtime_error); + BOOST_CHECK_NO_THROW(r=CallRPC("listunspent 0 1 []")); + BOOST_CHECK_THROW(r=CallRPC("listunspent 0 1 [] extra"), runtime_error); + BOOST_CHECK(r.get_array().empty()); + + BOOST_CHECK_THROW(CallRPC("createrawtransaction"), runtime_error); + BOOST_CHECK_THROW(CallRPC("createrawtransaction null null"), runtime_error); + BOOST_CHECK_THROW(CallRPC("createrawtransaction not_array"), runtime_error); + BOOST_CHECK_THROW(CallRPC("createrawtransaction [] []"), runtime_error); + BOOST_CHECK_THROW(CallRPC("createrawtransaction {} {}"), runtime_error); + BOOST_CHECK_NO_THROW(CallRPC("createrawtransaction [] {}")); + BOOST_CHECK_THROW(CallRPC("createrawtransaction [] {} extra"), runtime_error); + + BOOST_CHECK_THROW(CallRPC("decoderawtransaction"), runtime_error); + BOOST_CHECK_THROW(CallRPC("decoderawtransaction null"), runtime_error); + BOOST_CHECK_THROW(CallRPC("decoderawtransaction DEADBEEF"), runtime_error); + string rawtx = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000"; + BOOST_CHECK_NO_THROW(r = CallRPC(string("decoderawtransaction ")+rawtx)); + BOOST_CHECK_EQUAL(find_value(r.get_obj(), "version").get_int(), 1); + BOOST_CHECK_EQUAL(find_value(r.get_obj(), "locktime").get_int(), 0); + BOOST_CHECK_THROW(r = CallRPC(string("decoderawtransaction ")+rawtx+" extra"), runtime_error); + + BOOST_CHECK_THROW(CallRPC("signrawtransaction"), runtime_error); + BOOST_CHECK_THROW(CallRPC("signrawtransaction null"), runtime_error); + BOOST_CHECK_THROW(CallRPC("signrawtransaction ff00"), runtime_error); + BOOST_CHECK_NO_THROW(CallRPC(string("signrawtransaction ")+rawtx)); + BOOST_CHECK_NO_THROW(CallRPC(string("signrawtransaction ")+rawtx+" null null NONE|ANYONECANPAY")); + BOOST_CHECK_NO_THROW(CallRPC(string("signrawtransaction ")+rawtx+" [] [] NONE|ANYONECANPAY")); + BOOST_CHECK_THROW(CallRPC(string("signrawtransaction ")+rawtx+" null null badenum"), runtime_error); + + // Only check failure cases for sendrawtransaction, there's no network to send to... + BOOST_CHECK_THROW(CallRPC("sendrawtransaction"), runtime_error); + BOOST_CHECK_THROW(CallRPC("sendrawtransaction null"), runtime_error); + BOOST_CHECK_THROW(CallRPC("sendrawtransaction DEADBEEF"), runtime_error); + BOOST_CHECK_THROW(CallRPC(string("sendrawtransaction ")+rawtx+" extra"), runtime_error); +} + +BOOST_AUTO_TEST_CASE(rpc_rawsign) +{ + Value r; + // input is a 1-of-2 multisig (so is output): + string prevout = + "[{\"txid\":\"b4cc287e58f87cdae59417329f710f3ecd75a4ee1d2872b7248f50977c8493f3\"," + "\"vout\":1,\"scriptPubKey\":\"a914b10c9df5f7edf436c697f02f1efdba4cf399615187\"," + "\"redeemScript\":\"512103debedc17b3df2badbcdd86d5feb4562b86fe182e5998abd8bcd4f122c6155b1b21027e940bb73ab8732bfdf7f9216ecefca5b94d6df834e77e108f68e66f126044c052ae\"}]"; + r = CallRPC(string("createrawtransaction ")+prevout+" "+ + "{\"3HqAe9LtNBjnsfM4CyYaWTnvCaUYT7v4oZ\":11}"); + string notsigned = r.get_str(); + string privkey1 = "\"KzsXybp9jX64P5ekX1KUxRQ79Jht9uzW7LorgwE65i5rWACL6LQe\""; + string privkey2 = "\"Kyhdf5LuKTRx4ge69ybABsiUAWjVRK4XGxAKk2FQLp2HjGMy87Z4\""; + r = CallRPC(string("signrawtransaction ")+notsigned+" "+prevout+" "+"[]"); + BOOST_CHECK(find_value(r.get_obj(), "complete").get_bool() == false); + r = CallRPC(string("signrawtransaction ")+notsigned+" "+prevout+" "+"["+privkey1+","+privkey2+"]"); + BOOST_CHECK(find_value(r.get_obj(), "complete").get_bool() == true); +} + BOOST_AUTO_TEST_SUITE_END()