Completed
Branch master (7ca1f6)
by
unknown
02:09
created

SizeEstimation::estimateInputFromScripts()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7.3471

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 4
nop 4
dl 0
loc 16
ccs 6
cts 11
cp 0.5455
crap 7.3471
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
13
class SizeEstimation
14
{
15
    const SIZE_DER_SIGNATURE = 72;
16
    const SIZE_V0_P2WSH = 36;
17
18
    /**
19
     * @param bool $compressed
20
     * @return int
21
     */
22
    public static function getPublicKeySize($compressed = true) {
23
        return $compressed ? PublicKeyInterface::LENGTH_COMPRESSED : PublicKeyInterface::LENGTH_UNCOMPRESSED;
24
    }
25
26
    /**
27
     * @param int $length
28
     * @return int
29
     */
30 25
    public static function getLengthOfScriptLengthElement($length) {
31 25
        if ($length < 75) {
32 17
            return 1;
33 17
        } else if ($length <= 0xff) {
34 13
            return 2;
35 4
        } else if ($length <= 0xffff) {
36 3
            return 3;
37 1
        } else if ($length <= 0xffffffff) {
38 1
            return 5;
39
        } else {
40
            throw new \RuntimeException('Size of pushdata too large');
41
        }
42
    }
43
44
    /**
45
     * @param int $length
46
     * @return int
47
     */
48 29
    public static function getLengthOfVarInt($length) {
49 29
        if ($length < 253) {
50 12
            return 1;
51 19
        } else if ($length < 65535) {
52 12
            $num_bytes = 2;
53 7
        } else if ($length < 4294967295) {
54 4
            $num_bytes = 4;
55 3
        } else if ($length < 18446744073709551615) {
56 3
            $num_bytes = 8;
57
        } else {
58
            throw new \InvalidArgumentException("Invalid decimal");
59
        }
60 19
        return 1 + $num_bytes;
61
    }
62
63
    /**
64
     * @param array $vectorSizes
65
     * @return int|mixed
66
     */
67 7
    public static function getLengthOfVector(array $vectorSizes) {
68 7
        $vectorSize = self::getLengthOfVarInt(count($vectorSizes));
69 7
        foreach ($vectorSizes as $size) {
70 7
            $vectorSize += self::getLengthOfVarInt($size) + $size;
71
        }
72 7
        return $vectorSize;
73
    }
74
75
    /**
76
     * @param Multisig $multisig
77
     * @return array - first is array of stack sizes, second is script len
78
     */
79 19
    public static function estimateMultisigStackSize(Multisig $multisig) {
80 19
        $stackSizes = [0];
81 19
        for ($i = 0; $i < $multisig->getRequiredSigCount(); $i++) {
82 19
            $stackSizes[] = self::SIZE_DER_SIGNATURE;
83
        }
84
85 19
        $scriptSize = 1; // OP_$m
86 19
        $keys = $multisig->getKeyBuffers();
87 19
        for ($i = 0, $n = $multisig->getKeyCount(); $i < $n; $i++) {
88 19
            $scriptSize += 1 + $keys[$i]->getSize();
89
        }
90
91 19
        $scriptSize += 1 + 1; // OP_$n OP_CHECKMULTISIG
92 19
        return [$stackSizes, $scriptSize];
93
    }
94
95
    /**
96
     * @param PayToPubKey $info
97
     * @return array - first is array of stack sizes, second is script len
98
     */
99
    public static function estimateP2PKStackSize(PayToPubKey $info) {
100
        $stackSizes = [self::SIZE_DER_SIGNATURE];
101
102
        $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...
103
        $scriptSize += 1; // OP_CHECKSIG
104
        return [$stackSizes, $scriptSize];
105
    }
106
107
    /**
108
     * @param bool $isCompressed
109
     * @return array - first is array of stack sizes, second is script len
110
     */
111
    public static function estimateP2PKHStackSize($isCompressed = true) {
112
        $pubKeySize = self::getPublicKeySize($isCompressed);
113
        $stackSizes = [self::SIZE_DER_SIGNATURE, $pubKeySize];
114
115
        $scriptSize = 1 + 1 + 1 + 20 + 1 + 1;
116
        return [$stackSizes, $scriptSize];
117
    }
118
119
    /**
120
     * @param array $stackSizes - array of integer size of a value (for scriptSig or witness)
121
     * @param bool $isWitness
122
     * @param ScriptInterface $redeemScript
123
     * @param ScriptInterface $witnessScript
124
     * @return array
125
     */
126 16
    public static function estimateSizeForStack(array $stackSizes, $isWitness, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null) {
127 16
        assert(($witnessScript === null) || $isWitness);
128
129 16
        if ($isWitness) {
130 7
            $scriptSigSizes = [];
131 7
            $witnessSizes = $stackSizes;
132 7
            if ($witnessScript instanceof ScriptInterface) {
133 7
                $witnessSizes[] = $witnessScript->getBuffer()->getSize();
134
            }
135
        } else {
136 11
            $scriptSigSizes = $stackSizes;
137 11
            $witnessSizes = [];
138
        }
139
140 16
        if ($redeemScript instanceof ScriptInterface) {
141 12
            $scriptSigSizes[] = $redeemScript->getBuffer()->getSize();
142
        }
143
144 16
        $scriptSigSize = 0;
145 16
        foreach ($scriptSigSizes as $size) {
146 14
            $len = self::getLengthOfScriptLengthElement($size);
147 14
            $scriptSigSize += $len;
148 14
            $scriptSigSize += $size;
149
        }
150
151
        // Make sure to count the CScriptBase length prefix..
152 16
        $scriptSigSize += self::getLengthOfVarInt($scriptSigSize);
153
154
        // make sure this is set to 0 when not used.
155
        // Summing WitnessSize is used in addition to $withWitness
156
        // because a tx without witnesses _cannot_ use witness serialization
157
        // total witnessSize = 0 means don't use that format
158 16
        $witnessSize = 0;
159 16
        if (count($witnessSizes) > 0) {
160
            // witness has a prefix indicating `n` elements in vector
161 7
            $witnessSize = self::getLengthOfVector($witnessSizes);
162
        }
163 16
        return [$scriptSigSize, $witnessSize];
164
    }
165
166
    /**
167
     * @param ScriptInterface $script - not the scriptPubKey, might be SPK,RS,WS
168
     * @param ScriptInterface|null $redeemScript
169
     * @param ScriptInterface $witnessScript
170
     * @param bool $isWitness
171
     * @return array
172
     */
173 12
    public static function estimateInputFromScripts(ScriptInterface $script, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null, $isWitness) {
174 12
        assert($witnessScript === null || $isWitness);
175 12
        $classifier = new OutputClassifier();
176 12
        if ($classifier->isMultisig($script)) {
177 12
            list ($stackSizes, ) = SizeEstimation::estimateMultisigStackSize(new Multisig($script));
178
        } else if ($classifier->isPayToPublicKey($script)) {
179
            list ($stackSizes, ) = SizeEstimation::estimateP2PKStackSize(new PayToPubKey($script));
180
        } else if ($classifier->isPayToPublicKeyHash($script)) {
181
            // defaults to compressed, tough luck
182
            list ($stackSizes, ) = SizeEstimation::estimateP2PKHStackSize();
183
        } else {
184
            throw new \InvalidArgumentException("Unsupported script type");
185
        }
186
187 12
        return self::estimateSizeForStack($stackSizes, $isWitness, $redeemScript, $witnessScript);
188
    }
189
190
    /**
191
     * @param UTXO $utxo
192
     * @return array
193
     */
194 12
    public static function estimateUtxo(UTXO $utxo) {
195 12
        $classifier = new OutputClassifier();
196 12
        $decodePK = $classifier->decode($utxo->scriptPubKey);
197 12
        $witness = false;
198 12
        if ($decodePK->getType() === ScriptType::P2SH) {
199 10
            if (null === $utxo->redeemScript) {
200
                throw new \RuntimeException("Can't estimate, missing redeem script");
201
            }
202 10
            $decodePK = $classifier->decode($utxo->redeemScript);
203
        }
204
205 12
        if ($decodePK->getType() === ScriptType::P2WKH) {
206
            $scriptSitu = ScriptFactory::scriptPubKey()->p2pkh($decodePK->getSolution());
207
            $decodePK = $classifier->decode($scriptSitu);
208
            $witness = true;
209 12
        } else if ($decodePK->getType() === ScriptType::P2WSH) {
210 5
            if (null === $utxo->witnessScript) {
211
                throw new \RuntimeException("Can't estimate, missing witness script");
212
            }
213 5
            $decodePK = $classifier->decode($utxo->witnessScript);
214 5
            $witness = true;
215
        }
216
217 12
        if (!in_array($decodePK->getType(), [ScriptType::MULTISIG, ScriptType::P2PKH, ScriptType::P2PK])) {
218
            throw new \RuntimeException("Unsupported script type");
219
        }
220
221 12
        $script = $decodePK->getScript();
222 12
        list ($scriptSig, $witness) = SizeEstimation::estimateInputFromScripts($script, $utxo->redeemScript, $utxo->witnessScript, $witness);
223
224
        return [
225 12
            "scriptSig" => $scriptSig,
226 12
            "witness" => $witness,
227
        ];
228
    }
229
230
    /**
231
     * @param UTXO[] $utxos
232
     * @param bool $withWitness
233
     * @return integer
234
     */
235 7
    public static function estimateInputsSize(array $utxos, $withWitness) {
236 7
        $inputSize = 0;
237 7
        $witnessSize = 0;
238 7
        foreach ($utxos as $utxo) {
239 7
            $estimate = SizeEstimation::estimateUtxo($utxo);
240 7
            $inputSize += 32 + 4 + 4;
241 7
            $inputSize += $estimate['scriptSig'];
242 7
            if ($withWitness) {
243 7
                $witnessSize += $estimate['witness'];
244
            }
245
        }
246
247 7
        if ($withWitness && $witnessSize != 0) {
248 3
            $inputSize += $witnessSize;
249 3
            $inputSize += 2; // flag bytes
250
        }
251
252 7
        return $inputSize;
253
    }
254
255
    /**
256
     * @param array $outputs
257
     * @return int
258
     */
259 7
    public static function estimateOutputsSize(array $outputs) {
260 7
        $outputSize = 0;
261 7
        foreach ($outputs as $output) {
262 7
            if (isset($output['scriptPubKey'])) {
263 2
                $outputSize += 8; // amount
264 2
                if ($output['scriptPubKey'] instanceof ScriptInterface) {
265 2
                    $outputSize += $output['scriptPubKey']->getBuffer()->getSize();
266
                } else {
267 2
                    $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
268
                }
269
            } else {
270 7
                $outputSize += 34;
271
            }
272
        }
273 7
        return $outputSize;
274
    }
275
276
    /**
277
     * @param UTXO[] $utxos
278
     * @param array $outputs
279
     * @return int
280
     */
281 7
    public static function estimateVsize(array $utxos, array $outputs) {
282 7
        return (int)ceil(self::estimateWeight($utxos, $outputs) / 4);
283
    }
284
285
    /**
286
     * @param UTXO[] $utxos
287
     * @param array $outputs
288
     * @return int
289
     */
290 7
    public static function estimateWeight(array $utxos, array $outputs) {
291 7
        $outputsSize = SizeEstimation::estimateOutputsSize($outputs);
292 7
        $baseSize = Wallet::estimateSize(SizeEstimation::estimateInputsSize($utxos, false), $outputsSize);
293 7
        $witnessSize = Wallet::estimateSize(SizeEstimation::estimateInputsSize($utxos, true), $outputsSize);
294
295 7
        return ($baseSize * 3) + $witnessSize;
296
    }
297
298
    /**
299
     * @param UTXO[] $utxos
300
     * @param array $outputs
301
     * @return int
302
     */
303
    public static function estimateLegacySize(array $utxos, array $outputs) {
304
        $outputsSize = SizeEstimation::estimateOutputsSize($outputs);
305
        $baseSize = Wallet::estimateSize(SizeEstimation::estimateInputsSize($utxos, false), $outputsSize);
306
307
        return $baseSize;
308
    }
309
}
310