Completed
Pull Request — master (#93)
by thomas
17:10
created

SizeEstimation::getLengthOfVector()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 9.4285
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 2
    public static function getPublicKeySize($compressed = true) {
24 2
        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 31
    public static function getLengthOfVarInt($length) {
50 31
        if ($length < 253) {
51 18
            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 8
    public static function getLengthOfVector(array $vectorSizes) {
69 8
        $vectorSize = self::getLengthOfVarInt(count($vectorSizes));
70 8
        foreach ($vectorSizes as $size) {
71 8
            $vectorSize += self::getLengthOfVarInt($size) + $size;
72
        }
73 8
        return $vectorSize;
74
    }
75
76
    /**
77
     * @param Multisig $multisig
78
     * @return array - first is array of stack sizes, second is script len
79
     */
80 19
    public static function estimateMultisigStackSize(Multisig $multisig) {
81 19
        $stackSizes = [0];
82 19
        for ($i = 0; $i < $multisig->getRequiredSigCount(); $i++) {
83 19
            $stackSizes[] = self::SIZE_DER_SIGNATURE;
84
        }
85
86 19
        $scriptSize = 1; // OP_$m
87 19
        $keys = $multisig->getKeyBuffers();
88 19
        for ($i = 0, $n = $multisig->getKeyCount(); $i < $n; $i++) {
89 19
            $scriptSize += 1 + $keys[$i]->getSize();
90
        }
91
92 19
        $scriptSize += 1 + 1; // OP_$n OP_CHECKMULTISIG
93 19
        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 2
    public static function estimateP2PKHStackSize($isCompressed = true) {
113 2
        $pubKeySize = self::getPublicKeySize($isCompressed);
114 2
        $stackSizes = [self::SIZE_DER_SIGNATURE, $pubKeySize];
115
116 2
        $scriptSize = 1 + 1 + 1 + 20 + 1 + 1;
117 2
        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 18
    public static function estimateSizeForStack(array $stackSizes, $isWitness, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null) {
128 18
        assert(($witnessScript === null) || $isWitness);
129
130 18
        if ($isWitness) {
131 8
            $scriptSigSizes = [];
132 8
            $witnessSizes = $stackSizes;
133 8
            if ($witnessScript instanceof ScriptInterface) {
134 8
                $witnessSizes[] = $witnessScript->getBuffer()->getSize();
135
            }
136
        } else {
137 11
            $scriptSigSizes = $stackSizes;
138 11
            $witnessSizes = [];
139
        }
140
141 18
        if ($redeemScript instanceof ScriptInterface) {
142 13
            $scriptSigSizes[] = $redeemScript->getBuffer()->getSize();
143
        }
144
145 18
        $scriptSigSize = 0;
146 18
        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 18
        $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 18
        $witnessSize = 0;
160 18
        if (count($witnessSizes) > 0) {
161
            // witness has a prefix indicating `n` elements in vector
162 8
            $witnessSize = self::getLengthOfVector($witnessSizes);
163
        }
164 18
        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 14
    public static function estimateInputFromScripts(ScriptInterface $script, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null, $isWitness) {
175 14
        assert($witnessScript === null || $isWitness);
176 14
        $classifier = new OutputClassifier();
177 14
        if ($classifier->isMultisig($script)) {
178 12
            list ($stackSizes, ) = SizeEstimation::estimateMultisigStackSize(new Multisig($script));
179 2
        } else if ($classifier->isPayToPublicKey($script)) {
180
            list ($stackSizes, ) = SizeEstimation::estimateP2PKStackSize(new PayToPubKey($script));
181 2
        } else if ($classifier->isPayToPublicKeyHash($script)) {
182
            // defaults to compressed, tough luck
183 2
            list ($stackSizes, ) = SizeEstimation::estimateP2PKHStackSize();
184
        } else {
185
            throw new \InvalidArgumentException("Unsupported script type");
186
        }
187
188 14
        return self::estimateSizeForStack($stackSizes, $isWitness, $redeemScript, $witnessScript);
189
    }
190
191
    /**
192
     * @param UTXO $utxo
193
     * @return array
194
     */
195 14
    public static function estimateUtxo(UTXO $utxo) {
196 14
        $classifier = new OutputClassifier();
197 14
        $decodePK = $classifier->decode($utxo->scriptPubKey);
198 14
        $witness = false;
199 14
        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 14
        if ($decodePK->getType() === ScriptType::P2WKH) {
207 2
            $scriptSitu = ScriptFactory::scriptPubKey()->p2pkh($decodePK->getSolution());
208 2
            $decodePK = $classifier->decode($scriptSitu);
209 2
            $witness = true;
210 12
        } 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 14
        if (!in_array($decodePK->getType(), [ScriptType::MULTISIG, ScriptType::P2PKH, ScriptType::P2PK])) {
219
            throw new \RuntimeException("Unsupported script type");
220
        }
221
222 14
        $script = $decodePK->getScript();
223 14
        list ($scriptSig, $witness) = SizeEstimation::estimateInputFromScripts($script, $utxo->redeemScript, $utxo->witnessScript, $witness);
224
225
        return [
226 14
            "scriptSig" => $scriptSig,
227 14
            "witness" => $witness,
228
        ];
229
    }
230
231
    /**
232
     * @param UTXO[] $utxos
233
     * @param bool $withWitness
234
     * @return integer
235
     */
236 9
    public static function estimateInputsSize(array $utxos, $withWitness) {
237 9
        $inputSize = 0;
238 9
        $witnessSize = 0;
239 9
        foreach ($utxos as $utxo) {
240 9
            $estimate = SizeEstimation::estimateUtxo($utxo);
241 9
            $inputSize += 32 + 4 + 4;
242 9
            $inputSize += $estimate['scriptSig'];
243 9
            if ($withWitness) {
244 9
                $witnessSize += $estimate['witness'];
245
            }
246
        }
247
248 9
        if ($withWitness && $witnessSize != 0) {
249 4
            $inputSize += $witnessSize;
250 4
            $inputSize += 2; // flag bytes
251
        }
252
253 9
        return $inputSize;
254
    }
255
256
    /**
257
     * @param array $outputs
258
     * @return int
259
     */
260 9
    public static function estimateOutputsSize(array $outputs) {
261 9
        $outputSize = 0;
262 9
        foreach ($outputs as $output) {
263 9
            $outputSize += 8;
264
265 9
            $scriptSize = null;
266 9
            if ($output instanceof TransactionOutputInterface) {
267 2
                $scriptSize = $output->getScript()->getBuffer()->getSize();
268
            } else {
269 9
                if (isset($output['scriptPubKey'])) {
270 4
                    if ($output['scriptPubKey'] instanceof ScriptInterface) {
271 4
                        $scriptSize = $output['scriptPubKey']->getBuffer()->getSize();
272
                    } else {
273 4
                        $scriptSize = strlen($output['scriptPubKey']) / 2;
274
                    }
275
                } else {
276 7
                    $scriptSize += 25;
277
                }
278
            }
279
280 9
            $outputSize += SizeEstimation::getLengthOfVarInt($scriptSize) + $scriptSize;
281
        }
282
283 9
        return $outputSize;
284
    }
285
286
    /**
287
     * @param UTXO[] $utxos
288
     * @param array $outputs
289
     * @return int
290
     */
291 9
    public static function estimateVsize(array $utxos, array $outputs) {
292 9
        return (int)ceil(self::estimateWeight($utxos, $outputs) / 4);
293
    }
294
295
    /**
296
     * @param UTXO[] $utxos
297
     * @param array $outputs
298
     * @return int
299
     */
300 9
    public static function estimateWeight(array $utxos, array $outputs) {
301 9
        $outputsSize = SizeEstimation::estimateOutputsSize($outputs);
302
        $baseSize = 4
303 9
            + SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, false)
304 9
            + SizeEstimation::getLengthOfVarInt(count($outputs)) + $outputsSize
305 9
            + 4;
306
        $witnessSize = 4
307 9
            + SizeEstimation::getLengthOfVarInt(count($utxos)) + SizeEstimation::estimateInputsSize($utxos, true)
308 9
            + SizeEstimation::getLengthOfVarInt(count($outputs)) + $outputsSize
309 9
            + 4;
310
311 9
        return ($baseSize * 3) + $witnessSize;
312
    }
313
}
314