Completed
Branch master (e62670)
by
unknown
02:05
created

WalletSweeper::setBitcoinLibMagicBytes()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 7
nop 3
dl 0
loc 22
ccs 0
cts 21
cp 0
crap 72
rs 6.6037
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\Network\NetworkFactory;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\WitnessScript;
12
use BitWasp\Bitcoin\Transaction\Factory\SignData;
13
use BitWasp\Bitcoin\Transaction\Factory\Signer;
14
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
15
use BitWasp\Bitcoin\Transaction\OutPoint;
16
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
17
use BitWasp\Bitcoin\Transaction\TransactionInterface;
18
use BitWasp\Bitcoin\Transaction\TransactionOutput;
19
use BitWasp\Buffertools\Buffer;
20
use BitWasp\Buffertools\BufferInterface;
21
use Blocktrail\SDK\Address\AddressReaderBase;
22
use Blocktrail\SDK\Address\BitcoinAddressReader;
23
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
24
use Blocktrail\SDK\Address\CashAddress;
25
use Blocktrail\SDK\Bitcoin\BIP32Key;
26
use Blocktrail\SDK\Bitcoin\BIP32Path;
27
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
28
use Blocktrail\SDK\Network\BitcoinCash;
29
use Blocktrail\SDK\Network\BitcoinCashRegtest;
30
31
abstract class WalletSweeper {
32
33
    /**
34
     * network to use - currently only supporting 'bitcoin'
35
     * @var string
36
     */
37
    protected $network;
38
39
    /**
40
     * using testnet or not
41
     * @var bool
42
     */
43
    protected $testnet;
44
45
    /**
46
     * backup private key
47
     * @var BIP32Key
48
     */
49
    protected $backupPrivateKey;
50
51
    /**
52
     * primary private key
53
     * @var BIP32Key
54
     */
55
    protected $primaryPrivateKey;
56
57
    /**
58
     * blocktrail public keys, mapped to their relevant keyIndex
59
     * @var
60
     */
61
    protected $blocktrailPublicKeys;
62
63
    /**
64
     * gets unspent outputs for addresses
65
     * @var UnspentOutputFinder
66
     */
67
    protected $utxoFinder;
68
69
    /**
70
     * holds wallet addresses, along with path, redeem script, and discovered unspent outputs
71
     * @var array
72
     */
73
    protected $sweepData;
74
75
    /**
76
     * @var AddressReaderBase
77
     */
78
    protected $addressReader;
79
80
    /**
81
     * process logging for debugging
82
     * @var bool
83
     */
84
    protected $debug = false;
85
86
    /**
87
     * @param BufferInterface     $primarySeed
88
     * @param BufferInterface     $backupSeed
89
     * @param array               $blocktrailPublicKeys =
90
     * @param UnspentOutputFinder $utxoFinder
91
     * @param string              $network
92
     * @param bool                $testnet
93
     * @throws \Exception
94
     */
95
    public function __construct(BufferInterface $primarySeed, BufferInterface $backupSeed, array $blocktrailPublicKeys, UnspentOutputFinder $utxoFinder, $network = 'btc', $testnet = false) {
96
        // normalize network and set bitcoinlib to the right magic-bytes
97
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
98
99
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
100
        $this->addressReader = $this->makeAddressReader([
101
            "use_cashaddress" => false,
102
        ]);
103
104
        //create BIP32 keys for the Blocktrail public keys
105
        foreach ($blocktrailPublicKeys as $blocktrailKey) {
106
            $this->blocktrailPublicKeys[$blocktrailKey['keyIndex']] = BlocktrailSDK::normalizeBIP32Key([$blocktrailKey['pubkey'], $blocktrailKey['path']]);
107
        }
108
109
        $this->utxoFinder = $utxoFinder;
110
111
        $this->primaryPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
112
        $this->backupPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m");
113
    }
114
115
    /**
116
     * @param array $options
117
     * @return AddressReaderBase
118
     */
119
    private function makeAddressReader(array $options) {
120
        if ($this->network == "bitcoincash") {
121
            $useCashAddress = false;
122
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
123
                $useCashAddress = true;
124
            }
125
            return new BitcoinCashAddressReader($useCashAddress);
126
        } else {
127
            return new BitcoinAddressReader();
128
        }
129
    }
130
131
    /**
132
     * set BitcoinLib to the correct magic-byte defaults for the selected network
133
     *
134
     * @param $network
135
     * @param $testnet
136
     */
137
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
138
        assert($network == "bitcoin" || $network == "bitcoincash");
139
        if ($network === "bitcoin") {
140
            if ($regtest) {
141
                $useNetwork = NetworkFactory::bitcoinRegtest();
142
            } else if ($testnet) {
143
                $useNetwork = NetworkFactory::bitcoinTestnet();
144
            } else {
145
                $useNetwork = NetworkFactory::bitcoin();
146
            }
147
        } else if ($network === "bitcoincash") {
148
            if ($regtest) {
149
                $useNetwork = new BitcoinCashRegtest();
150
            } else if ($testnet) {
151
                $useNetwork = new BitcoinCashTestnet();
152
            } else {
153
                $useNetwork = new BitcoinCash();
154
            }
155
        }
156
157
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
158
    }
159
160
    /**
161
     * enable debug info logging (just to console)
162
     */
163
    public function enableLogging() {
164
        $this->debug = true;
165
        $this->utxoFinder->enableLogging();
166
    }
167
168
    /**
169
     * disable debug info logging
170
     */
171
    public function disableLogging() {
172
        $this->debug = false;
173
        $this->utxoFinder->disableLogging();
174
    }
175
176
    /**
177
     * normalize network string
178
     *
179
     * @param $network
180
     * @param $testnet
181
     * @return array
182
     * @throws \Exception
183
     */
184
    protected function normalizeNetwork($network, $testnet) {
185
        return Util::normalizeNetwork($network, $testnet);
186
    }
187
188
    /**
189
     * generate multisig address for given path
190
     *
191
     * @param string|BIP32Path $path
192
     * @return array[string, ScriptInterface]                 [address, redeemScript]
0 ignored issues
show
Documentation introduced by
The doc-type array[string, could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
193
     * @throws \Exception
194
     */
195
    protected function createAddress($path) {
196
        $path = BIP32Path::path($path)->publicPath();
197
198
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
199
            $this->primaryPrivateKey->buildKey($path)->publicKey(),
200
            $this->backupPrivateKey->buildKey($path->unhardenedPath())->publicKey(),
201
            $this->getBlocktrailPublicKey($path)->buildKey($path)->publicKey()
202
        ]), false);
203
204
        if ($this->network !== "bitcoincash" && (int) $path[2] === 2) {
205
            $witnessScript = new WitnessScript($multisig);
206
            $redeemScript = new P2shScript($witnessScript);
207
            $address = $this->addressReader->fromOutputScript($redeemScript->getOutputScript())->getAddress();
208
        } else {
209
            $witnessScript = null;
210
            $redeemScript = new P2shScript($multisig);
211
            $address = $this->addressReader->fromOutputScript($redeemScript->getOutputScript())->getAddress();
212
        }
213
214
        return [$address, $redeemScript, $witnessScript];
215
    }
216
217
    /**
218
     * create a batch of multisig addresses
219
     *
220
     * @param $start
221
     * @param $count
222
     * @param $keyIndex
223
     * @return array
224
     */
225
    protected function createBatchAddresses($start, $count, $keyIndex) {
226
        $addresses = array();
227
228
        for ($i = 0; $i < $count; $i++) {
229
            //create a path subsequent address
230
            $path = (string)WalletPath::create($keyIndex, $_chain = 0, $start+$i)->path()->publicPath();
231
            list($address, $redeem, $witness) = $this->createAddress($path);
232
            $addresses[$address] = array(
233
                //'address' => $address,
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
234
                'redeem' => $redeem,
235
                'witness' => $witness,
236
                'path' => $path
237
            );
238
239
            if ($this->debug) {
240
                echo ".";
241
            }
242
        }
243
244
        return $addresses;
245
    }
246
247
248
    /**
249
     * gets the blocktrail pub key for the given path from the stored array of pub keys
250
     *
251
     * @param string|BIP32Path  $path
252
     * @return BIP32Key
253
     * @throws \Exception
254
     */
255
    protected function getBlocktrailPublicKey($path) {
256
        $path = BIP32Path::path($path);
257
258
        $keyIndex = str_replace("'", "", $path[1]);
259
260
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
261
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
262
        }
263
264
        return $this->blocktrailPublicKeys[$keyIndex];
265
    }
266
267
    /**
268
     * discover funds in the wallet
269
     *
270
     * @param int $increment how many addresses to scan at a time
271
     * @return array
272
     * @throws BlocktrailSDKException
273
     */
274
    public function discoverWalletFunds($increment = 200) {
275
        $totalBalance = 0;
276
        $totalUTXOs = 0;
277
        $totalAddressesGenerated = 0;
278
279
        $addressUTXOs = array();    //addresses and their utxos, paths and redeem scripts
280
281
        //for each blocktrail pub key, do fund discovery on batches of addresses
282
        foreach ($this->blocktrailPublicKeys as $keyIndex => $blocktrailPubKey) {
283
            $i = 0;
284
            do {
285
                if ($this->debug) {
286
                    echo "\ngenerating $increment addresses using blocktrail key index $keyIndex\n";
287
                }
288
                $addresses = $this->createBatchAddresses($i, $increment, $keyIndex);
289
                $totalAddressesGenerated += count($addresses);
290
291
                if ($this->debug) {
292
                    echo "\nstarting fund discovery for $increment addresses";
293
                }
294
295
                //get the unspent outputs for this batch of addresses
296
                $utxos = $this->utxoFinder->getUTXOs(array_keys($addresses));
297
                //save the address utxos, along with relevant path and redeem script
298
                foreach ($utxos as $utxo) {
299
                    if (!isset($utxo['address'], $utxo['value'])) {
300
                        throw new BlocktrailSDKException("Missing data");
301
                    }
302
303
                    $address = $utxo['address'];
304
305
                    if (!isset($addressUTXOs[$address])) {
306
                        $addressUTXOs[$address] = [
307
                            'path' =>  $addresses[$address]['path'],
308
                            'redeem' =>  $addresses[$address]['redeem'],
309
                            'witness' =>  $addresses[$address]['witness'],
310
                            'utxos' =>  [],
311
                        ];
312
                    }
313
314
                    $addressUTXOs[$address]['utxos'][] = $utxo;
315
                    $totalUTXOs ++;
316
317
                    //add up the total utxo value for all addresses
318
                    $totalBalance += $utxo['value'];
319
                }
320
321
                if ($this->debug) {
322
                    echo "\nfound ".count($utxos)." unspent outputs";
323
                }
324
325
                //increment for next batch
326
                $i += $increment;
327
            } while (count($utxos) > 0);
328
        }
329
330
        if ($this->debug) {
331
            echo "\nfinished fund discovery: $totalBalance Satoshi (in $totalUTXOs outputs) found when searching $totalAddressesGenerated addresses";
332
        }
333
334
        $this->sweepData = ['utxos' => $addressUTXOs, 'count' => $totalUTXOs, 'balance' => $totalBalance, 'addressesSearched' => $totalAddressesGenerated];
335
        return $this->sweepData;
336
    }
337
338
    /**
339
     * sweep the wallet of all funds and send to a single address
340
     *
341
     * @param string    $destinationAddress     address to receive found funds
342
     * @param int       $sweepBatchSize         number of addresses to search at a time
343
     * @return string                           HEX string of signed transaction
344
     * @throws \Exception
345
     */
346
    public function sweepWallet($destinationAddress, $sweepBatchSize = 200) {
347
        if ($this->debug) {
348
            echo "\nstarting wallet sweeping to address $destinationAddress";
349
        }
350
351
        //do wallet fund discovery
352
        if (!isset($this->sweepData)) {
353
            $this->discoverWalletFunds($sweepBatchSize);
354
        }
355
356
        if ($this->sweepData['balance'] === 0) {
357
            //no funds found
358
            throw new \Exception("No funds found after searching through {$this->sweepData['addressesSearched']} addresses");
359
        }
360
361
        //create and sign the transaction
362
        $transaction = $this->createTransaction($destinationAddress);
363
364
        //return or send the transaction
365
        return $transaction->getHex();
366
    }
367
368
    /**
369
     * @param $destinationAddress
370
     * @return TransactionInterface
371
     */
372
    protected function createTransaction($destinationAddress) {
373
        $txb = new TxBuilder();
374
375
        $signInfo = [];
376
        $utxos = [];
377
        foreach ($this->sweepData['utxos'] as $address => $data) {
378
            foreach ($data['utxos'] as $utxo) {
379
                $utxo = new UTXO(
380
                    $utxo['hash'],
381
                    $utxo['index'],
382
                    $utxo['value'],
383
                    AddressFactory::fromString($address),
384
                    ScriptFactory::fromHex($utxo['script_hex']),
385
                    $data['path'],
386
                    $data['redeem'],
387
                    $data['witness'],
388
                    SignInfo::MODE_SIGN
389
                );
390
391
                $utxos[] = $utxo;
392
                $signInfo[] = $utxo->getSignInfo();
393
            }
394
        }
395
396
        foreach ($utxos as $utxo) {
397
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash, 32), $utxo->index), $utxo->scriptPubKey);
398
        }
399
400
        if ($this->network === "bitcoincash") {
401
            $tAddress = $this->makeAddressReader(['use_cashaddr' => true])->fromString($destinationAddress);
402
            if ($tAddress instanceof CashAddress) {
403
                $destinationAddress = $tAddress->getLegacyAddress()->getAddress();
404
            }
405
        }
406
407
        $destAddress = $this->addressReader->fromString($destinationAddress);
408
        $vsizeEstimation = SizeEstimation::estimateVsize($utxos, [
409
            new TransactionOutput(0, $destAddress->getScriptPubKey())
410
        ]);
411
412
        $fee = Wallet::baseFeeForSize($vsizeEstimation);
413
414
        $txb->payToAddress($this->sweepData['balance'] - $fee, $destAddress);
415
416
        if ($this->debug) {
417
            echo "\nSigning transaction";
418
        }
419
420
        $tx = $this->signTransaction($txb->get(), $signInfo);
421
422
        return $tx;
423
    }
424
425
    /**
426
     * sign a raw transaction with the private keys that we have
427
     *
428
     * @param TransactionInterface $tx
429
     * @param SignInfo[]  $signInfo
430
     * @return TransactionInterface
431
     * @throws \Exception
432
     */
433
    protected function signTransaction(TransactionInterface $tx, array $signInfo) {
434
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
435
436
        $sigHash = SigHash::ALL;
437
        if ($this->network === "bitcoincash") {
438
            $sigHash |= SigHash::BITCOINCASH;
439
            $signer->redeemBitcoinCash(true);
440
        }
441
442
        assert(Util::all(function ($signInfo) {
443
            return $signInfo instanceof SignInfo;
444
        }, $signInfo), '$signInfo should be SignInfo[]');
445
446
        foreach ($signInfo as $idx => $info) {
447
            if ($info->mode === SignInfo::MODE_SIGN) {
448
                $path = BIP32Path::path($info->path)->privatePath();
449
                $redeemScript = $info->redeemScript;
450
                $output = $info->output;
451
452
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
453
                $backupKey = $this->backupPrivateKey->buildKey($path->unhardenedPath())->key()->getPrivateKey();
454
455
                $signData = (new SignData())
456
                    ->p2sh($redeemScript);
457
458
                if ($info->witnessScript) {
459
                    $signData->p2wsh($info->witnessScript);
460
                }
461
462
                $input = $signer->input($idx, $output, $signData);
463
464
                $input->sign($key, $sigHash);
465
                $input->sign($backupKey, $sigHash);
466
            }
467
        }
468
469
        return $signer->get();
470
    }
471
}
472