Completed
Pull Request — master (#758)
by thomas
22:04
created

TaprootHasher::hashOutputs()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 8
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 13
ccs 0
cts 12
cp 0
crap 42
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Transaction\SignatureHash;
6
7
use BitWasp\Bitcoin\Crypto\Hash;
8
use BitWasp\Bitcoin\Script\ScriptInterface;
9
use BitWasp\Bitcoin\Script\ScriptWitness;
10
use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializer;
11
use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializerInterface;
12
use BitWasp\Bitcoin\Serializer\Transaction\TransactionOutputSerializer;
13
use BitWasp\Bitcoin\Transaction\TransactionInterface;
14
use BitWasp\Bitcoin\Transaction\TransactionOutputInterface;
15
use BitWasp\Buffertools\Buffer;
16
use BitWasp\Buffertools\BufferInterface;
17
use BitWasp\Buffertools\Buffertools;
18
19
class TaprootHasher extends SigHash
20
{
21
    /**
22
     * @var TransactionInterface
23
     */
24
    protected $tx;
25
26
    /**
27
     * @var int
28
     */
29
    protected $amount;
30
31
    /**
32
     * @var array|TransactionOutputInterface[]
33
     */
34
    protected $spentOutputs;
35
36
    /**
37
     * @var TransactionOutputSerializer
38
     */
39
    protected $outputSerializer;
40
41
    /**
42
     * @var OutPointSerializerInterface
43
     */
44
    protected $outpointSerializer;
45
46
    /**
47
     * V1Hasher constructor.
48
     * @param TransactionInterface $transaction
49
     * @param int $amount
50
     * @param TransactionOutputInterface[] $txOuts
51
     * @param OutPointSerializerInterface $outpointSerializer
52
     * @param TransactionOutputSerializer|null $outputSerializer
53
     */
54
    public function __construct(
55
        TransactionInterface $transaction,
56
        int $amount,
57
        array $txOuts,
58
        OutPointSerializerInterface $outpointSerializer = null,
59
        TransactionOutputSerializer $outputSerializer = null
60
    ) {
61
        $this->amount = $amount;
62
        $this->spentOutputs = $txOuts;
63
        $this->outputSerializer = $outputSerializer ?: new TransactionOutputSerializer();
64
        $this->outpointSerializer = $outpointSerializer ?: new OutPointSerializer();
65
        parent::__construct($transaction);
66
    }
67
68
    /**
69
     * Same as V1Hasher, but with sha256 instead of sha256d
70
     * @param int $sighashType
71
     * @return BufferInterface
72
     */
73
    public function hashPrevOuts(int $sighashType): BufferInterface
74
    {
75
        if (!($sighashType & SigHash::ANYONECANPAY)) {
76
            $binary = '';
77
            foreach ($this->tx->getInputs() as $input) {
78
                $binary .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary();
79
            }
80
            return Hash::sha256(new Buffer($binary));
81
        }
82
83
        return new Buffer('', 32);
84
    }
85
86
    /**
87
     * Same as V1Hasher, but with sha256 instead of sha256d
88
     * @param int $sighashType
89
     * @return BufferInterface
90
     */
91
    public function hashSequences(int $sighashType): BufferInterface
92
    {
93
        if (!($sighashType & SigHash::ANYONECANPAY) && ($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) {
94
            $binary = '';
95
            foreach ($this->tx->getInputs() as $input) {
96
                $binary .= pack('V', $input->getSequence());
97
            }
98
99
            return Hash::sha256(new Buffer($binary));
100
        }
101
102
        return new Buffer('', 32);
103
    }
104
105
    /**
106
     * Same as V1Hasher, but with sha256 instead of sha256d
107
     * @param int $sighashType
108
     * @param int $inputToSign
109
     * @return BufferInterface
110
     */
111
    public function hashOutputs(int $sighashType, int $inputToSign): BufferInterface
112
    {
113
        if (($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) {
114
            $binary = '';
115
            foreach ($this->tx->getOutputs() as $output) {
116
                $binary .= $this->outputSerializer->serialize($output)->getBinary();
117
            }
118
            return Hash::sha256(new Buffer($binary));
119
        } elseif (($sighashType & 0x1f) === SigHash::SINGLE && $inputToSign < count($this->tx->getOutputs())) {
120
            return Hash::sha256($this->outputSerializer->serialize($this->tx->getOutput($inputToSign)));
121
        }
122
123
        return new Buffer('', 32);
124
    }
125
126
    /**
127
     * @param TransactionOutputInterface[] $txOuts
128
     * @return BufferInterface
129
     */
130
    public function hashSpentAmountsHash(array $txOuts): BufferInterface
131
    {
132
        $binary = '';
133
        foreach ($txOuts as $output) {
134
            $binary .= pack("P", $output->getValue());
135
        }
136
        return Hash::sha256(new Buffer($binary));
137
    }
138
139
    /**
140
     * Calculate the hash of the current transaction, when you are looking to
141
     * spend $txOut, and are signing $inputToSign. The SigHashType defaults to
142
     * SIGHASH_ALL
143
     *
144
     * Note: this function doesn't use txOutScript, as we have access to it via
145
     * spentOutputs.
146
     *
147
     * @param ScriptInterface $txOutScript
148
     * @param int $inputToSign
149
     * @param int $sighashType
150
     * @return BufferInterface
151
     * @throws \Exception
152
     */
153
    public function calculate(
154
        ScriptInterface $txOutScript,
155
        int $inputToSign,
156
        int $sighashType = SigHash::ALL
157
    ): BufferInterface {
158
        if (($sighashType > 3) && ($sighashType < 0x81 || $sighashType > 0x83)) {
159
            throw new \RuntimeException("invalid hash type");
160
        }
161
        $epoch = 0;
162
        $input = $this->tx->getInput($inputToSign);
163
164
        $ss = '';
165
        $ss .= pack("C", $epoch);
166
        $ss .= pack('CVV', $sighashType, $this->tx->getVersion(), $this->tx->getLockTime());
167
168
        $inputType = $sighashType & SigHash::TAPINPUTMASK;
169
        $outputType = $sighashType & SigHash::TAPOUTPUTMASK;
170
171
        if ($inputType === SigHash::TAPDEFAULT) {
172
            $ss .= $this->hashPrevOuts($sighashType)->getBinary();
173
            $ss .= $this->hashSpentAmountsHash($this->spentOutputs)->getBinary();
174
            $ss .= $this->hashSequences($sighashType)->getBinary();
175
        }
176
        if ($outputType === SigHash::TAPDEFAULT || $outputType === SigHash::ALL) {
177
            $ss .= $this->hashOutputs($sighashType, $inputToSign)->getBinary();
178
        }
179
180
        $scriptPubKey = $this->spentOutputs[$inputToSign]->getScript()->getBuffer();
181
        $spendType = 0;
182
        $witnesses = $this->tx->getWitnesses();
183
184
        // todo: does back() == bottom()?
185
        $witness = new ScriptWitness();
186
        if (array_key_exists($inputToSign, $witnesses)) {
187
            $witness = $witnesses[$inputToSign];
188
            if ($witness->count() > 1 && $witness->bottom()->getSize() > 0 && ord($witness->bottom()->getBinary()[0]) === 0xff) {
189
                $spendType |= 1;
190
            }
191
        }
192
193
        $ss .= pack('C', $spendType);
194
        $ss .= Buffertools::numToVarIntBin($scriptPubKey->getSize()) . $scriptPubKey->getBinary();
195
196
        if ($inputType === SigHash::ANYONECANPAY) {
197
            $ss .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary();
198
            $ss .= pack('P', $this->spentOutputs[$inputToSign]->getValue());
199
            $ss .= pack('V', $input->getSequence());
200
        } else {
201
            $ss .= pack('V', $inputToSign);
202
        }
203
        if (($spendType & 2) != 0) {
204
            $ss .= Hash::sha256($witness->bottom())->getBinary();
205
        }
206
207
        if ($outputType == SigHash::SINGLE) {
208
            $outputs = $this->tx->getOutputs();
209
            if ($inputToSign >= count($outputs)) {
210
                throw new \RuntimeException("sighash single input > #outputs");
211
            }
212
            $ss .= Hash::sha256($this->outputSerializer->serialize($outputs[$inputToSign]))->getBinary();
213
        }
214
215
        return Hash::taggedSha256('TapSighash', new Buffer($ss));
216
    }
217
}
218