Completed
Pull Request — master (#567)
by thomas
72:50
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
namespace BitWasp\Bitcoin\RpcTest;
4
5
6
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
7
use BitWasp\Bitcoin\Key\PrivateKeyFactory;
8
use BitWasp\Bitcoin\Script\ScriptFactory;
9
use BitWasp\Bitcoin\Transaction\Factory\Signer;
10
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
11
use BitWasp\Bitcoin\Transaction\TransactionInput;
12
use BitWasp\Bitcoin\Transaction\TransactionInterface;
13
use BitWasp\Bitcoin\Transaction\TransactionOutput;
14
use BitWasp\Bitcoin\Utxo\Utxo;
15
16
class RbfTransactionTest extends AbstractTestCase
17
{
18
    /**
19
     * @var RegtestBitcoinFactory
20
     */
21
    private $rpcFactory;
22
23
    public function __construct($name = null, array $data = [], $dataName = '')
24
    {
25
        parent::__construct($name, $data, $dataName);
26
27
        static $rpcFactory = null;
28
        if (null === $rpcFactory) {
29
            $rpcFactory = new RegtestBitcoinFactory();
30
        }
31
        $this->rpcFactory = $rpcFactory;
32
    }
33
34
    private function assertSendRawTransaction($result) {
35
        $this->assertInternalType('array', $result);
36
        $this->assertArrayHasKey('error', $result);
37
        $this->assertEquals(null, $result['error']);
38
39
        $this->assertArrayHasKey('result', $result);
40
        $this->assertEquals(64, strlen($result['result']));
41
    }
42
43
    private function assertBitcoindError($errorCode, $result)
44
    {
45
        $this->assertInternalType('array', $result);
46
        $this->assertArrayHasKey('error', $result);
47
        $this->assertInternalType('array', $result['error']);
48
        $this->assertEquals($errorCode, $result['error']['code']);
49
50
        $this->assertArrayHasKey('error', $result);
51
        $this->assertEquals(null, $result['result']);
52
    }
53
54
    /**
55
     * @param Utxo[] $utxos
56
     * @param PrivateKeyInterface[] $privKeys
57
     * @param TransactionOutput[] $outputs
58
     * @param array $sequences - sequence to set on inputs
59
     * @return TransactionInterface
60
     */
61
    private function createTransaction(array $utxos, array $privKeys, array $outputs, array $sequences)
62
    {
63
        $this->assertEquals(count($utxos), count($privKeys));
64
        $this->assertEquals(count($utxos), count($sequences));
65
66
        // First transaction, spends UTXO 0, to $destSPK 0.25 and $changeSPK 0.2499
67
        $txBuilder = new TxBuilder();
68
69
        $totalIn = 0;
70
        foreach ($utxos as $i => $utxo) {
71
            $txid = $utxo->getOutPoint()->getTxId();
72
            $vout = $utxo->getOutPoint()->getVout();
73
            $txBuilder->input($txid, $vout, null, $sequences[$i]);
74
            $totalIn += $utxo->getOutput()->getValue();
75
        }
76
77
        $totalOut = 0;
78
        foreach ($outputs as $output) {
79
            $txBuilder->output($output->getValue(), $output->getScript());
80
            $totalOut += $output->getValue();
81
        }
82
83
        $this->assertGreaterThanOrEqual($totalOut, $totalIn, "TotalIn should be greater than TotalOut");
84
85
        $signer = new Signer($txBuilder->get());
86
        foreach ($utxos as $i => $utxo) {
87
            $iSigner = $signer
88
                ->input($i, $utxo->getOutput())
89
                ->sign($privKeys[$i])
90
            ;
91
            $this->assertTrue($iSigner->isFullySigned());
92
        }
93
94
        return $signer->get();
95
    }
96
97
    public function testCanReplaceSingleOutputIfOptin()
98
    {
99
        $bitcoind = $this->rpcFactory->startBitcoind();
100
        $this->assertTrue($bitcoind->isRunning());
101
102
        $destKey = PrivateKeyFactory::create(true);
103
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
104
105
        $privateKey = PrivateKeyFactory::create(true);
106
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
107
        $amount = 100000000;
108
109
        /** @var Utxo[] $utxos */
110
        $utxos = [
111
            $bitcoind->fundOutput($amount, $scriptPubKey),
112
            $bitcoind->fundOutput($amount, $scriptPubKey),
113
        ];
114
115
        // Part 1: tx[#1: replacable once]
116
        $tx = $this->createTransaction(
117
            [$utxos[0]],
118
            [$privateKey],
119
            [new TransactionOutput(99999000, $destSPK),],
120
            [TransactionInput::SEQUENCE_FINAL - 2]
121
        );
122
123
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
124
        $this->assertSendRawTransaction($result);
125
126
        // Part 2: tx[#1: not replaceable | #2 not replaceable]
127
        $tx = $this->createTransaction(
128
            $utxos,
129
            [$privateKey, $privateKey],
130
            [new TransactionOutput($amount + 99980000, $destSPK),],
131
            [TransactionInput::SEQUENCE_FINAL - 1, TransactionInput::SEQUENCE_FINAL - 1]
132
        );
133
134
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
135
        $this->assertSendRawTransaction($result);
136
137
        $bitcoind->destroy();
138
139
    }
140
141
    public function testCannotReplaceIfNotOptin()
142
    {
143
        $bitcoind = $this->rpcFactory->startBitcoind();
144
        $this->assertTrue($bitcoind->isRunning());
145
146
        $destKey = PrivateKeyFactory::create(true);
147
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
148
149
        $privateKey = PrivateKeyFactory::create(true);
150
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
151
        $amount = 100000000;
152
153
        /** @var Utxo[] $utxos */
154
        $utxos = [
155
            $bitcoind->fundOutput($amount, $scriptPubKey),
156
            $bitcoind->fundOutput($amount, $scriptPubKey),
157
        ];
158
159
        // Part 1: tx[#1: not replacable]
160
        $tx = $this->createTransaction(
161
            [$utxos[0]],
162
            [$privateKey],
163
            [new TransactionOutput(99990000, $destSPK),],
164
            [TransactionInput::SEQUENCE_FINAL]
165
        );
166
167
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
168
        $this->assertSendRawTransaction($result);
169
170
        // Part 2: [fail] tx[#1: not replacable | #2 replacable]
171
        $tx = $this->createTransaction(
172
            $utxos,
173
            [$privateKey, $privateKey],
174
            [new TransactionOutput($amount + 99990000, $destSPK),],
175
            [TransactionInput::SEQUENCE_FINAL, 0]
176
        );
177
178
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
179
        $this->assertBitcoindError(RpcServer::ERROR_TX_MEMPOOL_CONFLICT, $result);
180
181
        $bitcoind->destroy();
182
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