Skip to content

Instantly share code, notes, and snippets.

@elmariachi111
Last active July 24, 2025 18:57
Show Gist options
  • Select an option

  • Save elmariachi111/3486fde2d4746d511eb2869ac51076e3 to your computer and use it in GitHub Desktop.

Select an option

Save elmariachi111/3486fde2d4746d511eb2869ac51076e3 to your computer and use it in GitHub Desktop.
secp256k1 signature verification
import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex } from "@noble/hashes/utils";
import { toHex } from "viem";
// this is some public address, which deals as the message to be signed
const ADDRESS = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8";
/**
* this is nillion's implemenation of the sign function
* https://github.com/NillionNetwork/nuc-ts/blob/main/src/keypair.ts#L102
*
* @param bytes msgBytes
* @param bytes privateKey
* @returns
*/
const sign = (msgString, privateKey) => {
const msgBytes = new TextEncoder().encode(msgString);
const signature = secp256k1.sign(msgBytes, privateKey, {
prehash: true,
});
//this is what the Nillion:Keypair class does, it doesn't contain the recovery bit:
return signature.toCompactRawBytes();
};
async function main() {
const privateKey = secp256k1.utils.randomSecretKey();
//const privateKey = hexToBytes(PK);
const publicKey = secp256k1.getPublicKey(privateKey);
console.log("Private", toHex(privateKey));
console.log("Public", toHex(publicKey));
const hashedAddress = sha256(ADDRESS);
const signature = sign(ADDRESS, privateKey);
//this correctly verifies the signature has been issued over the message
const isValidSignatureForPublicKey = secp256k1.verify(
signature,
new TextEncoder().encode(ADDRESS),
bytesToHex(publicKey),
{
prehash: true,
}
);
console.log(
isValidSignatureForPublicKey ? "✅" : "❌",
"isValidSignatureForPublicKey"
);
// as we don't know the recovery bit we cannot recover the public however.
// Using 0 here fails in 50% of the cases:
const secpSignature =
secp256k1.Signature.fromCompact(signature).addRecoveryBit(0);
const finalRecoveredKey = secpSignature.recoverPublicKey(hashedAddress);
const recoveredCorrectly = "0x" + finalRecoveredKey.toHex() === toHex(publicKey);
console.log(
recoveredCorrectly ? "✅" : "❌",
" Final recovered key:",
finalRecoveredKey.toHex()
);
}
main();
{
"name": "test-secp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@noble/curves": "^1.9.2",
"@noble/hashes": "^1.8.0",
"viem": "^2.33.0"
},
"packageManager": "[email protected]"
}
import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha2";
import { hexToBigInt, parseSignature, serializeSignature, toHex } from "viem";
// this is some public address, which deals as the message to be signed
const ADDRESS = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8";
/**
* this is nillion's implemenation of the sign function
* https://github.com/NillionNetwork/nuc-ts/blob/main/src/keypair.ts#L102
*
* @param bytes msgBytes
* @param bytes privateKey
* @returns
*/
const sign = (msgString, privateKey) => {
const msgBytes = new TextEncoder().encode(msgString);
const signature = secp256k1.sign(msgBytes, privateKey, {
prehash: true,
});
//this is what the Nillion:Keypair class does, it doesn't contain the recovery bit:
//return signature.toBytes("compact");
//return signature.toCompactRawBytes();
return signature;
};
async function main() {
const privateKey = secp256k1.utils.randomSecretKey();
//const privateKey = hexToBytes(PK);
const publicKey = secp256k1.getPublicKey(privateKey);
console.log("Private", toHex(privateKey));
console.log("Public", toHex(publicKey));
const hashedAddress = sha256(ADDRESS);
const signature = sign(ADDRESS, privateKey);
const recoveryBit = signature.recovery;
//const secpSignature = secp256k1.Signature.fromBytes(signature).addRecoveryBit(1);
const secpSignature = new secp256k1.Signature(
signature.r,
signature.s,
signature.recovery
);
const immediatelyRevoredPublicKey =
secpSignature.recoverPublicKey(hashedAddress);
console.log("immediately Recovered", immediatelyRevoredPublicKey.toHex());
// const signatureHex = signature.toHex("compact");
// console.log("Signature", signature, " | ", signatureHex);
const encodedSignature = serializeSignature({
...signature,
yParity: signature.recovery,
});
console.log("Encoded (65 bytes):", encodedSignature);
//const decodedSignature = decodeSignatureWithRecoveryBit(encodedSignature);
const decodedSignature = parseSignature(encodedSignature);
const secpSignature2 = new secp256k1.Signature(
hexToBigInt(decodedSignature.r),
hexToBigInt(decodedSignature.s),
decodedSignature.yParity
);
const finalRecoveredKey = secpSignature2.recoverPublicKey(hashedAddress);
const didItWork = "0x" + finalRecoveredKey.toHex() === toHex(publicKey);
console.log(
didItWork ? "✅" : "❌",
" Final recovered key:",
finalRecoveredKey.toHex()
);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment