ox 1.0.5 → 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 (217) hide show
  1. package/CHANGELOG.md +50 -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/internal/mine.wasm.d.ts +2 -4
  114. package/dist/tempo/internal/mine.wasm.d.ts.map +1 -1
  115. package/dist/tempo/internal/mine.wasm.js +7 -12
  116. package/dist/tempo/internal/mine.wasm.js.map +1 -1
  117. package/dist/tempo/internal/virtualMasterPool.js +4 -1
  118. package/dist/tempo/internal/virtualMasterPool.js.map +1 -1
  119. package/dist/wasm/Engine.d.ts +62 -0
  120. package/dist/wasm/Engine.d.ts.map +1 -0
  121. package/dist/wasm/Engine.js +57 -0
  122. package/dist/wasm/Engine.js.map +1 -0
  123. package/dist/wasm/Hash.d.ts +49 -0
  124. package/dist/wasm/Hash.d.ts.map +1 -0
  125. package/dist/wasm/Hash.js +70 -0
  126. package/dist/wasm/Hash.js.map +1 -0
  127. package/dist/wasm/index.d.ts +46 -0
  128. package/dist/wasm/index.d.ts.map +1 -0
  129. package/dist/wasm/index.js +46 -0
  130. package/dist/wasm/index.js.map +1 -0
  131. package/dist/wasm/internal/hash.d.ts +9 -0
  132. package/dist/wasm/internal/hash.d.ts.map +1 -0
  133. package/dist/wasm/internal/hash.js +26 -0
  134. package/dist/wasm/internal/hash.js.map +1 -0
  135. package/dist/wasm/internal/hashes.wasm.d.ts +5 -0
  136. package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -0
  137. package/dist/wasm/internal/hashes.wasm.js +12 -0
  138. package/dist/wasm/internal/hashes.wasm.js.map +1 -0
  139. package/dist/wasm/internal/instantiate.d.ts +56 -0
  140. package/dist/wasm/internal/instantiate.d.ts.map +1 -0
  141. package/dist/wasm/internal/instantiate.js +92 -0
  142. package/dist/wasm/internal/instantiate.js.map +1 -0
  143. package/dist/zod/TypedData.d.ts +12 -12
  144. package/package.json +36 -1
  145. package/src/core/Base64.ts +19 -166
  146. package/src/core/BinaryStateTree.ts +2 -3
  147. package/src/core/Bls.ts +27 -77
  148. package/src/core/Ed25519.ts +7 -7
  149. package/src/core/Engine.ts +235 -0
  150. package/src/core/Hash.ts +9 -12
  151. package/src/core/Keystore.ts +17 -17
  152. package/src/core/Mnemonic.ts +3 -6
  153. package/src/core/P256.ts +15 -24
  154. package/src/core/Rlp_tx.bench.ts +2 -4
  155. package/src/core/Secp256k1.ts +9 -14
  156. package/src/core/TxEnvelopeEip1559.bench.ts +0 -5
  157. package/src/core/WebCryptoP256.ts +2 -2
  158. package/src/core/X25519.ts +4 -3
  159. package/src/core/_test/AbiParameters.adversarial.fuzz.ts +1 -2
  160. package/src/core/_test/AbiParameters.fuzz.ts +1 -2
  161. package/src/core/_test/Base.fuzz.ts +1 -2
  162. package/src/core/_test/Cbor.fuzz.ts +1 -2
  163. package/src/core/_test/Engine.fuzz.ts +293 -0
  164. package/src/core/_test/Engine.test-d.ts +84 -0
  165. package/src/core/_test/Engine.test.ts +523 -0
  166. package/src/core/_test/Hash.vectors.test.ts +56 -0
  167. package/src/core/_test/Hex.fuzz.ts +1 -2
  168. package/src/core/_test/Rlp.fuzz.ts +1 -2
  169. package/src/core/_test/Signature.fuzz.ts +1 -2
  170. package/src/core/_test/curves.test.ts +7 -0
  171. package/src/core/_test/index.test.ts +1 -0
  172. package/src/core/internal/bls.ts +121 -0
  173. package/src/core/internal/codec/_test/base64.conformance.ts +156 -0
  174. package/src/core/internal/codec/_test/hex.conformance.ts +204 -0
  175. package/src/core/internal/codec/_test/hex.fuzz.ts +234 -0
  176. package/src/core/internal/codec/_test/hex.test.ts +124 -0
  177. package/src/core/internal/codec/base64.ts +222 -0
  178. package/src/core/internal/codec/hex.ts +115 -34
  179. package/src/core/internal/curves.ts +18 -0
  180. package/src/core/internal/ed25519.ts +64 -0
  181. package/src/core/internal/engine.ts +410 -0
  182. package/src/core/internal/hash.ts +48 -0
  183. package/src/core/internal/keystore.ts +107 -0
  184. package/src/core/internal/mnemonic.ts +18 -0
  185. package/src/core/internal/p256.ts +91 -0
  186. package/src/core/internal/secp256k1.ts +91 -0
  187. package/src/core/internal/x25519.ts +40 -0
  188. package/src/index.docs.ts +2 -0
  189. package/src/index.ts +36 -0
  190. package/src/node/Engine.ts +69 -0
  191. package/src/node/Hash.ts +56 -0
  192. package/src/node/_test/Engine.test.ts +62 -0
  193. package/src/node/_test/Hash.test-d.ts +26 -0
  194. package/src/node/_test/Hash.test.ts +102 -0
  195. package/src/node/index.ts +40 -0
  196. package/src/tempo/VirtualMaster.test.ts +75 -0
  197. package/src/tempo/VirtualMaster.ts +28 -6
  198. package/src/tempo/internal/mine.wasm.ts +7 -13
  199. package/src/tempo/internal/virtualMasterPool.ts +4 -1
  200. package/src/tsconfig.json +2 -1
  201. package/src/version.ts +1 -1
  202. package/src/wasm/Engine.ts +76 -0
  203. package/src/wasm/Hash.bench.ts +60 -0
  204. package/src/wasm/Hash.ts +109 -0
  205. package/src/wasm/_test/Engine.test.ts +62 -0
  206. package/src/wasm/_test/Hash.browser.test.ts +50 -0
  207. package/src/wasm/_test/Hash.fuzz.ts +188 -0
  208. package/src/wasm/_test/Hash.test-d.ts +28 -0
  209. package/src/wasm/_test/Hash.test.ts +211 -0
  210. package/src/wasm/_test/Hash.vectors.test.ts +117 -0
  211. package/src/wasm/_test/Runtime.test.ts +21 -0
  212. package/src/wasm/_test/runtime.wasm.ts +11 -0
  213. package/src/wasm/index.ts +47 -0
  214. package/src/wasm/internal/hash.ts +50 -0
  215. package/src/wasm/internal/hashes.wasm.ts +14 -0
  216. package/src/wasm/internal/instantiate.ts +135 -0
  217. package/src/tempo/internal/mine.c +0 -182
@@ -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
+ })
@@ -0,0 +1,211 @@
1
+ import { hmac } from '@noble/hashes/hmac.js'
2
+ import { ripemd160 } from '@noble/hashes/legacy.js'
3
+ import { sha256 } from '@noble/hashes/sha2.js'
4
+ import { keccak_256 } from '@noble/hashes/sha3.js'
5
+ import { Bytes, Engine, Hash } from 'ox'
6
+ import { Hash as WasmHash } from 'ox/wasm'
7
+ import { beforeAll, describe, expect, test } from 'vp/test'
8
+ import { hmacSha256ScratchSize, wasmBase64 } from '../internal/hashes.wasm.js'
9
+ import * as hash from '../internal/hash.js'
10
+ import * as internal from '../internal/instantiate.js'
11
+
12
+ let engine: WasmHash.create.ReturnType
13
+
14
+ beforeAll(async () => {
15
+ engine = await WasmHash.create()
16
+ })
17
+
18
+ describe('create', () => {
19
+ test('default', async () => {
20
+ expect(Object.keys(await WasmHash.create())).toMatchInlineSnapshot(`
21
+ [
22
+ "Hash",
23
+ ]
24
+ `)
25
+ expect(Object.keys((await WasmHash.create()).Hash)).toMatchInlineSnapshot(`
26
+ [
27
+ "hmacSha256",
28
+ "keccak256",
29
+ "ripemd160",
30
+ "sha256",
31
+ ]
32
+ `)
33
+ })
34
+
35
+ test('behavior: hands every caller its own engine', async () => {
36
+ const [a, b] = await Promise.all([WasmHash.create(), WasmHash.create()])
37
+
38
+ // Deliberately not the same object. A shared slot lets one caller's
39
+ // customisation for composition leak into what a later `create` -- or
40
+ // `Engine.load` -- installs.
41
+ expect(a).not.toBe(b)
42
+ expect(a.Hash).not.toBe(b.Hash)
43
+
44
+ a.Hash.keccak256 = () => new Uint8Array(32).fill(9)
45
+ const c = await WasmHash.create()
46
+ expect(c.Hash.keccak256(Bytes.from('0xdeadbeef'))).toEqual(
47
+ b.Hash.keccak256(Bytes.from('0xdeadbeef')),
48
+ )
49
+ })
50
+ })
51
+
52
+ // Sizes chosen around the block boundaries where padding bugs live: keccak256's
53
+ // rate is 136 bytes, sha256 and ripemd160 use 64-byte blocks and need a second
54
+ // block once the remainder reaches 56.
55
+ const sizes = [
56
+ 0, 1, 2, 31, 32, 55, 56, 63, 64, 65, 71, 72, 111, 135, 136, 137, 199, 200,
57
+ 271, 272, 1000,
58
+ ]
59
+
60
+ function input(size: number) {
61
+ return new Uint8Array(size).map((_, index) => (index * 37 + 11) & 0xff)
62
+ }
63
+
64
+ describe('Hash', () => {
65
+ test.each(sizes)('keccak256 matches the default at %i bytes', (size) => {
66
+ expect(engine.Hash.keccak256(input(size))).toEqual(keccak_256(input(size)))
67
+ })
68
+
69
+ test.each(sizes)('sha256 matches the default at %i bytes', (size) => {
70
+ expect(engine.Hash.sha256(input(size))).toEqual(sha256(input(size)))
71
+ })
72
+
73
+ test.each(sizes)('ripemd160 matches the default at %i bytes', (size) => {
74
+ expect(engine.Hash.ripemd160(input(size))).toEqual(ripemd160(input(size)))
75
+ })
76
+
77
+ test.each([0, 1, 32, 63, 64, 65, 200])(
78
+ 'hmacSha256 matches the default with a %i byte key',
79
+ (size) => {
80
+ const key = new Uint8Array(size).fill(0xa5)
81
+ const message = input(100)
82
+ expect(engine.Hash.hmacSha256(key, message)).toEqual(
83
+ hmac(sha256, key, message),
84
+ )
85
+ },
86
+ )
87
+
88
+ test('behavior: known vectors', () => {
89
+ expect(Bytes.toHex(engine.Hash.keccak256(new Uint8Array(0)))).toBe(
90
+ '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
91
+ )
92
+ expect(Bytes.toHex(engine.Hash.sha256(new Uint8Array(0)))).toBe(
93
+ '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
94
+ )
95
+ expect(Bytes.toHex(engine.Hash.ripemd160(new Uint8Array(0)))).toBe(
96
+ '0x9c1185a5c5e9fc54612808977ee8f548b2258d31',
97
+ )
98
+ })
99
+
100
+ test('behavior: grows memory past 64 MiB, then still hashes small inputs', () => {
101
+ // The input itself is exactly 64 MiB, so the heap base and digest force
102
+ // memory past that boundary. Growing detaches the previous `ArrayBuffer`; a
103
+ // retained view would silently read zero bytes in the assertion below.
104
+ const large = new Uint8Array(64 * 1024 * 1024).fill(7)
105
+ expect(engine.Hash.sha256(large)).toEqual(sha256(large))
106
+
107
+ const small = input(32)
108
+ expect(engine.Hash.sha256(small)).toEqual(sha256(small))
109
+ })
110
+
111
+ test('behavior: hmacSha256 clears its complete memory region', async () => {
112
+ const module = await internal.instantiate<hash.Exports>(wasmBase64)
113
+ const key = input(120)
114
+ const message = input(17)
115
+ const out = 32
116
+ const messagePtr = module.heapBase
117
+ const keyPtr = messagePtr + message.length
118
+ const outPtr = keyPtr + key.length
119
+ const scratchPtr = Math.ceil((outPtr + out) / 4) * 4
120
+ const end = scratchPtr + hmacSha256ScratchSize
121
+ const stack = module.view().slice(0, module.heapBase)
122
+
123
+ expect(hash.hmacSha256(module, key, message)).toEqual(
124
+ hmac(sha256, key, message),
125
+ )
126
+
127
+ expect(module.view().slice(0, module.heapBase)).toEqual(stack)
128
+ expect(module.view().slice(messagePtr, end)).toEqual(
129
+ new Uint8Array(end - messagePtr),
130
+ )
131
+ })
132
+
133
+ test('behavior: hmacSha256 clears its complete region after a trap', async () => {
134
+ const module = await internal.instantiate<hash.Exports>(wasmBase64)
135
+ const key = input(120)
136
+ const message = input(17)
137
+ const out = 32
138
+ const messagePtr = module.heapBase
139
+ const keyPtr = messagePtr + message.length
140
+ const validOutPtr = keyPtr + key.length
141
+ const scratchPtr = Math.ceil((validOutPtr + out) / 4) * 4
142
+ const end = scratchPtr + hmacSha256ScratchSize
143
+ const stack = module.view().slice(0, module.heapBase)
144
+ // Preserve the production boundary while making the real export trap only
145
+ // after it has populated the scratch buffer.
146
+ const exports = {
147
+ hmac_sha256(
148
+ keyPtr: number,
149
+ keyLength: number,
150
+ messagePtr: number,
151
+ messageLength: number,
152
+ _outPtr: number,
153
+ scratchPtr: number,
154
+ ) {
155
+ module.exports.hmac_sha256(
156
+ keyPtr,
157
+ keyLength,
158
+ messagePtr,
159
+ messageLength,
160
+ module.view().length - 16,
161
+ scratchPtr,
162
+ )
163
+ },
164
+ zero: (ptr: number, length: number) => module.exports.zero(ptr, length),
165
+ }
166
+ const trappingModule = { ...module, exports }
167
+
168
+ expect(() => hash.hmacSha256(trappingModule, key, message)).toThrow(
169
+ WebAssembly.RuntimeError,
170
+ )
171
+ expect(module.view().slice(0, module.heapBase)).toEqual(stack)
172
+ expect(module.view().slice(messagePtr, end)).toEqual(
173
+ new Uint8Array(end - messagePtr),
174
+ )
175
+
176
+ module.exports.zero(module.view().length - 16, 16)
177
+ expect(hash.hmacSha256(module, key, message)).toEqual(
178
+ hmac(sha256, key, message),
179
+ )
180
+ })
181
+
182
+ test('behavior: consecutive calls do not corrupt each other', () => {
183
+ const a = input(200)
184
+ const b = input(7)
185
+ expect(engine.Hash.keccak256(a)).toEqual(keccak_256(a))
186
+ expect(engine.Hash.keccak256(b)).toEqual(keccak_256(b))
187
+ expect(engine.Hash.keccak256(a)).toEqual(keccak_256(a))
188
+ })
189
+
190
+ test('behavior: results do not alias WASM memory', () => {
191
+ const first = engine.Hash.keccak256(input(32))
192
+ const snapshot = first.slice()
193
+ engine.Hash.keccak256(input(64))
194
+ expect(first).toEqual(snapshot)
195
+ })
196
+ })
197
+
198
+ describe('Engine.set', () => {
199
+ test('behavior: ox uses the WASM implementation once installed', () => {
200
+ Engine.set(engine)
201
+ expect(Hash.keccak256('0xdeadbeef')).toBe(
202
+ '0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1',
203
+ )
204
+ expect(Hash.sha256('0xdeadbeef')).toBe(
205
+ '0x5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953',
206
+ )
207
+ expect(Hash.ripemd160('0xdeadbeef')).toBe(
208
+ '0x226821c2f5423e11fe9af68bd285c249db2e4b5a',
209
+ )
210
+ })
211
+ })
@@ -0,0 +1,117 @@
1
+ import { Hash as WasmHash } from 'ox/wasm'
2
+ import { beforeAll, describe, expect, test } from 'vp/test'
3
+ import * as vectors from '../../../test/vectors/hashes/index.js'
4
+ import { wasmBase64 } from '../internal/hashes.wasm.js'
5
+ import { instantiate } from '../internal/instantiate.js'
6
+
7
+ /**
8
+ * Published vectors against the WASM implementations.
9
+ *
10
+ * The differential tests in `Hash.test.ts` and `Hash.fuzz.ts` prove the WASM
11
+ * agrees with `@noble/hashes`. These prove it agrees with the specifications, so
12
+ * a fault the two implementations happened to share would still be caught.
13
+ * See `test/vectors/hashes/README.md`.
14
+ */
15
+
16
+ let engine: WasmHash.create.ReturnType
17
+
18
+ beforeAll(async () => {
19
+ engine = await WasmHash.create()
20
+ })
21
+
22
+ describe('sha256', () => {
23
+ test(`matches ${vectors.sha256.length} NIST CAVP vectors`, () => {
24
+ for (const { digest, message } of vectors.sha256)
25
+ expect(engine.Hash.sha256(message)).toEqual(digest)
26
+ })
27
+ })
28
+
29
+ describe('hmacSha256', () => {
30
+ test(`matches ${vectors.hmacSha256.length} RFC 4231 vectors`, () => {
31
+ // Two of these use 131-byte keys, which is the only coverage of the
32
+ // longer-than-a-block path where the key is hashed before padding.
33
+ for (const { digest, key, message } of vectors.hmacSha256)
34
+ expect(engine.Hash.hmacSha256(key, message)).toEqual(digest)
35
+ })
36
+ })
37
+
38
+ describe('ripemd160', () => {
39
+ test(`matches ${vectors.ripemd160.length} reference vectors`, () => {
40
+ for (const { digest, message } of vectors.ripemd160)
41
+ expect(engine.Hash.ripemd160(message)).toEqual(digest)
42
+ })
43
+
44
+ test('matches the million-`a` reference vector', () => {
45
+ const { digest, message } = vectors.ripemd160MillionA
46
+ expect(engine.Hash.ripemd160(message)).toEqual(digest)
47
+ })
48
+ })
49
+
50
+ describe('keccak256', () => {
51
+ test(`matches ${vectors.keccak256.length} OpenSSL vectors`, () => {
52
+ for (const { digest, message } of vectors.keccak256)
53
+ expect(engine.Hash.keccak256(message)).toEqual(digest)
54
+ })
55
+ })
56
+
57
+ describe('keccak_f1600', () => {
58
+ type Exports = { keccak_f1600(state: number): void }
59
+
60
+ /** Writes 25 lanes as little-endian 64-bit words, permutes, and reads back. */
61
+ async function permute(lanes: readonly bigint[]) {
62
+ // A separate instance, because this export is not part of the engine and so
63
+ // is deliberately not reachable through `WasmHash.load`.
64
+ const module = await instantiate<Exports>(wasmBase64)
65
+ module.reserve(200)
66
+ const view = module.view()
67
+ for (let lane = 0; lane < 25; lane++) {
68
+ let value = lanes[lane]!
69
+ for (let byte = 0; byte < 8; byte++) {
70
+ view[module.heapBase + lane * 8 + byte] = Number(value & 0xffn)
71
+ value >>= 8n
72
+ }
73
+ }
74
+
75
+ module.exports.keccak_f1600(module.heapBase)
76
+
77
+ const after = module.view()
78
+ const result: bigint[] = []
79
+ for (let lane = 0; lane < 25; lane++) {
80
+ let value = 0n
81
+ for (let byte = 7; byte >= 0; byte--)
82
+ value =
83
+ (value << 8n) | BigInt(after[module.heapBase + lane * 8 + byte]!)
84
+ result.push(value)
85
+ }
86
+ return result
87
+ }
88
+
89
+ test('matches the Keccak team‘s reference examples', async () => {
90
+ for (const example of vectors.keccakPermutation.examples)
91
+ expect(await permute(example.input)).toEqual(example.output)
92
+ })
93
+
94
+ test('reference examples chain, so the second is not a restatement', () => {
95
+ // XKCP's second example feeds the first one's output back in. Asserting the
96
+ // link means a parser that silently read the same block twice would fail
97
+ // here rather than quietly halving the coverage above.
98
+ const [first, second] = vectors.keccakPermutation.examples
99
+ expect(second!.input).toEqual(first!.output)
100
+ expect(second!.output).not.toEqual(first!.output)
101
+ })
102
+
103
+ test('the parsed fixture is internally consistent', () => {
104
+ // Checks the fixture, not the implementation. Each `rounds[n]` is the state
105
+ // after round `n`'s iota step, and nothing exported reaches a single round,
106
+ // so per-round agreement is not observable from here -- the permutation
107
+ // itself is covered above, end to end against `input`/`output`. What this
108
+ // catches is a parser that mis-read the file and quietly weakened that.
109
+ const { examples, rhoOffsets, roundConstants } = vectors.keccakPermutation
110
+ expect(roundConstants).toHaveLength(24)
111
+ expect(rhoOffsets).toHaveLength(25)
112
+ for (const example of examples) {
113
+ expect(example.rounds).toHaveLength(24)
114
+ expect(example.rounds.at(-1)).toEqual(example.output)
115
+ }
116
+ })
117
+ })
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { expect, test } from 'vp/test'
3
+ import { instantiate } from '../internal/instantiate.js'
4
+ import { wasmBase64 } from './runtime.wasm.js'
5
+
6
+ test('freestanding runtime preserves memmove and realloc semantics', async () => {
7
+ const module = await instantiate<{ run(): number }>(wasmBase64)
8
+ expect(module.exports.run()).toBe(0)
9
+ })
10
+
11
+ test('memmove compares flat-memory offsets rather than C object pointers', () => {
12
+ const source = readFileSync(
13
+ new URL('../../../wasm/src/ox_rt.c', import.meta.url),
14
+ 'utf8',
15
+ )
16
+ const body = source.match(/void \*memmove\([\s\S]*?\n}\n\nvoid \*memset/)?.[0]
17
+
18
+ expect(body).toBeDefined()
19
+ expect(body).toContain('if ((size_t)d < (size_t)s)')
20
+ expect(body).not.toMatch(/if\s*\(\s*d\s*<\s*s\s*\)/)
21
+ })