Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864158
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
76 KB
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment