Completed
Pull Request — master (#112)
by thomas
70:44
created

SizeEstimation::estimateUtxo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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