diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cba5c1aa..195463582c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/3b5f7c7...HEAD) +### Added + +- Support for custom network identifiers other than `mainnet` or `testnet` https://github.com/o1-labs/o1js/pull/1444 + ## [0.16.1](https://github.com/o1-labs/o1js/compare/834a44002...3b5f7c7) ### Breaking changes diff --git a/src/bindings b/src/bindings index a6b6800186..1beb2fec84 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit a6b6800186752b3cf5c9a29b7eb167e494784286 +Subproject commit 1beb2fec847e18225adf6dd3687c3459550fe676 diff --git a/src/lib/account-update.ts b/src/lib/account-update.ts index f8673c7ad4..beb297c26c 100644 --- a/src/lib/account-update.ts +++ b/src/lib/account-update.ts @@ -41,7 +41,11 @@ import { protocolVersions, } from '../bindings/crypto/constants.js'; import { MlArray } from './ml/base.js'; -import { Signature, signFieldElement } from '../mina-signer/src/signature.js'; +import { + Signature, + signFieldElement, + zkAppBodyPrefix, +} from '../mina-signer/src/signature.js'; import { MlFieldConstArray } from './ml/fields.js'; import { accountUpdatesToCallForest, @@ -65,6 +69,7 @@ import { } from './mina/smart-contract-context.js'; import { assert } from './util/assert.js'; import { RandomId } from './provable-types/auxiliary.js'; +import { NetworkId } from '../mina-signer/src/types.js'; // external API export { @@ -1018,9 +1023,7 @@ class AccountUpdate implements Types.AccountUpdate { if (Provable.inCheckedComputation()) { let input = Types.AccountUpdate.toInput(this); return hashWithPrefix( - activeInstance.getNetworkId() === 'mainnet' - ? prefixes.zkappBodyMainnet - : prefixes.zkappBodyTestnet, + zkAppBodyPrefix(activeInstance.getNetworkId()), packToFields(input) ); } else { @@ -1028,7 +1031,7 @@ class AccountUpdate implements Types.AccountUpdate { return Field( Test.hashFromJson.accountUpdate( JSON.stringify(json), - activeInstance.getNetworkId() + NetworkId.toString(activeInstance.getNetworkId()) ) ); } @@ -1399,9 +1402,7 @@ class AccountUpdate implements Types.AccountUpdate { function hashAccountUpdate(update: AccountUpdate) { return genericHash( AccountUpdate, - activeInstance.getNetworkId() === 'mainnet' - ? prefixes.zkappBodyMainnet - : prefixes.zkappBodyTestnet, + zkAppBodyPrefix(activeInstance.getNetworkId()), update ); } diff --git a/src/lib/signature.ts b/src/lib/signature.ts index 261aab8cc2..c830f21319 100644 --- a/src/lib/signature.ts +++ b/src/lib/signature.ts @@ -4,6 +4,7 @@ import { hashWithPrefix } from './hash.js'; import { deriveNonce, Signature as SignatureBigint, + signaturePrefix, } from '../mina-signer/src/signature.js'; import { Bool as BoolBigint } from '../provable/field-bigint.js'; import { @@ -238,6 +239,9 @@ class Signature extends CircuitValue { static create(privKey: PrivateKey, msg: Field[]): Signature { const publicKey = PublicKey.fromPrivateKey(privKey).toGroup(); const d = privKey.s; + // we chose an arbitrary prefix for the signature, and it happened to be 'testnet' + // there's no consequences in practice and the signatures can be used with any network + // if there needs to be a custom nonce, include it in the message itself const kPrime = Scalar.fromBigInt( deriveNonce( { fields: msg.map((f) => f.toBigInt()) }, @@ -249,7 +253,7 @@ class Signature extends CircuitValue { let { x: r, y: ry } = Group.generator.scale(kPrime); const k = ry.toBits()[0].toBoolean() ? kPrime.neg() : kPrime; let h = hashWithPrefix( - prefixes.signatureTestnet, + signaturePrefix('testnet'), msg.concat([publicKey.x, publicKey.y, r]) ); // TODO: Scalar.fromBits interprets the input as a "shifted scalar" @@ -265,8 +269,11 @@ class Signature extends CircuitValue { */ verify(publicKey: PublicKey, msg: Field[]): Bool { const point = publicKey.toGroup(); + // we chose an arbitrary prefix for the signature, and it happened to be 'testnet' + // there's no consequences in practice and the signatures can be used with any network + // if there needs to be a custom nonce, include it in the message itself let h = hashWithPrefix( - prefixes.signatureTestnet, + signaturePrefix('testnet'), msg.concat([point.x, point.y, this.r]) ); // TODO: Scalar.fromBits interprets the input as a "shifted scalar" diff --git a/src/mina-signer/mina-signer.ts b/src/mina-signer/mina-signer.ts index b2d226affe..c1356b52e0 100644 --- a/src/mina-signer/mina-signer.ts +++ b/src/mina-signer/mina-signer.ts @@ -39,17 +39,10 @@ export { Client, Client as default, type NetworkId }; const defaultValidUntil = '4294967295'; class Client { - private network: NetworkId; // TODO: Rename to "networkId" for consistency with remaining codebase. + private network: NetworkId; - constructor(options: { network: NetworkId }) { - if (!options?.network) { - throw Error('Invalid Specified Network'); - } - const specifiedNetwork = options.network.toLowerCase(); - if (specifiedNetwork !== 'mainnet' && specifiedNetwork !== 'testnet') { - throw Error('Invalid Specified Network'); - } - this.network = specifiedNetwork; + constructor({ network }: { network: NetworkId }) { + this.network = network; } /** diff --git a/src/mina-signer/src/random-transaction.ts b/src/mina-signer/src/random-transaction.ts index 6ac2ab78ea..d86270790b 100644 --- a/src/mina-signer/src/random-transaction.ts +++ b/src/mina-signer/src/random-transaction.ts @@ -141,6 +141,8 @@ const RandomTransaction = { zkappCommand, zkappCommandAndFeePayerKey, zkappCommandJson, - networkId: Random.oneOf('testnet', 'mainnet'), + networkId: Random.oneOf('testnet', 'mainnet', { + custom: 'other', + }), accountUpdateWithCallDepth: accountUpdate, }; diff --git a/src/mina-signer/src/sign-legacy.unit-test.ts b/src/mina-signer/src/sign-legacy.unit-test.ts index 403c57287d..61a9de37d6 100644 --- a/src/mina-signer/src/sign-legacy.unit-test.ts +++ b/src/mina-signer/src/sign-legacy.unit-test.ts @@ -29,7 +29,7 @@ let networks: NetworkId[] = ['testnet', 'mainnet']; for (let network of networks) { let i = 0; - let reference = signatures[network]; + let reference = signatures[NetworkId.toString(network)]; for (let payment of payments) { let signature = signPayment(payment, privateKey, network); diff --git a/src/mina-signer/src/sign-zkapp-command.ts b/src/mina-signer/src/sign-zkapp-command.ts index e5a6985f08..c70f8556ff 100644 --- a/src/mina-signer/src/sign-zkapp-command.ts +++ b/src/mina-signer/src/sign-zkapp-command.ts @@ -15,6 +15,7 @@ import { Signature, signFieldElement, verifyFieldElement, + zkAppBodyPrefix, } from './signature.js'; import { mocks } from '../../bindings/crypto/constants.js'; import { NetworkId } from './types.js'; @@ -159,18 +160,6 @@ function accountUpdatesToCallForest( return forest; } -const zkAppBodyPrefix = (network: string) => { - switch (network) { - case 'mainnet': - return prefixes.zkappBodyMainnet; - case 'testnet': - return prefixes.zkappBodyTestnet; - - default: - return 'ZkappBody' + network; - } -}; - function accountUpdateHash(update: AccountUpdate, networkId: NetworkId) { assertAuthorizationKindValid(update); let input = AccountUpdate.toInput(update); diff --git a/src/mina-signer/src/sign-zkapp-command.unit-test.ts b/src/mina-signer/src/sign-zkapp-command.unit-test.ts index b11d9a02c9..a6db9a9327 100644 --- a/src/mina-signer/src/sign-zkapp-command.unit-test.ts +++ b/src/mina-signer/src/sign-zkapp-command.unit-test.ts @@ -82,42 +82,46 @@ expect(stringify(dummyInput.packed)).toEqual( stringify(dummyInputSnarky.packed) ); -test(Random.accountUpdate, (accountUpdate) => { - const testnetMinaInstance = Network({ - networkId: 'testnet', - mina: 'http://localhost:8080/graphql', - }); - const mainnetMinaInstance = Network({ - networkId: 'mainnet', - mina: 'http://localhost:8080/graphql', - }); - - fixVerificationKey(accountUpdate); - - // example account update - let accountUpdateJson: Json.AccountUpdate = - AccountUpdate.toJSON(accountUpdate); - - // account update hash - let accountUpdateSnarky = AccountUpdateSnarky.fromJSON(accountUpdateJson); - let inputSnarky = TypesSnarky.AccountUpdate.toInput(accountUpdateSnarky); - let input = AccountUpdate.toInput(accountUpdate); - expect(toJSON(input.fields)).toEqual(toJSON(inputSnarky.fields)); - expect(toJSON(input.packed)).toEqual(toJSON(inputSnarky.packed)); - - let packed = packToFields(input); - let packedSnarky = packToFieldsSnarky(inputSnarky); - expect(toJSON(packed)).toEqual(toJSON(packedSnarky)); - - let hashTestnet = accountUpdateHash(accountUpdate, 'testnet'); - let hashMainnet = accountUpdateHash(accountUpdate, 'mainnet'); - setActiveInstance(testnetMinaInstance); - let hashSnarkyTestnet = accountUpdateSnarky.hash(); - setActiveInstance(mainnetMinaInstance); - let hashSnarkyMainnet = accountUpdateSnarky.hash(); - expect(hashTestnet).toEqual(hashSnarkyTestnet.toBigInt()); - expect(hashMainnet).toEqual(hashSnarkyMainnet.toBigInt()); -}); +test( + Random.accountUpdate, + RandomTransaction.networkId, + (accountUpdate, networkId) => { + const minaInstance = Network({ + networkId, + mina: 'http://localhost:8080/graphql', + }); + + fixVerificationKey(accountUpdate); + + // example account update + let accountUpdateJson: Json.AccountUpdate = + AccountUpdate.toJSON(accountUpdate); + + // account update hash + let accountUpdateSnarky = AccountUpdateSnarky.fromJSON(accountUpdateJson); + let inputSnarky = TypesSnarky.AccountUpdate.toInput(accountUpdateSnarky); + let input = AccountUpdate.toInput(accountUpdate); + expect(toJSON(input.fields)).toEqual(toJSON(inputSnarky.fields)); + expect(toJSON(input.packed)).toEqual(toJSON(inputSnarky.packed)); + + let packed = packToFields(input); + let packedSnarky = packToFieldsSnarky(inputSnarky); + expect(toJSON(packed)).toEqual(toJSON(packedSnarky)); + + setActiveInstance(minaInstance); + let hashSnarky = accountUpdateSnarky.hash(); + let hash = accountUpdateHash(accountUpdate, networkId); + expect(hash).toEqual(hashSnarky.toBigInt()); + + // check against different network hash + expect(hash).not.toEqual( + accountUpdateHash( + accountUpdate, + NetworkId.toString(networkId) === 'mainnet' ? 'testnet' : 'mainnet' + ) + ); + } +); // private key to/from base58 test(Random.json.privateKey, (feePayerKeyBase58) => { @@ -140,19 +144,25 @@ test(memoGenerator, (memoString) => { }); // zkapp transaction - basic properties & commitment -test(RandomTransaction.zkappCommand, (zkappCommand, assert) => { - zkappCommand.accountUpdates.forEach(fixVerificationKey); - - assert(isCallDepthValid(zkappCommand)); - let zkappCommandJson = ZkappCommand.toJSON(zkappCommand); - let ocamlCommitments = Test.hashFromJson.transactionCommitments( - JSON.stringify(zkappCommandJson), - 'testnet' - ); - let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates); - let commitment = callForestHash(callForest, 'testnet'); - expect(commitment).toEqual(FieldConst.toBigint(ocamlCommitments.commitment)); -}); +test( + RandomTransaction.zkappCommand, + RandomTransaction.networkId, + (zkappCommand, networkId, assert) => { + zkappCommand.accountUpdates.forEach(fixVerificationKey); + + assert(isCallDepthValid(zkappCommand)); + let zkappCommandJson = ZkappCommand.toJSON(zkappCommand); + let ocamlCommitments = Test.hashFromJson.transactionCommitments( + JSON.stringify(zkappCommandJson), + NetworkId.toString(networkId) + ); + let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates); + let commitment = callForestHash(callForest, networkId); + expect(commitment).toEqual( + FieldConst.toBigint(ocamlCommitments.commitment) + ); + } +); // invalid zkapp transactions test.negative( @@ -192,7 +202,7 @@ test( // tx commitment let ocamlCommitments = Test.hashFromJson.transactionCommitments( JSON.stringify(zkappCommandJson), - networkId + NetworkId.toString(networkId) ); let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates); let commitment = callForestHash(callForest, networkId); @@ -242,7 +252,7 @@ test( let sigOCaml = Test.signature.signFieldElement( ocamlCommitments.fullCommitment, Ml.fromPrivateKey(feePayerKeySnarky), - networkId === 'mainnet' ? true : false + NetworkId.toString(networkId) ); expect(Signature.toBase58(sigFieldElements)).toEqual(sigOCaml); diff --git a/src/mina-signer/src/signature.ts b/src/mina-signer/src/signature.ts index 14a2096ec0..a80ef3e80e 100644 --- a/src/mina-signer/src/signature.ts +++ b/src/mina-signer/src/signature.ts @@ -39,6 +39,8 @@ export { signLegacy, verifyLegacy, deriveNonce, + signaturePrefix, + zkAppBodyPrefix, }; const networkIdMainnet = 0x01n; @@ -150,10 +152,10 @@ function deriveNonce( ): Scalar { let { x, y } = publicKey; let d = Field(privateKey); - let id = networkId === 'mainnet' ? networkIdMainnet : networkIdTestnet; + let id = getNetworkIdHashInput(networkId); let input = HashInput.append(message, { fields: [x, y, d], - packed: [[id, 8]], + packed: [id], }); let packedInput = packToFields(input); let inputBits = packedInput.map(Field.toBits).flat(); @@ -189,11 +191,7 @@ function hashMessage( ): Scalar { let { x, y } = publicKey; let input = HashInput.append(message, { fields: [x, y, r] }); - let prefix = - networkId === 'mainnet' - ? prefixes.signatureMainnet - : prefixes.signatureTestnet; - return hashWithPrefix(prefix, packToFields(input)); + return hashWithPrefix(signaturePrefix(networkId), packToFields(input)); } /** @@ -280,7 +278,7 @@ function deriveNonceLegacy( ): Scalar { let { x, y } = publicKey; let scalarBits = Scalar.toBits(privateKey); - let id = networkId === 'mainnet' ? networkIdMainnet : networkIdTestnet; + let id = getNetworkIdHashInput(networkId)[0]; let idBits = bytesToBits([Number(id)]); let input = HashInputLegacy.append(message, { fields: [x, y], @@ -311,9 +309,68 @@ function hashMessageLegacy( ): Scalar { let { x, y } = publicKey; let input = HashInputLegacy.append(message, { fields: [x, y, r], bits: [] }); - let prefix = - networkId === 'mainnet' - ? prefixes.signatureMainnet - : prefixes.signatureTestnet; + let prefix = signaturePrefix(networkId); return HashLegacy.hashWithPrefix(prefix, packToFieldsLegacy(input)); } + +const numberToBytePadded = (b: number) => b.toString(2).padStart(8, '0'); + +function networkIdOfString(n: string): [bigint, number] { + let l = n.length; + let acc = ''; + for (let i = l - 1; i >= 0; i--) { + let b = n.charCodeAt(i); + let padded = numberToBytePadded(b); + acc = acc.concat(padded); + } + return [BigInt('0b' + acc), acc.length]; +} + +function getNetworkIdHashInput(network: NetworkId): [bigint, number] { + let s = NetworkId.toString(network); + switch (s) { + case 'mainnet': + return [networkIdMainnet, 8]; + case 'testnet': + return [networkIdTestnet, 8]; + default: + return networkIdOfString(s); + } +} + +const createCustomPrefix = (prefix: string) => { + const maxLength = 20; + const paddingChar = '*'; + let length = prefix.length; + + if (length <= maxLength) { + let diff = maxLength - length; + return prefix + paddingChar.repeat(diff); + } else { + return prefix.substring(0, maxLength); + } +}; + +const signaturePrefix = (network: NetworkId) => { + let s = NetworkId.toString(network); + switch (s) { + case 'mainnet': + return prefixes.signatureMainnet; + case 'testnet': + return prefixes.signatureTestnet; + default: + return createCustomPrefix(s + 'Signature'); + } +}; + +const zkAppBodyPrefix = (network: NetworkId) => { + let s = NetworkId.toString(network); + switch (s) { + case 'mainnet': + return prefixes.zkappBodyMainnet; + case 'testnet': + return prefixes.zkappBodyTestnet; + default: + return createCustomPrefix(s + 'ZkappBody'); + } +}; diff --git a/src/mina-signer/src/signature.unit-test.ts b/src/mina-signer/src/signature.unit-test.ts index c6ae459e04..ae8384cd24 100644 --- a/src/mina-signer/src/signature.unit-test.ts +++ b/src/mina-signer/src/signature.unit-test.ts @@ -17,32 +17,40 @@ import { AccountUpdate } from '../../bindings/mina-transaction/gen/transaction-b import { HashInput } from '../../bindings/lib/provable-bigint.js'; import { Ml } from '../../lib/ml/conversion.js'; import { FieldConst } from '../../lib/field.js'; +import { NetworkId } from './types.js'; // check consistency with OCaml, where we expose the function to sign 1 field element with "testnet" function checkConsistentSingle( msg: Field, key: PrivateKey, keySnarky: PrivateKeySnarky, - pk: PublicKey + pk: PublicKey, + networkId: NetworkId ) { - let sigTest = signFieldElement(msg, key, 'testnet'); - let sigMain = signFieldElement(msg, key, 'mainnet'); + let sig = signFieldElement(msg, key, networkId); + // verify - let okTestnetTestnet = verifyFieldElement(sigTest, msg, pk, 'testnet'); - let okMainnetTestnet = verifyFieldElement(sigMain, msg, pk, 'testnet'); - let okTestnetMainnet = verifyFieldElement(sigTest, msg, pk, 'mainnet'); - let okMainnetMainnet = verifyFieldElement(sigMain, msg, pk, 'mainnet'); - expect(okTestnetTestnet).toEqual(true); - expect(okMainnetTestnet).toEqual(false); - expect(okTestnetMainnet).toEqual(false); - expect(okMainnetMainnet).toEqual(true); + expect(verifyFieldElement(sig, msg, pk, networkId)).toEqual(true); + + // verify against different network + expect( + verifyFieldElement( + sig, + msg, + pk, + networkId === 'mainnet' ? 'testnet' : 'mainnet' + ) + ).toEqual(false); + // consistent with OCaml let msgMl = FieldConst.fromBigint(msg); let keyMl = Ml.fromPrivateKey(keySnarky); - let actualTest = Test.signature.signFieldElement(msgMl, keyMl, false); - let actualMain = Test.signature.signFieldElement(msgMl, keyMl, true); - expect(Signature.toBase58(sigTest)).toEqual(actualTest); - expect(Signature.toBase58(sigMain)).toEqual(actualMain); + let actualTest = Test.signature.signFieldElement( + msgMl, + keyMl, + NetworkId.toString(networkId) + ); + expect(Signature.toBase58(sig)).toEqual(actualTest); } // check that various multi-field hash inputs can be verified @@ -96,12 +104,16 @@ for (let i = 0; i < 10; i++) { // hard coded single field elements let hardcoded = [0n, 1n, 2n, p - 1n]; for (let x of hardcoded) { - checkConsistentSingle(x, key, keySnarky, publicKey); + checkConsistentSingle(x, key, keySnarky, publicKey, 'testnet'); + checkConsistentSingle(x, key, keySnarky, publicKey, 'mainnet'); + checkConsistentSingle(x, key, keySnarky, publicKey, { custom: 'other' }); } // random single field elements for (let i = 0; i < 10; i++) { let x = randomFields[i]; - checkConsistentSingle(x, key, keySnarky, publicKey); + checkConsistentSingle(x, key, keySnarky, publicKey, 'testnet'); + checkConsistentSingle(x, key, keySnarky, publicKey, 'mainnet'); + checkConsistentSingle(x, key, keySnarky, publicKey, { custom: 'other' }); } // hard-coded multi-element hash inputs let messages: HashInput[] = [ diff --git a/src/mina-signer/src/test-vectors/legacySignatures.ts b/src/mina-signer/src/test-vectors/legacySignatures.ts index d31f538f58..7090587475 100644 --- a/src/mina-signer/src/test-vectors/legacySignatures.ts +++ b/src/mina-signer/src/test-vectors/legacySignatures.ts @@ -90,7 +90,7 @@ let strings = [ * - the 3 stake delegations, * - the 3 strings. */ -let signatures = { +let signatures: { [k: string]: { field: string; scalar: string }[] } = { testnet: [ { field: diff --git a/src/mina-signer/src/types.ts b/src/mina-signer/src/types.ts index d0e2c6991a..87739e059b 100644 --- a/src/mina-signer/src/types.ts +++ b/src/mina-signer/src/types.ts @@ -9,7 +9,13 @@ export type Field = number | bigint | string; export type PublicKey = string; export type PrivateKey = string; export type Signature = SignatureJson; -export type NetworkId = 'mainnet' | 'testnet'; +export type NetworkId = 'mainnet' | 'testnet' | { custom: string }; + +export const NetworkId = { + toString(network: NetworkId) { + return typeof network === 'string' ? network : network.custom; + }, +}; export type Keypair = { readonly privateKey: PrivateKey; diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 51a43dcb40..3e7daca766 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -647,7 +647,7 @@ declare const Test: { signFieldElement( messageHash: FieldConst, privateKey: ScalarConst, - isMainnet: boolean + networkId: string ): string; /** * Returns a dummy signature.