Page MenuHomePhabricator

No OneTemporary

diff --git a/src/univalue/include/univalue.h b/src/univalue/include/univalue.h
index cb15c88a7..f29c77b25 100644
--- a/src/univalue/include/univalue.h
+++ b/src/univalue/include/univalue.h
@@ -1,215 +1,215 @@
// Copyright 2014 BitPay Inc.
// Copyright 2015 Bitcoin Core Developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_UNIVALUE_INCLUDE_UNIVALUE_H
#define BITCOIN_UNIVALUE_INCLUDE_UNIVALUE_H
#include <charconv>
#include <cstdint>
#include <cstring>
#include <map>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>
namespace {
struct UniValueStreamWriter;
}
class UniValue {
friend struct ::UniValueStreamWriter;
public:
enum VType { VNULL, VOBJ, VARR, VSTR, VNUM, VBOOL, };
UniValue() : typ(VNULL) {}
UniValue(UniValue::VType type, const std::string& value = std::string()) : typ(type), val(value) {}
template <typename Ref, typename T = std::remove_cv_t<std::remove_reference_t<Ref>>,
std::enable_if_t<std::is_floating_point_v<T> || // setFloat
std::is_same_v<bool, T> || // setBool
std::is_signed_v<T> || std::is_unsigned_v<T> || // setInt
std::is_constructible_v<std::string, T>, // setStr
bool> = true>
UniValue(Ref&& val)
{
if constexpr (std::is_floating_point_v<T>) {
setFloat(val);
} else if constexpr (std::is_same_v<bool, T>) {
setBool(val);
} else if constexpr (std::is_signed_v<T>) {
setInt(int64_t{val});
} else if constexpr (std::is_unsigned_v<T>) {
setInt(uint64_t{val});
} else {
setStr(std::string{std::forward<Ref>(val)});
}
}
void clear();
void reserve(size_t n) {
if (typ == VOBJ || typ == VARR) {
if (typ == VOBJ)
keys.reserve(n);
values.reserve(n);
} else if (typ != VNULL) {
val.reserve(n);
}
}
bool setNull();
bool setBool(bool val);
bool setNumStr(const std::string& val);
bool setInt(uint64_t val);
bool setInt(int64_t val);
bool setInt(int val_) { return setInt((int64_t)val_); }
bool setFloat(double val);
bool setStr(const std::string& val);
bool setArray();
bool setObject();
enum VType getType() const { return typ; }
const std::string& getValStr() const { return val; }
bool empty() const { return (values.size() == 0); }
size_t size() const { return values.size(); }
bool getBool() const { return isTrue(); }
void getObjMap(std::map<std::string,UniValue>& kv) const;
bool checkObject(const std::map<std::string,UniValue::VType>& memberTypes) const;
const UniValue& operator[](const std::string& key) const;
const UniValue& operator[](size_t index) const;
bool exists(const std::string& key) const { size_t i; return findKey(key, i); }
bool isNull() const { return (typ == VNULL); }
bool isTrue() const { return (typ == VBOOL) && (val == "1"); }
bool isFalse() const { return (typ == VBOOL) && (val != "1"); }
bool isBool() const { return (typ == VBOOL); }
bool isStr() const { return (typ == VSTR); }
bool isNum() const { return (typ == VNUM); }
bool isArray() const { return (typ == VARR); }
bool isObject() const { return (typ == VOBJ); }
void push_back(const UniValue& val);
void push_backV(const std::vector<UniValue>& vec);
template <class It>
void push_backV(It first, It last);
void __pushKV(const std::string& key, const UniValue& val);
void pushKV(const std::string& key, const UniValue& val);
void pushKVs(const UniValue& obj);
std::string write(unsigned int prettyIndent = 0,
unsigned int indentLevel = 0) const;
bool read(const char *raw, size_t len);
bool read(const char *raw) { return read(raw, strlen(raw)); }
bool read(const std::string& rawStr) {
return read(rawStr.data(), rawStr.size());
}
private:
UniValue::VType typ;
std::string val; // numbers are stored as C++ strings
std::vector<std::string> keys;
std::vector<UniValue> values;
+ void checkType(const VType& expected) const;
bool findKey(const std::string& key, size_t& retIdx) const;
public:
// Strict type-specific getters, these throw std::runtime_error if the
// value is of unexpected type
const std::vector<std::string>& getKeys() const;
const std::vector<UniValue>& getValues() const;
template <typename Int>
- auto getInt() const
- {
- static_assert(std::is_integral<Int>::value);
- if (typ != VNUM) {
- throw std::runtime_error("JSON value is not an integer as expected");
- }
- Int result;
- const auto [first_nonmatching, error_condition] = std::from_chars(val.data(), val.data() + val.size(), result);
- if (first_nonmatching != val.data() + val.size() || error_condition != std::errc{}) {
- throw std::runtime_error("JSON integer out of range");
- }
- return result;
- }
+ Int getInt() const;
bool get_bool() const;
const std::string& get_str() const;
double get_real() const;
const UniValue& get_obj() const;
const UniValue& get_array() const;
enum VType type() const { return getType(); }
const UniValue& find_value(std::string_view key) const;
};
template <class It>
void UniValue::push_backV(It first, It last)
{
- if (typ != VARR) {
- throw std::runtime_error{"JSON value is not an array as expected"};
- }
+ checkType(VARR);
values.insert(values.end(), first, last);
}
+template <typename Int>
+Int UniValue::getInt() const
+{
+ static_assert(std::is_integral<Int>::value);
+ checkType(VNUM);
+ Int result;
+ const auto [first_nonmatching, error_condition] = std::from_chars(val.data(), val.data() + val.size(), result);
+ if (first_nonmatching != val.data() + val.size() || error_condition != std::errc{}) {
+ throw std::runtime_error("JSON integer out of range");
+ }
+ return result;
+}
+
enum jtokentype {
JTOK_ERR = -1,
JTOK_NONE = 0, // eof
JTOK_OBJ_OPEN,
JTOK_OBJ_CLOSE,
JTOK_ARR_OPEN,
JTOK_ARR_CLOSE,
JTOK_COLON,
JTOK_COMMA,
JTOK_KW_NULL,
JTOK_KW_TRUE,
JTOK_KW_FALSE,
JTOK_NUMBER,
JTOK_STRING,
};
extern enum jtokentype getJsonToken(std::string& tokenVal,
unsigned int& consumed, const char *raw, const char *end);
extern const char *uvTypeName(UniValue::VType t);
static inline bool jsonTokenIsValue(enum jtokentype jtt)
{
switch (jtt) {
case JTOK_KW_NULL:
case JTOK_KW_TRUE:
case JTOK_KW_FALSE:
case JTOK_NUMBER:
case JTOK_STRING:
return true;
default:
return false;
}
// not reached
}
static inline bool json_isspace(int ch)
{
switch (ch) {
case 0x20:
case 0x09:
case 0x0a:
case 0x0d:
return true;
default:
return false;
}
// not reached
}
extern const UniValue NullUniValue;
#endif // BITCOIN_UNIVALUE_INCLUDE_UNIVALUE_H
diff --git a/src/univalue/lib/univalue.cpp b/src/univalue/lib/univalue.cpp
index 256cad10e..6218296e3 100644
--- a/src/univalue/lib/univalue.cpp
+++ b/src/univalue/lib/univalue.cpp
@@ -1,240 +1,249 @@
// Copyright 2014 BitPay Inc.
// Copyright 2015 Bitcoin Core Developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://opensource.org/licenses/mit-license.php.
#include <univalue.h>
#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
const UniValue NullUniValue;
void UniValue::clear()
{
typ = VNULL;
val.clear();
keys.clear();
values.clear();
}
bool UniValue::setNull()
{
clear();
return true;
}
bool UniValue::setBool(bool val_)
{
clear();
typ = VBOOL;
if (val_)
val = "1";
return true;
}
static bool validNumStr(const std::string& s)
{
std::string tokenVal;
unsigned int consumed;
enum jtokentype tt = getJsonToken(tokenVal, consumed, s.data(), s.data() + s.size());
return (tt == JTOK_NUMBER);
}
bool UniValue::setNumStr(const std::string& val_)
{
if (!validNumStr(val_))
return false;
clear();
typ = VNUM;
val = val_;
return true;
}
bool UniValue::setInt(uint64_t val_)
{
std::ostringstream oss;
oss << val_;
return setNumStr(oss.str());
}
bool UniValue::setInt(int64_t val_)
{
std::ostringstream oss;
oss << val_;
return setNumStr(oss.str());
}
bool UniValue::setFloat(double val_)
{
std::ostringstream oss;
oss << std::setprecision(16) << val_;
bool ret = setNumStr(oss.str());
typ = VNUM;
return ret;
}
bool UniValue::setStr(const std::string& val_)
{
clear();
typ = VSTR;
val = val_;
return true;
}
bool UniValue::setArray()
{
clear();
typ = VARR;
return true;
}
bool UniValue::setObject()
{
clear();
typ = VOBJ;
return true;
}
void UniValue::push_back(const UniValue& val_)
{
- if (typ != VARR) throw std::runtime_error{"JSON value is not an array as expected"};
+ checkType(VARR);
values.push_back(val_);
}
void UniValue::push_backV(const std::vector<UniValue>& vec)
{
- if (typ != VARR) throw std::runtime_error{"JSON value is not an array as expected"};
+ checkType(VARR);
values.insert(values.end(), vec.begin(), vec.end());
}
void UniValue::__pushKV(const std::string& key, const UniValue& val_)
{
- if (typ != VOBJ) throw std::runtime_error{"JSON value is not an object as expected"};
+ checkType(VOBJ);
keys.push_back(key);
values.push_back(val_);
}
void UniValue::pushKV(const std::string& key, const UniValue& val_)
{
- if (typ != VOBJ) throw std::runtime_error{"JSON value is not an object as expected"};
+ checkType(VOBJ);
size_t idx;
if (findKey(key, idx))
values[idx] = val_;
else
__pushKV(key, val_);
}
void UniValue::pushKVs(const UniValue& obj)
{
- if (typ != VOBJ || obj.typ != VOBJ) throw std::runtime_error{"JSON value is not an object as expected"};
+ checkType(VOBJ);
+ obj.checkType(VOBJ);
for (size_t i = 0; i < obj.keys.size(); i++)
__pushKV(obj.keys[i], obj.values.at(i));
}
void UniValue::getObjMap(std::map<std::string,UniValue>& kv) const
{
if (typ != VOBJ)
return;
kv.clear();
for (size_t i = 0; i < keys.size(); i++)
kv[keys[i]] = values[i];
}
bool UniValue::findKey(const std::string& key, size_t& retIdx) const
{
for (size_t i = 0; i < keys.size(); i++) {
if (keys[i] == key) {
retIdx = i;
return true;
}
}
return false;
}
bool UniValue::checkObject(const std::map<std::string,UniValue::VType>& t) const
{
if (typ != VOBJ) {
return false;
}
for (const auto& object: t) {
size_t idx = 0;
if (!findKey(object.first, idx)) {
return false;
}
if (values.at(idx).getType() != object.second) {
return false;
}
}
return true;
}
const UniValue& UniValue::operator[](const std::string& key) const
{
if (typ != VOBJ)
return NullUniValue;
size_t index = 0;
if (!findKey(key, index))
return NullUniValue;
return values.at(index);
}
const UniValue& UniValue::operator[](size_t index) const
{
if (typ != VOBJ && typ != VARR)
return NullUniValue;
if (index >= values.size())
return NullUniValue;
return values.at(index);
}
+void UniValue::checkType(const VType& expected) const
+{
+ if (typ != expected) {
+ throw std::runtime_error{"JSON value of type " + std::string{uvTypeName(typ)} + " is not of expected type " +
+ std::string{uvTypeName(expected)}};
+ }
+}
+
const char *uvTypeName(UniValue::VType t)
{
switch (t) {
case UniValue::VNULL: return "null";
case UniValue::VBOOL: return "bool";
case UniValue::VOBJ: return "object";
case UniValue::VARR: return "array";
case UniValue::VSTR: return "string";
case UniValue::VNUM: return "number";
}
// not reached
return nullptr;
}
const UniValue& UniValue::find_value(std::string_view key) const
{
for (unsigned int i = 0; i < keys.size(); ++i) {
if (keys[i] == key) {
return values.at(i);
}
}
return NullUniValue;
}
diff --git a/src/univalue/lib/univalue_get.cpp b/src/univalue/lib/univalue_get.cpp
index 9bbdb1fe6..5c58f388d 100644
--- a/src/univalue/lib/univalue_get.cpp
+++ b/src/univalue/lib/univalue_get.cpp
@@ -1,98 +1,91 @@
// Copyright 2014 BitPay Inc.
// Copyright 2015 Bitcoin Core Developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://opensource.org/licenses/mit-license.php.
#include <univalue.h>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <locale>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace
{
static bool ParsePrechecks(const std::string& str)
{
if (str.empty()) // No empty string allowed
return false;
if (str.size() >= 1 && (json_isspace(str[0]) || json_isspace(str[str.size()-1]))) // No padding allowed
return false;
if (str.size() != strlen(str.c_str())) // No embedded NUL characters allowed
return false;
return true;
}
bool ParseDouble(const std::string& str, double *out)
{
if (!ParsePrechecks(str))
return false;
if (str.size() >= 2 && str[0] == '0' && str[1] == 'x') // No hexadecimal floats allowed
return false;
std::istringstream text(str);
text.imbue(std::locale::classic());
double result;
text >> result;
if(out) *out = result;
return text.eof() && !text.fail();
}
}
const std::vector<std::string>& UniValue::getKeys() const
{
- if (typ != VOBJ)
- throw std::runtime_error("JSON value is not an object as expected");
+ checkType(VOBJ);
return keys;
}
const std::vector<UniValue>& UniValue::getValues() const
{
if (typ != VOBJ && typ != VARR)
throw std::runtime_error("JSON value is not an object or array as expected");
return values;
}
bool UniValue::get_bool() const
{
- if (typ != VBOOL)
- throw std::runtime_error("JSON value is not a boolean as expected");
+ checkType(VBOOL);
return getBool();
}
const std::string& UniValue::get_str() const
{
- if (typ != VSTR)
- throw std::runtime_error("JSON value is not a string as expected");
+ checkType(VSTR);
return getValStr();
}
double UniValue::get_real() const
{
- if (typ != VNUM)
- throw std::runtime_error("JSON value is not a number as expected");
+ checkType(VNUM);
double retval;
if (!ParseDouble(getValStr(), &retval))
throw std::runtime_error("JSON double out of range");
return retval;
}
const UniValue& UniValue::get_obj() const
{
- if (typ != VOBJ)
- throw std::runtime_error("JSON value is not an object as expected");
+ checkType(VOBJ);
return *this;
}
const UniValue& UniValue::get_array() const
{
- if (typ != VARR)
- throw std::runtime_error("JSON value is not an array as expected");
+ checkType(VARR);
return *this;
}
-
diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py
index 2ebe4ac34..fae8f83d5 100644
--- a/test/functional/rpc_rawtransaction.py
+++ b/test/functional/rpc_rawtransaction.py
@@ -1,952 +1,956 @@
# Copyright (c) 2014-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the rawtranscation RPCs.
Test the following RPCs:
- createrawtransaction
- signrawtransactionwithwallet
- sendrawtransaction
- decoderawtransaction
- getrawtransaction
"""
from collections import OrderedDict
from decimal import Decimal
from io import BytesIO
from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut, ToHex
from test_framework.script import CScript
from test_framework.test_framework import BitcoinTestFramework
from test_framework.txtools import pad_raw_tx
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_raises_rpc_error,
find_vout_for_address,
)
class multidict(dict):
"""Dictionary that allows duplicate keys.
Constructed with a list of (key, value) tuples. When dumped by the json module,
will output invalid json with repeated keys, eg:
>>> json.dumps(multidict([(1,2),(1,2)])
'{"1": 2, "1": 2}'
Used to test calls to rpc methods with repeated keys in the json object."""
def __init__(self, x):
dict.__init__(self, x)
self.x = x
def items(self):
return self.x
# Create one-input, one-output, no-fee transaction:
class RawTransactionsTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 3
self.extra_args = [["-txindex"], ["-txindex"], ["-txindex"]]
# whitelist all peers to speed up tx relay / mempool sync
for args in self.extra_args:
args.append("-whitelist=noban@127.0.0.1")
self.supports_cli = False
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def setup_network(self):
super().setup_network()
self.connect_nodes(0, 2)
def run_test(self):
self.log.info(
"Test getrawtransaction on genesis block coinbase returns an error"
)
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
assert_raises_rpc_error(
-5,
"The genesis block coinbase is not considered an ordinary transaction",
self.nodes[0].getrawtransaction,
block["merkleroot"],
)
self.log.info(
"Check parameter types and required parameters of createrawtransaction"
)
# Test `createrawtransaction` required parameters
assert_raises_rpc_error(
-1, "createrawtransaction", self.nodes[0].createrawtransaction
)
assert_raises_rpc_error(
-1, "createrawtransaction", self.nodes[0].createrawtransaction, []
)
# Test `createrawtransaction` invalid extra parameters
assert_raises_rpc_error(
-1,
"createrawtransaction",
self.nodes[0].createrawtransaction,
[],
{},
0,
"foo",
)
# Test `createrawtransaction` invalid `inputs`
txid = "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000"
assert_raises_rpc_error(
-3, "Expected type array", self.nodes[0].createrawtransaction, "foo", {}
)
assert_raises_rpc_error(
-1,
- "JSON value is not an object as expected",
+ "JSON value of type string is not of expected type object",
self.nodes[0].createrawtransaction,
["foo"],
{},
)
assert_raises_rpc_error(
-1,
- "JSON value is not a string as expected",
+ "JSON value of type null is not of expected type string",
self.nodes[0].createrawtransaction,
[{}],
{},
)
assert_raises_rpc_error(
-8,
"txid must be of length 64 (not 3, for 'foo')",
self.nodes[0].createrawtransaction,
[{"txid": "foo"}],
{},
)
assert_raises_rpc_error(
-8,
"txid must be hexadecimal string (not"
" 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844')",
self.nodes[0].createrawtransaction,
[
{
"txid": "ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844"
}
],
{},
)
assert_raises_rpc_error(
-8,
"Invalid parameter, missing vout key",
self.nodes[0].createrawtransaction,
[{"txid": txid}],
{},
)
assert_raises_rpc_error(
-8,
"Invalid parameter, vout must be a number",
self.nodes[0].createrawtransaction,
[{"txid": txid, "vout": "foo"}],
{},
)
assert_raises_rpc_error(
-8,
"Invalid parameter, vout cannot be negative",
self.nodes[0].createrawtransaction,
[{"txid": txid, "vout": -1}],
{},
)
assert_raises_rpc_error(
-8,
"Invalid parameter, sequence number is out of range",
self.nodes[0].createrawtransaction,
[{"txid": txid, "vout": 0, "sequence": -1}],
{},
)
# Test `createrawtransaction` invalid `outputs`
address = self.nodes[0].getnewaddress()
address2 = self.nodes[0].getnewaddress()
assert_raises_rpc_error(
-1,
- "JSON value is not an array as expected",
+ "JSON value of type string is not of expected type array",
self.nodes[0].createrawtransaction,
[],
"foo",
)
# Should not throw for backwards compatibility
self.nodes[0].createrawtransaction(inputs=[], outputs={})
self.nodes[0].createrawtransaction(inputs=[], outputs=[])
assert_raises_rpc_error(
-8,
"Data must be hexadecimal string",
self.nodes[0].createrawtransaction,
[],
{"data": "foo"},
)
assert_raises_rpc_error(
-5,
"Invalid Bitcoin address",
self.nodes[0].createrawtransaction,
[],
{"foo": 0},
)
assert_raises_rpc_error(
-3,
"Invalid amount",
self.nodes[0].createrawtransaction,
[],
{address: "foo"},
)
assert_raises_rpc_error(
-3,
"Amount out of range",
self.nodes[0].createrawtransaction,
[],
{address: -1},
)
assert_raises_rpc_error(
-8,
f"Invalid parameter, duplicated address: {address}",
self.nodes[0].createrawtransaction,
[],
multidict([(address, 1), (address, 1)]),
)
assert_raises_rpc_error(
-8,
f"Invalid parameter, duplicated address: {address}",
self.nodes[0].createrawtransaction,
[],
[{address: 1}, {address: 1}],
)
assert_raises_rpc_error(
-8,
"Invalid parameter, duplicate key: data",
self.nodes[0].createrawtransaction,
[],
[{"data": "aa"}, {"data": "bb"}],
)
assert_raises_rpc_error(
-8,
"Invalid parameter, duplicate key: data",
self.nodes[0].createrawtransaction,
[],
multidict([("data", "aa"), ("data", "bb")]),
)
assert_raises_rpc_error(
-8,
"Invalid parameter, key-value pair must contain exactly one key",
self.nodes[0].createrawtransaction,
[],
[{"a": 1, "b": 2}],
)
assert_raises_rpc_error(
-8,
"Invalid parameter, key-value pair not an object as expected",
self.nodes[0].createrawtransaction,
[],
[["key-value pair1"], ["2"]],
)
# Test `createrawtransaction` invalid `locktime`
assert_raises_rpc_error(
-3,
"Expected type number",
self.nodes[0].createrawtransaction,
[],
{},
"foo",
)
assert_raises_rpc_error(
-8,
"Invalid parameter, locktime out of range",
self.nodes[0].createrawtransaction,
[],
{},
-1,
)
assert_raises_rpc_error(
-8,
"Invalid parameter, locktime out of range",
self.nodes[0].createrawtransaction,
[],
{},
4294967296,
)
self.log.info(
"Check that createrawtransaction accepts an array and object as outputs"
)
tx = CTransaction()
# One output
tx.deserialize(
BytesIO(
bytes.fromhex(
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}], outputs={address: 99}
)
)
)
)
assert_equal(len(tx.vout), 1)
assert_equal(
tx.serialize().hex(),
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}], outputs=[{address: 99}]
),
)
# Two outputs
tx.deserialize(
BytesIO(
bytes.fromhex(
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}],
outputs=OrderedDict([(address, 99), (address2, 99)]),
)
)
)
)
assert_equal(len(tx.vout), 2)
assert_equal(
tx.serialize().hex(),
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}],
outputs=[{address: 99}, {address2: 99}],
),
)
# Multiple mixed outputs
tx.deserialize(
BytesIO(
bytes.fromhex(
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}],
outputs=multidict(
[(address, 99), (address2, 99), ("data", "99")]
),
)
)
)
)
assert_equal(len(tx.vout), 3)
assert_equal(
tx.serialize().hex(),
self.nodes[2].createrawtransaction(
inputs=[{"txid": txid, "vout": 9}],
outputs=[{address: 99}, {address2: 99}, {"data": "99"}],
),
)
for addr_type in ["legacy"]:
addr = self.nodes[0].getnewaddress("", addr_type)
addrinfo = self.nodes[0].getaddressinfo(addr)
pubkey = addrinfo["scriptPubKey"]
self.log.info(f"sendrawtransaction with missing prevtx info ({addr_type})")
# Test `signrawtransactionwithwallet` invalid `prevtxs`
inputs = [{"txid": txid, "vout": 3, "sequence": 1000}]
outputs = {self.nodes[0].getnewaddress(): 1}
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
prevtx = {"txid": txid, "scriptPubKey": pubkey, "vout": 3, "amount": 1}
succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx])
assert succ["complete"]
assert_raises_rpc_error(
-8,
"Missing amount",
self.nodes[0].signrawtransactionwithwallet,
rawtx,
[
{
"txid": txid,
"scriptPubKey": pubkey,
"vout": 3,
}
],
)
assert_raises_rpc_error(
-3,
"Missing vout",
self.nodes[0].signrawtransactionwithwallet,
rawtx,
[
{
"txid": txid,
"scriptPubKey": pubkey,
"amount": 1,
}
],
)
assert_raises_rpc_error(
-3,
"Missing txid",
self.nodes[0].signrawtransactionwithwallet,
rawtx,
[
{
"scriptPubKey": pubkey,
"vout": 3,
"amount": 1,
}
],
)
assert_raises_rpc_error(
-3,
"Missing scriptPubKey",
self.nodes[0].signrawtransactionwithwallet,
rawtx,
[{"txid": txid, "vout": 3, "amount": 1}],
)
#########################################
# sendrawtransaction with missing input #
#########################################
self.log.info("sendrawtransaction with missing input")
# won't exists
inputs = [
{
"txid": (
"1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000"
),
"vout": 1,
}
]
outputs = {self.nodes[0].getnewaddress(): 4998000}
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
rawtx = pad_raw_tx(rawtx)
rawtx = self.nodes[2].signrawtransactionwithwallet(rawtx)
# This will raise an exception since there are missing inputs
assert_raises_rpc_error(
-25,
"bad-txns-inputs-missingorspent",
self.nodes[2].sendrawtransaction,
rawtx["hex"],
)
#####################################
# getrawtransaction with block hash #
#####################################
# make a tx by sending then generate 2 blocks; block1 has the tx in it
tx = self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), 1000000)
block1, block2 = self.generate(self.nodes[2], 2)
self.sync_all()
# We should be able to get the raw transaction by providing the correct
# block
gottx = self.nodes[0].getrawtransaction(tx, True, block1)
assert_equal(gottx["txid"], tx)
assert_equal(gottx["in_active_chain"], True)
# We should not have the 'in_active_chain' flag when we don't provide a
# block
gottx = self.nodes[0].getrawtransaction(tx, True)
assert_equal(gottx["txid"], tx)
assert "in_active_chain" not in gottx
# We should not get the tx if we provide an unrelated block
assert_raises_rpc_error(
-5,
"No such transaction found",
self.nodes[0].getrawtransaction,
tx,
True,
block2,
)
# An invalid block hash should raise the correct errors
assert_raises_rpc_error(
-3,
"Expected type string, got bool",
self.nodes[0].getrawtransaction,
tx,
True,
True,
)
assert_raises_rpc_error(
-8,
"parameter 3 must be of length 64 (not 6, for 'foobar')",
self.nodes[0].getrawtransaction,
tx,
True,
"foobar",
)
assert_raises_rpc_error(
-8,
"parameter 3 must be of length 64 (not 8, for 'abcd1234')",
self.nodes[0].getrawtransaction,
tx,
True,
"abcd1234",
)
assert_raises_rpc_error(
-8,
"parameter 3 must be hexadecimal string (not"
" 'ZZZ0000000000000000000000000000000000000000000000000000000000000')",
self.nodes[0].getrawtransaction,
tx,
True,
"ZZZ0000000000000000000000000000000000000000000000000000000000000",
)
assert_raises_rpc_error(
-5,
"Block hash not found",
self.nodes[0].getrawtransaction,
tx,
True,
"0000000000000000000000000000000000000000000000000000000000000000",
)
# Undo the blocks and check in_active_chain
self.nodes[0].invalidateblock(block1)
gottx = self.nodes[0].getrawtransaction(txid=tx, verbose=True, blockhash=block1)
assert_equal(gottx["in_active_chain"], False)
self.nodes[0].reconsiderblock(block1)
assert_equal(self.nodes[0].getbestblockhash(), block2)
if not self.options.descriptors:
# The traditional multisig workflow does not work with descriptor
# wallets so these are legacy only.
# The multisig workflow with descriptor wallets uses PSBTs and is
# tested elsewhere, no need to do them here.
#
# RAW TX MULTISIG TESTS #
#
# 2of2 test
addr1 = self.nodes[2].getnewaddress()
addr2 = self.nodes[2].getnewaddress()
addr1Obj = self.nodes[2].getaddressinfo(addr1)
addr2Obj = self.nodes[2].getaddressinfo(addr2)
# Tests for createmultisig and addmultisigaddress
assert_raises_rpc_error(
-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]
)
# createmultisig can only take public keys
self.nodes[0].createmultisig(2, [addr1Obj["pubkey"], addr2Obj["pubkey"]])
# addmultisigaddress can take both pubkeys and addresses so long as
# they are in the wallet, which is tested here.
assert_raises_rpc_error(
-5,
"Invalid public key",
self.nodes[0].createmultisig,
2,
[addr1Obj["pubkey"], addr1],
)
mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj["pubkey"], addr1])[
"address"
]
# use balance deltas instead of absolute values
bal = self.nodes[2].getbalance()
# send 1,200,000 XEC to msig adr
txId = self.nodes[0].sendtoaddress(mSigObj, 1200000)
self.sync_all()
self.generate(self.nodes[0], 1)
self.sync_all()
# node2 has both keys of the 2of2 ms addr., tx should affect the
# balance
assert_equal(self.nodes[2].getbalance(), bal + Decimal("1200000.00"))
# 2of3 test from different nodes
bal = self.nodes[2].getbalance()
addr1 = self.nodes[1].getnewaddress()
addr2 = self.nodes[2].getnewaddress()
addr3 = self.nodes[2].getnewaddress()
addr1Obj = self.nodes[1].getaddressinfo(addr1)
addr2Obj = self.nodes[2].getaddressinfo(addr2)
addr3Obj = self.nodes[2].getaddressinfo(addr3)
mSigObj = self.nodes[2].addmultisigaddress(
2, [addr1Obj["pubkey"], addr2Obj["pubkey"], addr3Obj["pubkey"]]
)["address"]
txId = self.nodes[0].sendtoaddress(mSigObj, 2200000)
decTx = self.nodes[0].gettransaction(txId)
rawTx = self.nodes[0].decoderawtransaction(decTx["hex"])
self.sync_all()
self.generate(self.nodes[0], 1)
self.sync_all()
# THIS IS AN INCOMPLETE FEATURE
# NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND
# COUNT AT BALANCE CALCULATION
# for now, assume the funds of a 2of3 multisig tx are not marked as
# spendable
assert_equal(self.nodes[2].getbalance(), bal)
txDetails = self.nodes[0].gettransaction(txId, True)
rawTx = self.nodes[0].decoderawtransaction(txDetails["hex"])
vout = next(o for o in rawTx["vout"] if o["value"] == Decimal("2200000.00"))
bal = self.nodes[0].getbalance()
inputs = [
{
"txid": txId,
"vout": vout["n"],
"scriptPubKey": vout["scriptPubKey"]["hex"],
"amount": vout["value"],
}
]
outputs = {self.nodes[0].getnewaddress(): 2190000}
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(
rawTx, inputs
)
# node1 only has one key, can't comp. sign the tx
assert_equal(rawTxPartialSigned["complete"], False)
rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs)
# node2 can sign the tx compl., own two of three keys
assert_equal(rawTxSigned["complete"], True)
self.nodes[2].sendrawtransaction(rawTxSigned["hex"])
rawTx = self.nodes[0].decoderawtransaction(rawTxSigned["hex"])
self.sync_all()
self.generate(self.nodes[0], 1)
self.sync_all()
assert_equal(
self.nodes[0].getbalance(),
bal + Decimal("50000000.00") + Decimal("2190000.00"),
) # block reward + tx
rawTxBlock = self.nodes[0].getblock(self.nodes[0].getbestblockhash())
# 2of2 test for combining transactions
bal = self.nodes[2].getbalance()
addr1 = self.nodes[1].getnewaddress()
addr2 = self.nodes[2].getnewaddress()
addr1Obj = self.nodes[1].getaddressinfo(addr1)
addr2Obj = self.nodes[2].getaddressinfo(addr2)
self.nodes[1].addmultisigaddress(
2, [addr1Obj["pubkey"], addr2Obj["pubkey"]]
)["address"]
mSigObj = self.nodes[2].addmultisigaddress(
2, [addr1Obj["pubkey"], addr2Obj["pubkey"]]
)["address"]
mSigObjValid = self.nodes[2].getaddressinfo(mSigObj)
txId = self.nodes[0].sendtoaddress(mSigObj, 2200000)
decTx = self.nodes[0].gettransaction(txId)
rawTx2 = self.nodes[0].decoderawtransaction(decTx["hex"])
self.sync_all()
self.generate(self.nodes[0], 1)
self.sync_all()
# the funds of a 2of2 multisig tx should not be marked as spendable
assert_equal(self.nodes[2].getbalance(), bal)
txDetails = self.nodes[0].gettransaction(txId, True)
rawTx2 = self.nodes[0].decoderawtransaction(txDetails["hex"])
vout = next(
o for o in rawTx2["vout"] if o["value"] == Decimal("2200000.00")
)
bal = self.nodes[0].getbalance()
inputs = [
{
"txid": txId,
"vout": vout["n"],
"scriptPubKey": vout["scriptPubKey"]["hex"],
"redeemScript": mSigObjValid["hex"],
"amount": vout["value"],
}
]
outputs = {self.nodes[0].getnewaddress(): 2190000}
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(
rawTx2, inputs
)
self.log.debug(rawTxPartialSigned1)
# node1 only has one key, can't comp. sign the tx
assert_equal(rawTxPartialSigned1["complete"], False)
rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(
rawTx2, inputs
)
self.log.debug(rawTxPartialSigned2)
# node2 only has one key, can't comp. sign the tx
assert_equal(rawTxPartialSigned2["complete"], False)
rawTxComb = self.nodes[2].combinerawtransaction(
[rawTxPartialSigned1["hex"], rawTxPartialSigned2["hex"]]
)
self.log.debug(rawTxComb)
self.nodes[2].sendrawtransaction(rawTxComb)
rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb)
self.sync_all()
self.generate(self.nodes[0], 1)
self.sync_all()
# block reward + tx
assert_equal(
self.nodes[0].getbalance(),
bal + Decimal("50000000.00") + Decimal("2190000.00"),
)
# Sanity checks on verbose getrawtransaction output
txId = rawTx["txid"]
rawTxOutput = self.nodes[0].getrawtransaction(txId, True)
assert_equal(rawTxOutput["hex"], rawTxSigned["hex"])
assert_equal(rawTxOutput["txid"], txId)
assert_equal(rawTxOutput["hash"], txId)
assert_greater_than(rawTxOutput["size"], 300)
assert_equal(rawTxOutput["version"], 0x02)
assert_equal(rawTxOutput["locktime"], 0)
assert_equal(len(rawTxOutput["vin"]), 1)
assert_equal(len(rawTxOutput["vout"]), 1)
assert_equal(rawTxOutput["blockhash"], rawTxBlock["hash"])
assert_equal(rawTxOutput["confirmations"], 3)
assert_equal(rawTxOutput["time"], rawTxBlock["time"])
assert_equal(rawTxOutput["blocktime"], rawTxBlock["time"])
# Basic signrawtransaction test
addr = self.nodes[1].getnewaddress()
txid = self.nodes[0].sendtoaddress(addr, 10_000_000)
self.generate(self.nodes[0], 1)
self.sync_all()
vout = find_vout_for_address(self.nodes[1], txid, addr)
rawTx = self.nodes[1].createrawtransaction(
[{"txid": txid, "vout": vout}], {self.nodes[1].getnewaddress(): 9_999_000}
)
rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx)
txId = self.nodes[1].sendrawtransaction(rawTxSigned["hex"])
self.generate(self.nodes[0], 1)
self.sync_all()
# getrawtransaction tests
# 1. valid parameters - only supply txid
assert_equal(self.nodes[0].getrawtransaction(txId), rawTxSigned["hex"])
# 2. valid parameters - supply txid and 0 for non-verbose
assert_equal(self.nodes[0].getrawtransaction(txId, 0), rawTxSigned["hex"])
# 3. valid parameters - supply txid and False for non-verbose
assert_equal(self.nodes[0].getrawtransaction(txId, False), rawTxSigned["hex"])
# 4. valid parameters - supply txid and 1 for verbose.
# We only check the "hex" field of the output so we don't need to
# update this test every time the output format changes.
assert_equal(
self.nodes[0].getrawtransaction(txId, 1)["hex"], rawTxSigned["hex"]
)
# 5. valid parameters - supply txid and True for non-verbose
assert_equal(
self.nodes[0].getrawtransaction(txId, True)["hex"], rawTxSigned["hex"]
)
# 6. invalid parameters - supply txid and string "Flase"
assert_raises_rpc_error(
- -1, "not a boolean", self.nodes[0].getrawtransaction, txId, "Flase"
+ -1,
+ "not of expected type bool",
+ self.nodes[0].getrawtransaction,
+ txId,
+ "Flase",
)
# 7. invalid parameters - supply txid and empty array
assert_raises_rpc_error(
- -1, "not a boolean", self.nodes[0].getrawtransaction, txId, []
+ -1, "not of expected type bool", self.nodes[0].getrawtransaction, txId, []
)
# 8. invalid parameters - supply txid and empty dict
assert_raises_rpc_error(
- -1, "not a boolean", self.nodes[0].getrawtransaction, txId, {}
+ -1, "not of expected type bool", self.nodes[0].getrawtransaction, txId, {}
)
inputs = [
{
"txid": (
"1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000"
),
"sequence": 1000,
}
]
outputs = {self.nodes[0].getnewaddress(): 1}
assert_raises_rpc_error(
-8,
"Invalid parameter, missing vout key",
self.nodes[0].createrawtransaction,
inputs,
outputs,
)
inputs[0]["vout"] = "1"
assert_raises_rpc_error(
-8,
"Invalid parameter, vout must be a number",
self.nodes[0].createrawtransaction,
inputs,
outputs,
)
inputs[0]["vout"] = -1
assert_raises_rpc_error(
-8,
"Invalid parameter, vout cannot be negative",
self.nodes[0].createrawtransaction,
inputs,
outputs,
)
inputs[0]["vout"] = 1
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx["vin"][0]["sequence"], 1000)
# 9. invalid parameters - sequence number out of range
inputs[0]["sequence"] = -1
assert_raises_rpc_error(
-8,
"Invalid parameter, sequence number is out of range",
self.nodes[0].createrawtransaction,
inputs,
outputs,
)
# 10. invalid parameters - sequence number out of range
inputs[0]["sequence"] = 4294967296
assert_raises_rpc_error(
-8,
"Invalid parameter, sequence number is out of range",
self.nodes[0].createrawtransaction,
inputs,
outputs,
)
inputs[0]["sequence"] = 4294967294
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx["vin"][0]["sequence"], 4294967294)
####################################
# TRANSACTION VERSION NUMBER TESTS #
####################################
# Test the minimum transaction version number that fits in a signed
# 32-bit integer.
# As transaction version is unsigned, this should convert to its
# unsigned equivalent.
tx = CTransaction()
tx.nVersion = -0x80000000
rawtx = ToHex(tx)
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx["version"], 0x80000000)
# Test the maximum transaction version number that fits in a signed
# 32-bit integer.
tx = CTransaction()
tx.nVersion = 0x7FFFFFFF
rawtx = ToHex(tx)
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx["version"], 0x7FFFFFFF)
self.log.info("sendrawtransaction/testmempoolaccept with maxfeerate")
# Test a transaction with a small fee.
txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1000000)
rawTx = self.nodes[0].getrawtransaction(txId, True)
vout = next(o for o in rawTx["vout"] if o["value"] == Decimal("1000000.00"))
self.sync_all()
inputs = [{"txid": txId, "vout": vout["n"]}]
# Fee 10,000 satoshis, (1,000,000 - (10000 sat * 0.01 XEC/sat)) =
# 999900
outputs = {self.nodes[0].getnewaddress(): Decimal("999900.00")}
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx)
assert_equal(rawTxSigned["complete"], True)
# Fee 10,000 satoshis, ~200 b transaction, fee rate should land around 50 sat/byte = 500 XEC/kB
# Thus, testmempoolaccept should reject
testres = self.nodes[2].testmempoolaccept([rawTxSigned["hex"]], 500.00)[0]
assert_equal(testres["allowed"], False)
assert_equal(testres["reject-reason"], "max-fee-exceeded")
# and sendrawtransaction should throw
assert_raises_rpc_error(
-25,
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
self.nodes[2].sendrawtransaction,
rawTxSigned["hex"],
10.00,
)
# and the following calls should both succeed
testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned["hex"]])[0]
assert_equal(testres["allowed"], True)
self.nodes[2].sendrawtransaction(hexstring=rawTxSigned["hex"])
# Test a transaction with a large fee.
txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1000000)
rawTx = self.nodes[0].getrawtransaction(txId, True)
vout = next(o for o in rawTx["vout"] if o["value"] == Decimal("1000000.00"))
self.sync_all()
inputs = [{"txid": txId, "vout": vout["n"]}]
# Fee 2,000,000 satoshis, (1,000,000 - (2,000,000 sat * 0.01 XEC/sat)) =
# 980000
outputs = {self.nodes[0].getnewaddress(): Decimal("980000.00")}
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx)
assert_equal(rawTxSigned["complete"], True)
# Fee 2,000,000 satoshis, ~100 b transaction, fee rate should land around 20,000 sat/byte = 200,000 XEC/kB
# Thus, testmempoolaccept should reject
testres = self.nodes[2].testmempoolaccept([rawTxSigned["hex"]])[0]
assert_equal(testres["allowed"], False)
assert_equal(testres["reject-reason"], "max-fee-exceeded")
# and sendrawtransaction should throw
assert_raises_rpc_error(
-25,
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
self.nodes[2].sendrawtransaction,
rawTxSigned["hex"],
)
# and the following calls should both succeed
testres = self.nodes[2].testmempoolaccept(
rawtxs=[rawTxSigned["hex"]], maxfeerate="200000.00"
)[0]
assert_equal(testres["allowed"], True)
self.nodes[2].sendrawtransaction(
hexstring=rawTxSigned["hex"], maxfeerate="200000.00"
)
self.log.info(
"sendrawtransaction/testmempoolaccept with tx that is already in the chain"
)
self.generate(self.nodes[2], 1)
for node in self.nodes:
testres = node.testmempoolaccept([rawTxSigned["hex"]])[0]
assert_equal(testres["allowed"], False)
assert_equal(testres["reject-reason"], "txn-already-known")
assert_raises_rpc_error(
-27,
"Transaction already in block chain",
node.sendrawtransaction,
rawTxSigned["hex"],
)
##########################################
# Decoding weird scripts in transactions #
##########################################
self.log.info("Decode correctly-formatted but weird transactions")
tx = CTransaction()
# empty
self.nodes[0].decoderawtransaction(ToHex(tx))
# truncated push
tx.vin.append(CTxIn(COutPoint(42, 0), b"\x4e\x00\x00"))
tx.vin.append(CTxIn(COutPoint(42, 0), b"\x4c\x10TRUNC"))
tx.vout.append(CTxOut(0, b"\x4e\x00\x00"))
tx.vout.append(CTxOut(0, b"\x4c\x10TRUNC"))
self.nodes[0].decoderawtransaction(ToHex(tx))
# giant pushes and long scripts
tx.vin.append(CTxIn(COutPoint(42, 0), CScript([b"giant push" * 10000])))
tx.vout.append(CTxOut(0, CScript([b"giant push" * 10000])))
self.nodes[0].decoderawtransaction(ToHex(tx))
self.log.info("Refuse garbage after transaction")
assert_raises_rpc_error(
-22,
"TX decode failed",
self.nodes[0].decoderawtransaction,
f"{ToHex(tx)}00",
)
if __name__ == "__main__":
RawTransactionsTest().main()
diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py
index 5dfc65972..903fa5bd6 100644
--- a/test/functional/wallet_multiwallet.py
+++ b/test/functional/wallet_multiwallet.py
@@ -1,647 +1,649 @@
# Copyright (c) 2017-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test multiwallet.
Verify that a bitcoind node can load multiple wallet files
"""
import os
import shutil
import stat
import sys
import time
from decimal import Decimal
from threading import Thread
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.test_node import ErrorMatch
from test_framework.util import assert_equal, assert_raises_rpc_error, get_rpc_proxy
got_loading_error = False
def test_load_unload(node, name, timeout=60.0):
global got_loading_error
t0 = time.time()
while time.time() - t0 < timeout and not got_loading_error:
try:
node.loadwallet(name)
node.unloadwallet(name)
except JSONRPCException as e:
if (
e.error["code"] == -4
and "Wallet already being loading" in e.error["message"]
):
got_loading_error = True
return
# Add a small sleep to avoid CPU exhaustion in the unlikely case
# the race never happens.
time.sleep(0.001)
class MultiWalletTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2
self.rpc_timeout = 120
self.extra_args = [["-nowallet"], []]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def add_options(self, parser):
parser.add_argument(
"--data_wallets_dir",
default=os.path.join(
os.path.dirname(os.path.realpath(__file__)), "data/wallets/"
),
help="Test data with wallet directories (default: %(default)s)",
)
def run_test(self):
node = self.nodes[0]
def data_dir(*p):
return os.path.join(node.datadir, self.chain, *p)
def wallet_dir(*p):
return data_dir("wallets", *p)
def wallet(name):
return node.get_wallet_rpc(name)
def wallet_file(name):
if os.path.isdir(wallet_dir(name)):
return wallet_dir(name, self.wallet_data_filename)
return wallet_dir(name)
assert_equal(
self.nodes[0].listwalletdir(),
{"wallets": [{"name": self.default_wallet_name}]},
)
# check wallet.dat is created
self.stop_nodes()
assert_equal(
os.path.isfile(
wallet_dir(self.default_wallet_name, self.wallet_data_filename)
),
True,
)
# create symlink to verify wallet directory path can be referenced
# through symlink
if os.name != "nt":
os.mkdir(wallet_dir("w7"))
os.symlink("w7", wallet_dir("w7_symlink"))
os.symlink("..", wallet_dir("recursive_dir_symlink"))
os.mkdir(wallet_dir("self_walletdat_symlink"))
os.symlink("wallet.dat", wallet_dir("self_walletdat_symlink/wallet.dat"))
# rename wallet.dat to make sure plain wallet file paths (as opposed to
# directory paths) can be loaded
# create another dummy wallet for use in testing backups later
self.start_node(0)
node.createwallet("empty", descriptors=False)
node.createwallet("plain", descriptors=False)
node.createwallet("created")
self.stop_nodes()
empty_wallet = os.path.join(self.options.tmpdir, "empty.dat")
os.rename(wallet_file("empty"), empty_wallet)
shutil.rmtree(wallet_dir("empty"))
empty_created_wallet = os.path.join(self.options.tmpdir, "empty.created.dat")
os.rename(
wallet_dir("created", self.wallet_data_filename), empty_created_wallet
)
shutil.rmtree(wallet_dir("created"))
os.rename(wallet_file("plain"), wallet_dir("w8"))
shutil.rmtree(wallet_dir("plain"))
# restart node with a mix of wallet names:
# w1, w2, w3 - to verify new wallets created when non-existing paths specified
# w - to verify wallet name matching works when one wallet path is prefix of another
# sub/w5 - to verify relative wallet path is created correctly
# extern/w6 - to verify absolute wallet path is created correctly
# w7_symlink - to verify symlinked wallet path is initialized correctly
# w8 - to verify existing wallet file is loaded correctly
# '' - to verify default wallet file is created correctly
wallet_names = [
"w1",
"w2",
"w3",
"w",
"sub/w5",
os.path.join(self.options.tmpdir, "extern/w6"),
"w7_symlink",
"w8",
self.default_wallet_name,
]
if os.name == "nt":
wallet_names.remove("w7_symlink")
self.start_node(0)
for wallet_name in wallet_names[:-2]:
self.nodes[0].createwallet(wallet_name, descriptors=False)
for wallet_name in wallet_names[-2:]:
self.nodes[0].loadwallet(wallet_name)
os.mkdir(wallet_dir("no_access"))
os.chmod(wallet_dir("no_access"), 0)
try:
with self.nodes[0].assert_debug_log(
expected_msgs=["Too many levels of symbolic links", "Error scanning"]
):
walletlist = self.nodes[0].listwalletdir()["wallets"]
finally:
# Need to ensure access is restored for cleanup
os.chmod(
wallet_dir("no_access"), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
)
assert_equal(
sorted(w["name"] for w in walletlist),
[
self.default_wallet_name,
os.path.join("sub", "w5"),
"w",
"w1",
"w2",
"w3",
"w7",
"w7_symlink",
"w8",
],
)
assert_equal(set(node.listwallets()), set(wallet_names))
# should raise rpc error if wallet path can't be created
assert_raises_rpc_error(
-1,
"filesystem error:" if sys.platform != "win32" else "create_directories:",
self.nodes[0].createwallet,
"w8/bad",
descriptors=False,
)
# check that all requested wallets were created
self.stop_node(0)
for wallet_name in wallet_names:
assert_equal(os.path.isfile(wallet_file(wallet_name)), True)
self.nodes[0].assert_start_raises_init_error(
["-walletdir=wallets"],
'Error: Specified -walletdir "wallets" does not exist',
)
self.nodes[0].assert_start_raises_init_error(
["-walletdir=wallets"],
'Error: Specified -walletdir "wallets" is a relative path',
cwd=data_dir(),
)
self.nodes[0].assert_start_raises_init_error(
["-walletdir=debug.log"],
'Error: Specified -walletdir "debug.log" is not a directory',
cwd=data_dir(),
)
self.start_node(0, ["-wallet=w1", "-wallet=w1"])
self.stop_node(0, "Warning: Ignoring duplicate -wallet w1.")
# should not initialize if one wallet is a copy of another
shutil.copyfile(wallet_dir("w8"), wallet_dir("w8_copy"))
exp_stderr = (
r"BerkeleyDatabase: Can't open database w8_copy \(duplicates fileid \w+"
r" from w8\)"
)
self.nodes[0].assert_start_raises_init_error(
["-wallet=w8", "-wallet=w8_copy"],
exp_stderr,
match=ErrorMatch.PARTIAL_REGEX,
)
# should not initialize if wallet file is a symlink
if os.name != "nt":
os.symlink("w8", wallet_dir("w8_symlink"))
self.nodes[0].assert_start_raises_init_error(
["-wallet=w8_symlink"],
r"Error: Invalid -wallet path \'w8_symlink\'\. .*",
match=ErrorMatch.FULL_REGEX,
)
# should not initialize if the specified walletdir does not exist
self.nodes[0].assert_start_raises_init_error(
["-walletdir=bad"], 'Error: Specified -walletdir "bad" does not exist'
)
# should not initialize if the specified walletdir is not a directory
not_a_dir = wallet_dir("notadir")
open(not_a_dir, "a", encoding="utf8").close()
self.nodes[0].assert_start_raises_init_error(
[f"-walletdir={not_a_dir}"],
f'Error: Specified -walletdir "{not_a_dir}" is not a directory',
)
# if wallets/ doesn't exist, datadir should be the default wallet dir
wallet_dir2 = data_dir("walletdir")
os.rename(wallet_dir(), wallet_dir2)
self.start_node(0)
self.nodes[0].createwallet("w4")
self.nodes[0].createwallet("w5")
assert_equal(set(node.listwallets()), {"w4", "w5"})
w5 = wallet("w5")
self.generatetoaddress(
node, nblocks=1, address=w5.getnewaddress(), sync_fun=self.no_op
)
# now if wallets/ exists again, but the rootdir is specified as the
# walletdir, w4 and w5 should still be loaded
os.rename(wallet_dir2, wallet_dir())
self.restart_node(0, ["-nowallet", f"-walletdir={data_dir()}"])
self.nodes[0].loadwallet("w4")
self.nodes[0].loadwallet("w5")
assert_equal(set(node.listwallets()), {"w4", "w5"})
w5 = wallet("w5")
w5_info = w5.getwalletinfo()
assert_equal(w5_info["immature_balance"], 50000000)
competing_wallet_dir = os.path.join(self.options.tmpdir, "competing_walletdir")
os.mkdir(competing_wallet_dir)
self.restart_node(0, ["-nowallet", f"-walletdir={competing_wallet_dir}"])
self.nodes[0].createwallet(self.default_wallet_name, descriptors=False)
exp_stderr = (
r"Error: Error initializing wallet database environment"
r" \"\S+competing_walletdir\S*\"!"
)
self.nodes[1].assert_start_raises_init_error(
[f"-walletdir={competing_wallet_dir}"],
exp_stderr,
match=ErrorMatch.PARTIAL_REGEX,
)
self.restart_node(0)
for wallet_name in wallet_names:
self.nodes[0].loadwallet(wallet_name)
assert_equal(
sorted(w["name"] for w in self.nodes[0].listwalletdir()["wallets"]),
[
self.default_wallet_name,
os.path.join("sub", "w5"),
"w",
"w1",
"w2",
"w3",
"w7",
"w7_symlink",
"w8",
"w8_copy",
],
)
wallets = [wallet(w) for w in wallet_names]
wallet_bad = wallet("bad")
# check wallet names and balances
self.generatetoaddress(
node, nblocks=1, address=wallets[0].getnewaddress(), sync_fun=self.no_op
)
for wallet_name, wallet in zip(wallet_names, wallets):
info = wallet.getwalletinfo()
assert_equal(
info["immature_balance"], 50000000 if wallet is wallets[0] else 0
)
assert_equal(info["walletname"], wallet_name)
# accessing invalid wallet fails
assert_raises_rpc_error(
-18,
"Requested wallet does not exist or is not loaded",
wallet_bad.getwalletinfo,
)
# accessing wallet RPC without using wallet endpoint fails
assert_raises_rpc_error(
-19,
"Wallet file not specified (must request wallet RPC through"
" /wallet/<filename> uri-path).",
node.getwalletinfo,
)
w1, w2, w3, w4, *_ = wallets
self.generatetoaddress(
node, nblocks=101, address=w1.getnewaddress(), sync_fun=self.no_op
)
assert_equal(w1.getbalance(), 100000000)
assert_equal(w2.getbalance(), 0)
assert_equal(w3.getbalance(), 0)
assert_equal(w4.getbalance(), 0)
w1.sendtoaddress(w2.getnewaddress(), 1000000)
w1.sendtoaddress(w3.getnewaddress(), 2000000)
w1.sendtoaddress(w4.getnewaddress(), 3000000)
self.generatetoaddress(
node, nblocks=1, address=w1.getnewaddress(), sync_fun=self.no_op
)
assert_equal(w2.getbalance(), 1000000)
assert_equal(w3.getbalance(), 2000000)
assert_equal(w4.getbalance(), 3000000)
batch = w1.batch(
[w1.getblockchaininfo.get_request(), w1.getwalletinfo.get_request()]
)
assert_equal(batch[0]["result"]["chain"], self.chain)
assert_equal(batch[1]["result"]["walletname"], "w1")
self.log.info("Check for per-wallet settxfee call")
assert_equal(w1.getwalletinfo()["paytxfee"], 0)
assert_equal(w2.getwalletinfo()["paytxfee"], 0)
w2.settxfee(1000)
assert_equal(w1.getwalletinfo()["paytxfee"], 0)
assert_equal(w2.getwalletinfo()["paytxfee"], Decimal("1000.00"))
self.log.info("Test dynamic wallet loading")
self.restart_node(0, ["-nowallet"])
assert_equal(node.listwallets(), [])
assert_raises_rpc_error(
-18,
"No wallet is loaded. Load a wallet using loadwallet or create a new"
" one with createwallet. (Note: A default wallet is no longer "
"automatically created)",
node.getwalletinfo,
)
self.log.info("Load first wallet")
loadwallet_name = node.loadwallet(wallet_names[0])
assert_equal(loadwallet_name["name"], wallet_names[0])
assert_equal(node.listwallets(), wallet_names[0:1])
node.getwalletinfo()
w1 = node.get_wallet_rpc(wallet_names[0])
w1.getwalletinfo()
self.log.info("Load second wallet")
loadwallet_name = node.loadwallet(wallet_names[1])
assert_equal(loadwallet_name["name"], wallet_names[1])
assert_equal(node.listwallets(), wallet_names[0:2])
assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo)
w2 = node.get_wallet_rpc(wallet_names[1])
w2.getwalletinfo()
self.log.info("Concurrent wallet loading")
threads = []
for _ in range(3):
n = (
node.cli
if self.options.usecli
else get_rpc_proxy(
node.url, 1, timeout=600, coveragedir=node.coverage_dir
)
)
t = Thread(
target=test_load_unload,
args=(n, wallet_names[2], 20 * self.rpc_timeout),
)
t.start()
threads.append(t)
for t in threads:
t.join()
global got_loading_error
assert_equal(got_loading_error, True)
self.log.info("Load remaining wallets")
for wallet_name in wallet_names[2:]:
loadwallet_name = self.nodes[0].loadwallet(wallet_name)
assert_equal(loadwallet_name["name"], wallet_name)
assert_equal(set(self.nodes[0].listwallets()), set(wallet_names))
# Fail to load if wallet doesn't exist
path = os.path.join(
self.options.tmpdir, "node0", "regtest", "wallets", "wallets"
)
assert_raises_rpc_error(
-18,
"Wallet file verification failed. Failed to load database path"
f" '{path}'. Path does not exist.",
self.nodes[0].loadwallet,
"wallets",
)
# Fail to load duplicate wallets
path = os.path.join(
self.options.tmpdir,
"node0",
"regtest",
"wallets",
"w1",
"wallet.dat",
)
assert_raises_rpc_error(
-4,
"Wallet file verification failed. Refusing to load database. "
f"Data file '{path}' is already loaded.",
self.nodes[0].loadwallet,
wallet_names[0],
)
# Fail to load duplicate wallets by different ways (directory and
# filepath)
if not self.options.descriptors:
path = os.path.join(
self.options.tmpdir,
"node0",
"regtest",
"wallets",
"wallet.dat",
)
assert_raises_rpc_error(
-4,
"Wallet file verification failed. Refusing to load database. "
f"Data file '{path}' is already loaded.",
self.nodes[0].loadwallet,
"wallet.dat",
)
# Fail to load if one wallet is a copy of another
assert_raises_rpc_error(
-4,
"BerkeleyDatabase: Can't open database w8_copy (duplicates fileid",
self.nodes[0].loadwallet,
"w8_copy",
)
# Fail to load if one wallet is a copy of another.
# Test this twice to make sure that we don't re-introduce
# https://github.com/bitcoin/bitcoin/issues/14304
assert_raises_rpc_error(
-4,
"BerkeleyDatabase: Can't open database w8_copy (duplicates fileid",
self.nodes[0].loadwallet,
"w8_copy",
)
# Fail to load if wallet file is a symlink
if os.name != "nt":
assert_raises_rpc_error(
-4,
"Wallet file verification failed. Invalid -wallet path 'w8_symlink'",
self.nodes[0].loadwallet,
"w8_symlink",
)
# Fail to load if a directory is specified that doesn't contain a
# wallet
os.mkdir(wallet_dir("empty_wallet_dir"))
path = os.path.join(
self.options.tmpdir, "node0", "regtest", "wallets", "empty_wallet_dir"
)
assert_raises_rpc_error(
-18,
"Wallet file verification failed. Failed to load database path"
f" '{path}'. Data is not in recognized format.",
self.nodes[0].loadwallet,
"empty_wallet_dir",
)
self.log.info("Test dynamic wallet creation.")
# Fail to create a wallet if it already exists.
path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w2")
assert_raises_rpc_error(
-4,
f"Failed to create database path '{path}'. Database already exists.",
self.nodes[0].createwallet,
"w2",
)
# Successfully create a wallet with a new name
loadwallet_name = self.nodes[0].createwallet("w9")
assert_equal(loadwallet_name["name"], "w9")
w9 = node.get_wallet_rpc("w9")
assert_equal(w9.getwalletinfo()["walletname"], "w9")
assert "w9" in self.nodes[0].listwallets()
# Successfully create a wallet using a full path
new_wallet_dir = os.path.join(self.options.tmpdir, "new_walletdir")
new_wallet_name = os.path.join(new_wallet_dir, "w10")
loadwallet_name = self.nodes[0].createwallet(new_wallet_name)
assert_equal(loadwallet_name["name"], new_wallet_name)
w10 = node.get_wallet_rpc(new_wallet_name)
assert_equal(w10.getwalletinfo()["walletname"], new_wallet_name)
assert new_wallet_name in self.nodes[0].listwallets()
self.log.info("Test dynamic wallet unloading")
# Test `unloadwallet` errors
assert_raises_rpc_error(
- -1, "JSON value is not a string as expected", self.nodes[0].unloadwallet
+ -1,
+ "JSON value of type null is not of expected type string",
+ self.nodes[0].unloadwallet,
)
assert_raises_rpc_error(
-18,
"Requested wallet does not exist or is not loaded",
self.nodes[0].unloadwallet,
"dummy",
)
assert_raises_rpc_error(
-18,
"Requested wallet does not exist or is not loaded",
node.get_wallet_rpc("dummy").unloadwallet,
)
assert_raises_rpc_error(
-8, "Cannot unload the requested wallet", w1.unloadwallet, "w2"
),
# Successfully unload the specified wallet name
self.nodes[0].unloadwallet("w1")
assert "w1" not in self.nodes[0].listwallets()
# Successfully unload the wallet referenced by the request endpoint
# Also ensure unload works during walletpassphrase timeout
w2.encryptwallet("test")
w2.walletpassphrase("test", 1)
w2.unloadwallet()
time.sleep(1.1)
assert "w2" not in self.nodes[0].listwallets()
# Successfully unload all wallets
for wallet_name in self.nodes[0].listwallets():
self.nodes[0].unloadwallet(wallet_name)
assert_equal(self.nodes[0].listwallets(), [])
assert_raises_rpc_error(
-18,
"No wallet is loaded. Load a wallet using loadwallet or create a new"
" one with createwallet. (Note: A default wallet is no longer "
"automatically created)",
self.nodes[0].getwalletinfo,
)
# Successfully load a previously unloaded wallet
self.nodes[0].loadwallet("w1")
assert_equal(self.nodes[0].listwallets(), ["w1"])
assert_equal(w1.getwalletinfo()["walletname"], "w1")
assert_equal(
sorted(w["name"] for w in self.nodes[0].listwalletdir()["wallets"]),
[
self.default_wallet_name,
os.path.join("sub", "w5"),
"w",
"w1",
"w2",
"w3",
"w7",
"w7_symlink",
"w8",
"w8_copy",
"w9",
],
)
# Test backing up and restoring wallets
self.log.info("Test wallet backup")
self.restart_node(0, ["-nowallet"])
for wallet_name in wallet_names:
self.nodes[0].loadwallet(wallet_name)
for wallet_name in wallet_names:
rpc = self.nodes[0].get_wallet_rpc(wallet_name)
addr = rpc.getnewaddress()
backup = os.path.join(self.options.tmpdir, "backup.dat")
if os.path.exists(backup):
os.unlink(backup)
rpc.backupwallet(backup)
self.nodes[0].unloadwallet(wallet_name)
shutil.copyfile(
(
empty_created_wallet
if wallet_name == self.default_wallet_name
else empty_wallet
),
wallet_file(wallet_name),
)
self.nodes[0].loadwallet(wallet_name)
assert_equal(rpc.getaddressinfo(addr)["ismine"], False)
self.nodes[0].unloadwallet(wallet_name)
shutil.copyfile(backup, wallet_file(wallet_name))
self.nodes[0].loadwallet(wallet_name)
assert_equal(rpc.getaddressinfo(addr)["ismine"], True)
# Test .walletlock file is closed
self.start_node(1)
wallet = os.path.join(self.options.tmpdir, "my_wallet")
self.nodes[0].createwallet(wallet)
assert_raises_rpc_error(
-4,
"Error initializing wallet database environment",
self.nodes[1].loadwallet,
wallet,
)
self.nodes[0].unloadwallet(wallet)
self.nodes[1].loadwallet(wallet)
if __name__ == "__main__":
MultiWalletTest().main()

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 17:33 (1 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865600
Default Alt Text
(76 KB)

Event Timeline