Completed
Branch master (8754f2)
by
unknown
02:10
created

SizeEstimation   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 91.49%

Importance

Changes 0
Metric Value
dl 0
loc 300
ccs 129
cts 141
cp 0.9149
rs 8.6206
c 0
b 0
f 0
wmc 50
lcom 1
cbo 11

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getPublicKeySize() 0 3 2
B getLengthOfScriptLengthElement() 0 13 5
B getLengthOfVarInt() 0 14 5
A getLengthOfVector() 0 7 2
A estimateMultisigStackSize() 0 15 3
A estimateP2PKStackSize() 0 7 1
A estimateP2PKHStackSize() 0 7 1
C estimateSizeForStack() 0 39 7
B estimateInputFromScripts() 0 16 5
C estimateUtxo() 0 35 7
B estimateInputsSize() 0 19 5
B estimateOutputsSize() 0 25 5
A estimateVsize() 0 3 1
A estimateWeight() 0 13 1

How to fix   Complexity   

Complex Class

Complex classes like SizeEstimation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SizeEstimation, and based on these observations, apply Extract Interface, too.

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 3
    public static function getPublicKeySize($compressed = true) {
24 3
        return $compressed ? PublicKeyInterface::LENGTH_COMPRESSED : PublicKeyInterface::LENGTH_UNCOMPRESSED;
25
    }
26
27
    /**
28
     * @param int $length
29
     * @return int
30
     */
31 26
    public static function getLengthOfScriptLengthElement($length) {
32 26
        if ($length < 75) {
33 18
            return 1;
34 17
        } else if ($length <= 0xff) {
35 13
            return 2;
36 4
        } else if ($length <= 0xffff) {
37 3
            return 3;
38 1
        } else if ($length <= 0xffffffff) {
39 1
            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 33
    public static function getLengthOfVarInt($length) {
50 33
        if ($length < 253) {
51 20
            return 1;
52 19
        } else if ($length < 65535) {
53 12
            $num_bytes = 2;
54 7
        } else if ($length < 4294967295) {
55 4
            $num_bytes = 4;
56 3
        } else if ($length < 18446744073709551615) {
57 3
            $num_bytes = 8;
58
        } else {
59
            throw new \InvalidArgumentException("Invalid decimal");
60
        }
61 19
        return 1 + $num_bytes;
62
    }
63
64
    /**
65
     * @param array $vectorSizes
66
     * @return int|mixed
67
     */
68 9
    public static function getLengthOfVector(array $vectorSizes) {
69 9
        $vectorSize = self::getLengthOfVarInt(count($vectorSizes));
70 9
        foreach ($vectorSizes as $size) {
71 9
            $vectorSize += self::getLengthOfVarInt($size) + $size;
72
        }
73 9
        return $vectorSize;
74
    }
75
76
    /**
77
     * @param Multisig $multisig
78
     * @return array - first is array of stack sizes, second is script len
79
     */
80 20
    public static function estimateMultisigStackSize(Multisig $multisig) {
81 20
        $stackSizes = [0];
82 20
        for ($i = 0; $i < $multisig->getRequiredSigCount(); $i++) {
83 20
            $stackSizes[] = self::SIZE_DER_SIGNATURE;
84
        }
85
86 20
        $scriptSize = 1; // OP_$m
87 20
        $keys = $multisig->getKeyBuffers();
88 20
        for ($i = 0, $n = $multisig->getKeyCount(); $i < $n; $i++) {
89 20
            $scriptSize += 1 + $keys[$i]->getSize();
90
        }
91
92 20
        $scriptSize += 1 + 1; // OP_$n OP_CHECKMULTISIG
93 20
        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 3
    public static function estimateP2PKHStackSize($isCompressed = true) {
113 3
        $pubKeySize = self::getPublicKeySize($isCompressed);
114 3
        $stackSizes = [self::SIZE_DER_SIGNATURE, $pubKeySize];
115
116 3
        $scriptSize = 1 + 1 + 1 + 20 + 1 + 1;
117 3
        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 20
    public static function estimateSizeForStack(array $stackSizes, $isWitness, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null) {
128 20
        assert(($witnessScript === null) || $isWitness);
129
130 20
        if ($isWitness) {
131 9
            $scriptSigSizes = [];
132 9
            $witnessSizes = $stackSizes;
133 9
            if ($witnessScript instanceof ScriptInterface) {
134 9
                $witnessSizes[] = $witnessScript->getBuffer()->getSize();
135
            }
136
        } else {
137 11
            $scriptSigSizes = $stackSizes;
138 11
            $witnessSizes = [];
139
        }
140
141 20
        if ($redeemScript instanceof ScriptInterface) {
142 13
            $scriptSigSizes[] = $redeemScript->getBuffer()->getSize();
143
        }
144
145 20
        $scriptSigSize = 0;
146 20
        foreach ($scriptSigSizes as $size) {
147 15
            $len = self::getLengthOfScriptLengthElement($size);
148 15
            $scriptSigSize += $len;
149 15
            $scriptSigSize += $size;
150
        }
151
152
        // Make sure to count the CScriptBase length prefix..
153 20
        $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 20
        $witnessSize = 0;
160 20
        if (count($witnessSizes) > 0) {
161
            // witness has a prefix indicating `n` elements in vector
162 9
            $witnessSize = self::getLengthOfVector($witnessSizes);
163
        }
164 20
        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 16
    public static function estimateInputFromScripts(ScriptInterface $script, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null, $isWitness) {
175 16
        assert($witnessScript === null || $isWitness);
176 16
        $classifier = new OutputClassifier();
177 16
        if ($classifier->isMultisig($script)) {
178 13
            list ($stackSizes, ) = SizeEstimation::estimateMultisigStackSize(new Multisig($script));
179 3
        } else if ($classifier->isPayToPublicKey($script)) {
180
            list ($stackSizes, ) = SizeEstimation::estimateP2PKStackSize(new PayToPubKey($script));
181 3
        } else if ($classifier->isPayToPublicKeyHash($script)) {
182
            // defaults to compressed, tough luck
183 3
            list ($stackSizes, ) = SizeEstimation::estimateP2PKHStackSize();
184
        } else {
185
            throw new \InvalidArgumentException("Unsupported script type");
186
        }
187
188 16
        return self::estimateSizeForStack($stackSizes, $isWitness, $redeemScript, $witnessScript);
189
    }
190
191
    /**
192
     * @param UTXO $utxo
193
     * @return array
194
     */
195 16
    public static function estimateUtxo(UTXO $utxo) {
196 16
        $classifier = new OutputClassifier();
197 16
        $decodePK = $classifier->decode($utxo->scriptPubKey);
198 16
        $witness = false;
199 16
        if ($decodePK->getType() === ScriptType::P2SH) {
200 11
            if (null === $utxo->redeemScript) {
201
                throw new \RuntimeException("Can't estimate, missing redeem script");
202
            }
203 11
            $decodePK = $classifier->decode($utxo->redeemScript);
204
        }
205
206 16
        if ($decodePK->getType() === ScriptType::P2WKH) {
207 3
            $scriptSitu = ScriptFactory::scriptPubKey()->p2pkh($decodePK->getSolution());
208 3
            $decodePK = $classifier->decode($scriptSitu);
209 3
            $witness = true;
210 13
        } else if ($decodePK->getType() === ScriptType::P2WSH) {
211 4
            if (null === $utxo->witnessScript) {
212
                throw new \RuntimeException("Can't estimate, missing witness script");
213
            }
214 4
            $decodePK = $classifier->decode($utxo->witnessScript);
215 4
            $witness = true;
216
        }
217
218 16
        if (!in_array($decodePK->getType(), [ScriptType::MULTISIG, ScriptType::P2PKH, ScriptType::P2PK])) {
219
            throw new \RuntimeException("Unsupported script type");
220
        }
221
222 16
        $script = $decodePK->getScript();
223 16
        list ($scriptSig, $witness) = SizeEstimation::estimateInputFromScripts($script, $utxo->redeemScript, $utxo->witnessScript, $witness);
224
225
        return [
226 16
            "scriptSig" => $scriptSig,
227 16
            "witness" => $witness,
228
        ];
229
    }
230
231
    /**
232
     * @param UTXO[] $utxos
233
     * @param bool $withWitness
234
     * @return integer
235
     */
236 11
    public static function estimateInputsSize(array $utxos, $withWitness) {
237 11
        $inputSize = 0;
238 11
        $witnessSize = 0;
239 11
        foreach ($utxos as $utxo) {
240 11
            $estimate = SizeEstimation::estimateUtxo($utxo);
241 11
            $inputSize += 32 + 4 + 4;
242 11
            $inputSize += $estimate['scriptSig'];
243 11
            if ($withWitness) {
244 11
                $witnessSize += $estimate['witness'];
245
            }
246
        }
247
248 11
        if ($withWitness && $witnessSize != 0) {
249 5
            $inputSize += $witnessSize;
250 5
            $inputSize += 2; // flag bytes
251
        }
252
253 11
        return $inputSize;
254
    }
255
256
    /**
257
     * @param array $outputs
258
     * @return int
259
     */
260 11
    public static function estimateOutputsSize(array $outputs) {
261 11
        $outputSize = 0;
262 11
        foreach ($outputs as $output) {
263 11
            $outputSize += 8;
264
265 11
            $scriptSize = null;
266 11
            if ($output instanceof TransactionOutputInterface) {
267 4
                $scriptSize = $output->getScript()->getBuffer()->getSize();
268
            } else {
269 11
                if (isset($output['scriptPubKey'])) {
270 5
                    if ($output['scriptPubKey'] instanceof ScriptInterface) {
271 5
                        $scriptSize = $output['scriptPubKey']->getBuffer()->getSize();
272
                    } else {
273 5
                        $scriptSize = strlen($output['scriptPubKey']) / 2;
274
                    }
275
                } else {
276 7
                    $scriptSize += 25;
277
                }
278
            }
279
280 11
            $outputSize += SizeEstimation::getLengthOfVarInt($scriptSize) + $scriptSize;
281
        }
282
283 11
        return $outputSize;
284
    }
285
286
    /**
287
     * @param UTXO[] $utxos
288
     * @param array $outputs
289
     * @return int
290
     */
291 11
    public static function estimateVsize(array $utxos, array $outputs) {
292 11
        return (int)ceil(self::estimateWeight($utxos, $outputs) / 4);
293
    }
294
295
    /**
296
     * @param UTXO[] $utxos
297
     * @param array $outputs
298
     * @return int
299
     */
300 11
    public static function estimateWeight(array $utxos, array $outputs) {
301 11
        $outputsSize = SizeEstimation::estimateOutputsSize($outputs);
302
        $baseSize = 4
303 11
            + SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, false)
304 11
            + SizeEstimation::getLengthOfVarInt(count($outputs)) + $outputsSize
305 11
            + 4;
306
        $witnessSize = 4
307 11
            + SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, true)
308 11
            + SizeEstimation::getLengthOfVarInt(count($outputs)) + $outputsSize
309 11
            + 4;
310
311 11
        return ($baseSize * 3) + $witnessSize;
312
    }
313
}
314