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

CheckerBase::checkSequence()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8.0189

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 14
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 26
ccs 14
cts 15
cp 0.9333
crap 8.0189
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Script\Interpreter;
6
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\PublicKeySerializerInterface;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\DerSignatureSerializerInterface;
13
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\SchnorrSignatureSerializerInterface;
14
use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException;
15
use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical;
16
use BitWasp\Bitcoin\Locktime;
17
use BitWasp\Bitcoin\Script\ScriptInterface;
18
use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer;
19
use BitWasp\Bitcoin\Signature\TransactionSignature;
20
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
21
use BitWasp\Bitcoin\Transaction\SignatureHash\TaprootHasher;
22
use BitWasp\Bitcoin\Transaction\TransactionInput;
23
use BitWasp\Bitcoin\Transaction\TransactionInputInterface;
24
use BitWasp\Bitcoin\Transaction\TransactionInterface;
25
use BitWasp\Bitcoin\Transaction\TransactionOutputInterface;
26
use BitWasp\Buffertools\Buffer;
27
use BitWasp\Buffertools\BufferInterface;
28
29
abstract class CheckerBase
30
{
31
    /**
32
     * @var EcAdapterInterface
33
     */
34
    protected $adapter;
35
36
    /**
37
     * @var TransactionInterface
38
     */
39
    protected $transaction;
40
41
    /**
42
     * @var int
43
     */
44
    protected $nInput;
45
46
    /**
47
     * @var int
48
     */
49
    protected $amount;
50
51
    /**
52
     * @var array
53
     */
54
    protected $sigCache = [];
55
56
    /**
57
     * @var array
58
     */
59
    protected $schnorrSigHashCache = [];
60
61
    /**
62
     * @var TransactionOutputInterface[]
63
     */
64
    protected $spentOutputs = [];
65
66
    /**
67
     * @var TransactionSignatureSerializer
68
     */
69
    private $sigSerializer;
70
71
    /**
72
     * @var PublicKeySerializerInterface
73
     */
74
    private $pubKeySerializer;
75
76
    /**
77
     * @var XOnlyPublicKeySerializerInterface
78
     */
79
    private $xonlyKeySerializer;
80
81
    /**
82
     * @var SchnorrSignatureSerializerInterface
83
     */
84
    private $schnorrSigSerializer;
85
86
    /**
87
     * @var int
88
     */
89
    protected $sigHashOptionalBits = SigHash::ANYONECANPAY;
90
91
    /**
92
     * CheckerBase constructor.
93
     * @param EcAdapterInterface $ecAdapter
94
     * @param TransactionInterface $transaction
95
     * @param int $nInput
96
     * @param int $amount
97
     * @param TransactionSignatureSerializer|null $sigSerializer
98
     * @param PublicKeySerializerInterface|null $pubKeySerializer
99
     * @param XOnlyPublicKeySerializerInterface|null $xonlyKeySerializer
100
     * @param SchnorrSignatureSerializerInterface|null $schnorrSigSerializer
101
     */
102 4808
    public function __construct(
103
        EcAdapterInterface $ecAdapter,
104
        TransactionInterface $transaction,
105
        int $nInput,
106
        int $amount,
107
        TransactionSignatureSerializer $sigSerializer = null,
108
        PublicKeySerializerInterface $pubKeySerializer = null,
109
        XOnlyPublicKeySerializerInterface $xonlyKeySerializer = null,
110
        SchnorrSignatureSerializerInterface $schnorrSigSerializer = null
111
    ) {
112 4808
        $this->sigSerializer = $sigSerializer ?: new TransactionSignatureSerializer(EcSerializer::getSerializer(DerSignatureSerializerInterface::class, true, $ecAdapter));
113 4808
        $this->pubKeySerializer = $pubKeySerializer ?: EcSerializer::getSerializer(PublicKeySerializerInterface::class, true, $ecAdapter);
114 4808
        $this->xonlyKeySerializer = $xonlyKeySerializer ?: EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $ecAdapter);
115 4808
        $this->schnorrSigSerializer = $schnorrSigSerializer ?: EcSerializer::getSerializer(SchnorrSignatureSerializerInterface::class, true, $ecAdapter);
116 4808
        $this->adapter = $ecAdapter;
117 4808
        $this->transaction = $transaction;
118 4808
        $this->nInput = $nInput;
119 4808
        $this->amount = $amount;
120 4808
    }
121
122
    public function setSpentOutputs(array $txOuts)
123
    {
124
        if (count($txOuts) !== count($this->transaction->getInputs())) {
125
            throw new \RuntimeException("number of spent txouts should equal number of inputs");
126
        }
127
        $this->spentOutputs = $txOuts;
128
    }
129
130
    /**
131
     * @param ScriptInterface $script
132
     * @param int $hashType
133
     * @param int $sigVersion
134
     * @return BufferInterface
135
     */
136
    abstract public function getSigHash(ScriptInterface $script, int $hashType, int $sigVersion): BufferInterface;
137
138
    /**
139
     * @param BufferInterface $signature
140
     * @return bool
141
     */
142 195
    public function isValidSignatureEncoding(BufferInterface $signature): bool
143
    {
144
        try {
145 195
            TransactionSignature::isDERSignature($signature);
146 105
            return true;
147 98
        } catch (SignatureNotCanonical $e) {
148
            /* In any case, we will return false outside this block */
149
        }
150
151 98
        return false;
152
    }
153
154
    /**
155
     * @param BufferInterface $signature
156
     * @return bool
157
     * @throws ScriptRuntimeException
158
     * @throws \Exception
159
     */
160 7
    public function isLowDerSignature(BufferInterface $signature): bool
161
    {
162 7
        if (!$this->isValidSignatureEncoding($signature)) {
163 1
            throw new ScriptRuntimeException(Interpreter::VERIFY_DERSIG, 'Signature with incorrect encoding');
164
        }
165
166 6
        $binary = $signature->getBinary();
167 6
        $nLenR = ord($binary[3]);
168 6
        $nLenS = ord($binary[5 + $nLenR]);
169 6
        $s = $signature->slice(6 + $nLenR, $nLenS)->getGmp();
170
171 6
        return $this->adapter->validateSignatureElement($s, true);
172
    }
173
174
    /**
175
     * @param int $hashType
176
     * @return bool
177
     */
178 137
    public function isDefinedHashtype(int $hashType): bool
179
    {
180 137
        $nHashType = $hashType & (~($this->sigHashOptionalBits));
181
182 137
        return !(($nHashType < SigHash::ALL) || ($nHashType > SigHash::SINGLE));
183
    }
184
185
    /**
186
     * Determine whether the sighash byte appended to the signature encodes
187
     * a valid sighash type.
188
     *
189
     * @param BufferInterface $signature
190
     * @return bool
191
     */
192 44
    public function isDefinedHashtypeSignature(BufferInterface $signature): bool
193
    {
194 44
        if ($signature->getSize() === 0) {
195 1
            return false;
196
        }
197
198 43
        $binary = $signature->getBinary();
199 43
        return $this->isDefinedHashtype(ord(substr($binary, -1)));
200
    }
201
202
    /**
203
     * @param BufferInterface $signature
204
     * @param int $flags
205
     * @return $this
206
     * @throws \BitWasp\Bitcoin\Exceptions\ScriptRuntimeException
207
     */
208 578
    public function checkSignatureEncoding(BufferInterface $signature, int $flags)
209
    {
210 578
        if ($signature->getSize() === 0) {
211 85
            return $this;
212
        }
213
214 501
        if (($flags & (Interpreter::VERIFY_DERSIG | Interpreter::VERIFY_LOW_S | Interpreter::VERIFY_STRICTENC)) !== 0 && !$this->isValidSignatureEncoding($signature)) {
215 97
            throw new ScriptRuntimeException(Interpreter::VERIFY_DERSIG, 'Signature with incorrect encoding');
216 412
        } else if (($flags & Interpreter::VERIFY_LOW_S) !== 0 && !$this->isLowDerSignature($signature)) {
217 5
            throw new ScriptRuntimeException(Interpreter::VERIFY_LOW_S, 'Signature s element was not low');
218 407
        } else if (($flags & Interpreter::VERIFY_STRICTENC) !== 0 && !$this->isDefinedHashtypeSignature($signature)) {
219 9
            throw new ScriptRuntimeException(Interpreter::VERIFY_STRICTENC, 'Signature with invalid hashtype');
220
        }
221
222 398
        return $this;
223
    }
224
225
    /**
226
     * @param BufferInterface $publicKey
227
     * @param int $flags
228
     * @return $this
229
     * @throws \Exception
230
     */
231 473
    public function checkPublicKeyEncoding(BufferInterface $publicKey, int $flags)
232
    {
233 473
        if (($flags & Interpreter::VERIFY_STRICTENC) !== 0 && !PublicKey::isCompressedOrUncompressed($publicKey)) {
234 21
            throw new ScriptRuntimeException(Interpreter::VERIFY_STRICTENC, 'Public key with incorrect encoding');
235
        }
236
237 452
        return $this;
238
    }
239
240
    /**
241
     * @param ScriptInterface $script
242
     * @param BufferInterface $sigBuf
243
     * @param BufferInterface $keyBuf
244
     * @param int $sigVersion
245
     * @param int $flags
246
     * @return bool
247
     * @throws ScriptRuntimeException
248
     */
249 571
    public function checkSig(ScriptInterface $script, BufferInterface $sigBuf, BufferInterface $keyBuf, int $sigVersion, int $flags)
250
    {
251
        $this
252 571
            ->checkSignatureEncoding($sigBuf, $flags)
253 471
            ->checkPublicKeyEncoding($keyBuf, $flags);
254
255
        try {
256 451
            $cacheCheck = "{$script->getBinary()}{$sigVersion}{$keyBuf->getBinary()}{$sigBuf->getBinary()}";
257 451
            if (!isset($this->sigCache[$cacheCheck])) {
258 451
                $txSignature = $this->sigSerializer->parse($sigBuf);
259 339
                $publicKey = $this->pubKeySerializer->parse($keyBuf);
260
261 323
                $hash = $this->getSigHash($script, $txSignature->getHashType(), $sigVersion);
262 323
                $result = $this->sigCache[$cacheCheck] = $publicKey->verify($hash, $txSignature->getSignature());
263
            } else {
264 8
                $result = $this->sigCache[$cacheCheck];
265
            }
266
267 323
            return $result;
268 136
        } catch (\Exception $e) {
269 136
            return false;
270
        }
271
    }
272
273
    public function getTaprootSigHash(int $sigHashType, int $sigVersion): BufferInterface
274
    {
275
        $cacheCheck = $sigVersion . $sigHashType;
276
        if (!isset($this->schnorrSigHashCache[$cacheCheck])) {
277
            $hasher = new TaprootHasher($this->transaction, $this->amount, $this->spentOutputs);
278
279
            $hash = $hasher->calculate($this->spentOutputs[$this->nInput]->getScript(), $this->nInput, $sigHashType);
280
            $this->schnorrSigHashCache[$cacheCheck] = $hash->getBinary();
281
        } else {
282
            $hash = new Buffer($this->schnorrSigHashCache[$cacheCheck], 32);
283
        }
284
285
        return $hash;
286
    }
287
288
    public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, int $sigVersion): bool
289
    {
290
        if ($sig64->getSize() === 0) {
291
            return false;
292
        }
293
        if ($key32->getSize() !== 32) {
294
            return false;
295
        }
296
297
        $hashType = SigHash::TAPDEFAULT;
298
        if ($sig64->getSize() === 65) {
299
            $hashType = $sig64->slice(64, 1);
300
            if ($hashType == SigHash::TAPDEFAULT) {
0 ignored issues
show
introduced by
The condition $hashType == BitWasp\Bit...ash\SigHash::TAPDEFAULT is always false.
Loading history...
301
                return false;
302
            }
303
            $sig64 = $sig64->slice(0, 64);
304
        }
305
306
        if ($sig64->getSize() !== 64) {
307
            return false;
308
        }
309
310
        try {
311
            $sig = $this->schnorrSigSerializer->parse($sig64);
312
            $pubKey = $this->xonlyKeySerializer->parse($key32);
313
            $sigHash = $this->getTaprootSigHash($hashType, $sigVersion);
0 ignored issues
show
Bug introduced by
It seems like $hashType can also be of type BitWasp\Buffertools\BufferInterface; however, parameter $sigHashType of BitWasp\Bitcoin\Script\I...se::getTaprootSigHash() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
            $sigHash = $this->getTaprootSigHash(/** @scrutinizer ignore-type */ $hashType, $sigVersion);
Loading history...
314
            return $pubKey->verifySchnorr($sigHash, $sig);
315
        } catch (\Exception $e) {
316
            return false;
317
        }
318
    }
319
320
    /**
321
     * @param \BitWasp\Bitcoin\Script\Interpreter\Number $scriptLockTime
322
     * @return bool
323
     */
324 8
    public function checkLockTime(\BitWasp\Bitcoin\Script\Interpreter\Number $scriptLockTime): bool
325
    {
326 8
        $input = $this->transaction->getInput($this->nInput);
327 8
        $nLockTime = $scriptLockTime->getInt();
328 8
        $txLockTime = $this->transaction->getLockTime();
329
330 8
        if (!(($txLockTime < Locktime::BLOCK_MAX && $nLockTime < Locktime::BLOCK_MAX) ||
331 8
            ($txLockTime >= Locktime::BLOCK_MAX && $nLockTime >= Locktime::BLOCK_MAX))
332
        ) {
333 2
            return false;
334
        }
335
336 6
        if ($nLockTime > $txLockTime) {
337 2
            return false;
338
        }
339
340 4
        if ($input->isFinal()) {
341 1
            return false;
342
        }
343
344 3
        return true;
345
    }
346
347
    /**
348
     * @param \BitWasp\Bitcoin\Script\Interpreter\Number $sequence
349
     * @return bool
350
     */
351 15
    public function checkSequence(\BitWasp\Bitcoin\Script\Interpreter\Number $sequence): bool
352
    {
353 15
        $txSequence = $this->transaction->getInput($this->nInput)->getSequence();
354 15
        if ($this->transaction->getVersion() < 2) {
355 10
            return false;
356
        }
357
358 5
        if (($txSequence & TransactionInputInterface::SEQUENCE_LOCKTIME_DISABLE_FLAG) !== 0) {
359 1
            return false;
360
        }
361
362 4
        $mask = TransactionInputInterface::SEQUENCE_LOCKTIME_TYPE_FLAG | TransactionInputInterface::SEQUENCE_LOCKTIME_MASK;
363
364 4
        $txToSequenceMasked = $txSequence & $mask;
365 4
        $nSequenceMasked = $sequence->getInt() & $mask;
366 4
        if (!(($txToSequenceMasked < TransactionInput::SEQUENCE_LOCKTIME_TYPE_FLAG && $nSequenceMasked < TransactionInput::SEQUENCE_LOCKTIME_TYPE_FLAG) ||
367 4
            ($txToSequenceMasked >= TransactionInput::SEQUENCE_LOCKTIME_TYPE_FLAG && $nSequenceMasked >= TransactionInput::SEQUENCE_LOCKTIME_TYPE_FLAG))
368
        ) {
369 2
            return false;
370
        }
371
372 2
        if ($nSequenceMasked > $txToSequenceMasked) {
373
            return false;
374
        }
375
376 2
        return true;
377
    }
378
}
379