1 | <?php |
||
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 |