Completed
Pull Request — master (#118)
by Ruben de
09:49 queued 04:39
created

SizeEstimation::estimateInputsSize()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 6
nop 2
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Script\ScriptFactory;
6
use BitWasp\Bitcoin\Script\ScriptType;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
8
use \BitWasp\Bitcoin\Script\ScriptInfo\Multisig;
9
use \BitWasp\Bitcoin\Script\ScriptInfo\PayToPubKey;
10
use \BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Transaction\TransactionOutputInterface;
13
14
class SizeEstimation
15
{
16
    const SIZE_DER_SIGNATURE = 72;
17
    const SIZE_V0_P2WSH = 36;
18
19
    /**
20
     * @param bool $compressed
21
     * @return int
22
     */
23
    public static function getPublicKeySize($compressed = true) {
24
        return $compressed ? PublicKeyInterface::LENGTH_COMPRESSED : PublicKeyInterface::LENGTH_UNCOMPRESSED;
25
    }
26
27
    /**
28
     * @param int $length
29
     * @return int
30
     */
31 7
    public static function getLengthOfScriptLengthElement($length) {
32 7
        if ($length < 75) {
33 7
            return 1;
34 6
        } else if ($length <= 0xff) {
35 6
            return 2;
36
        } else if ($length <= 0xffff) {
37
            return 3;
38
        } else if ($length <= 0xffffffff) {
39
            return 5;
40
        } else {
41
            throw new \RuntimeException('Size of pushdata too large');
42
        }
43
    }
44
45
    /**
46
     * @param int $length
47
     * @return int
48
     */
49 7
    public static function getLengthOfVarInt($length) {
50 7
        if ($length < 253) {
51 7
            return 1;
52 6
        } else if ($length < 65535) {
53 6
            $num_bytes = 2;
54
        } else if ($length < 4294967295) {
55
            $num_bytes = 4;
56
        } else if ($length < 18446744073709551615) {
57
            $num_bytes = 8;
58
        } else {
59
            throw new \InvalidArgumentException("Invalid decimal");
60
        }
61 6
        return 1 + $num_bytes;
62
    }
63
64
    /**
65
     * @param array $vectorSizes
66
     * @return int|mixed
67
     */
68 2
    public static function getLengthOfVector(array $vectorSizes) {
69 2
        $vectorSize = self::getLengthOfVarInt(count($vectorSizes));
70 2
        foreach ($vectorSizes as $size) {
71 2
            $vectorSize += self::getLengthOfVarInt($size) + $size;
72
        }
73 2
        return $vectorSize;
74
    }
75
76
    /**
77
     * @param Multisig $multisig
78
     * @return array - first is array of stack sizes, second is script len
79
     */
80 7
    public static function estimateMultisigStackSize(Multisig $multisig) {
81 7
        $stackSizes = [0];
82 7
        for ($i = 0; $i < $multisig->getRequiredSigCount(); $i++) {
83 7
            $stackSizes[] = self::SIZE_DER_SIGNATURE;
84
        }
85
86 7
        $scriptSize = 1; // OP_$m
87 7
        $keys = $multisig->getKeyBuffers();
88 7
        for ($i = 0, $n = $multisig->getKeyCount(); $i < $n; $i++) {
89 7
            $scriptSize += 1 + $keys[$i]->getSize();
90
        }
91
92 7
        $scriptSize += 1 + 1; // OP_$n OP_CHECKMULTISIG
93 7
        return [$stackSizes, $scriptSize];
94
    }
95
96
    /**
97
     * @param PayToPubKey $info
98
     * @return array - first is array of stack sizes, second is script len
99
     */
100
    public static function estimateP2PKStackSize(PayToPubKey $info) {
101
        $stackSizes = [self::SIZE_DER_SIGNATURE];
102
103
        $scriptSize = 1 + $info->getKeyBuffer()->getSize(); // PUSHDATA[<75] PUBLICKEY
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
104
        $scriptSize += 1; // OP_CHECKSIG
105
        return [$stackSizes, $scriptSize];
106
    }
107
108
    /**
109
     * @param bool $isCompressed
110
     * @return array - first is array of stack sizes, second is script len
111
     */
112
    public static function estimateP2PKHStackSize($isCompressed = true) {
113
        $pubKeySize = self::getPublicKeySize($isCompressed);
114
        $stackSizes = [self::SIZE_DER_SIGNATURE, $pubKeySize];
115
116
        $scriptSize = 1 + 1 + 1 + 20 + 1 + 1;
117
        return [$stackSizes, $scriptSize];
118
    }
119
120
    /**
121
     * @param array $stackSizes - array of integer size of a value (for scriptSig or witness)
122
     * @param bool $isWitness
123
     * @param ScriptInterface $redeemScript
124
     * @param ScriptInterface $witnessScript
125
     * @return array
126
     */
127 7
    public static function estimateSizeForStack(array $stackSizes, $isWitness, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null) {
128 7
        assert(($witnessScript === null) || $isWitness);
129
130 7
        if ($isWitness) {
131 2
            $scriptSigSizes = [];
132 2
            $witnessSizes = $stackSizes;
133 2
            if ($witnessScript instanceof ScriptInterface) {
134 2
                $witnessSizes[] = $witnessScript->getBuffer()->getSize();
135
            }
136
        } else {
137 6
            $scriptSigSizes = $stackSizes;
138 6
            $witnessSizes = [];
139
        }
140
141 7
        if ($redeemScript instanceof ScriptInterface) {
142 7
            $scriptSigSizes[] = $redeemScript->getBuffer()->getSize();
143
        }
144
145 7
        $scriptSigSize = 0;
146 7
        foreach ($scriptSigSizes as $size) {
147 7
            $len = self::getLengthOfScriptLengthElement($size);
148 7
            $scriptSigSize += $len;
149 7
            $scriptSigSize += $size;
150
        }
151
152
        // Make sure to count the CScriptBase length prefix..
153 7
        $scriptSigSize += self::getLengthOfVarInt($scriptSigSize);
154
155
        // make sure this is set to 0 when not used.
156
        // Summing WitnessSize is used in addition to $withWitness
157
        // because a tx without witnesses _cannot_ use witness serialization
158
        // total witnessSize = 0 means don't use that format
159 7
        $witnessSize = 0;
160 7
        if (count($witnessSizes) > 0) {
161
            // witness has a prefix indicating `n` elements in vector
162 2
            $witnessSize = self::getLengthOfVector($witnessSizes);
163
        }
164 7
        return [$scriptSigSize, $witnessSize];
165
    }
166
167
    /**
168
     * @param ScriptInterface $script - not the scriptPubKey, might be SPK,RS,WS
169
     * @param ScriptInterface|null $redeemScript
170
     * @param ScriptInterface $witnessScript
171
     * @param bool $isWitness
172
     * @return array
173
     */
174 7
    public static function estimateInputFromScripts(ScriptInterface $script, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null, $isWitness) {
175 7
        assert($witnessScript === null || $isWitness);
176 7
        $classifier = new OutputClassifier();
177 7
        if ($classifier->isMultisig($script)) {
178 7
            list ($stackSizes, ) = SizeEstimation::estimateMultisigStackSize(new Multisig($script));
179
        } else if ($classifier->isPayToPublicKey($script)) {
180
            list ($stackSizes, ) = SizeEstimation::estimateP2PKStackSize(new PayToPubKey($script));
181
        } else if ($classifier->isPayToPublicKeyHash($script)) {
182
            // defaults to compressed, tough luck
183
            list ($stackSizes, ) = SizeEstimation::estimateP2PKHStackSize();
184
        } else {
185
            throw new \InvalidArgumentException("Unsupported script type");
186
        }
187
188 7
        return self::estimateSizeForStack($stackSizes, $isWitness, $redeemScript, $witnessScript);
189
    }
190
191
    /**
192
     * @param UTXO $utxo
193
     * @return array
194
     */
195 7
    public static function estimateUtxo(UTXO $utxo) {
196 7
        return self::estimateUtxoFromScripts($utxo->scriptPubKey, $utxo->redeemScript, $utxo->witnessScript);
197
    }
198
199
    /**
200
     * @param ScriptInterface $scriptPubKey
201
     * @param ScriptInterface $redeemScript
202
     * @param ScriptInterface $witnessScript
203
     * @return array
204
     */
205 7
    public static function estimateUtxoFromScripts(
206
        ScriptInterface $scriptPubKey,
207
        ScriptInterface $redeemScript = null,
208
        ScriptInterface $witnessScript = null
209
    ) {
210 7
        $classifier = new OutputClassifier();
211 7
        $decodePK = $classifier->decode($scriptPubKey);
212 7
        $witness = false;
213 7
        if ($decodePK->getType() === ScriptType::P2SH) {
214 7
            if (null === $redeemScript) {
215
                throw new \RuntimeException("Can't estimate, missing redeem script");
216
            }
217 7
            $decodePK = $classifier->decode($redeemScript);
218
        }
219
220 7
        if ($decodePK->getType() === ScriptType::P2WKH) {
221
            $scriptSitu = ScriptFactory::scriptPubKey()->p2pkh($decodePK->getSolution());
222
            $decodePK = $classifier->decode($scriptSitu);
223
            $witness = true;
224 7
        } else if ($decodePK->getType() === ScriptType::P2WSH) {
225 2
            if (null === $witnessScript) {
226
                throw new \RuntimeException("Can't estimate, missing witness script");
227
            }
228 2
            $decodePK = $classifier->decode($witnessScript);
229 2
            $witness = true;
230
        }
231
232 7
        if (!in_array($decodePK->getType(), [ScriptType::MULTISIG, ScriptType::P2PKH, ScriptType::P2PK])) {
233
            throw new \RuntimeException("Unsupported script type");
234
        }
235
236 7
        $script = $decodePK->getScript();
237 7
        list ($scriptSig, $witness) = SizeEstimation::estimateInputFromScripts($script, $redeemScript, $witnessScript, $witness);
238
239
        return [
240 7
            "scriptSig" => $scriptSig,
241 7
            "witness" => $witness,
242
        ];
243
    }
244
245
    /**
246
     * @param UTXO[] $utxos
247
     * @param bool $withWitness
248
     * @return integer
249
     */
250 7
    public static function estimateInputsSize(array $utxos, $withWitness) {
251 7
        $inputSize = 0;
252 7
        $witnessSize = 0;
253 7
        foreach ($utxos as $utxo) {
254 7
            $estimate = SizeEstimation::estimateUtxo($utxo);
255 7
            $inputSize += 32 + 4 + 4;
256 7
            $inputSize += $estimate['scriptSig'];
257 7
            if ($withWitness) {
258 7
                $witnessSize += $estimate['witness'];
259
            }
260
        }
261
262 7
        if ($withWitness && $witnessSize != 0) {
263 2
            $inputSize += $witnessSize;
264 2
            $inputSize += 2; // flag bytes
265
        }
266
267 7
        return $inputSize;
268
    }
269
270
    /**
271
     * @param array $outputs
272
     * @return int
273
     */
274 7
    public static function estimateOutputsSize(array $outputs) {
275 7
        $outputSize = 0;
276 7
        foreach ($outputs as $output) {
277 7
            $size = 8;
278
279 7
            $scriptSize = null;
0 ignored issues
show
Unused Code introduced by
$scriptSize is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
280 7
            if ($output instanceof TransactionOutputInterface) {
281
                $scriptSize = $output->getScript()->getBuffer()->getSize();
282
            } else {
283 7
                if (isset($output['scriptPubKey'])) {
284 7
                    if ($output['scriptPubKey'] instanceof ScriptInterface) {
285 7
                        $scriptSize = $output['scriptPubKey']->getBuffer()->getSize();
286
                    } else {
287 7
                        $scriptSize = strlen($output['scriptPubKey']) / 2;
288
                    }
289
                } else {
290 7
                    $scriptSize = 25;
291
                }
292
            }
293
294 7
            $size += SizeEstimation::getLengthOfVarInt($scriptSize) + $scriptSize;
295 7
            $outputSize += $size;
296
        }
297 7
        return $outputSize;
298
    }
299
300
    /**
301
     * @param UTXO[] $utxos
302
     * @param array $outputs
303
     * @return int
304
     */
305 7
    public static function estimateVsize(array $utxos, array $outputs) {
306 7
        return (int)ceil(self::estimateWeight($utxos, $outputs) / 4);
307
    }
308
309
    /**
310
     * @param UTXO[] $utxos
311
     * @param array $outputs
312
     * @return int
313
     */
314 7
    public static function estimateWeight(array $utxos, array $outputs) {
315 7
        $outputsSize = SizeEstimation::estimateOutputsSize($outputs);
316 7
        $outputsSize += SizeEstimation::getLengthOfVarInt(count($outputs));
317
318
        $baseSize = 4 +
319 7
            SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, false) +
320 7
            $outputsSize + 4;
321
322
        $witnessSize = 4 +
323 7
            SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, true) +
324 7
            $outputsSize + 4;
325
326 7
        return ($baseSize * 3) + $witnessSize;
327
    }
328
}
329