ox 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/core/Base64.d.ts +10 -21
  3. package/dist/core/Base64.d.ts.map +1 -1
  4. package/dist/core/Base64.js +8 -127
  5. package/dist/core/Base64.js.map +1 -1
  6. package/dist/core/BinaryStateTree.d.ts.map +1 -1
  7. package/dist/core/BinaryStateTree.js +2 -2
  8. package/dist/core/BinaryStateTree.js.map +1 -1
  9. package/dist/core/Bls.d.ts.map +1 -1
  10. package/dist/core/Bls.js +19 -44
  11. package/dist/core/Bls.js.map +1 -1
  12. package/dist/core/Ed25519.d.ts.map +1 -1
  13. package/dist/core/Ed25519.js +7 -6
  14. package/dist/core/Ed25519.js.map +1 -1
  15. package/dist/core/Engine.d.ts +124 -0
  16. package/dist/core/Engine.d.ts.map +1 -0
  17. package/dist/core/Engine.js +180 -0
  18. package/dist/core/Engine.js.map +1 -0
  19. package/dist/core/Hash.d.ts +4 -4
  20. package/dist/core/Hash.d.ts.map +1 -1
  21. package/dist/core/Hash.js +9 -12
  22. package/dist/core/Hash.js.map +1 -1
  23. package/dist/core/Keystore.d.ts.map +1 -1
  24. package/dist/core/Keystore.js +15 -10
  25. package/dist/core/Keystore.js.map +1 -1
  26. package/dist/core/Mnemonic.d.ts.map +1 -1
  27. package/dist/core/Mnemonic.js +3 -2
  28. package/dist/core/Mnemonic.js.map +1 -1
  29. package/dist/core/P256.d.ts.map +1 -1
  30. package/dist/core/P256.js +8 -9
  31. package/dist/core/P256.js.map +1 -1
  32. package/dist/core/Secp256k1.d.ts.map +1 -1
  33. package/dist/core/Secp256k1.js +8 -9
  34. package/dist/core/Secp256k1.js.map +1 -1
  35. package/dist/core/WebCryptoP256.d.ts.map +1 -1
  36. package/dist/core/WebCryptoP256.js +2 -2
  37. package/dist/core/WebCryptoP256.js.map +1 -1
  38. package/dist/core/X25519.d.ts.map +1 -1
  39. package/dist/core/X25519.js +4 -3
  40. package/dist/core/X25519.js.map +1 -1
  41. package/dist/core/internal/bls.d.ts +12 -0
  42. package/dist/core/internal/bls.d.ts.map +1 -0
  43. package/dist/core/internal/bls.js +79 -0
  44. package/dist/core/internal/bls.js.map +1 -0
  45. package/dist/core/internal/codec/base64.d.ts +84 -0
  46. package/dist/core/internal/codec/base64.d.ts.map +1 -0
  47. package/dist/core/internal/codec/base64.js +177 -0
  48. package/dist/core/internal/codec/base64.js.map +1 -0
  49. package/dist/core/internal/codec/hex.d.ts +49 -4
  50. package/dist/core/internal/codec/hex.d.ts.map +1 -1
  51. package/dist/core/internal/codec/hex.js +109 -35
  52. package/dist/core/internal/codec/hex.js.map +1 -1
  53. package/dist/core/internal/curves.d.ts +17 -0
  54. package/dist/core/internal/curves.d.ts.map +1 -0
  55. package/dist/core/internal/curves.js +17 -0
  56. package/dist/core/internal/curves.js.map +1 -0
  57. package/dist/core/internal/ed25519.d.ts +14 -0
  58. package/dist/core/internal/ed25519.d.ts.map +1 -0
  59. package/dist/core/internal/ed25519.js +29 -0
  60. package/dist/core/internal/ed25519.js.map +1 -0
  61. package/dist/core/internal/engine.d.ts +278 -0
  62. package/dist/core/internal/engine.d.ts.map +1 -0
  63. package/dist/core/internal/engine.js +131 -0
  64. package/dist/core/internal/engine.js.map +1 -0
  65. package/dist/core/internal/hash.d.ts +12 -0
  66. package/dist/core/internal/hash.d.ts.map +1 -0
  67. package/dist/core/internal/hash.js +30 -0
  68. package/dist/core/internal/hash.js.map +1 -0
  69. package/dist/core/internal/keystore.d.ts +14 -0
  70. package/dist/core/internal/keystore.d.ts.map +1 -0
  71. package/dist/core/internal/keystore.js +32 -0
  72. package/dist/core/internal/keystore.js.map +1 -0
  73. package/dist/core/internal/mnemonic.d.ts +4 -0
  74. package/dist/core/internal/mnemonic.d.ts.map +1 -0
  75. package/dist/core/internal/mnemonic.js +14 -0
  76. package/dist/core/internal/mnemonic.js.map +1 -0
  77. package/dist/core/internal/p256.d.ts +14 -0
  78. package/dist/core/internal/p256.d.ts.map +1 -0
  79. package/dist/core/internal/p256.js +36 -0
  80. package/dist/core/internal/p256.js.map +1 -0
  81. package/dist/core/internal/secp256k1.d.ts +14 -0
  82. package/dist/core/internal/secp256k1.d.ts.map +1 -0
  83. package/dist/core/internal/secp256k1.js +36 -0
  84. package/dist/core/internal/secp256k1.js.map +1 -0
  85. package/dist/core/internal/x25519.d.ts +8 -0
  86. package/dist/core/internal/x25519.d.ts.map +1 -0
  87. package/dist/core/internal/x25519.js +20 -0
  88. package/dist/core/internal/x25519.js.map +1 -0
  89. package/dist/index.d.ts +35 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.docs.d.ts +2 -0
  92. package/dist/index.docs.d.ts.map +1 -1
  93. package/dist/index.docs.js +2 -0
  94. package/dist/index.docs.js.map +1 -1
  95. package/dist/index.js +35 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/node/Engine.d.ts +55 -0
  98. package/dist/node/Engine.d.ts.map +1 -0
  99. package/dist/node/Engine.js +51 -0
  100. package/dist/node/Engine.js.map +1 -0
  101. package/dist/node/Hash.d.ts +39 -0
  102. package/dist/node/Hash.d.ts.map +1 -0
  103. package/dist/node/Hash.js +37 -0
  104. package/dist/node/Hash.js.map +1 -0
  105. package/dist/node/index.d.ts +39 -0
  106. package/dist/node/index.d.ts.map +1 -0
  107. package/dist/node/index.js +39 -0
  108. package/dist/node/index.js.map +1 -0
  109. package/dist/tempo/VirtualMaster.d.ts +2 -0
  110. package/dist/tempo/VirtualMaster.d.ts.map +1 -1
  111. package/dist/tempo/VirtualMaster.js +19 -5
  112. package/dist/tempo/VirtualMaster.js.map +1 -1
  113. package/dist/tempo/ZoneId.d.ts +35 -17
  114. package/dist/tempo/ZoneId.d.ts.map +1 -1
  115. package/dist/tempo/ZoneId.js +41 -16
  116. package/dist/tempo/ZoneId.js.map +1 -1
  117. package/dist/tempo/index.d.ts +5 -6
  118. package/dist/tempo/index.d.ts.map +1 -1
  119. package/dist/tempo/index.js +5 -6
  120. package/dist/tempo/index.js.map +1 -1
  121. package/dist/tempo/internal/mine.wasm.d.ts +2 -4
  122. package/dist/tempo/internal/mine.wasm.d.ts.map +1 -1
  123. package/dist/tempo/internal/mine.wasm.js +7 -12
  124. package/dist/tempo/internal/mine.wasm.js.map +1 -1
  125. package/dist/tempo/internal/virtualMasterPool.js +4 -1
  126. package/dist/tempo/internal/virtualMasterPool.js.map +1 -1
  127. package/dist/wasm/Engine.d.ts +62 -0
  128. package/dist/wasm/Engine.d.ts.map +1 -0
  129. package/dist/wasm/Engine.js +57 -0
  130. package/dist/wasm/Engine.js.map +1 -0
  131. package/dist/wasm/Hash.d.ts +49 -0
  132. package/dist/wasm/Hash.d.ts.map +1 -0
  133. package/dist/wasm/Hash.js +70 -0
  134. package/dist/wasm/Hash.js.map +1 -0
  135. package/dist/wasm/index.d.ts +46 -0
  136. package/dist/wasm/index.d.ts.map +1 -0
  137. package/dist/wasm/index.js +46 -0
  138. package/dist/wasm/index.js.map +1 -0
  139. package/dist/wasm/internal/hash.d.ts +9 -0
  140. package/dist/wasm/internal/hash.d.ts.map +1 -0
  141. package/dist/wasm/internal/hash.js +26 -0
  142. package/dist/wasm/internal/hash.js.map +1 -0
  143. package/dist/wasm/internal/hashes.wasm.d.ts +5 -0
  144. package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -0
  145. package/dist/wasm/internal/hashes.wasm.js +12 -0
  146. package/dist/wasm/internal/hashes.wasm.js.map +1 -0
  147. package/dist/wasm/internal/instantiate.d.ts +56 -0
  148. package/dist/wasm/internal/instantiate.d.ts.map +1 -0
  149. package/dist/wasm/internal/instantiate.js +92 -0
  150. package/dist/wasm/internal/instantiate.js.map +1 -0
  151. package/dist/zod/TypedData.d.ts +12 -12
  152. package/package.json +36 -1
  153. package/src/core/Base64.ts +19 -166
  154. package/src/core/BinaryStateTree.ts +2 -3
  155. package/src/core/Bls.ts +27 -77
  156. package/src/core/Ed25519.ts +7 -7
  157. package/src/core/Engine.ts +235 -0
  158. package/src/core/Hash.ts +9 -12
  159. package/src/core/Keystore.ts +17 -17
  160. package/src/core/Mnemonic.ts +3 -6
  161. package/src/core/P256.ts +15 -24
  162. package/src/core/Rlp_tx.bench.ts +2 -4
  163. package/src/core/Secp256k1.ts +9 -14
  164. package/src/core/TxEnvelopeEip1559.bench.ts +0 -5
  165. package/src/core/WebCryptoP256.ts +2 -2
  166. package/src/core/X25519.ts +4 -3
  167. package/src/core/_test/AbiParameters.adversarial.fuzz.ts +1 -2
  168. package/src/core/_test/AbiParameters.fuzz.ts +1 -2
  169. package/src/core/_test/Base.fuzz.ts +1 -2
  170. package/src/core/_test/Cbor.fuzz.ts +1 -2
  171. package/src/core/_test/Engine.fuzz.ts +293 -0
  172. package/src/core/_test/Engine.test-d.ts +84 -0
  173. package/src/core/_test/Engine.test.ts +523 -0
  174. package/src/core/_test/Hash.vectors.test.ts +56 -0
  175. package/src/core/_test/Hex.fuzz.ts +1 -2
  176. package/src/core/_test/Rlp.fuzz.ts +1 -2
  177. package/src/core/_test/Signature.fuzz.ts +1 -2
  178. package/src/core/_test/curves.test.ts +7 -0
  179. package/src/core/_test/index.test.ts +1 -0
  180. package/src/core/internal/bls.ts +121 -0
  181. package/src/core/internal/codec/_test/base64.conformance.ts +156 -0
  182. package/src/core/internal/codec/_test/hex.conformance.ts +204 -0
  183. package/src/core/internal/codec/_test/hex.fuzz.ts +234 -0
  184. package/src/core/internal/codec/_test/hex.test.ts +124 -0
  185. package/src/core/internal/codec/base64.ts +222 -0
  186. package/src/core/internal/codec/hex.ts +115 -34
  187. package/src/core/internal/curves.ts +18 -0
  188. package/src/core/internal/ed25519.ts +64 -0
  189. package/src/core/internal/engine.ts +410 -0
  190. package/src/core/internal/hash.ts +48 -0
  191. package/src/core/internal/keystore.ts +107 -0
  192. package/src/core/internal/mnemonic.ts +18 -0
  193. package/src/core/internal/p256.ts +91 -0
  194. package/src/core/internal/secp256k1.ts +91 -0
  195. package/src/core/internal/x25519.ts +40 -0
  196. package/src/index.docs.ts +2 -0
  197. package/src/index.ts +36 -0
  198. package/src/node/Engine.ts +69 -0
  199. package/src/node/Hash.ts +56 -0
  200. package/src/node/_test/Engine.test.ts +62 -0
  201. package/src/node/_test/Hash.test-d.ts +26 -0
  202. package/src/node/_test/Hash.test.ts +102 -0
  203. package/src/node/index.ts +40 -0
  204. package/src/tempo/VirtualMaster.test.ts +75 -0
  205. package/src/tempo/VirtualMaster.ts +28 -6
  206. package/src/tempo/ZoneId.test-d.ts +16 -0
  207. package/src/tempo/ZoneId.test.ts +48 -13
  208. package/src/tempo/ZoneId.ts +55 -19
  209. package/src/tempo/index.ts +5 -6
  210. package/src/tempo/internal/mine.wasm.ts +7 -13
  211. package/src/tempo/internal/virtualMasterPool.ts +4 -1
  212. package/src/tsconfig.json +2 -1
  213. package/src/version.ts +1 -1
  214. package/src/wasm/Engine.ts +76 -0
  215. package/src/wasm/Hash.bench.ts +60 -0
  216. package/src/wasm/Hash.ts +109 -0
  217. package/src/wasm/_test/Engine.test.ts +62 -0
  218. package/src/wasm/_test/Hash.browser.test.ts +50 -0
  219. package/src/wasm/_test/Hash.fuzz.ts +188 -0
  220. package/src/wasm/_test/Hash.test-d.ts +28 -0
  221. package/src/wasm/_test/Hash.test.ts +211 -0
  222. package/src/wasm/_test/Hash.vectors.test.ts +117 -0
  223. package/src/wasm/_test/Runtime.test.ts +21 -0
  224. package/src/wasm/_test/runtime.wasm.ts +11 -0
  225. package/src/wasm/index.ts +47 -0
  226. package/src/wasm/internal/hash.ts +50 -0
  227. package/src/wasm/internal/hashes.wasm.ts +14 -0
  228. package/src/wasm/internal/instantiate.ts +135 -0
  229. package/src/zod/tempo/_test/ZoneId.test.ts +2 -2
  230. package/src/tempo/internal/mine.c +0 -182
@@ -0,0 +1,76 @@
1
+ import * as CoreEngine from '../core/Engine.js'
2
+ import type * as Errors from '../core/Errors.js'
3
+ import * as Hash from './Hash.js'
4
+ import type * as internal from './internal/instantiate.js'
5
+
6
+ /**
7
+ * Compiles every implementation this entrypoint provides and installs it.
8
+ *
9
+ * WASM must be compiled asynchronously, so this is where the `await` lives.
10
+ * Everything afterwards, including the hashing itself, is synchronous.
11
+ *
12
+ * Call it once, during startup, before any crypto call. ox resolves the engine
13
+ * at call time, so values computed beforehand used whatever was installed then.
14
+ *
15
+ * @example
16
+ * ```ts twoslash
17
+ * // @noErrors
18
+ * import { Hash } from 'ox'
19
+ * import { Engine } from 'ox/wasm'
20
+ *
21
+ * await Engine.load()
22
+ *
23
+ * Hash.keccak256('0xdeadbeef')
24
+ * ```
25
+ *
26
+ * @returns The engine that was installed.
27
+ */
28
+ export async function load(): Promise<load.ReturnType> {
29
+ const engine = await create()
30
+ CoreEngine.set(engine)
31
+ return engine
32
+ }
33
+
34
+ export declare namespace load {
35
+ type ReturnType = create.ReturnType
36
+
37
+ type ErrorType =
38
+ | create.ErrorType
39
+ | CoreEngine.set.ErrorType
40
+ | Errors.GlobalErrorType
41
+ }
42
+
43
+ /**
44
+ * Compiles every implementation this entrypoint provides, without installing
45
+ * it.
46
+ *
47
+ * Reach for this where an engine has to exist as a value: measuring one
48
+ * implementation against another, or installing through
49
+ * [`Engine.with`](/api/Engine/with) for the duration of a call. Combining
50
+ * engines does not need it, because
51
+ * [`Engine.set`](/api/Engine/set) merges -- `await load()` followed by
52
+ * `Engine.set({ Secp256k1 })` leaves both in place.
53
+ *
54
+ * @example
55
+ * ```ts twoslash
56
+ * // @noErrors
57
+ * import { Engine, Hash } from 'ox'
58
+ * import { Engine as Wasm } from 'ox/wasm'
59
+ *
60
+ * const wasm = await Wasm.create()
61
+ *
62
+ * Engine.with(wasm, () => Hash.keccak256('0xdeadbeef'))
63
+ * ```
64
+ *
65
+ * @returns An engine, ready to install.
66
+ */
67
+ export async function create(): Promise<create.ReturnType> {
68
+ return { ...(await Hash.create()) }
69
+ }
70
+
71
+ export declare namespace create {
72
+ /** Every slot this entrypoint supplies, each with its primitives present. */
73
+ type ReturnType = Hash.create.ReturnType
74
+
75
+ type ErrorType = internal.MemoryError | Errors.GlobalErrorType
76
+ }
@@ -0,0 +1,60 @@
1
+ import { hmac } from '@noble/hashes/hmac.js'
2
+ import { ripemd160 as noble_ripemd160 } from '@noble/hashes/legacy.js'
3
+ import { sha256 as noble_sha256 } from '@noble/hashes/sha2.js'
4
+ import { keccak_256 as noble_keccak256 } from '@noble/hashes/sha3.js'
5
+ import { bench, describe } from 'vp/test'
6
+ import { create as createWasm } from './Hash.js'
7
+
8
+ // Sizes expose fixed boundary overhead and sustained throughput. Crossovers
9
+ // vary by primitive, runtime, and processor.
10
+ //
11
+ // Run `pnpm bench:engines` for the full engine comparison. Vitest cannot
12
+ // run Rust, and this portable benchmark must remain browser-compatible.
13
+ const sizes = [32, 64, 256, 1024, 4096, 65_536, 1_048_576] as const
14
+
15
+ const wasm = (await createWasm()).Hash
16
+ const key = Uint8Array.from({ length: 32 }, (_, index) => index % 97)
17
+
18
+ for (const size of sizes) {
19
+ const bytes = Uint8Array.from({ length: size }, (_, index) => index % 251)
20
+
21
+ describe(`keccak256 (${size} bytes input)`, () => {
22
+ bench('ox', () => {
23
+ noble_keccak256(bytes)
24
+ })
25
+
26
+ bench('ox/wasm', () => {
27
+ wasm.keccak256(bytes)
28
+ })
29
+ })
30
+
31
+ describe(`sha256 (${size} bytes input)`, () => {
32
+ bench('ox', () => {
33
+ noble_sha256(bytes)
34
+ })
35
+
36
+ bench('ox/wasm', () => {
37
+ wasm.sha256(bytes)
38
+ })
39
+ })
40
+
41
+ describe(`ripemd160 (${size} bytes input)`, () => {
42
+ bench('ox', () => {
43
+ noble_ripemd160(bytes)
44
+ })
45
+
46
+ bench('ox/wasm', () => {
47
+ wasm.ripemd160(bytes)
48
+ })
49
+ })
50
+
51
+ describe(`hmacSha256 (${size} bytes input)`, () => {
52
+ bench('ox', () => {
53
+ hmac(noble_sha256, key, bytes)
54
+ })
55
+
56
+ bench('ox/wasm', () => {
57
+ wasm.hmacSha256(key, bytes)
58
+ })
59
+ })
60
+ }
@@ -0,0 +1,109 @@
1
+ import type * as Engine from '../core/Engine.js'
2
+ import type * as Errors from '../core/Errors.js'
3
+ import * as hash from './internal/hash.js'
4
+ import { wasmBase64 } from './internal/hashes.wasm.js'
5
+ import * as internal from './internal/instantiate.js'
6
+
7
+ export { MemoryError } from './internal/instantiate.js'
8
+
9
+ type Exports = hash.Exports & {
10
+ keccak256(input: number, length: number, out: number): void
11
+ ripemd160(input: number, length: number, out: number): void
12
+ sha256(input: number, length: number, out: number): void
13
+ }
14
+
15
+ /** Digest sizes, in bytes. */
16
+ const digestSize = { keccak256: 32, ripemd160: 20, sha256: 32 }
17
+
18
+ /**
19
+ * Compilation is memoized; the engine object deliberately is not.
20
+ *
21
+ * Handing every caller the same object means one of them customising a slot
22
+ * for composition silently changes what a later `create` -- or `Engine.load`
23
+ * -- installs.
24
+ *
25
+ * @internal
26
+ */
27
+ const instantiate = /*#__PURE__*/ internal.memoize(() =>
28
+ internal.instantiate<Exports>(wasmBase64),
29
+ )
30
+
31
+ /**
32
+ * Compiles the WASM implementation of the [`Hash`](/api/Hash) primitives, without
33
+ * installing it.
34
+ *
35
+ * Most callers want {@link ox#Engine.load} instead, which compiles every
36
+ * implementation this entrypoint provides and installs them in one call. Reach
37
+ * for this to take the `Hash` slot on its own, or to hold the implementation
38
+ * without touching the installed engine.
39
+ *
40
+ * :::note
41
+ * Performance varies by primitive, input size, runtime, and processor. Run
42
+ * `pnpm bench:engines` to compare Ox's default, Node, WASM, and Alloy
43
+ * implementations on your target machine.
44
+ * :::
45
+ *
46
+ * @example
47
+ * ```ts twoslash
48
+ * // @noErrors
49
+ * import { Engine, Hash } from 'ox'
50
+ * import * as WasmHash from 'ox/wasm/Hash'
51
+ *
52
+ * Engine.set(await WasmHash.create())
53
+ *
54
+ * Hash.keccak256('0xdeadbeef')
55
+ * ```
56
+ *
57
+ * @returns An engine supplying the `Hash` slot.
58
+ */
59
+ export async function create(): Promise<create.ReturnType> {
60
+ {
61
+ const module = await instantiate()
62
+
63
+ // Copies `input` in, runs `hash`, and copies the digest back out.
64
+ function call(
65
+ hash: 'keccak256' | 'ripemd160' | 'sha256',
66
+ input: Uint8Array,
67
+ ): Uint8Array {
68
+ const out = digestSize[hash]
69
+ module.reserve(input.length + out)
70
+ const inputPtr = module.heapBase
71
+ const outPtr = inputPtr + input.length
72
+ module.view().set(input, inputPtr)
73
+ module.exports[hash](inputPtr, input.length, outPtr)
74
+ // `slice`, not `subarray`: the result must not alias WASM memory, which the
75
+ // next call overwrites and a `grow` detaches entirely.
76
+ return module.view().slice(outPtr, outPtr + out)
77
+ }
78
+
79
+ return {
80
+ Hash: {
81
+ hmacSha256: (key, message) => hash.hmacSha256(module, key, message),
82
+ keccak256: (input) => call('keccak256', input),
83
+ ripemd160: (input) => call('ripemd160', input),
84
+ sha256: (input) => call('sha256', input),
85
+ },
86
+ }
87
+ }
88
+ }
89
+
90
+ export declare namespace create {
91
+ /**
92
+ * The `Hash` slot, carrying every primitive this module implements.
93
+ *
94
+ * {@link ox#Engine.Engine} is optional all the way down so that an engine can
95
+ * fill in as little as it likes. This one always fills the same four, and
96
+ * saying so is what spares callers a non-null assertion per primitive.
97
+ */
98
+ type ReturnType = {
99
+ Hash: {
100
+ [key in
101
+ | 'hmacSha256'
102
+ | 'keccak256'
103
+ | 'ripemd160'
104
+ | 'sha256']-?: NonNullable<Engine.Hash[key]>
105
+ }
106
+ }
107
+
108
+ type ErrorType = internal.MemoryError | Errors.GlobalErrorType
109
+ }
@@ -0,0 +1,62 @@
1
+ import { Engine as CoreEngine, Hash } from 'ox'
2
+ import { Engine } from 'ox/wasm'
3
+ import { describe, expect, test } from 'vp/test'
4
+
5
+ describe('create', () => {
6
+ test('behavior: does not install', async () => {
7
+ const engine = await Engine.create()
8
+ expect(Object.keys(engine)).toMatchInlineSnapshot(`
9
+ [
10
+ "Hash",
11
+ ]
12
+ `)
13
+ // The whole point of `create` over `load`: benches, differential tests and
14
+ // composition all need the implementation without touching global state.
15
+ expect(CoreEngine.get()).toMatchInlineSnapshot('{}')
16
+ })
17
+
18
+ test('behavior: installs for the duration of a call', async () => {
19
+ // `with` is the case that genuinely needs a value: there is nothing
20
+ // installed for `set` to merge into.
21
+ const wasm = await Engine.create()
22
+ const inside = CoreEngine.with(wasm, () => Hash.keccak256('0xdeadbeef'))
23
+ expect(inside).toEqual(Hash.keccak256('0xdeadbeef'))
24
+ expect(CoreEngine.get()).toMatchInlineSnapshot('{}')
25
+ })
26
+ })
27
+
28
+ describe('load', () => {
29
+ test('behavior: merges with a later set, rather than being replaced', async () => {
30
+ await Engine.load()
31
+ CoreEngine.set({ Mnemonic: { toSeed: () => new Uint8Array(64) } })
32
+ expect(Object.keys(CoreEngine.get()).sort()).toMatchInlineSnapshot(`
33
+ [
34
+ "Hash",
35
+ "Mnemonic",
36
+ ]
37
+ `)
38
+ })
39
+
40
+ test('behavior: installs, and returns what it installed', async () => {
41
+ const before = Hash.keccak256('0xdeadbeef')
42
+
43
+ const engine = await Engine.load()
44
+
45
+ expect(Object.keys(CoreEngine.get())).toEqual(Object.keys(engine))
46
+ // Installing changes which implementation runs, never the answer.
47
+ expect(Hash.keccak256('0xdeadbeef')).toEqual(before)
48
+ })
49
+
50
+ test('behavior: routes every slot it reports', async () => {
51
+ await Engine.load()
52
+ expect(Object.keys(CoreEngine.get().Hash ?? {}).sort())
53
+ .toMatchInlineSnapshot(`
54
+ [
55
+ "hmacSha256",
56
+ "keccak256",
57
+ "ripemd160",
58
+ "sha256",
59
+ ]
60
+ `)
61
+ })
62
+ })
@@ -0,0 +1,50 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3.js'
2
+ import { describe, expect, test, vi } from 'vp/test'
3
+ import * as Engine from '../../core/Engine.js'
4
+ import * as Hash from '../../core/Hash.js'
5
+ import * as WasmHash from '../Hash.js'
6
+
7
+ describe('create', () => {
8
+ test('behavior: instantiates asynchronously', async () => {
9
+ // Chrome refuses to compile modules above a few kilobytes synchronously on
10
+ // the main thread, so `new WebAssembly.Module` must never be reached. A spy
11
+ // is the only way to assert that, since the artifact happens to be small
12
+ // enough today that a synchronous compile would succeed.
13
+ const Module = vi.spyOn(globalThis.WebAssembly, 'Module')
14
+ const Instance = vi.spyOn(globalThis.WebAssembly, 'Instance')
15
+ try {
16
+ const engine = await WasmHash.create()
17
+ expect(Module).not.toHaveBeenCalled()
18
+ expect(Instance).not.toHaveBeenCalled()
19
+
20
+ const input = new Uint8Array(32).fill(1)
21
+ expect(engine.Hash.keccak256(input)).toEqual(keccak_256(input))
22
+ } finally {
23
+ Module.mockRestore()
24
+ Instance.mockRestore()
25
+ }
26
+ })
27
+
28
+ test('behavior: grows memory in a real browser, then hashes a small input', async () => {
29
+ const engine = await WasmHash.create()
30
+
31
+ const large = new Uint8Array(3 * 1024 * 1024).fill(7)
32
+ expect(engine.Hash.keccak256(large)).toEqual(keccak_256(large))
33
+
34
+ // `memory.grow` detached the previous `ArrayBuffer`; a retained view would
35
+ // now read zero bytes.
36
+ const small = new Uint8Array(32).fill(1)
37
+ expect(engine.Hash.keccak256(small)).toEqual(keccak_256(small))
38
+ })
39
+
40
+ test('behavior: ox uses the WASM implementation once installed', async () => {
41
+ Engine.set(await WasmHash.create())
42
+ try {
43
+ expect(Hash.keccak256('0xdeadbeef')).toBe(
44
+ '0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1',
45
+ )
46
+ } finally {
47
+ Engine.reset()
48
+ }
49
+ })
50
+ })
@@ -0,0 +1,188 @@
1
+ import { fc, test } from '@fast-check/vitest'
2
+ import { hmac } from '@noble/hashes/hmac.js'
3
+ import { ripemd160 } from '@noble/hashes/legacy.js'
4
+ import { sha256 } from '@noble/hashes/sha2.js'
5
+ import { keccak_256 } from '@noble/hashes/sha3.js'
6
+ import { Hash as WasmHash } from 'ox/wasm'
7
+ import { beforeAll, describe, expect } from 'vp/test'
8
+ import { numRuns } from '../../../test/fuzz/numRuns.js'
9
+
10
+ let engine: WasmHash.create.ReturnType
11
+
12
+ beforeAll(async () => {
13
+ engine = await WasmHash.create()
14
+ })
15
+
16
+ /**
17
+ * `@noble/hashes` is the oracle: it is audited and it is what ox ships by
18
+ * default, so any divergence here is a bug in the WASM implementation. Published
19
+ * vectors live in `Hash.vectors.test.ts` and cover the case where both
20
+ * implementations could be wrong together.
21
+ *
22
+ * The distribution is weighted toward the sizes where padding and block-boundary
23
+ * bugs hide -- empty input, exact multiples of the keccak256 rate (136) and the
24
+ * SHA-2 block (64), and the 56-byte threshold at which the length counter no
25
+ * longer fits in the final block.
26
+ */
27
+ const arbitraryInput = fc.oneof(
28
+ { arbitrary: fc.uint8Array({ maxLength: 300, minLength: 0 }), weight: 6 },
29
+ {
30
+ arbitrary: fc
31
+ .constantFrom(0, 1, 55, 56, 63, 64, 65, 111, 112, 135, 136, 137, 272)
32
+ .chain((size) => fc.uint8Array({ maxLength: size, minLength: size })),
33
+ weight: 3,
34
+ },
35
+ {
36
+ arbitrary: fc.uint8Array({ maxLength: 4096, minLength: 1024 }),
37
+ weight: 1,
38
+ },
39
+ )
40
+
41
+ /**
42
+ * HMAC key lengths straddling the 64-byte block. At or under it the key is
43
+ * zero-padded; over it, it is hashed first -- a different code path that the
44
+ * message-shaped arbitrary above would only reach by chance.
45
+ */
46
+ const arbitraryKey = fc.oneof(
47
+ {
48
+ arbitrary: fc
49
+ .constantFrom(0, 1, 32, 63, 64, 65, 96, 128, 129)
50
+ .chain((size) => fc.uint8Array({ maxLength: size, minLength: size })),
51
+ weight: 3,
52
+ },
53
+ { arbitrary: fc.uint8Array({ maxLength: 200, minLength: 0 }), weight: 1 },
54
+ )
55
+
56
+ const primitives = {
57
+ hmacSha256: {
58
+ reference: (input: Uint8Array, key: Uint8Array) => hmac(sha256, key, input),
59
+ wasm: (input: Uint8Array, key: Uint8Array) =>
60
+ engine.Hash.hmacSha256(key, input),
61
+ },
62
+ keccak256: {
63
+ reference: (input: Uint8Array) => keccak_256(input),
64
+ wasm: (input: Uint8Array) => engine.Hash.keccak256(input),
65
+ },
66
+ ripemd160: {
67
+ reference: (input: Uint8Array) => ripemd160(input),
68
+ wasm: (input: Uint8Array) => engine.Hash.ripemd160(input),
69
+ },
70
+ sha256: {
71
+ reference: (input: Uint8Array) => sha256(input),
72
+ wasm: (input: Uint8Array) => engine.Hash.sha256(input),
73
+ },
74
+ } as const
75
+
76
+ type Name = keyof typeof primitives
77
+
78
+ describe('Hash', () => {
79
+ test.prop({ input: arbitraryInput }, { numRuns })(
80
+ 'keccak256 ≡ @noble/hashes',
81
+ ({ input }) => {
82
+ expect(engine.Hash.keccak256(input)).toEqual(keccak_256(input))
83
+ },
84
+ )
85
+
86
+ test.prop({ input: arbitraryInput }, { numRuns })(
87
+ 'sha256 ≡ @noble/hashes',
88
+ ({ input }) => {
89
+ expect(engine.Hash.sha256(input)).toEqual(sha256(input))
90
+ },
91
+ )
92
+
93
+ test.prop({ input: arbitraryInput }, { numRuns })(
94
+ 'ripemd160 ≡ @noble/hashes',
95
+ ({ input }) => {
96
+ expect(engine.Hash.ripemd160(input)).toEqual(ripemd160(input))
97
+ },
98
+ )
99
+
100
+ test.prop({ input: arbitraryInput, key: arbitraryKey }, { numRuns })(
101
+ 'hmacSha256 ≡ @noble/hashes',
102
+ ({ input, key }) => {
103
+ expect(engine.Hash.hmacSha256(key, input)).toEqual(
104
+ hmac(sha256, key, input),
105
+ )
106
+ },
107
+ )
108
+ })
109
+
110
+ describe('memory', () => {
111
+ test.prop(
112
+ {
113
+ operations: fc.array(
114
+ fc.record({
115
+ input: arbitraryInput,
116
+ key: arbitraryKey,
117
+ name: fc.constantFrom<Name>(
118
+ 'hmacSha256',
119
+ 'keccak256',
120
+ 'ripemd160',
121
+ 'sha256',
122
+ ),
123
+ }),
124
+ { maxLength: 24, minLength: 2 },
125
+ ),
126
+ },
127
+ { numRuns },
128
+ )('an arbitrary sequence of calls agrees at every step', ({ operations }) => {
129
+ // Every primitive shares one region of WASM linear memory, reusing it call
130
+ // after call. A mis-sized reservation, an off-by-one write, or a digest read
131
+ // from the wrong offset can leave earlier bytes behind and still produce a
132
+ // correct answer in isolation -- it only shows up when calls of differing
133
+ // sizes and kinds are interleaved. Comparing every step of a random sequence
134
+ // is what catches that; a fixed a/b/a pattern does not.
135
+ for (const { input, key, name } of operations) {
136
+ const primitive = primitives[name]
137
+ expect(
138
+ primitive.wasm(input, key),
139
+ `${name} with a ${input.length} byte input`,
140
+ ).toEqual(primitive.reference(input, key))
141
+ }
142
+ })
143
+
144
+ test.prop(
145
+ {
146
+ sizes: fc.array(fc.integer({ max: 20, min: 0 }), {
147
+ maxLength: 6,
148
+ minLength: 2,
149
+ }),
150
+ },
151
+ { numRuns: Math.min(numRuns, 20) },
152
+ )('repeated growth keeps results correct', ({ sizes }) => {
153
+ // Each size is a page count. Going up grows linear memory and detaches the
154
+ // previous `ArrayBuffer`; coming back down must not, since WASM memory never
155
+ // shrinks. Either way the view has to be re-derived, and a stale one reads
156
+ // as zeroes rather than throwing.
157
+ for (const pages of sizes) {
158
+ const input = new Uint8Array(pages * 65_536).fill(pages & 0xff)
159
+ expect(engine.Hash.keccak256(input), `${pages} pages`).toEqual(
160
+ keccak_256(input),
161
+ )
162
+ }
163
+ })
164
+
165
+ test.prop({ input: arbitraryInput }, { numRuns })(
166
+ 'digests never alias WASM memory',
167
+ ({ input }) => {
168
+ // A digest returned as a view over linear memory would be silently
169
+ // rewritten by the next call. Hold one across a larger call and check it.
170
+ const digest = engine.Hash.keccak256(input)
171
+ const snapshot = digest.slice()
172
+ engine.Hash.keccak256(new Uint8Array(input.length + 1024).fill(0xaa))
173
+ expect(digest).toEqual(snapshot)
174
+ },
175
+ )
176
+
177
+ test.prop({ input: arbitraryInput }, { numRuns })(
178
+ 'inputs are not mutated',
179
+ ({ input }) => {
180
+ const snapshot = input.slice()
181
+ engine.Hash.keccak256(input)
182
+ engine.Hash.sha256(input)
183
+ engine.Hash.ripemd160(input)
184
+ engine.Hash.hmacSha256(input, input)
185
+ expect(input).toEqual(snapshot)
186
+ },
187
+ )
188
+ })
@@ -0,0 +1,28 @@
1
+ import type { Engine } from 'ox'
2
+ import { Hash as WasmHash } from 'ox/wasm'
3
+ import { expectTypeOf, test } from 'vp/test'
4
+
5
+ type Created = Awaited<ReturnType<typeof WasmHash.create>>
6
+
7
+ test('every primitive is present, so callers need no assertion', () => {
8
+ expectTypeOf<Created['Hash']['keccak256']>().toEqualTypeOf<
9
+ (input: Uint8Array) => Uint8Array
10
+ >()
11
+ expectTypeOf<Created['Hash']['sha256']>().toEqualTypeOf<
12
+ (input: Uint8Array) => Uint8Array
13
+ >()
14
+ expectTypeOf<Created['Hash']['ripemd160']>().toEqualTypeOf<
15
+ (input: Uint8Array) => Uint8Array
16
+ >()
17
+ expectTypeOf<Created['Hash']['hmacSha256']>().toEqualTypeOf<
18
+ (key: Uint8Array, message: Uint8Array) => Uint8Array
19
+ >()
20
+ })
21
+
22
+ test('blake3 is absent, because the WASM module does not implement it', () => {
23
+ expectTypeOf<Created['Hash']>().not.toHaveProperty('blake3')
24
+ })
25
+
26
+ test('the result is still an engine, so `Engine.set` accepts it', () => {
27
+ expectTypeOf<Created>().toExtend<Engine.Engine>()
28
+ })