Skip to content

Commit a77f3d0

Browse files
authored
Merge pull request #378 from bsv-blockchain/serialization-speedups
Serialization speedups
2 parents 59c42a4 + a5dfcd8 commit a77f3d0

22 files changed

+1261
-321
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Benchmark Comparison
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
jobs:
8+
benchmarks:
9+
name: Benchmarks (Node 22)
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
steps:
15+
- name: Checkout PR branch
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: 22
24+
25+
- name: Install dependencies
26+
run: npm ci
27+
28+
- name: Build PR branch
29+
run: npm run build
30+
31+
- name: Run PR benchmarks
32+
run: node scripts/run-benchmarks.js --repo . --output pr-benchmarks.json
33+
34+
- name: Checkout master baseline
35+
uses: actions/checkout@v4
36+
with:
37+
ref: master
38+
path: master
39+
40+
- name: Install dependencies (master)
41+
working-directory: master
42+
run: npm ci
43+
44+
- name: Build master
45+
working-directory: master
46+
run: npm run build
47+
48+
- name: Run master benchmarks
49+
run: node scripts/run-benchmarks.js --repo master --output master-benchmarks.json
50+
51+
- name: Capture baseline sha
52+
id: baseline_sha
53+
run: echo "value=$(cd master && git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
54+
55+
- name: Generate benchmark report
56+
run: >
57+
node scripts/format-benchmark-comment.js
58+
--baseline master-benchmarks.json
59+
--branch pr-benchmarks.json
60+
--output benchmark-report.md
61+
--branch-ref $GITHUB_SHA
62+
--baseline-ref ${{ steps.baseline_sha.outputs.value }}
63+
64+
- name: Upload benchmark artifacts
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: benchmark-results
68+
path: |
69+
pr-benchmarks.json
70+
master-benchmarks.json
71+
benchmark-report.md
72+
73+
- name: Comment on PR
74+
if: github.event.pull_request.number != ''
75+
uses: actions/github-script@v7
76+
with:
77+
github-token: ${{ secrets.GITHUB_TOKEN }}
78+
script: |
79+
const fs = require('fs');
80+
const body = fs.readFileSync('benchmark-report.md', 'utf8');
81+
await github.rest.issues.createComment({
82+
...context.repo,
83+
issue_number: context.payload.pull_request.number,
84+
body
85+
});

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ All notable changes to this project will be documented in this file. The format
181181

182182
---
183183

184+
### [1.9.0] - 2025-11-09
185+
186+
### Added
187+
188+
- Faster serialization and caching improvements
189+
- Use node- and browser-specific fast-paths opportunistically
190+
- Cache signatures in Spend to avoid repeated verifications
191+
- Cache serialized values and use Uint8Array in more places
192+
- These changes are not intended to be breaking, but a minor version increment is performed out of an abundance of caution
193+
194+
---
195+
184196
### [1.8.12] - 2025-11-06
185197

186198
### Fixed

benchmarks/script-serialization-bench.js

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/primitives.md

Lines changed: 87 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
44

55
## Interfaces
66

7+
| |
8+
| --- |
9+
| [JacobianPointBI](#interface-jacobianpointbi) |
10+
| [SignatureHashCache](#interface-signaturehashcache) |
11+
12+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
13+
14+
---
15+
716
### Interface: JacobianPointBI
817

918
```ts
@@ -16,6 +25,20 @@ export interface JacobianPointBI {
1625

1726
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1827

28+
---
29+
### Interface: SignatureHashCache
30+
31+
```ts
32+
export interface SignatureHashCache {
33+
hashPrevouts?: number[];
34+
hashSequence?: number[];
35+
hashOutputsAll?: number[];
36+
hashOutputsSingle?: Map<number, number[]>;
37+
}
38+
```
39+
40+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
41+
1942
---
2043
## Classes
2144

@@ -3898,7 +3921,7 @@ const sha256 = new SHA256();
38983921
```ts
38993922
export class SHA256 {
39003923
constructor()
3901-
update(msg: number[] | string, enc?: "hex" | "utf8"): this
3924+
update(msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): this
39023925
digest(): number[]
39033926
digestHex(): string
39043927
}
@@ -3919,8 +3942,8 @@ This class also uses the SHA-256 cryptographic hash algorithm that produces a 25
39193942
export class SHA256HMAC {
39203943
blockSize = 64;
39213944
outSize = 32;
3922-
constructor(key: number[] | string)
3923-
update(msg: number[] | string, enc?: "hex"): SHA256HMAC
3945+
constructor(key: Uint8Array | number[] | string)
3946+
update(msg: Uint8Array | number[] | string, enc?: "hex"): SHA256HMAC
39243947
digest(): number[]
39253948
digestHex(): string
39263949
}
@@ -3935,7 +3958,7 @@ If the key size is larger than the blockSize, it is digested using SHA-256.
39353958
If the key size is less than the blockSize, it is padded with zeroes.
39363959
39373960
```ts
3938-
constructor(key: number[] | string)
3961+
constructor(key: Uint8Array | number[] | string)
39393962
```
39403963
39413964
Argument Details
@@ -4006,7 +4029,7 @@ let hashedMessage = myHMAC.digestHex();
40064029
Updates the `SHA256HMAC` object with part of the message to be hashed.
40074030
40084031
```ts
4009-
update(msg: number[] | string, enc?: "hex"): SHA256HMAC
4032+
update(msg: Uint8Array | number[] | string, enc?: "hex"): SHA256HMAC
40104033
```
40114034
See also: [SHA256HMAC](./primitives.md#class-sha256hmac)
40124035
@@ -4067,8 +4090,8 @@ This class also uses the SHA-512 cryptographic hash algorithm that produces a 51
40674090
export class SHA512HMAC {
40684091
blockSize = 128;
40694092
outSize = 32;
4070-
constructor(key: number[] | string)
4071-
update(msg: number[] | string, enc?: "hex" | "utf8"): SHA512HMAC
4093+
constructor(key: Uint8Array | number[] | string)
4094+
update(msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): SHA512HMAC
40724095
digest(): number[]
40734096
digestHex(): string
40744097
}
@@ -4083,7 +4106,7 @@ If the key size is larger than the blockSize, it is digested using SHA-512.
40834106
If the key size is less than the blockSize, it is padded with zeroes.
40844107
40854108
```ts
4086-
constructor(key: number[] | string)
4109+
constructor(key: Uint8Array | number[] | string)
40874110
```
40884111
40894112
Argument Details
@@ -4154,7 +4177,7 @@ let hashedMessage = myHMAC.digestHex();
41544177
Updates the `SHA512HMAC` object with part of the message to be hashed.
41554178
41564179
```ts
4157-
update(msg: number[] | string, enc?: "hex" | "utf8"): SHA512HMAC
4180+
update(msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): SHA512HMAC
41584181
```
41594182
See also: [SHA512HMAC](./primitives.md#class-sha512hmac)
41604183
@@ -4682,27 +4705,50 @@ export default class TransactionSignature extends Signature {
46824705
public static readonly SIGHASH_FORKID = 64;
46834706
public static readonly SIGHASH_ANYONECANPAY = 128;
46844707
scope: number;
4685-
static format(params: {
4686-
sourceTXID: string;
4687-
sourceOutputIndex: number;
4688-
sourceSatoshis: number;
4689-
transactionVersion: number;
4690-
otherInputs: TransactionInput[];
4691-
outputs: TransactionOutput[];
4692-
inputIndex: number;
4693-
subscript: Script;
4694-
inputSequence: number;
4695-
lockTime: number;
4696-
scope: number;
4697-
}): number[]
4708+
static format(params: TransactionSignatureFormatParams): number[]
4709+
static formatBytes(params: TransactionSignatureFormatParams): Uint8Array
46984710
static fromChecksigFormat(buf: number[]): TransactionSignature
46994711
constructor(r: BigNumber, s: BigNumber, scope: number)
47004712
public hasLowS(): boolean
47014713
toChecksigFormat(): number[]
47024714
}
47034715
```
47044716
4705-
See also: [BigNumber](./primitives.md#class-bignumber), [Script](./script.md#class-script), [Signature](./primitives.md#class-signature), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput)
4717+
See also: [BigNumber](./primitives.md#class-bignumber), [Signature](./primitives.md#class-signature)
4718+
4719+
#### Method format
4720+
4721+
Formats the SIGHASH preimage for the targeted input, optionally using a cache to skip recomputing shared hash prefixes.
4722+
4723+
```ts
4724+
static format(params: TransactionSignatureFormatParams): number[]
4725+
```
4726+
4727+
Argument Details
4728+
4729+
+ **params**
4730+
+ Context for the signing input plus transaction metadata.
4731+
+ **params.cache**
4732+
+ Optional cache storing previously computed `hashPrevouts`, `hashSequence`, or `hashOutputs*` values; it will be populated if present.
4733+
4734+
#### Method formatBytes
4735+
4736+
Formats the same SIGHASH preimage bytes as `format`, supporting the optional cache for hash reuse.
4737+
4738+
```ts
4739+
static formatBytes(params: TransactionSignatureFormatParams): Uint8Array
4740+
```
4741+
4742+
Returns
4743+
4744+
Bytes for signing.
4745+
4746+
Argument Details
4747+
4748+
+ **params**
4749+
+ Context for the signing operation.
4750+
+ **params.cache**
4751+
+ Optional `SignatureHashCache` that may already contain hashed prefixes and is populated during formatting.
47064752
47074753
#### Method hasLowS
47084754
@@ -4721,11 +4767,12 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
47214767
47224768
```ts
47234769
export class Writer {
4724-
public bufs: number[][];
4725-
constructor(bufs?: number[][])
4770+
public bufs: WriterChunk[];
4771+
constructor(bufs?: WriterChunk[])
47264772
getLength(): number
4773+
toUint8Array(): Uint8Array
47274774
toArray(): number[]
4728-
write(buf: number[]): this
4775+
write(buf: WriterChunk): this
47294776
writeReverse(buf: number[]): this
47304777
writeUInt8(n: number): this
47314778
writeInt8(n: number): this
@@ -5309,7 +5356,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
53095356
### Variable: hash160
53105357
53115358
```ts
5312-
hash160 = (msg: number[] | string, enc?: "hex" | "utf8"): number[] => {
5359+
hash160 = (msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): number[] => {
53135360
const first = new SHA256().update(msg, enc).digest();
53145361
return new RIPEMD160().update(first).digest();
53155362
}
@@ -5323,7 +5370,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
53235370
### Variable: hash256
53245371
53255372
```ts
5326-
hash256 = (msg: number[] | string, enc?: "hex" | "utf8"): number[] => {
5373+
hash256 = (msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): number[] => {
53275374
const first = new SHA256().update(msg, enc).digest();
53285375
return new SHA256().update(first).digest();
53295376
}
@@ -5655,7 +5702,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
56555702
### Variable: sha256
56565703
56575704
```ts
5658-
sha256 = (msg: number[] | string, enc?: "hex" | "utf8"): number[] => {
5705+
sha256 = (msg: Uint8Array | number[] | string, enc?: "hex" | "utf8"): number[] => {
56595706
return new SHA256().update(msg, enc).digest();
56605707
}
56615708
```
@@ -5668,7 +5715,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
56685715
### Variable: sha256hmac
56695716
56705717
```ts
5671-
sha256hmac = (key: number[] | string, msg: number[] | string, enc?: "hex"): number[] => {
5718+
sha256hmac = (key: Uint8Array | number[] | string, msg: Uint8Array | number[] | string, enc?: "hex"): number[] => {
56725719
return new SHA256HMAC(key).update(msg, enc).digest();
56735720
}
56745721
```
@@ -5694,7 +5741,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
56945741
### Variable: sha512hmac
56955742
56965743
```ts
5697-
sha512hmac = (key: number[] | string, msg: number[] | string, enc?: "hex"): number[] => {
5744+
sha512hmac = (key: Uint8Array | number[] | string, msg: Uint8Array | number[] | string, enc?: "hex"): number[] => {
56985745
return new SHA512HMAC(key).update(msg, enc).digest();
56995746
}
57005747
```
@@ -5851,16 +5898,19 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
58515898
58525899
```ts
58535900
toHex = (msg: number[]): string => {
5854-
let res = "";
5855-
for (const num of msg) {
5856-
res += zero2(num.toString(16));
5901+
if (CAN_USE_BUFFER) {
5902+
return BufferCtor.from(msg).toString("hex");
58575903
}
5858-
return res;
5904+
if (msg.length === 0)
5905+
return "";
5906+
const out = new Array(msg.length);
5907+
for (let i = 0; i < msg.length; i++) {
5908+
out[i] = HEX_BYTE_STRINGS[msg[i] & 255];
5909+
}
5910+
return out.join("");
58595911
}
58605912
```
58615913
5862-
See also: [zero2](./primitives.md#variable-zero2)
5863-
58645914
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
58655915
58665916
---

0 commit comments

Comments
 (0)