diff --git a/package-lock.json b/package-lock.json index 97ff6f03..8bc136b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6396,6 +6396,10 @@ "resolved": "packages/cardano-contracts/inverse-whirlpool", "link": true }, + "node_modules/@paima/mina-delegation": { + "resolved": "packages/contracts/mina-delegation", + "link": true + }, "node_modules/@paima/mw-core": { "resolved": "packages/paima-sdk/paima-mw-core", "link": true @@ -13184,6 +13188,15 @@ "node": ">=8" } }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -16341,6 +16354,13 @@ "strip-bom": "^3.0.0" } }, + "node_modules/eslint-plugin-o1js": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-o1js/-/eslint-plugin-o1js-0.4.0.tgz", + "integrity": "sha512-12qI6OvAMtUIh8x9lB5uVzJbRMSR6tGrbCRM98fcCmll1FNvVSUIaat3CWhH17tkcjoyVSaFy0I/WzZcqPqaUA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -20923,6 +20943,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -25545,6 +25575,32 @@ "node": "*" } }, + "node_modules/o1js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/o1js/-/o1js-1.5.0.tgz", + "integrity": "sha512-VZGEjky4w+srX2inh1uzoviFY8Bb0vez27N6Z9MmtYv317jf8FTCcoRXnmCJInmeVZOWnl60NJ2Jyyk/p4gtJA==", + "license": "Apache-2.0", + "dependencies": { + "blakejs": "1.2.1", + "cachedir": "^2.4.0", + "isomorphic-fetch": "^3.0.0", + "js-sha256": "^0.9.0", + "reflect-metadata": "^0.1.13", + "tslib": "^2.3.0" + }, + "bin": { + "snarky-run": "src/build/run.js" + }, + "engines": { + "node": ">=18.14.0" + } + }, + "node_modules/o1js/node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -34785,6 +34841,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -35332,6 +35394,7 @@ } }, "packages/cardano-contracts/inverse-whirlpool": { + "name": "@paima/inverse-whirlpool", "version": "1.0.0", "license": "MIT" }, @@ -35372,6 +35435,17 @@ "hardhat": "^2.19.3" } }, + "packages/contracts/mina-delegation": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "o1js": "^1.3.1" + }, + "devDependencies": { + "eslint-plugin-o1js": "^0.4.0", + "typescript": "^5.3.3" + } + }, "packages/engine/paima-funnel": { "name": "@paima/funnel", "version": "1.0.0", @@ -35591,6 +35665,7 @@ } }, "packages/paima-sdk/paima-precompiles": { + "name": "@paima/precompiles", "version": "2.4.0", "license": "See license file", "dependencies": { diff --git a/packages/contracts/mina-delegation/README.md b/packages/contracts/mina-delegation/README.md new file mode 100644 index 00000000..04823522 --- /dev/null +++ b/packages/contracts/mina-delegation/README.md @@ -0,0 +1,3 @@ +# Paima Mina contracts + +NPM package for Mina programs and contracts for Paima Engine and related utilities. diff --git a/packages/contracts/mina-delegation/package.json b/packages/contracts/mina-delegation/package.json new file mode 100644 index 00000000..cd8d1f9d --- /dev/null +++ b/packages/contracts/mina-delegation/package.json @@ -0,0 +1,37 @@ +{ + "name": "@paima/mina-delegation", + "version": "3.1.0", + "description": "Mina ZkProgram for EVM->Mina delegation", + "author": "Paima Studios", + "license": "MIT", + "type": "module", + "files": [ + "build" + ], + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "default": "./build/index.js", + "types": "./build/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "clean": "rm -r tsconfig.tsbuildinfo build/", + "prepare": "npm run build", + "prepack": "npm run clean" + }, + "keywords": [ + "mina", + "zk", + "o1js" + ], + "dependencies": { + "o1js": "^1.3.1" + }, + "devDependencies": { + "eslint-plugin-o1js": "^0.4.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/contracts/mina-delegation/src/delegate.ts b/packages/contracts/mina-delegation/src/delegate.ts new file mode 100644 index 00000000..3e0d48ea --- /dev/null +++ b/packages/contracts/mina-delegation/src/delegate.ts @@ -0,0 +1,123 @@ +import { + Bool, + Bytes, + Crypto, + Poseidon, + PublicKey, + Struct, + UInt8, + ZkProgram, + createEcdsa, + createForeignCurve, +} from 'o1js'; + +// ---------------------------------------------------------------------------- +// Common data types + +/** A Mina foreign curve for Secp256k1, like Ethereum uses. */ +export class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) { + /** Convert a standard 0x04{128 hex digits} public key into this provable struct. */ + static fromHex(publicKey: `0x${string}`): Secp256k1 { + if (!publicKey.startsWith('0x04') || publicKey.length != 4 + 64 + 64) { + throw new Error('Bad public key format'); + } + return Secp256k1.from({ + x: BigInt('0x' + publicKey.substring(4, 4 + 64)), + y: BigInt('0x' + publicKey.substring(4 + 64, 4 + 64 + 64)), + }); + } +} + +/** A Mina-provable ECDSA signature on the Secp256k1 curve, like Ethereum uses. */ +export class Ecdsa extends createEcdsa(Secp256k1) { + // o1js-provided fromHex is good enough +} + +// Ethereum's fixed prefix. +const ethereumPrefix = Bytes.fromString('\x19Ethereum Signed Message:\n'); +// A prefix to distinguish this delegation order scheme from what might be +// similar-looking messages. +const delegationPrefix = Bytes.fromString('DELEGATE-WALLET:'); + +/** + * An order that a particular EVM address has signed to authorize (delegate) + * a Mina address to act on its behalf. + */ +export class DelegationOrder extends Struct({ + /** Mina public key that the delegation order is issued for. */ + target: PublicKey, + /** Ethereum public key that signed the delegation order. */ + signer: Secp256k1.provable, +}) { + private _innerMessage(): Bytes { + return Bytes.from([...delegationPrefix.bytes, ...encodeKey(this.target)]); + } + + /** Get the message for an Etherum wallet to sign, WITHOUT the Ethereum prefix. */ + bytesToSign(): Uint8Array { + return this._innerMessage().toBytes(); + } + + /** Validate that the given Ethereum signature matches this order, WITH the Ethereum prefix. */ + assertSignatureMatches(signature: Ecdsa) { + const inner = this._innerMessage(); + const fullMessage = Bytes.from([ + ...ethereumPrefix.bytes, + ...Bytes.fromString(String(inner.length)).bytes, + ...inner.bytes, + ]); + signature.verifyV2(fullMessage, this.signer).assertTrue(); + } + + /** Hash this entire order for use as a MerkleMap key. */ + hash() { + return Poseidon.hashWithPrefix('DelegationOrder', [ + ...this.target.toFields(), + ...this.signer.x.toFields(), + ...this.signer.y.toFields(), + ]); + } +} + +function encodeKey(k: PublicKey): UInt8[] { + const bytes = []; + const bits = [...k.x.toBits(254), k.isOdd]; + for (let i = 0; i < bits.length; i += 8) { + let value = new UInt8(0); + for (let j = 0; j < 8; j++) { + value = value.mul(2).add(boolToU8(bits[i + j] ?? Bool(false))); + } + bytes.push(value); + } + return bytes; +} + +function boolToU8(bool: Bool): UInt8 { + return UInt8.from(bool.toField()); +} + +// ---------------------------------------------------------------------------- +// The provable program itself + +/** + * A simple {@link ZkProgram} that proves that a valid signature exists for an + * input {@link DelegationOrder}. + */ +export const DelegationOrderProgram = ZkProgram({ + name: 'DelegationOrderProgram', + + publicInput: DelegationOrder, + + methods: { + sign: { + privateInputs: [Ecdsa.provable], + + async method(order: DelegationOrder, signature: Ecdsa) { + order.assertSignatureMatches(signature); + }, + }, + }, +}); + +/** A verifiable proof of {@link DelegationOrderProgram}'s success. */ +export class DelegationOrderProof extends ZkProgram.Proof(DelegationOrderProgram) {} diff --git a/packages/contracts/mina-delegation/src/index.ts b/packages/contracts/mina-delegation/src/index.ts new file mode 100644 index 00000000..4e44ce4b --- /dev/null +++ b/packages/contracts/mina-delegation/src/index.ts @@ -0,0 +1,7 @@ +export { + DelegationOrder, + DelegationOrderProgram, + DelegationOrderProof, + Ecdsa, + Secp256k1, +} from './delegate.js'; diff --git a/packages/contracts/mina-delegation/tsconfig.json b/packages/contracts/mina-delegation/tsconfig.json new file mode 100644 index 00000000..34725b1b --- /dev/null +++ b/packages/contracts/mina-delegation/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + + // TS decorator metadata is necessary for SmartContracts to compile. + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + }, + "include": ["./src"], +} diff --git a/tsconfig.base.json b/tsconfig.base.json index d330e6d8..fa2b44d0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -40,6 +40,7 @@ "@paima/db/*": ["./packages/node-sdk/paima-db/*"], "@paima/aiken-mdx/*": ["./packages/cardano-contracts/aiken-mdx/*"], "@paima/evm-contracts/*": ["./packages/contracts/evm-contracts/*"], + "@paima/mina-delegation/*": ["./packages/contracts/mina-delegation/*"], "@paima/executors/*": ["./packages/paima-sdk/paima-executors/*"], "@paima/funnel/*": ["./packages/engine/paima-funnel/*"], "@paima/mw-core/*": ["./packages/paima-sdk/paima-mw-core/*"],