Skip to content

Commit 1fd2d39

Browse files
committed
Merge pull request #5420
6b4feb8 [QA] rest.py RPC test: change setgenerate() to generate() (Jonas Schnelli) 97ee866 [REST] getutxos REST command (based on Bip64) (Jonas Schnelli)
2 parents 12f3488 + 6b4feb8 commit 1fd2d39

File tree

5 files changed

+414
-26
lines changed

5 files changed

+414
-26
lines changed

doc/REST-interface.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Only supports JSON as output format.
4646
* verificationprogress : (numeric) estimate of verification progress [0..1]
4747
* chainwork : (string) total amount of work in active chain, in hexadecimal
4848

49+
`GET /rest/getutxos`
50+
51+
The getutxo command allows querying of the UTXO set given a set of of outpoints.
52+
See BIP64 for input and output serialisation:
53+
https://github.com/bitcoin/bips/blob/master/bip-0064.mediawiki
54+
55+
4956
Risks
5057
-------------
5158
Running a webbrowser on the same node with a REST enabled bitcoind can be a risk. Accessing prepared XSS websites could read out tx/block data of your node by placing links like `<script src="http://127.0.0.1:8332/rest/tx/1234567890.json">` which might break the nodes privacy.

qa/rpc-tests/rest.py

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from test_framework import BitcoinTestFramework
1111
from util import *
12+
from struct import *
13+
import binascii
1214
import json
15+
import StringIO
1316

1417
try:
1518
import http.client as httplib
@@ -20,45 +23,210 @@
2023
except ImportError:
2124
import urlparse
2225

23-
def http_get_call(host, port, path, response_object = 0):
26+
def deser_uint256(f):
27+
r = 0
28+
for i in range(8):
29+
t = unpack(b"<I", f.read(4))[0]
30+
r += t << (i * 32)
31+
return r
32+
33+
#allows simple http get calls with a request body
34+
def http_get_call(host, port, path, requestdata = '', response_object = 0):
2435
conn = httplib.HTTPConnection(host, port)
25-
conn.request('GET', path)
36+
conn.request('GET', path, requestdata)
2637

2738
if response_object:
2839
return conn.getresponse()
2940

3041
return conn.getresponse().read()
3142

32-
3343
class RESTTest (BitcoinTestFramework):
3444
FORMAT_SEPARATOR = "."
3545

46+
def setup_chain(self):
47+
print("Initializing test directory "+self.options.tmpdir)
48+
initialize_chain_clean(self.options.tmpdir, 3)
49+
50+
def setup_network(self, split=False):
51+
self.nodes = start_nodes(3, self.options.tmpdir)
52+
connect_nodes_bi(self.nodes,0,1)
53+
connect_nodes_bi(self.nodes,1,2)
54+
connect_nodes_bi(self.nodes,0,2)
55+
self.is_network_split=False
56+
self.sync_all()
57+
3658
def run_test(self):
3759
url = urlparse.urlparse(self.nodes[0].url)
60+
print "Mining blocks..."
61+
62+
self.nodes[0].generate(1)
63+
self.sync_all()
64+
self.nodes[2].generate(100)
65+
self.sync_all()
66+
67+
assert_equal(self.nodes[0].getbalance(), 50)
68+
69+
txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1)
70+
self.sync_all()
71+
self.nodes[2].generate(1)
72+
self.sync_all()
73+
bb_hash = self.nodes[0].getbestblockhash()
74+
75+
assert_equal(self.nodes[1].getbalance(), Decimal("0.1")) #balance now should be 0.1 on node 1
76+
77+
# load the latest 0.1 tx over the REST API
78+
json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json")
79+
json_obj = json.loads(json_string)
80+
vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then)
81+
# get n of 0.1 outpoint
82+
n = 0
83+
for vout in json_obj['vout']:
84+
if vout['value'] == 0.1:
85+
n = vout['n']
86+
87+
88+
######################################
89+
# GETUTXOS: query a unspent outpoint #
90+
######################################
91+
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}'
92+
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request)
93+
json_obj = json.loads(json_string)
94+
95+
#check chainTip response
96+
assert_equal(json_obj['chaintipHash'], bb_hash)
97+
98+
#make sure there is one utxo
99+
assert_equal(len(json_obj['utxos']), 1)
100+
assert_equal(json_obj['utxos'][0]['value'], 0.1)
101+
102+
103+
################################################
104+
# GETUTXOS: now query a already spent outpoint #
105+
################################################
106+
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+vintx+'","n":0}]}'
107+
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request)
108+
json_obj = json.loads(json_string)
109+
110+
#check chainTip response
111+
assert_equal(json_obj['chaintipHash'], bb_hash)
112+
113+
#make sure there is no utox in the response because this oupoint has been spent
114+
assert_equal(len(json_obj['utxos']), 0)
115+
116+
#check bitmap
117+
assert_equal(json_obj['bitmap'], "0")
118+
119+
120+
##################################################
121+
# GETUTXOS: now check both with the same request #
122+
##################################################
123+
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'},{"txid":"'+vintx+'","n":0}]}'
124+
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request)
125+
json_obj = json.loads(json_string)
126+
assert_equal(len(json_obj['utxos']), 1)
127+
assert_equal(json_obj['bitmap'], "10")
128+
129+
#test binary response
38130
bb_hash = self.nodes[0].getbestblockhash()
39131

132+
binaryRequest = b'\x01\x02'
133+
binaryRequest += binascii.unhexlify(txid)
134+
binaryRequest += pack("i", n);
135+
binaryRequest += binascii.unhexlify(vintx);
136+
binaryRequest += pack("i", 0);
137+
138+
bin_response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', binaryRequest)
139+
140+
output = StringIO.StringIO()
141+
output.write(bin_response)
142+
output.seek(0)
143+
chainHeight = unpack("i", output.read(4))[0]
144+
hashFromBinResponse = hex(deser_uint256(output))[2:].zfill(65).rstrip("L")
145+
146+
assert_equal(bb_hash, hashFromBinResponse) #check if getutxo's chaintip during calculation was fine
147+
assert_equal(chainHeight, 102) #chain height must be 102
148+
149+
150+
############################
151+
# GETUTXOS: mempool checks #
152+
############################
153+
154+
# do a tx and don't sync
155+
txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1)
156+
json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json")
157+
json_obj = json.loads(json_string)
158+
vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then)
159+
# get n of 0.1 outpoint
160+
n = 0
161+
for vout in json_obj['vout']:
162+
if vout['value'] == 0.1:
163+
n = vout['n']
164+
165+
json_request = '{"checkmempool":false,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}'
166+
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request)
167+
json_obj = json.loads(json_string)
168+
assert_equal(len(json_obj['utxos']), 0) #there should be a outpoint because it has just added to the mempool
169+
170+
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}'
171+
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request)
172+
json_obj = json.loads(json_string)
173+
assert_equal(len(json_obj['utxos']), 1) #there should be a outpoint because it has just added to the mempool
174+
175+
#do some invalid requests
176+
json_request = '{"checkmempool'
177+
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
178+
assert_equal(response.status, 500) #must be a 500 because we send a invalid json request
179+
180+
json_request = '{"checkmempool'
181+
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', json_request, True)
182+
assert_equal(response.status, 500) #must be a 500 because we send a invalid bin request
183+
184+
#test limits
185+
json_request = '{"checkmempool":true,"outpoints":['
186+
for x in range(0, 200):
187+
json_request += '{"txid":"'+txid+'","n":'+str(n)+'},'
188+
json_request = json_request.rstrip(",")
189+
json_request+="]}";
190+
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
191+
assert_equal(response.status, 500) #must be a 500 because we exceeding the limits
192+
193+
json_request = '{"checkmempool":true,"outpoints":['
194+
for x in range(0, 90):
195+
json_request += '{"txid":"'+txid+'","n":'+str(n)+'},'
196+
json_request = json_request.rstrip(",")
197+
json_request+="]}";
198+
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
199+
assert_equal(response.status, 200) #must be a 500 because we exceeding the limits
200+
201+
self.nodes[0].generate(1) #generate block to not affect upcomming tests
202+
self.sync_all()
203+
204+
################
205+
# /rest/block/ #
206+
################
207+
40208
# check binary format
41-
response = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"bin", True)
209+
response = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"bin", "", True)
42210
assert_equal(response.status, 200)
43211
assert_greater_than(int(response.getheader('content-length')), 80)
44212
response_str = response.read()
45213

46214
# compare with block header
47-
response_header = http_get_call(url.hostname, url.port, '/rest/headers/1/'+bb_hash+self.FORMAT_SEPARATOR+"bin", True)
215+
response_header = http_get_call(url.hostname, url.port, '/rest/headers/1/'+bb_hash+self.FORMAT_SEPARATOR+"bin", "", True)
48216
assert_equal(response_header.status, 200)
49217
assert_equal(int(response_header.getheader('content-length')), 80)
50218
response_header_str = response_header.read()
51219
assert_equal(response_str[0:80], response_header_str)
52220

53221
# check block hex format
54-
response_hex = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"hex", True)
222+
response_hex = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"hex", "", True)
55223
assert_equal(response_hex.status, 200)
56224
assert_greater_than(int(response_hex.getheader('content-length')), 160)
57225
response_hex_str = response_hex.read()
58226
assert_equal(response_str.encode("hex")[0:160], response_hex_str[0:160])
59227

60228
# compare with hex block header
61-
response_header_hex = http_get_call(url.hostname, url.port, '/rest/headers/1/'+bb_hash+self.FORMAT_SEPARATOR+"hex", True)
229+
response_header_hex = http_get_call(url.hostname, url.port, '/rest/headers/1/'+bb_hash+self.FORMAT_SEPARATOR+"hex", "", True)
62230
assert_equal(response_header_hex.status, 200)
63231
assert_greater_than(int(response_header_hex.getheader('content-length')), 160)
64232
response_header_hex_str = response_header_hex.read()
@@ -77,9 +245,11 @@ def run_test(self):
77245
assert_equal(json_obj['txid'], tx_hash)
78246

79247
# check hex format response
80-
hex_string = http_get_call(url.hostname, url.port, '/rest/tx/'+tx_hash+self.FORMAT_SEPARATOR+"hex", True)
248+
hex_string = http_get_call(url.hostname, url.port, '/rest/tx/'+tx_hash+self.FORMAT_SEPARATOR+"hex", "", True)
81249
assert_equal(hex_string.status, 200)
82250
assert_greater_than(int(response.getheader('content-length')), 10)
251+
252+
83253

84254
# check block tx details
85255
# let's make 3 tx and mine them on node 1

0 commit comments

Comments
 (0)