Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion solidity/falcon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,28 @@ source ./emsdk_env.sh
make js
node test_falcon.js
```
An example of use (key generation, sign and verify) is provided in test_example.js
A reference example of use (key generation, sign and verify) is provided in test_falcon.js, it corresponds to the solidity test vector.



### Interact with the smart contracts

Use deterministic_falcon_sign to deterministically generate a key pair from seed and sign. Input are the 32 bytes seed and the hexadecimal value of the hash of the message to sign.
```bash
Usage: deterministic_falcon_sign.js <32_byte_seed_hex> <message_hex_with_0x_prefix> [--output-signature-only | --output-publickey-only]
```

Output only the public key (second argument is useless but shall be provided):

```bash
node deterministic_falcon_sign.js 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef 0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750 --output-publickey-only
```

Then, using the same seed, use it to produce signature for messages

```bash
node deterministic_falcon_sign.js 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef 0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750 --output-signature-only
```

## Go bindings

Expand Down
77 changes: 77 additions & 0 deletions solidity/falcon/deterministic_ecdsa_sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// first install npm install ethers
import { Wallet, hashMessage, recoverAddress, SigningKey, sha256 } from "ethers";

// --- Get arguments from command line ---
// Expected usage: node your_script_name.js <32_byte_seed_hex> <message_string>
const args = process.argv.slice(2); // Slice to get only the actual arguments

if (args.length < 2) {
console.error("Usage: node your_script_name.js <32_byte_seed_hex> <message_string>");
process.exit(1);
}

const deterministicSeedInput = args[0];
const message = args[1]; // The message is now taken directly from the second argument



// Validate seed length (must be 32 bytes = 64 hex characters)
if (deterministicSeedInput.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(deterministicSeedInput)) {
console.error("Error: Seed must be a 32-byte (64 hex characters) hexadecimal string.");
process.exit(1);
}

// Prepend "0x" if it's not there, as ethers.sha256 typically expects it or a Uint8Array
const deterministicSeed = deterministicSeedInput.startsWith("0x") ? deterministicSeedInput : "0x" + deterministicSeedInput;

async function signMessage() {
// Derive the private key deterministically from the seed using SHA256
const privateKey = sha256(deterministicSeed);

// Create a Wallet instance from the derived private key
const wallet = new Wallet(privateKey);

const messageHash = hashMessage(message);
const signature = await wallet.signMessage(message);
const signatureObject= wallet.signingKey.sign(messageHash);
const signature2 = signatureObject.serialized;
const recoveredAddress = recoverAddress(messageHash, signature);

// Helper to parse the signature
function parseSignature(signature) {
const sig = signature.startsWith("0x") ? signature.slice(2) : signature;
return {
r: "0x" + sig.slice(0, 64),
s: "0x" + sig.slice(64, 128),
v: parseInt(sig.slice(128, 130), 16),
};
}
const sig = parseSignature(signature);

const signingKey = new SigningKey(wallet.privateKey);
const publicKey = signingKey.publicKey;

// Extract X and Y coordinates from the uncompressed public key
const x = "0x" + publicKey.slice(4, 68); // Slice from index 4 to 68 for X (skipping 0x04 prefix)
const y = "0x" + publicKey.slice(68); // Slice from index 68 to end for Y

console.log("Message: ", message);
console.log("Input Seed (hex):", deterministicSeedInput);
console.log("Derived Private Key:", privateKey);
console.log("Wallet Address: ", wallet.address);
console.log("Message Hash: ", messageHash);
console.log("Signature: ", signature);
console.log("Signature: ", signature2);

console.log("r (signature): ", sig.r);
console.log("s (signature): ", sig.s);
console.log("v (signature): ", sig.v);
console.log("Public Key (uncompressed):", publicKey); // The full 0x04... key
console.log("Public Key X: ", x);
console.log("Public Key Y: ", y);
console.log("Recovered Addr: ", recoveredAddress);
}

signMessage(); // Call the async function

console.log("ending script...");
179 changes: 179 additions & 0 deletions solidity/falcon/deterministic_falcon_sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Filename: test_falcon.js (modified)

const fs = require('fs');
const Module = require('./falcon.js'); // Ensure falcon.js is in the same directory

// --- Get arguments from command line ---
// Expected usage:
// node test_falcon.js <32_byte_seed_hex> <message_hex_with_0x_prefix>
// node test_falcon.js <32_byte_seed_hex> <message_hex_with_0x_prefix> --output-signature-only
// node test_falcon.js <32_byte_seed_hex> <message_hex_with_0x_prefix> --output-publickey-only

const args = process.argv.slice(2);

let seedHexInput;
let messageHexInput;
let outputSignatureOnly = false;
let outputPublicKeyOnly = false;

// Basic argument parsing
if (args.length < 2 || args.length > 3) {
console.error("Usage: node test_falcon.js <32_byte_seed_hex> <message_hex_with_0x_prefix> [--output-signature-only | --output-publickey-only]");
process.exit(1);
}

seedHexInput = args[0];
messageHexInput = args[1];

if (args.length === 3) {
if (args[2] === "--output-signature-only") {
outputSignatureOnly = true;
} else if (args[2] === "--output-publickey-only") {
outputPublicKeyOnly = true;
} else {
console.error("Error: Invalid third argument. Use '--output-signature-only' or '--output-publickey-only' or omit it.");
process.exit(1);
}
}

// Ensure flags are mutually exclusive
if (outputSignatureOnly && outputPublicKeyOnly) {
console.error("Error: Cannot use both '--output-signature-only' and '--output-publickey-only' simultaneously.");
process.exit(1);
}

// Validate seed length (must be 32 bytes = 64 hex characters)
if (seedHexInput.length !== 64) {
console.error("Error: Seed must be 32 bytes (64 hex characters).");
process.exit(1);
}

// Convert seed and message hex strings to Buffers
const seed = Buffer.from(seedHexInput, "hex");
const message = Buffer.from(messageHexInput.startsWith("0x") ? messageHexInput.slice(2) : messageHexInput, "hex");

// --- Main FALCON Module execution ---
Module().then((falcon) => {
const pkLen = 897;
const skLen = 1281;
const sigMaxLen = 690;
const seedLen = 32;

// Allocate memory for key pair
const pkPtr = falcon._malloc(pkLen);
const skPtr = falcon._malloc(skLen);
const seedPtr = falcon._malloc(seedLen);

falcon.HEAPU8.set(seed, seedPtr);

// Generate keypair using the provided seed
falcon.ccall(
'crypto_keypair',
'number',
['number', 'number', 'number'],
[pkPtr, skPtr, seedPtr]
);

const publicKey = Buffer.from(falcon.HEAPU8.subarray(pkPtr, pkPtr + pkLen));

if (outputPublicKeyOnly) {
// --- CRUCIAL LINE FOR PUBLIC KEY EXTRACTION ---
console.log(publicKey.toString("hex"));
// Free memory before exiting
[pkPtr, skPtr, seedPtr].forEach(ptr => falcon._free(ptr));
return; // Exit script after outputting public key
}

// --- Normal / Signature Output Path ---
// Allocate memory for message
const msgPtr = falcon._malloc(message.length);
falcon.HEAPU8.set(message, msgPtr);


// Conditional console logs for human readability
if (!outputSignatureOnly) { // This means either no flag or --output-publickey-only was NOT set
console.log("🔑 Message (hex):", message.toString("hex"));
const secretKey = Buffer.from(falcon.HEAPU8.subarray(skPtr, skPtr + skLen));
console.log("🔑 Secret Key (hex):", secretKey.toString("hex"));
console.log("🔑 Public Key (base64):", publicKey.toString("base64"));
console.log("🔑 Public Key (hex):", publicKey.toString("hex")); // Full output includes hex PK
}


// Sign the message
const signedMsgMaxLen = message.length + sigMaxLen;
const signedMsgPtr = falcon._malloc(signedMsgMaxLen);
const signedMsgLenPtr = falcon._malloc(8); // 64-bit space

const signRet = falcon._crypto_sign(
signedMsgPtr,
signedMsgLenPtr,
msgPtr,
BigInt(message.length),
skPtr
);

if (signRet !== 0) {
console.error("❌ Signing failed.");
// Free memory before exiting on error
[pkPtr, skPtr, msgPtr, seedPtr, signedMsgPtr, signedMsgLenPtr].forEach(ptr => falcon._free(ptr));
process.exit(1); // Exit with error code
}

// Read 64-bit signature length (low + high)
function readUint64(ptr) {
const low = falcon.HEAPU32[ptr >> 2];
const high = falcon.HEAPU32[(ptr >> 2) + 1];
return BigInt(high) << 32n | BigInt(low);
}

const sigLen = Number(readUint64(signedMsgLenPtr));
const signedMessage = Buffer.from(falcon.HEAPU8.subarray(signedMsgPtr, signedMsgPtr + sigLen));

if (!outputSignatureOnly) {
console.log("✅ Signature generated.");
console.log("🔐 Sig+Msg (base64):", signedMessage.toString("base64"));
}

// --- CRUCIAL LINE FOR SIGNATURE EXTRACTION (when outputSignatureOnly is true) ---
// When outputSignatureOnly is true, this is the ONLY output
if (outputSignatureOnly) {
console.log(signedMessage.toString("hex"));
}


// Verify the message
if (!outputSignatureOnly) { // Only verify if not in "signature only" mode
const recoveredMsgPtr = falcon._malloc(sigLen); // Max length of recovered message is sigLen (signed message length)
const recoveredLenPtr = falcon._malloc(8);

const verifyRet = falcon._crypto_sign_open(
recoveredMsgPtr,
recoveredLenPtr,
signedMsgPtr,
BigInt(sigLen),
pkPtr
);

if (verifyRet === 0) {
const recLen = Number(readUint64(recoveredLenPtr));
const recoveredMessage = Buffer.from(falcon.HEAPU8.subarray(recoveredMsgPtr, recoveredMsgPtr + recLen));
console.log("✅ Verification success.");
console.log("📦 Recovered message (hex):", recoveredMessage.toString("hex"));
console.log("🧪 Match:", message.equals(recoveredMessage));
} else {
console.error("❌ Signature verification failed.");
}

// Free memory for verification parts
[recoveredMsgPtr, recoveredLenPtr].forEach(ptr => falcon._free(ptr));
}

// Free all remaining memory
[pkPtr, skPtr, msgPtr, seedPtr, signedMsgPtr, signedMsgLenPtr]
.forEach(ptr => falcon._free(ptr));

}).catch(error => {
console.error("An error occurred during FALCON module initialization or execution:", error);
process.exit(1); // Exit with error code
});
6 changes: 5 additions & 1 deletion solidity/falcon/test_falcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ Module().then((falcon) => {
const sigMaxLen = 690;
const seedLen= 32;

const message = Buffer.from("hello from ZKNOX!");
const seed=Buffer.from("12345678123456781234567812345679")

const hexString = "0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750";

// Remove the "0x" prefix before passing to Buffer.from
const message = Buffer.from(hexString.slice(2), "hex");

// Allocate memory
const pkPtr = falcon._malloc(pkLen);
const skPtr = falcon._malloc(skLen);
Expand Down
3 changes: 2 additions & 1 deletion solidity/script/DelegationExample.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ contract SignDelegationTest is BaseScript {
address iVerifier_algo = address(falcon);
address iPublicKey = DeployPolynomial(salty, pkc);

Verifier = new ZKNOX_Verifier(iAlgoID, iVerifier_algo, iPublicKey);
Verifier = new ZKNOX_Verifier();
Verifier.initialize(iAlgoID, iVerifier_algo, iPublicKey);
console.log("param Verifier:", Verifier.algoID(), Verifier.CoreAddress(), Verifier.authorizedPublicKey());

// Deploy an ERC-20 token contract where Alice is the minter.
Expand Down
12 changes: 8 additions & 4 deletions solidity/src/ZKNOX_delegate_noproxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ contract ZKNOX_Verifier {
/// @notice Internal nonce used for replay protection, must be tracked and included into prehashed message.
uint256 public nonce;

constructor() {}

//input are AlgoIdentifier, Signature verification address, publickey storing contract
constructor(uint256 iAlgoID, address iCore, address iPublicKey) {
function initialize(uint256 iAlgoID, address iCore, address iPublicKey) external {
require(CoreAddress == address(0), "already initialized");
CoreAddress = iCore; // Address of contract of Signature verification (FALCON, DILITHIUM)
algoID = iAlgoID;
authorizedPublicKey = iPublicKey;
Expand All @@ -74,7 +77,8 @@ contract ZKNOX_Verifier {
uint256[] memory nttpk;
require(authorizedPublicKey != address(0), "authorizedPublicKey null");

//nttpk = Core.GetPublicKey(authorizedPublicKey);
nttpk = Core.GetPublicKey(authorizedPublicKey);
require(nttpk[0]!=0, "wrong extraction");
//require(Core.verify(abi.encodePacked(digest), salt, s2, nttpk), "Invalid signature");

(bool success,) = to.call{value: val}(data);
Expand All @@ -83,11 +87,11 @@ contract ZKNOX_Verifier {
}

//debug function for now: todo, remove when transact successfully tested
function verify(
function isValid(
bytes memory data,
bytes memory salt, // compacted signature salt part
uint256[] memory s2
) public view returns (bool) {
) external payable returns (bool) {
ISigVerifier Core = ISigVerifier(CoreAddress);
uint256[] memory nttpk;
nttpk = Core.GetPublicKey(authorizedPublicKey);
Expand Down
3 changes: 3 additions & 0 deletions solidity/src/ZKNOX_falcon_deploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;



function _Swap16(uint256 inw) pure returns (uint256 outw) {
for (uint256 i = 0; i < 256; i += 16) {
outw ^= (inw >> (240 - i) & 0xffff) << (i);
Expand Down Expand Up @@ -116,3 +118,4 @@ function DeployPolynomial(bytes32 salt, uint256[] memory polynomial) returns (ad
}
require(a_polynomial != address(0), "Deployment failed");
}

Loading