File: fakebitcoinproxy.py

package info (click to toggle)
python-bitcoinlib 0.12.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,356 kB
  • sloc: python: 8,212; makefile: 132; sh: 6
file content (327 lines) | stat: -rw-r--r-- 11,100 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""
`FakeBitcoinProxy` allows for unit testing of code that normally uses bitcoin
RPC without requiring a running bitcoin node.

`FakeBitcoinProxy` has an interface similar to `bitcoin.rpc.Proxy`, but does not
connect to a local bitcoin RPC node. Hence, `FakeBitcoinProxy` is similar to a
mock for the RPC tests.

`FakeBitcoinProxy` does _not_ implement a full bitcoin RPC node. Instead, it
currently implements only a subset of the available RPC commands. Test setup is
responsible for populating a `FakeBitcoinProxy` object with reasonable mock
data.

:author: Bryan Bishop <kanzure@gmail.com>
"""

import random
import hashlib

from bitcoin.core import (
    # bytes to hex (see x)
    b2x,

    # convert hex string to bytes (see b2x)
    x,

    # convert little-endian hex string to bytes (see b2lx)
    lx,

    # convert bytes to little-endian hex string (see lx)
    b2lx,

    # number of satoshis per bitcoin
    COIN,

    # a type for a transaction that isn't finished building
    CMutableTransaction,
    CMutableTxIn,
    CMutableTxOut,
    COutPoint,
    CTxIn,
)

from bitcoin.wallet import (
    # bitcoin address initialized from base58-encoded string
    CBitcoinAddress,

    # base58-encoded secret key
    CBitcoinSecret,

    # has a nifty function from_pubkey
    P2PKHBitcoinAddress,
)

def make_address_from_passphrase(passphrase, compressed=True, as_str=True):
    """
    Create a Bitcoin address from a passphrase. The passphrase is hashed and
    then used as the secret bytes to construct the CBitcoinSecret.
    """
    if not isinstance(passphrase, bytes):
        passphrase = bytes(passphrase, "utf-8")
    passphrasehash = hashlib.sha256(passphrase).digest()
    private_key = CBitcoinSecret.from_secret_bytes(passphrasehash, compressed=compressed)
    address = P2PKHBitcoinAddress.from_pubkey(private_key.pub)
    if as_str:
        return str(address)
    else:
        return address

def make_txout(amount=None, address=None, counter=None):
    """
    Make a CTxOut object based on the parameters. Otherwise randomly generate a
    CTxOut to represent a transaction output.

    :param amount: amount in satoshis
    """
    passphrase_template = "correct horse battery staple txout {counter}"

    if not counter:
        counter = random.randrange(0, 2**50)

    if not address:
        passphrase = passphrase_template.format(counter=counter)
        address = make_address_from_passphrase(bytes(passphrase, "utf-8"))

    if not amount:
        maxsatoshis = (21 * 1000 * 1000) * (100 * 1000 * 1000) # 21 million BTC * 100 million satoshi per BTC
        amount = random.randrange(0, maxsatoshis) # between 0 satoshi and 21 million BTC

    txout = CMutableTxOut(amount, CBitcoinAddress(address).to_scriptPubKey())

    return txout

def make_blocks_from_blockhashes(blockhashes):
    """
    Create some block data suitable for FakeBitcoinProxy to consume during
    instantiation.
    """
    blocks = []

    for (height, blockhash) in enumerate(blockhashes):
        block = {"hash": blockhash, "height": height, "tx": []}
        if height != 0:
            block["previousblockhash"] = previousblockhash
        blocks.append(block)
        previousblockhash = blockhash

    return blocks

def make_rpc_batch_request_entry(rpc_name, params):
    """
    Construct an entry for the list of commands that will be passed as a batch
    (for `_batch`).
    """
    return {
        "id": "50",
        "version": "1.1",
        "method": rpc_name,
        "params": params,
    }

class FakeBitcoinProxyException(Exception):
    """
    Incorrect usage of fake proxy.
    """
    pass

class FakeBitcoinProxy(object):
    """
    This is an alternative to using `bitcoin.rpc.Proxy` in tests. This class
    can store a number of blocks and transactions, which can then be retrieved
    by calling various "RPC" methods.
    """

    def __init__(self, blocks=None, transactions=None, getnewaddress_offset=None, getnewaddress_passphrase_template="getnewaddress passphrase template {}", num_fundrawtransaction_inputs=5):
        """
        :param getnewaddress_offset: a number to start using and incrementing
        in template used by getnewaddress.
        :type getnewaddress_offset: int
        :param int num_fundrawtransaction_inputs: number of inputs to create
        during fundrawtransaction.
        """
        self.blocks = blocks or {}
        self.transactions = transactions or {}

        if getnewaddress_offset == None:
            self._getnewaddress_offset = 0
        else:
            self._getnewaddress_offset = getnewaddress_offset

        self._getnewaddress_passphrase_template = getnewaddress_passphrase_template
        self._num_fundrawtransaction_inputs = num_fundrawtransaction_inputs
        self.populate_blocks_with_blockheights()

    def _call(self, rpc_method_name, *args, **kwargs):
        """
        This represents a "raw" RPC call, which has output that
        python-bitcoinlib does not parse.
        """
        method = getattr(self, rpc_method_name)
        return method(*args, **kwargs)

    def populate_blocks_with_blockheights(self):
        """
        Helper method to correctly apply "height" on all blocks.
        """
        for (height, block) in enumerate(self.blocks):
            block["height"] = height

    def getblock(self, blockhash, *args, **kwargs):
        """
        :param blockhash: hash of the block to retrieve data for
        :raise IndexError: invalid blockhash
        """

        # Note that the actual "getblock" bitcoind RPC call from
        # python-bitcoinlib returns a CBlock object, not a dictionary.

        if isinstance(blockhash, bytes):
            blockhash = b2lx(blockhash)

        for block in self.blocks:
            if block["hash"] == blockhash:
                return block

        raise IndexError("no block found for blockhash {}".format(blockhash))

    def getblockhash(self, blockheight):
        """
        Get block by blockheight.

        :type blockheight: int
        :rtype: dict
        """
        for block in self.blocks:
            if block["height"] == int(blockheight):
                return block["hash"]

    def getblockcount(self):
        """
        Return the total number of blocks. When there is only one block in the
        blockchain, this function will return zero.

        :rtype: int
        """
        return len(self.blocks) - 1

    def getrawtransaction(self, txid, *args, **kwargs):
        """
        Get parsed transaction.

        :type txid: bytes or str
        :rtype: dict
        """
        if isinstance(txid, bytes):
            txid = b2lx(txid)
        return self.transactions[txid]

    def getnewaddress(self):
        """
        Construct a new address based on a passphrase template. As more
        addresses are generated, the template value goes up.
        """
        passphrase = self._getnewaddress_passphrase_template.format(self._getnewaddress_offset)
        address = make_address_from_passphrase(bytes(passphrase, "utf-8"))
        self._getnewaddress_offset += 1
        return CBitcoinAddress(address)

    def importaddress(self, *args, **kwargs):
        """
        Completely unrealistic fake version of importaddress.
        """
        return True

    # This was implemented a long time ago and it's possible that this does not
    # match the current behavior of fundrawtransaction.
    def fundrawtransaction(self, given_transaction, *args, **kwargs):
        """
        Make up some inputs for the given transaction.
        """
        # just use any txid here
        vintxid = lx("99264749804159db1e342a0c8aa3279f6ef4031872051a1e52fb302e51061bef")

        if isinstance(given_transaction, str):
            given_bytes = x(given_transaction)
        elif isinstance(given_transaction, CMutableTransaction):
            given_bytes = given_transaction.serialize()
        else:
            raise FakeBitcoinProxyException("Wrong type passed to fundrawtransaction.")

        # this is also a clever way to not cause a side-effect in this function
        transaction = CMutableTransaction.deserialize(given_bytes)

        for vout_counter in range(0, self._num_fundrawtransaction_inputs):
            txin = CMutableTxIn(COutPoint(vintxid, vout_counter))
            transaction.vin.append(txin)

        # also allocate a single output (for change)
        txout = make_txout()
        transaction.vout.append(txout)

        transaction_hex = b2x(transaction.serialize())

        return {"hex": transaction_hex, "fee": 5000000}

    def signrawtransaction(self, given_transaction):
        """
        This method does not actually sign the transaction, but it does return
        a transaction based on the given transaction.
        """
        if isinstance(given_transaction, str):
            given_bytes = x(given_transaction)
        elif isinstance(given_transaction, CMutableTransaction):
            given_bytes = given_transaction.serialize()
        else:
            raise FakeBitcoinProxyException("Wrong type passed to signrawtransaction.")

        transaction = CMutableTransaction.deserialize(given_bytes)
        transaction_hex = b2x(transaction.serialize())
        return {"hex": transaction_hex}

    def sendrawtransaction(self, given_transaction):
        """
        Pretend to broadcast and relay the transaction. Return the txid of the
        given transaction.
        """
        if isinstance(given_transaction, str):
            given_bytes = x(given_transaction)
        elif isinstance(given_transaction, CMutableTransaction):
            given_bytes = given_transaction.serialize()
        else:
            raise FakeBitcoinProxyException("Wrong type passed to sendrawtransaction.")
        transaction = CMutableTransaction.deserialize(given_bytes)
        return b2lx(transaction.GetHash())

    def _batch(self, batch_request_entries):
        """
        Process a bunch of requests all at once. This mimics the _batch RPC
        feature found in python-bitcoinlib and bitcoind RPC.
        """
        necessary_keys = ["id", "version", "method", "params"]

        results = []

        for (idx, request) in enumerate(batch_request_entries):
            error = None
            result = None

            # assert presence of important details
            for necessary_key in necessary_keys:
                if not necessary_key in request.keys():
                    raise FakeBitcoinProxyException("Missing necessary key {} for _batch request number {}".format(necessary_key, idx))

            if isinstance(request["params"], list):
                method = getattr(self, request["method"])
                result = method(*request["params"])
            else:
                # matches error message received through python-bitcoinrpc
                error = {"message": "Params must be an array", "code": -32600}

            results.append({
                "error": error,
                "id": request["id"],
                "result": result,
            })

        return results