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