Completed
Pull Request — master (#591)
by thomas
22:54 queued 09:12
created

RbfTransactionTest::testCannotReplaceIfNotOptin()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 26
nc 1
nop 0
dl 0
loc 43
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\RpcTest;
6
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
8
use BitWasp\Bitcoin\Key\PrivateKeyFactory;
9
use BitWasp\Bitcoin\Script\ScriptFactory;
10
use BitWasp\Bitcoin\Transaction\Factory\Signer;
11
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
12
use BitWasp\Bitcoin\Transaction\TransactionInput;
13
use BitWasp\Bitcoin\Transaction\TransactionInterface;
14
use BitWasp\Bitcoin\Transaction\TransactionOutput;
15
use BitWasp\Bitcoin\Utxo\Utxo;
16
17
class RbfTransactionTest extends AbstractTestCase
18
{
19
    /**
20
     * @var RegtestBitcoinFactory
21
     */
22
    private $rpcFactory;
23
24
    public function __construct($name = null, array $data = [], $dataName = '')
25
    {
26
        parent::__construct($name, $data, $dataName);
27
28
        static $rpcFactory = null;
29
        if (null === $rpcFactory) {
30
            $rpcFactory = new RegtestBitcoinFactory();
31
        }
32
        $this->rpcFactory = $rpcFactory;
33
    }
34
35
    private function assertSendRawTransaction($result)
36
    {
37
        $this->assertInternalType('array', $result);
38
        $this->assertArrayHasKey('error', $result);
39
        $this->assertEquals(null, $result['error']);
40
41
        $this->assertArrayHasKey('result', $result);
42
        $this->assertEquals(64, strlen($result['result']));
43
    }
44
45
    private function assertBitcoindError($errorCode, $result)
46
    {
47
        $this->assertInternalType('array', $result);
48
        $this->assertArrayHasKey('error', $result);
49
        $this->assertInternalType('array', $result['error']);
50
        $this->assertEquals($errorCode, $result['error']['code']);
51
52
        $this->assertArrayHasKey('error', $result);
53
        $this->assertEquals(null, $result['result']);
54
    }
55
56
    /**
57
     * @param Utxo[] $utxos
58
     * @param PrivateKeyInterface[] $privKeys
59
     * @param TransactionOutput[] $outputs
60
     * @param array $sequences - sequence to set on inputs
61
     * @return TransactionInterface
62
     */
63
    private function createTransaction(array $utxos, array $privKeys, array $outputs, array $sequences)
64
    {
65
        $this->assertEquals(count($utxos), count($privKeys));
66
        $this->assertEquals(count($utxos), count($sequences));
67
68
        // First transaction, spends UTXO 0, to $destSPK 0.25 and $changeSPK 0.2499
69
        $txBuilder = new TxBuilder();
70
71
        $totalIn = 0;
72
        foreach ($utxos as $i => $utxo) {
73
            $txid = $utxo->getOutPoint()->getTxId();
74
            $vout = $utxo->getOutPoint()->getVout();
75
            $txBuilder->input($txid, $vout, null, $sequences[$i]);
76
            $totalIn += $utxo->getOutput()->getValue();
77
        }
78
79
        $totalOut = 0;
80
        foreach ($outputs as $output) {
81
            $txBuilder->output($output->getValue(), $output->getScript());
82
            $totalOut += $output->getValue();
83
        }
84
85
        $this->assertGreaterThanOrEqual($totalOut, $totalIn, "TotalIn should be greater than TotalOut");
86
87
        $signer = new Signer($txBuilder->get());
88
        foreach ($utxos as $i => $utxo) {
89
            $iSigner = $signer
90
                ->input($i, $utxo->getOutput())
91
                ->sign($privKeys[$i])
92
            ;
93
            $this->assertTrue($iSigner->isFullySigned());
94
        }
95
96
        return $signer->get();
97
    }
98
99
    public function testCanReplaceSingleOutputIfOptin()
100
    {
101
        $bitcoind = $this->rpcFactory->startBitcoind();
102
        $this->assertTrue($bitcoind->isRunning());
103
104
        $destKey = PrivateKeyFactory::create(true);
105
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
106
107
        $privateKey = PrivateKeyFactory::create(true);
108
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
109
        $amount = 100000000;
110
111
        /** @var Utxo[] $utxos */
112
        $utxos = [
113
            $bitcoind->fundOutput($amount, $scriptPubKey),
114
            $bitcoind->fundOutput($amount, $scriptPubKey),
115
        ];
116
117
        // Part 1: tx[#1: replacable once]
118
        $tx = $this->createTransaction(
119
            [$utxos[0]],
120
            [$privateKey],
121
            [new TransactionOutput(99999000, $destSPK),],
122
            [TransactionInput::SEQUENCE_FINAL - 2]
123
        );
124
125
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
126
        $this->assertSendRawTransaction($result);
127
128
        // Part 2: tx[#1: not replaceable | #2 not replaceable]
129
        $tx = $this->createTransaction(
130
            $utxos,
131
            [$privateKey, $privateKey],
132
            [new TransactionOutput($amount + 99980000, $destSPK),],
133
            [TransactionInput::SEQUENCE_FINAL - 1, TransactionInput::SEQUENCE_FINAL - 1]
134
        );
135
136
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
137
        $this->assertSendRawTransaction($result);
138
139
        $bitcoind->destroy();
140
    }
141
142
    public function testCannotReplaceIfNotOptin()
143
    {
144
        $bitcoind = $this->rpcFactory->startBitcoind();
145
        $this->assertTrue($bitcoind->isRunning());
146
147
        $destKey = PrivateKeyFactory::create(true);
148
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
149
150
        $privateKey = PrivateKeyFactory::create(true);
151
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
152
        $amount = 100000000;
153
154
        /** @var Utxo[] $utxos */
155
        $utxos = [
156
            $bitcoind->fundOutput($amount, $scriptPubKey),
157
            $bitcoind->fundOutput($amount, $scriptPubKey),
158
        ];
159
160
        // Part 1: tx[#1: not replacable]
161
        $tx = $this->createTransaction(
162
            [$utxos[0]],
163
            [$privateKey],
164
            [new TransactionOutput(99990000, $destSPK),],
165
            [TransactionInput::SEQUENCE_FINAL]
166
        );
167
168
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
169
        $this->assertSendRawTransaction($result);
170
171
        // Part 2: [fail] tx[#1: not replacable | #2 replacable]
172
        $tx = $this->createTransaction(
173
            $utxos,
174
            [$privateKey, $privateKey],
175
            [new TransactionOutput($amount + 99990000, $destSPK),],
176
            [TransactionInput::SEQUENCE_FINAL, 0]
177
        );
178
179
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
180
        $this->assertBitcoindError(RpcServer::ERROR_TX_MEMPOOL_CONFLICT, $result);
181
182
        $bitcoind->destroy();
183
    }
184
185
    public function testCanReplaceIfOptin()
186
    {
187
        $bitcoind = $this->rpcFactory->startBitcoind();
188
        $this->assertTrue($bitcoind->isRunning());
189
190
        $destKey = PrivateKeyFactory::create(true);
191
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
192
193
        $changeKey = PrivateKeyFactory::create(true);
194
        $changeSPK = ScriptFactory::scriptPubKey()->p2wkh($changeKey->getPubKeyHash());
195
196
        $privateKey = PrivateKeyFactory::create(true);
197
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
198
        $amount = 100000000;
199
200
        /** @var Utxo[] $utxos */
201
        $utxos = [
202
            $bitcoind->fundOutput($amount, $scriptPubKey),
203
            $bitcoind->fundOutput($amount, $scriptPubKey),
204
            $bitcoind->fundOutput($amount, $scriptPubKey),
205
            $bitcoind->fundOutput($amount, $scriptPubKey),
206
        ];
207
208
209
        // Part 1: replacable tx[#1: replaceable 2]
210
        $nIn = 1;
211
        $tx = $this->createTransaction(
212
            array_slice($utxos, 0, $nIn),
213
            array_fill(0, $nIn, $privateKey),
214
            [
215
                new TransactionOutput(25000000, $destSPK),
216
                new TransactionOutput(74990000, $changeSPK),
217
            ],
218
            array_fill(0, $nIn, TransactionInput::SEQUENCE_FINAL - 3)
219
        );
220
221
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
222
        $this->assertSendRawTransaction($result);
223
224
225
        // Part 2: replace tx[#1: replaceable 1 | #2: replaceable 1]
226
        $nIn = 2;
227
        $tx = $this->createTransaction(
228
            array_slice($utxos, 0, $nIn),
229
            array_fill(0, $nIn, $privateKey),
230
            [
231
                new TransactionOutput(25000000, $destSPK),
232
                new TransactionOutput($amount + 74950000, $changeSPK),
233
            ],
234
            array_fill(0, $nIn, TransactionInput::SEQUENCE_FINAL - 2)
235
        );
236
237
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
238
        $this->assertSendRawTransaction($result);
239
240
241
        // Part 3: replace tx[#1: replaceable 0 | #2: replaceable 0 | #3: replaceable 0]
242
        $nIn = 3;
243
        $tx = $this->createTransaction(
244
            array_slice($utxos, 0, $nIn),
245
            array_fill(0, $nIn, $privateKey),
246
            [
247
                new TransactionOutput(25000000, $destSPK),
248
                new TransactionOutput((2 * $amount) + 74920000, $changeSPK),
249
            ],
250
            array_fill(0, $nIn, TransactionInput::SEQUENCE_FINAL - 1)
251
        );
252
253
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
254
        $this->assertSendRawTransaction($result);
255
256
257
        // Part 4: this one won't work, inputs are all irreplacable
258
        $nIn = 4;
259
        $tx = $this->createTransaction(
260
            array_slice($utxos, 0, $nIn),
261
            array_fill(0, $nIn, $privateKey),
262
            [
263
                new TransactionOutput(25000000, $destSPK),
264
                new TransactionOutput((3 * $amount) + 74900000, $changeSPK),
265
            ],
266
            array_fill(0, $nIn, TransactionInput::SEQUENCE_FINAL)
267
        );
268
269
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
270
        $this->assertBitcoindError(RpcServer::ERROR_TX_MEMPOOL_CONFLICT, $result);
271
272
        $bitcoind->destroy();
273
    }
274
}
275