Completed
Pull Request — master (#567)
by thomas
71:15
created

RbfTransactionTest   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 134
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 134
rs 9.1666
c 0
b 0
f 0
wmc 9
lcom 1
cbo 15

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A assertSendRawTransaction() 0 8 1
A assertBitcoindError() 0 10 1
B createTransaction() 0 35 4
A testCanReplaceTransactionOnce() 0 52 1
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\TransactionInterface;
12
use BitWasp\Bitcoin\Transaction\TransactionOutput;
13
use BitWasp\Bitcoin\Utxo\Utxo;
14
15
class RbfTransactionTest extends AbstractTestCase
16
{
17
    /**
18
     * @var RegtestBitcoinFactory
19
     */
20
    private $rpcFactory;
21
22
    public function __construct($name = null, array $data = [], $dataName = '')
23
    {
24
        parent::__construct($name, $data, $dataName);
25
26
        static $rpcFactory = null;
27
        if (null === $rpcFactory) {
28
            $rpcFactory = new RegtestBitcoinFactory();
29
        }
30
        $this->rpcFactory = $rpcFactory;
31
    }
32
33
    private function assertSendRawTransaction($result) {
34
        $this->assertInternalType('array', $result);
35
        $this->assertArrayHasKey('error', $result);
36
        $this->assertEquals(null, $result['error']);
37
38
        $this->assertArrayHasKey('result', $result);
39
        $this->assertEquals(64, strlen($result['result']));
40
    }
41
42
    private function assertBitcoindError($errorCode, $result)
43
    {
44
        $this->assertInternalType('array', $result);
45
        $this->assertArrayHasKey('error', $result);
46
        $this->assertInternalType('array', $result['error']);
47
        $this->assertEquals($errorCode, $result['error']['code']);
48
49
        $this->assertArrayHasKey('error', $result);
50
        $this->assertEquals(null, $result['result']);
51
    }
52
53
    /**
54
     * @param Utxo[] $utxos
55
     * @param PrivateKeyInterface[] $privKeys
56
     * @param TransactionOutput[] $outputs
57
     * @param array $sequences - sequence to set on inputs
58
     * @return TransactionInterface
59
     */
60
    private function createTransaction(array $utxos, array $privKeys, array $outputs, array $sequences)
61
    {
62
        $this->assertEquals(count($utxos), count($privKeys));
63
        $this->assertEquals(count($utxos), count($sequences));
64
65
        // First transaction, spends UTXO 0, to $destSPK 0.25 and $changeSPK 0.2499
66
        $txBuilder = new TxBuilder();
67
68
        $totalIn = 0;
69
        foreach ($utxos as $i => $utxo) {
70
            $txid = $utxo->getOutPoint()->getTxId();
71
            $vout= $utxo->getOutPoint()->getVout();
72
            $txBuilder->input($txid, $vout, null, $sequences[$i]);
73
            $totalIn += $utxo->getOutput()->getValue();
74
        }
75
76
        $totalOut = 0;
77
        foreach ($outputs as $output) {
78
            $txBuilder->output($output->getValue(), $output->getScript());
79
            $totalOut += $output->getValue();
80
        }
81
82
        $this->assertGreaterThanOrEqual($totalOut, $totalIn, "TotalIn should be greater than TotalOut");
83
84
        $signer = new Signer($txBuilder->get());
85
        foreach ($utxos as $i => $utxo) {
86
            $iSigner = $signer
87
                ->input($i, $utxo->getOutput())
88
                ->sign($privKeys[$i])
89
            ;
90
            $this->assertTrue($iSigner->isFullySigned());
91
        }
92
93
        return $signer->get();
94
    }
95
96
    public function testCanReplaceTransactionOnce()
97
    {
98
        $bitcoind = $this->rpcFactory->startBitcoind();
99
100
        $this->assertTrue($bitcoind->isRunning());
101
102
        $destKey = PrivateKeyFactory::create(true);
103
        $destSPK = ScriptFactory::scriptPubKey()->p2wkh($destKey->getPubKeyHash());
104
105
        $changeKey = PrivateKeyFactory::create(true);
106
        $changeSPK = ScriptFactory::scriptPubKey()->p2wkh($changeKey->getPubKeyHash());
107
108
        $privateKey = PrivateKeyFactory::create(true);
109
        $scriptPubKey = ScriptFactory::scriptPubKey()->p2wkh($privateKey->getPubKeyHash());
110
        $amount = 100000000;
111
112
        /** @var Utxo[] $utxos */
113
        $utxos = [
114
            $bitcoind->fundOutput($amount, $scriptPubKey),
115
            $bitcoind->fundOutput($amount, $scriptPubKey),
116
            $bitcoind->fundOutput($amount, $scriptPubKey),
117
        ];
118
119
        // replacable tx
120
        $tx = $this->createTransaction([$utxos[0]], [$privateKey], [
121
            new TransactionOutput(25000000, $destSPK),
122
            new TransactionOutput(74990000, $changeSPK),
123
        ], [TransactionInterface::MAX_LOCKTIME - 2]);
124
125
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
126
        $this->assertSendRawTransaction($result);
127
128
        // replace tx
129
        $tx = $this->createTransaction([$utxos[0], $utxos[1]], [$privateKey, $privateKey], [
130
            new TransactionOutput(25000000, $destSPK),
131
            new TransactionOutput($utxos[1]->getOutput()->getValue() + 74950000, $changeSPK),
132
        ], [TransactionInterface::MAX_LOCKTIME - 1, TransactionInterface::MAX_LOCKTIME]);
133
134
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
135
        $this->assertSendRawTransaction($result);
136
137
        // cant replace again, mempool tx has final input
138
        $tx = $this->createTransaction($utxos, [$privateKey, $privateKey, $privateKey], [
139
            new TransactionOutput(25000000, $destSPK),
140
            new TransactionOutput((2 * $utxos[1]->getOutput()->getValue()) + 74920000, $changeSPK),
141
        ], [TransactionInterface::MAX_LOCKTIME, TransactionInterface::MAX_LOCKTIME, TransactionInterface::MAX_LOCKTIME]);
142
143
        $result = $bitcoind->makeRpcRequest('sendrawtransaction', [$tx->getHex()]);
144
        $this->assertBitcoindError(RpcServer::ERROR_TX_MEMPOOL_CONFLICT, $result);
145
146
        $bitcoind->destroy();
147
    }
148
}
149