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.
- package/CHANGELOG.md +50 -0
- package/dist/core/Base64.d.ts +10 -21
- package/dist/core/Base64.d.ts.map +1 -1
- package/dist/core/Base64.js +8 -127
- package/dist/core/Base64.js.map +1 -1
- package/dist/core/BinaryStateTree.d.ts.map +1 -1
- package/dist/core/BinaryStateTree.js +2 -2
- package/dist/core/BinaryStateTree.js.map +1 -1
- package/dist/core/Bls.d.ts.map +1 -1
- package/dist/core/Bls.js +19 -44
- package/dist/core/Bls.js.map +1 -1
- package/dist/core/Ed25519.d.ts.map +1 -1
- package/dist/core/Ed25519.js +7 -6
- package/dist/core/Ed25519.js.map +1 -1
- package/dist/core/Engine.d.ts +124 -0
- package/dist/core/Engine.d.ts.map +1 -0
- package/dist/core/Engine.js +180 -0
- package/dist/core/Engine.js.map +1 -0
- package/dist/core/Hash.d.ts +4 -4
- package/dist/core/Hash.d.ts.map +1 -1
- package/dist/core/Hash.js +9 -12
- package/dist/core/Hash.js.map +1 -1
- package/dist/core/Keystore.d.ts.map +1 -1
- package/dist/core/Keystore.js +15 -10
- package/dist/core/Keystore.js.map +1 -1
- package/dist/core/Mnemonic.d.ts.map +1 -1
- package/dist/core/Mnemonic.js +3 -2
- package/dist/core/Mnemonic.js.map +1 -1
- package/dist/core/P256.d.ts.map +1 -1
- package/dist/core/P256.js +8 -9
- package/dist/core/P256.js.map +1 -1
- package/dist/core/Secp256k1.d.ts.map +1 -1
- package/dist/core/Secp256k1.js +8 -9
- package/dist/core/Secp256k1.js.map +1 -1
- package/dist/core/WebCryptoP256.d.ts.map +1 -1
- package/dist/core/WebCryptoP256.js +2 -2
- package/dist/core/WebCryptoP256.js.map +1 -1
- package/dist/core/X25519.d.ts.map +1 -1
- package/dist/core/X25519.js +4 -3
- package/dist/core/X25519.js.map +1 -1
- package/dist/core/internal/bls.d.ts +12 -0
- package/dist/core/internal/bls.d.ts.map +1 -0
- package/dist/core/internal/bls.js +79 -0
- package/dist/core/internal/bls.js.map +1 -0
- package/dist/core/internal/codec/base64.d.ts +84 -0
- package/dist/core/internal/codec/base64.d.ts.map +1 -0
- package/dist/core/internal/codec/base64.js +177 -0
- package/dist/core/internal/codec/base64.js.map +1 -0
- package/dist/core/internal/codec/hex.d.ts +49 -4
- package/dist/core/internal/codec/hex.d.ts.map +1 -1
- package/dist/core/internal/codec/hex.js +109 -35
- package/dist/core/internal/codec/hex.js.map +1 -1
- package/dist/core/internal/curves.d.ts +17 -0
- package/dist/core/internal/curves.d.ts.map +1 -0
- package/dist/core/internal/curves.js +17 -0
- package/dist/core/internal/curves.js.map +1 -0
- package/dist/core/internal/ed25519.d.ts +14 -0
- package/dist/core/internal/ed25519.d.ts.map +1 -0
- package/dist/core/internal/ed25519.js +29 -0
- package/dist/core/internal/ed25519.js.map +1 -0
- package/dist/core/internal/engine.d.ts +278 -0
- package/dist/core/internal/engine.d.ts.map +1 -0
- package/dist/core/internal/engine.js +131 -0
- package/dist/core/internal/engine.js.map +1 -0
- package/dist/core/internal/hash.d.ts +12 -0
- package/dist/core/internal/hash.d.ts.map +1 -0
- package/dist/core/internal/hash.js +30 -0
- package/dist/core/internal/hash.js.map +1 -0
- package/dist/core/internal/keystore.d.ts +14 -0
- package/dist/core/internal/keystore.d.ts.map +1 -0
- package/dist/core/internal/keystore.js +32 -0
- package/dist/core/internal/keystore.js.map +1 -0
- package/dist/core/internal/mnemonic.d.ts +4 -0
- package/dist/core/internal/mnemonic.d.ts.map +1 -0
- package/dist/core/internal/mnemonic.js +14 -0
- package/dist/core/internal/mnemonic.js.map +1 -0
- package/dist/core/internal/p256.d.ts +14 -0
- package/dist/core/internal/p256.d.ts.map +1 -0
- package/dist/core/internal/p256.js +36 -0
- package/dist/core/internal/p256.js.map +1 -0
- package/dist/core/internal/secp256k1.d.ts +14 -0
- package/dist/core/internal/secp256k1.d.ts.map +1 -0
- package/dist/core/internal/secp256k1.js +36 -0
- package/dist/core/internal/secp256k1.js.map +1 -0
- package/dist/core/internal/x25519.d.ts +8 -0
- package/dist/core/internal/x25519.d.ts.map +1 -0
- package/dist/core/internal/x25519.js +20 -0
- package/dist/core/internal/x25519.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.docs.d.ts +2 -0
- package/dist/index.docs.d.ts.map +1 -1
- package/dist/index.docs.js +2 -0
- package/dist/index.docs.js.map +1 -1
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/dist/node/Engine.d.ts +55 -0
- package/dist/node/Engine.d.ts.map +1 -0
- package/dist/node/Engine.js +51 -0
- package/dist/node/Engine.js.map +1 -0
- package/dist/node/Hash.d.ts +39 -0
- package/dist/node/Hash.d.ts.map +1 -0
- package/dist/node/Hash.js +37 -0
- package/dist/node/Hash.js.map +1 -0
- package/dist/node/index.d.ts +39 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +39 -0
- package/dist/node/index.js.map +1 -0
- package/dist/tempo/VirtualMaster.d.ts +2 -0
- package/dist/tempo/VirtualMaster.d.ts.map +1 -1
- package/dist/tempo/VirtualMaster.js +19 -5
- package/dist/tempo/VirtualMaster.js.map +1 -1
- package/dist/tempo/internal/mine.wasm.d.ts +2 -4
- package/dist/tempo/internal/mine.wasm.d.ts.map +1 -1
- package/dist/tempo/internal/mine.wasm.js +7 -12
- package/dist/tempo/internal/mine.wasm.js.map +1 -1
- package/dist/tempo/internal/virtualMasterPool.js +4 -1
- package/dist/tempo/internal/virtualMasterPool.js.map +1 -1
- package/dist/wasm/Engine.d.ts +62 -0
- package/dist/wasm/Engine.d.ts.map +1 -0
- package/dist/wasm/Engine.js +57 -0
- package/dist/wasm/Engine.js.map +1 -0
- package/dist/wasm/Hash.d.ts +49 -0
- package/dist/wasm/Hash.d.ts.map +1 -0
- package/dist/wasm/Hash.js +70 -0
- package/dist/wasm/Hash.js.map +1 -0
- package/dist/wasm/index.d.ts +46 -0
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js +46 -0
- package/dist/wasm/index.js.map +1 -0
- package/dist/wasm/internal/hash.d.ts +9 -0
- package/dist/wasm/internal/hash.d.ts.map +1 -0
- package/dist/wasm/internal/hash.js +26 -0
- package/dist/wasm/internal/hash.js.map +1 -0
- package/dist/wasm/internal/hashes.wasm.d.ts +5 -0
- package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -0
- package/dist/wasm/internal/hashes.wasm.js +12 -0
- package/dist/wasm/internal/hashes.wasm.js.map +1 -0
- package/dist/wasm/internal/instantiate.d.ts +56 -0
- package/dist/wasm/internal/instantiate.d.ts.map +1 -0
- package/dist/wasm/internal/instantiate.js +92 -0
- package/dist/wasm/internal/instantiate.js.map +1 -0
- package/dist/zod/TypedData.d.ts +12 -12
- package/package.json +36 -1
- package/src/core/Base64.ts +19 -166
- package/src/core/BinaryStateTree.ts +2 -3
- package/src/core/Bls.ts +27 -77
- package/src/core/Ed25519.ts +7 -7
- package/src/core/Engine.ts +235 -0
- package/src/core/Hash.ts +9 -12
- package/src/core/Keystore.ts +17 -17
- package/src/core/Mnemonic.ts +3 -6
- package/src/core/P256.ts +15 -24
- package/src/core/Rlp_tx.bench.ts +2 -4
- package/src/core/Secp256k1.ts +9 -14
- package/src/core/TxEnvelopeEip1559.bench.ts +0 -5
- package/src/core/WebCryptoP256.ts +2 -2
- package/src/core/X25519.ts +4 -3
- package/src/core/_test/AbiParameters.adversarial.fuzz.ts +1 -2
- package/src/core/_test/AbiParameters.fuzz.ts +1 -2
- package/src/core/_test/Base.fuzz.ts +1 -2
- package/src/core/_test/Cbor.fuzz.ts +1 -2
- package/src/core/_test/Engine.fuzz.ts +293 -0
- package/src/core/_test/Engine.test-d.ts +84 -0
- package/src/core/_test/Engine.test.ts +523 -0
- package/src/core/_test/Hash.vectors.test.ts +56 -0
- package/src/core/_test/Hex.fuzz.ts +1 -2
- package/src/core/_test/Rlp.fuzz.ts +1 -2
- package/src/core/_test/Signature.fuzz.ts +1 -2
- package/src/core/_test/curves.test.ts +7 -0
- package/src/core/_test/index.test.ts +1 -0
- package/src/core/internal/bls.ts +121 -0
- package/src/core/internal/codec/_test/base64.conformance.ts +156 -0
- package/src/core/internal/codec/_test/hex.conformance.ts +204 -0
- package/src/core/internal/codec/_test/hex.fuzz.ts +234 -0
- package/src/core/internal/codec/_test/hex.test.ts +124 -0
- package/src/core/internal/codec/base64.ts +222 -0
- package/src/core/internal/codec/hex.ts +115 -34
- package/src/core/internal/curves.ts +18 -0
- package/src/core/internal/ed25519.ts +64 -0
- package/src/core/internal/engine.ts +410 -0
- package/src/core/internal/hash.ts +48 -0
- package/src/core/internal/keystore.ts +107 -0
- package/src/core/internal/mnemonic.ts +18 -0
- package/src/core/internal/p256.ts +91 -0
- package/src/core/internal/secp256k1.ts +91 -0
- package/src/core/internal/x25519.ts +40 -0
- package/src/index.docs.ts +2 -0
- package/src/index.ts +36 -0
- package/src/node/Engine.ts +69 -0
- package/src/node/Hash.ts +56 -0
- package/src/node/_test/Engine.test.ts +62 -0
- package/src/node/_test/Hash.test-d.ts +26 -0
- package/src/node/_test/Hash.test.ts +102 -0
- package/src/node/index.ts +40 -0
- package/src/tempo/VirtualMaster.test.ts +75 -0
- package/src/tempo/VirtualMaster.ts +28 -6
- package/src/tempo/internal/mine.wasm.ts +7 -13
- package/src/tempo/internal/virtualMasterPool.ts +4 -1
- package/src/tsconfig.json +2 -1
- package/src/version.ts +1 -1
- package/src/wasm/Engine.ts +76 -0
- package/src/wasm/Hash.bench.ts +60 -0
- package/src/wasm/Hash.ts +109 -0
- package/src/wasm/_test/Engine.test.ts +62 -0
- package/src/wasm/_test/Hash.browser.test.ts +50 -0
- package/src/wasm/_test/Hash.fuzz.ts +188 -0
- package/src/wasm/_test/Hash.test-d.ts +28 -0
- package/src/wasm/_test/Hash.test.ts +211 -0
- package/src/wasm/_test/Hash.vectors.test.ts +117 -0
- package/src/wasm/_test/Runtime.test.ts +21 -0
- package/src/wasm/_test/runtime.wasm.ts +11 -0
- package/src/wasm/index.ts +47 -0
- package/src/wasm/internal/hash.ts +50 -0
- package/src/wasm/internal/hashes.wasm.ts +14 -0
- package/src/wasm/internal/instantiate.ts +135 -0
- 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
|
+
})
|