Completed
Pull Request — master (#567)
by thomas
72:50
created

RpcServer::activateSoftforks()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 3
nop 0
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\RpcTest;
4
5
6
use BitWasp\Bitcoin\Network\NetworkInterface;
7
use BitWasp\Bitcoin\Script\ScriptInterface;
8
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
9
use BitWasp\Bitcoin\Transaction\OutPoint;
10
use BitWasp\Bitcoin\Transaction\TransactionFactory;
11
use BitWasp\Bitcoin\Transaction\TransactionOutput;
12
use BitWasp\Bitcoin\Utxo\Utxo;
13
use BitWasp\Buffertools\Buffer;
14
use Nbobtc\Command\Command;
15
use Nbobtc\Http\Client;
16
17
class RpcServer
18
{
19
    const ERROR_STARTUP = -28;
20
    const ERROR_TX_MEMPOOL_CONFLICT = -26;
21
22
    /**
23
     * @var string
24
     */
25
    private $dataDir;
26
27
    /**
28
     * @var string
29
     */
30
    private $bitcoind;
31
32
    /**
33
     * @var NetworkInterface
34
     */
35
    private $network;
36
37
    /**
38
     * @var RpcCredential
39
     */
40
    private $credential;
41
42
    /**
43
     * @var Client
44
     */
45
    private $client;
46
47
    /**
48
     * @var bool
49
     */
50
    private $softforks = false;
51
52
    private $defaultOptions = [
53
        "daemon" => 1,
54
        "server" => 1,
55
        "regtest" => 1,
56
    ];
57
58
    private $options = [];
59
60
    /**
61
     * RpcServer constructor.
62
     * @param $bitcoind
63
     * @param $dataDir
64
     * @param NetworkInterface $network
65
     * @param RpcCredential $credential
66
     * @param array $options
67
     */
68
    public function __construct($bitcoind, $dataDir, NetworkInterface $network, RpcCredential $credential, array $options = [])
69
    {
70
        $this->bitcoind = $bitcoind;
71
        $this->dataDir = $dataDir;
72
        $this->network = $network;
73
        $this->credential = $credential;
74
        $this->options = array_merge($options, $this->defaultOptions);
75
    }
76
77
    /**
78
     * @return string
79
     */
80
    private function getPidFile()
81
    {
82
        return "{$this->dataDir}/regtest/bitcoind.pid";
83
    }
84
85
    /**
86
     * @return string
87
     */
88
    private function getConfigFile()
89
    {
90
        return "{$this->dataDir}/bitcoin.conf";
91
    }
92
93
    /**
94
     * @param RpcCredential $rpcCredential
95
     */
96
    private function writeConfigToFile(RpcCredential $rpcCredential)
97
    {
98
        $fd = fopen($this->getConfigFile(), "w");
99
        if (!$fd) {
100
            throw new \RuntimeException("Failed to open bitcoin.conf for writing");
101
        }
102
103
        $config = array_merge(
104
            $this->options,
105
            $rpcCredential->getConfigArray()
106
        );
107
108
        $iniConfig = implode("\n", array_map(function ($value, $key) {
109
            return "{$key}={$value}";
110
        }, $config, array_keys($config)));
111
112
        if (!fwrite($fd, $iniConfig)) {
113
            throw new \RuntimeException("Failed to write to bitcoin.conf");
114
        }
115
116
        fclose($fd);
117
    }
118
119
    /**
120
     * @return void
121
     */
122
    public function start()
123
    {
124
        if ($this->isRunning()) {
125
            return;
126
        }
127
128
        $this->writeConfigToFile($this->credential);
129
        $res = 0;
130
        $out = '';
131
        $result = exec(sprintf("%s -datadir=%s", $this->bitcoind, $this->dataDir), $out, $res);
132
133
        if ($res !== 0) {
134
            throw new \RuntimeException("Failed to start bitcoind: {$this->dataDir}\n");
135
        }
136
137
        $start = microtime(true);
138
        $limit = 10;
139
        $connected = false;
140
141
        $conn = $this->getClient();
142
        do {
143
            try {
144
                $result = json_decode($conn->sendCommand(new Command("getblockchaininfo"))->getBody()->getContents(), true);
145
                if ($result['error'] === null) {
146
                    $connected = true;
147
                } else {
148
                    if ($result['error']['code'] !== self::ERROR_STARTUP) {
149
                        throw new \RuntimeException("Unexpected error code during startup");
150
                    }
151
152
                    sleep(0.2);
153
                }
154
155
            } catch (\Exception $e) {
156
                sleep(0.2);
157
            }
158
159
            if (microtime(true) > $start + $limit) {
160
                throw new \RuntimeException("Timeout elapsed, never made connection to bitcoind");
161
            }
162
        } while (!$connected);
163
    }
164
165
    private function getClient() {
166
        $client = new \Nbobtc\Http\Client($this->credential->getDsn());
167
        $client->withDriver(new CurlDriver());
168
        return $client;
169
    }
170
171
    private function activateSoftforks()
172
    {
173
        if ($this->softforks) {
174
            return;
175
        }
176
177
        $chainInfo = $this->makeRpcRequest('getblockchaininfo');
178
        $bestHeight = $chainInfo['result']['blocks'];
179
180
        while($bestHeight < 150 || $chainInfo['result']['bip9_softforks']['segwit']['status'] !== 'active') {
181
            // ought to finish in 1!
182
            $this->makeRpcRequest("generate", [435]);
183
            $chainInfo = $this->makeRpcRequest('getblockchaininfo');
184
            $bestHeight = $chainInfo['result']['blocks'];
185
        }
186
187
        $this->softforks = true;
188
    }
189
190
    /**
191
     * @param int $value
192
     * @param ScriptInterface $script
193
     * @return Utxo
194
     */
195
    public function fundOutput($value, $script)
196
    {
197
        $this->activateSoftforks();
198
199
        $builder = new TxBuilder();
200
        $builder->output($value, $script);
201
        $hex = $builder->get()->getHex();
202
203
        $result = $this->makeRpcRequest('fundrawtransaction', [$hex, ['feeRate'=>0.0001]]);
204
        $unsigned = $result['result']['hex'];
205
        $result = $this->makeRpcRequest('signrawtransaction', [$unsigned]);
206
        $signedHex = $result['result']['hex'];
207
        $signed = TransactionFactory::fromHex($signedHex);
208
209
        $outIdx = -1;
210
        foreach ($signed->getOutputs() as $i => $output) {
211
            if ($output->getScript()->equals($script)) {
212
                $outIdx = $i;
213
            }
214
        }
215
216
        if ($outIdx === -1) {
217
            throw new \RuntimeException("Sanity check failed, should have found the output we funded");
218
        }
219
220
        $result = $this->makeRpcRequest('sendrawtransaction', [$signedHex]);
221
        $txid = $result['result'];
222
        $this->makeRpcRequest("generate", [1]);
223
224
        return new Utxo(new OutPoint(Buffer::hex($txid), $outIdx), new TransactionOutput($value, $script));
225
    }
226
227
    /**
228
     * @param string $src
229
     */
230
    private function recursiveDelete($src)
231
    {
232
        $dir = opendir($src);
233
        while(false !== ( $file = readdir($dir)) ) {
234
            if (( $file != '.' ) && ( $file != '..' )) {
235
                $full = $src . '/' . $file;
236
                if ( is_dir($full) ) {
237
                    $this->recursiveDelete($full);
238
                }
239
                else {
240
                    unlink($full);
241
                }
242
            }
243
        }
244
        closedir($dir);
245
        rmdir($src);
246
    }
247
248
    /**
249
     * @return void
250
     */
251
    public function destroy()
252
    {
253
        if ($this->isRunning()) {
254
            $this->request("stop");
255
256
            do {
257
                sleep(0.2);
258
            } while($this->isRunning());
259
260
            $this->recursiveDelete($this->dataDir);
261
        }
262
    }
263
264
    /**
265
     * @return bool
266
     */
267
    public function isRunning()
268
    {
269
        return file_exists($this->getPidFile());
270
    }
271
272
    /**
273
     * @return Client
274
     */
275
    public function makeClient()
276
    {
277
        if (!$this->isRunning()) {
278
            throw new \RuntimeException("No client, server not running");
279
        }
280
281
        if (null === $this->client) {
282
            $this->client = $this->getClient();
283
        }
284
285
        return $this->client;
286
    }
287
288
    /**
289
     * @param string $method
290
     * @param array $params
291
     * @return mixed
292
     */
293
    public function request($method, array $params = [])
294
    {
295
        $unsorted = $this->makeClient()->sendCommand(new Command($method, $params));
296
        $jsonResult = $unsorted->getBody()->getContents();
297
        $json = json_decode($jsonResult, true);
298
        if (false === $json) {
299
            throw new \RuntimeException("Invalid JSON from server");
300
        }
301
        return $json;
302
    }
303
304
    /**
305
     * @param string $method
306
     * @param array $params
307
     * @return mixed
308
     */
309
    public function makeRpcRequest($method, $params = [])
310
    {
311
        return $this->request($method, $params);
312
    }
313
314
}
315