diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 400d575..284d8de 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update \ git \ nano \ vim-tiny \ + ca-certificates \ && apt-get auto-remove -y \ && apt-get clean -y \ && chsh -s $(which bash) bun \ diff --git a/libs/as-sdk-integration-tests/assembly/crypto.ts b/libs/as-sdk-integration-tests/assembly/crypto.ts new file mode 100644 index 0000000..e1dd043 --- /dev/null +++ b/libs/as-sdk-integration-tests/assembly/crypto.ts @@ -0,0 +1,53 @@ +import { + Bytes, + OracleProgram, + Process, + decodeHex, + secp256k1Verify, +} from "../../as-sdk/assembly"; + +export class TestSecp256k1VerifyValid extends OracleProgram { + execution(): void { + const message = Bytes.fromString("Hello, SEDA!"); + const signature = Bytes.fromBytes( + decodeHex( + "58376cc76f4d4959b0adf8070ecf0079db889915a75370f6e39a8451ba5be0c35f091fa4d2fda3ced5b6e6acd1dbb4a45f2c6a1e643622ee4cf8b802b373d38f", + ), + ); + const publicKey = Bytes.fromBytes( + decodeHex( + "02a2bebd272aa28e410cc74cef28e5ce74a9ffc94caf817ed9bd23b01ce2068c7b", + ), + ); + const isValidSignature = secp256k1Verify(message, signature, publicKey); + + if (isValidSignature) { + Process.success(Bytes.fromString("valid secp256k1 signature")); + } else { + Process.error(Bytes.fromString("invalid secp256k1 signature")); + } + } +} + +export class TestSecp256k1VerifyInvalid extends OracleProgram { + execution(): void { + const message = Bytes.fromString("Hello, this is an invalid message!"); + const signature = Bytes.fromBytes( + decodeHex( + "58376cc76f4d4959b0adf8070ecf0079db889915a75370f6e39a8451ba5be0c35f091fa4d2fda3ced5b6e6acd1dbb4a45f2c6a1e643622ee4cf8b802b373d38f", + ), + ); + const publicKey = Bytes.fromBytes( + decodeHex( + "02a2bebd272aa28e410cc74cef28e5ce74a9ffc94caf817ed9bd23b01ce2068c7b", + ), + ); + const isValidSignature = secp256k1Verify(message, signature, publicKey); + + if (isValidSignature) { + Process.success(Bytes.fromString("valid secp256k1 signature")); + } else { + Process.error(Bytes.fromString("invalid secp256k1 signature")); + } + } +} diff --git a/libs/as-sdk-integration-tests/assembly/index.ts b/libs/as-sdk-integration-tests/assembly/index.ts index 71e3002..17b3dcf 100644 --- a/libs/as-sdk-integration-tests/assembly/index.ts +++ b/libs/as-sdk-integration-tests/assembly/index.ts @@ -1,4 +1,5 @@ import { Bytes, Process } from "../../as-sdk/assembly/index"; +import { TestSecp256k1VerifyInvalid, TestSecp256k1VerifyValid } from "./crypto"; import { TestHttpRejection, TestHttpSuccess, @@ -26,6 +27,10 @@ if (args === "testHttpRejection") { new TestTallyVmRevealsFiltered().run(); } else if (args === "testProxyHttpFetch") { new TestProxyHttpFetch().run(); +} else if (args === "testSecp256k1VerifyValid") { + new TestSecp256k1VerifyValid().run(); +} else if (args === "testSecp256k1VerifyInvalid") { + new TestSecp256k1VerifyInvalid().run(); } Process.error(Bytes.fromString("No argument given")); diff --git a/libs/as-sdk-integration-tests/src/crypto.test.ts b/libs/as-sdk-integration-tests/src/crypto.test.ts new file mode 100644 index 0000000..c7220e4 --- /dev/null +++ b/libs/as-sdk-integration-tests/src/crypto.test.ts @@ -0,0 +1,34 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { readFile } from "node:fs/promises"; +import { executeDrWasm } from "@seda/dev-tools"; +import { Response } from "node-fetch"; + +const mockHttpFetch = mock(); + +describe("Crypto", () => { + it("Test valid Secp256k1 signature", async () => { + const wasmBinary = await readFile( + "dist/libs/as-sdk-integration-tests/debug.wasm", + ); + + const result = await executeDrWasm( + wasmBinary, + Buffer.from("testSecp256k1VerifyValid"), + ); + + expect(result.resultAsString).toEqual("valid secp256k1 signature"); + }); + + it("Test invalid Secp256k1 signature", async () => { + const wasmBinary = await readFile( + "dist/libs/as-sdk-integration-tests/debug.wasm", + ); + + const result = await executeDrWasm( + wasmBinary, + Buffer.from("testSecp256k1VerifyInvalid"), + ); + + expect(result.resultAsString).toEqual("invalid secp256k1 signature"); + }); +}); diff --git a/libs/as-sdk/assembly/bindings/seda_v1.ts b/libs/as-sdk/assembly/bindings/seda_v1.ts index 2ab74db..5d0c921 100644 --- a/libs/as-sdk/assembly/bindings/seda_v1.ts +++ b/libs/as-sdk/assembly/bindings/seda_v1.ts @@ -14,3 +14,12 @@ export declare function execution_result( result: usize, result_length: u32, ): void; + +export declare function secp256k1_verify( + message: usize, + message_length: u32, + signature: usize, + signature_length: u32, + public_key: usize, + public_key_length: u32, +): u8; diff --git a/libs/as-sdk/assembly/crypto.ts b/libs/as-sdk/assembly/crypto.ts new file mode 100644 index 0000000..ccdd159 --- /dev/null +++ b/libs/as-sdk/assembly/crypto.ts @@ -0,0 +1,40 @@ +import { call_result_write, secp256k1_verify } from "./bindings/seda_v1"; +import { Bytes } from "./bytes"; +import { PromiseStatus } from "./promise"; + +export function secp256k1Verify( + message: Bytes, + signature: Bytes, + publicKey: Bytes, +): bool { + const messageBuffer = message.buffer(); + const messagePtr = changetype(messageBuffer); + + const signatureBuffer = signature.buffer(); + const signaturePtr = changetype(signatureBuffer); + + const publicKeyBuffer = publicKey.buffer(); + const publicKeyPtr = changetype(publicKeyBuffer); + + // Call secp256k1_verify and get the response length + const responseLength = secp256k1_verify( + messagePtr, + messageBuffer.byteLength, + signaturePtr, + signatureBuffer.byteLength, + publicKeyPtr, + publicKeyBuffer.byteLength, + ); + + // Allocate an ArrayBuffer for the response + const responseBuffer = new ArrayBuffer(responseLength); + // Get the pointer to the response buffer + const responseBufferPtr = changetype(responseBuffer); + // Write the result to the allocated buffer + call_result_write(responseBufferPtr, responseLength); + // Convert the response buffer into a Uint8Array + const responseArray = Uint8Array.wrap(responseBuffer); + + // Return true if the response is [1] (valid) + return responseArray[0] === 1; +} diff --git a/libs/as-sdk/assembly/index.ts b/libs/as-sdk/assembly/index.ts index d22fd61..e7df0fd 100644 --- a/libs/as-sdk/assembly/index.ts +++ b/libs/as-sdk/assembly/index.ts @@ -19,3 +19,4 @@ export { Console } from "./console"; export { Bytes } from "./bytes"; export { decodeHex, encodeHex } from "./hex"; export { OracleProgram } from "./oracle-program"; +export { secp256k1Verify } from "./crypto"; diff --git a/libs/vm/project.json b/libs/vm/project.json index 83f0acf..d6d9654 100644 --- a/libs/vm/project.json +++ b/libs/vm/project.json @@ -17,6 +17,17 @@ "generateExportsField": true }, "outputs": ["{options.outputPath}"] + }, + "test": { + "executor": "nx:run-commands", + "options": { + "command": "bun test libs/vm/" + }, + "dependsOn": [ + { + "target": "build" + } + ] } } } diff --git a/libs/vm/src/services/crypto.test.ts b/libs/vm/src/services/crypto.test.ts new file mode 100644 index 0000000..a9982b2 --- /dev/null +++ b/libs/vm/src/services/crypto.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "bun:test"; +import { secp256k1Verify } from "./crypto"; + +describe("Crypto", () => { + it("should Secp256k1 verify", async () => { + const message = Buffer.from("Hello, SEDA!"); + const signature = Buffer.from( + "58376cc76f4d4959b0adf8070ecf0079db889915a75370f6e39a8451ba5be0c35f091fa4d2fda3ced5b6e6acd1dbb4a45f2c6a1e643622ee4cf8b802b373d38f", + "hex", + ); + const publicKey = Buffer.from( + "02a2bebd272aa28e410cc74cef28e5ce74a9ffc94caf817ed9bd23b01ce2068c7b", + "hex", + ); + const result = secp256k1Verify(message, signature, publicKey); + + // Check if the result is a Uint8Array and has the value [1] + expect(result).toEqual(new Uint8Array([1])); + }); +}); diff --git a/libs/vm/src/services/crypto.ts b/libs/vm/src/services/crypto.ts new file mode 100644 index 0000000..3e56eb2 --- /dev/null +++ b/libs/vm/src/services/crypto.ts @@ -0,0 +1,25 @@ +import { keccak_256 } from "@noble/hashes/sha3"; +import * as Secp256k1 from "@noble/secp256k1"; + +export function keccak256(input: Buffer): Buffer { + const hasher = keccak_256.create(); + hasher.update(input); + + return Buffer.from(hasher.digest()); +} + +export function secp256k1Verify( + message: Buffer, + signature: Buffer, + publicKey: Buffer, +): Uint8Array { + const signedMessage = keccak256(message); + const isValidSignature = Secp256k1.verify( + signature, + signedMessage, + publicKey, + ); + + // Return 1 as Uint8Array if valid, 0 if not + return new Uint8Array([isValidSignature ? 1 : 0]); +} diff --git a/libs/vm/src/services/keccak256.ts b/libs/vm/src/services/keccak256.ts deleted file mode 100644 index 0473abd..0000000 --- a/libs/vm/src/services/keccak256.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { keccak_256 } from "@noble/hashes/sha3"; - -export function keccak256(input: Buffer): Buffer { - const hasher = keccak_256.create(); - hasher.update(input); - - return Buffer.from(hasher.digest()); -} diff --git a/libs/vm/src/vm-imports.ts b/libs/vm/src/vm-imports.ts index 3a6ff05..e941833 100644 --- a/libs/vm/src/vm-imports.ts +++ b/libs/vm/src/vm-imports.ts @@ -1,6 +1,7 @@ import * as Secp256k1 from "@noble/secp256k1"; import { Maybe } from "true-myth"; -import { keccak256 } from "./services/keccak256"; +import { keccak256, secp256k1Verify } from "./services/crypto"; +import { trySync } from "./services/try"; import { type HttpFetchAction, HttpFetchResponse } from "./types/vm-actions"; import { PromiseStatus } from "./types/vm-promise"; import { WorkerToHost } from "./worker-host-communication.js"; @@ -8,7 +9,9 @@ import { WorkerToHost } from "./worker-host-communication.js"; export default class VmImports { memory?: WebAssembly.Memory; workerToHost: WorkerToHost; + // Used for async calls (for knowing the length of the buffer) callResult: Uint8Array = new Uint8Array(); + // Execution result result: Uint8Array = new Uint8Array(); usedPublicKeys: string[] = []; processId: string; @@ -113,6 +116,53 @@ export default class VmImports { } } + secp256k1Verify( + messagePtr: number, + messageLength: number, + signaturePtr: number, + signatureLength: number, + publicKeyPtr: number, + publicKeyLength: number, + ) { + const message = Buffer.from( + new Uint8Array( + this.memory?.buffer.slice(messagePtr, messagePtr + messageLength) ?? [], + ), + ); + const signature = Buffer.from( + new Uint8Array( + this.memory?.buffer.slice( + signaturePtr, + signaturePtr + signatureLength, + ) ?? [], + ), + ); + const publicKey = Buffer.from( + new Uint8Array( + this.memory?.buffer.slice( + publicKeyPtr, + publicKeyPtr + publicKeyLength, + ) ?? [], + ), + ); + + const result = trySync(() => + secp256k1Verify(message, signature, publicKey), + ); + + if (result.isErr) { + console.error( + `[${this.processId}] - @secp256k1Verify: ${message}`, + result.error, + ); + this.callResult = new Uint8Array(); + return 0; + } + + this.callResult = result.value; + return this.callResult.length; + } + callResultWrite(ptr: number, length: number) { try { const memory = new Uint8Array(this.memory?.buffer ?? []); @@ -137,6 +187,7 @@ export default class VmImports { // TODO: Should be this.proxyHttpFetch but since thats broken for now we will use httpFetch proxy_http_fetch: this.httpFetch.bind(this), http_fetch: this.httpFetch.bind(this), + secp256k1_verify: this.secp256k1Verify.bind(this), call_result_write: this.callResultWrite.bind(this), execution_result: this.executionResult.bind(this), },