From 48f5c78251bf213314427b9d5795b39baae75bda Mon Sep 17 00:00:00 2001 From: robrobbins Date: Wed, 7 Feb 2024 00:32:50 +0000 Subject: [PATCH 1/6] implement and test signatureToHex for rosetta.ts --- src/mina-signer/src/rosetta.ts | 20 ++++++++++++++++++-- src/mina-signer/tests/rosetta.test.ts | 25 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/mina-signer/src/rosetta.ts b/src/mina-signer/src/rosetta.ts index 835bec83a..649c67efd 100644 --- a/src/mina-signer/src/rosetta.ts +++ b/src/mina-signer/src/rosetta.ts @@ -2,9 +2,15 @@ import { Binable } from '../../bindings/lib/binable.js'; import { PublicKey, Scalar } from '../../provable/curve-bigint.js'; import { Field } from '../../provable/field-bigint.js'; import { Memo } from './memo.js'; -import { Signature } from './signature.js'; +import { Signature, SignatureJson } from './signature.js'; -export { publicKeyToHex, rosettaTransactionToSignedCommand }; +export { + publicKeyToHex, + signatureFromHex, + signatureToHex, + signatureJsonToHex, + rosettaTransactionToSignedCommand +}; function publicKeyToHex(publicKey: PublicKey) { return fieldToHex(Field, publicKey.x, !!publicKey.isOdd); @@ -20,6 +26,16 @@ function signatureFromHex(signatureHex: string): Signature { }; } +function signatureJsonToHex(signatureJson: SignatureJson): string { + return signatureToHex(Signature.fromJSON(signatureJson)); +} + +function signatureToHex(signature: Signature): string { + let rHex = fieldToHex(Field, signature.r); + let sHex = fieldToHex(Field, signature.s); + return `${rHex}${sHex}`; +} + function fieldToHex( binable: Binable, x: T, diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 1981b135f..b6273fd30 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -1,11 +1,13 @@ -import Client from '../dist/node/mina-signer/mina-signer.js'; +import Client from '../dist/node/mina-signer/MinaSigner.js'; +import { signatureFromHex, signatureToHex } from '../dist/node/mina-signer/src/rosetta.js'; describe('Rosetta', () => { let client: Client; + const rosettaTnxMockSignature = '389ac7d4077f3d485c1494782870979faa222cd906b25b2687333a92f41e40b925adb08705eddf2a7098e5ac9938498e8a0ce7c70b25ea392f4846b854086d43'; const signedRosettaTnxMock = ` { - "signature": "389ac7d4077f3d485c1494782870979faa222cd906b25b2687333a92f41e40b925adb08705eddf2a7098e5ac9938498e8a0ce7c70b25ea392f4846b854086d43", + "signature": "${rosettaTnxMockSignature}", "payment": { "to": "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", "from": "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", @@ -23,6 +25,25 @@ describe('Rosetta', () => { client = new Client({ network: 'mainnet' }); }); + it('generates a valid signature from a hex string', () => { + let { r, s } = signatureFromHex(rosettaTnxMockSignature); + expect(r).not.toBeNaN(); + expect(r).not.toBeFalsy(); + expect(s).not.toBeNaN(); + expect(s).not.toBeFalsy(); + + // TODO we could go further and use validate method on the return? + // or leave as is with the assumption that the above hex is known to be valid + }); + + it('generates a valid hex string from a signature', () => { + let signature = signatureFromHex(rosettaTnxMockSignature); + let hex = signatureToHex(signature); + + // fails + expect(hex).toEqual(rosettaTnxMockSignature); + }); + it('generates a valid rosetta transaction', () => { const signedGraphQLCommand = client.signedRosettaTransactionToSignedCommand(signedRosettaTnxMock); From 0164b93482490adffa0b9312ff0821d467e7689a Mon Sep 17 00:00:00 2001 From: robrobbins Date: Wed, 7 Feb 2024 22:14:38 +0000 Subject: [PATCH 2/6] use legacy signatures vector to test fixup --- src/mina-signer/tests/rosetta.test.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index b6273fd30..9912e6669 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -1,5 +1,5 @@ import Client from '../dist/node/mina-signer/MinaSigner.js'; -import { signatureFromHex, signatureToHex } from '../dist/node/mina-signer/src/rosetta.js'; +import { signatureFromHex, signatureJsonToHex } from '../dist/node/mina-signer/src/rosetta.js'; describe('Rosetta', () => { let client: Client; @@ -21,6 +21,13 @@ describe('Rosetta', () => { "stake_delegation": null }`; + // NOTE: copied from the test-vectors legacy signatures file + // RE: the rosettaTnxMockSignature above does not correctly convert + const legacySignatureJson = { + field: '2290465734865973481454975811990842289349447524565721011257265781466170720513', + scalar: '174718295375042423373378066296864207343460524320417038741346483351503066865', + }; + beforeAll(async () => { client = new Client({ network: 'mainnet' }); }); @@ -34,14 +41,24 @@ describe('Rosetta', () => { // TODO we could go further and use validate method on the return? // or leave as is with the assumption that the above hex is known to be valid + + // what would the correct method be to verify? }); it('generates a valid hex string from a signature', () => { - let signature = signatureFromHex(rosettaTnxMockSignature); - let hex = signatureToHex(signature); + // leaving this in place ATM as a comparison to what appears to be simply an incorrect assumption + // i.e -> the mock signature may be testnet vs main or ... + // let signature = signatureFromHex(rosettaTnxMockSignature); + // let hex = signatureToHex(signature); // fails - expect(hex).toEqual(rosettaTnxMockSignature); + // expect(hex).toEqual(rosettaTnxMockSignature); + + let hex = signatureJsonToHex(legacySignatureJson); + let sig = signatureFromHex(hex); + + expect(sig.r).toEqual(BigInt(legacySignatureJson.field)); + expect(sig.s).toEqual(BigInt(legacySignatureJson.scalar)); }); it('generates a valid rosetta transaction', () => { From 522a8972a20cb1efc8c7cbe62a7a2368ffa56bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos=20Reis?= Date: Wed, 21 Feb 2024 16:01:24 +0000 Subject: [PATCH 3/6] Fix field <-> hex conversion. Add Rosetta transaction sign/verify mechanism. Add tests for these changes. --- src/mina-signer/mina-signer.ts | 52 +++++-- src/mina-signer/src/rosetta.ts | 190 ++++++++++++++++++++++---- src/mina-signer/src/types.ts | 3 + src/mina-signer/tests/rosetta.test.ts | 116 +++++++++++----- 4 files changed, 288 insertions(+), 73 deletions(-) diff --git a/src/mina-signer/mina-signer.ts b/src/mina-signer/mina-signer.ts index c1356b52e..01dfb6a85 100644 --- a/src/mina-signer/mina-signer.ts +++ b/src/mina-signer/mina-signer.ts @@ -1,6 +1,6 @@ import { PrivateKey, PublicKey } from '../provable/curve-bigint.js'; -import * as Json from './src/types.js'; -import type { SignedLegacy, Signed, NetworkId } from './src/types.js'; +import * as Json from './src/TSTypes.js'; +import type { SignedLegacy, SignedRosetta, Signed, NetworkId } from './src/TSTypes.js'; import { isPayment, @@ -27,10 +27,7 @@ import { } from './src/sign-legacy.js'; import { hashPayment, hashStakeDelegation } from './src/transaction-hash.js'; import { Memo } from './src/memo.js'; -import { - publicKeyToHex, - rosettaTransactionToSignedCommand, -} from './src/rosetta.js'; +import * as Rosetta from './src/rosetta.js'; import { sign, Signature, verify } from './src/signature.js'; import { createNullifier } from './src/nullifier.js'; @@ -172,6 +169,32 @@ class Client { return verifyStringSignature(data, signature, publicKey, this.network); } + /** + * Signs a Rosetta transaction + * + * @param transaction An object describing the transaction to be signed. + * @param privateKey The private key used to sign the transaction (in Base58 + * format). + * @returns A signature of the transaction in Rosetta format. + */ + signRosettaTransaction( + transaction: Rosetta.UnsignedTransaction, + privateKey: Json.PrivateKey + ): SignedRosetta { + return Rosetta.signTransaction(transaction, privateKey, this.network); + } + + /** + * Verifies a signature created by {@link signRosettaTransaction}. + * + * @param signedTransaction The signed transaction (in Rosetta format) + * @returns True if the `signedTransaction` contains a valid signature + * matching the transaction and publicKey. + */ + verifyRosettaTransaction(signedTransaction: SignedRosetta): boolean { + return Rosetta.verifyTransaction(signedTransaction, this.network); + } + /** * Signs a payment transaction using a private key. * @@ -387,10 +410,23 @@ class Client { */ signedRosettaTransactionToSignedCommand(signedRosettaTxn: string): string { let parsedTx = JSON.parse(signedRosettaTxn); - let command = rosettaTransactionToSignedCommand(parsedTx); + let command = Rosetta.rosettaTransactionToSignedCommand(parsedTx); return JSON.stringify({ data: command }); } + /** + * Creates the payload for Rosetta /construction/combine using a response + * from /construction/payloads. + * + * @param signingPayload A payload resulting from /construction/payloads + * @param privateKey The private key used to sign the transaction + * @returns A string with the resulting payload for /construction/combine. + */ + rosettaCombinePayload(signingPayload: string, privateKey: Json.PrivateKey) { + let parsedPayload = JSON.parse(signingPayload); + return JSON.stringify(Rosetta.rosettaCombinePayload(parsedPayload, privateKey, this.network)); + } + /** * Return the hex-encoded format of a valid public key. This will throw an exception if * the key is invalid or the conversion fails. @@ -400,7 +436,7 @@ class Client { */ publicKeyToRaw(publicKeyBase58: string): string { let publicKey = PublicKey.fromBase58(publicKeyBase58); - return publicKeyToHex(publicKey); + return Rosetta.publicKeyToHex(publicKey); } /** diff --git a/src/mina-signer/src/rosetta.ts b/src/mina-signer/src/rosetta.ts index 649c67efd..286a14b37 100644 --- a/src/mina-signer/src/rosetta.ts +++ b/src/mina-signer/src/rosetta.ts @@ -1,17 +1,33 @@ import { Binable } from '../../bindings/lib/binable.js'; -import { PublicKey, Scalar } from '../../provable/curve-bigint.js'; +import { PublicKey, PrivateKey, Scalar } from '../../provable/curve-bigint.js'; import { Field } from '../../provable/field-bigint.js'; import { Memo } from './memo.js'; import { Signature, SignatureJson } from './signature.js'; +import { DelegationJson, PaymentJson, signPayment, signStakeDelegation, verifyPayment, verifyStakeDelegation } from './sign-legacy.js'; +import { NetworkId, SignedRosetta } from './TSTypes.js'; +import * as Json from './TSTypes.js'; + export { publicKeyToHex, signatureFromHex, + signatureJsonFromHex, signatureToHex, signatureJsonToHex, - rosettaTransactionToSignedCommand + fieldFromHex, + fieldToHex, + rosettaTransactionToSignedCommand, + signTransaction, + verifyTransaction, + rosettaCombineSignature, + rosettaCombinePayload, + UnsignedPayload, + UnsignedTransaction, + SignedTransaction, }; +const defaultValidUntil = '4294967295'; + function publicKeyToHex(publicKey: PublicKey) { return fieldToHex(Field, publicKey.x, !!publicKey.isOdd); } @@ -26,13 +42,17 @@ function signatureFromHex(signatureHex: string): Signature { }; } +function signatureJsonFromHex(signatureHex: string): SignatureJson { + return Signature.toJSON(signatureFromHex(signatureHex)); +} + function signatureJsonToHex(signatureJson: SignatureJson): string { return signatureToHex(Signature.fromJSON(signatureJson)); } function signatureToHex(signature: Signature): string { let rHex = fieldToHex(Field, signature.r); - let sHex = fieldToHex(Field, signature.s); + let sHex = fieldToHex(Scalar, signature.s); return `${rHex}${sHex}`; } @@ -46,7 +66,7 @@ function fieldToHex( bytes[bytes.length - 1] |= Number(paddingBit) << 7; // map each byte to a 0-padded hex string of length 2 return bytes - .map((byte) => byte.toString(16).padStart(2, '0').split('').reverse().join('')) + .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); } @@ -56,7 +76,7 @@ function fieldFromHex( ): [T, boolean] { let bytes: number[] = []; for (let i = 0; i < hex.length; i += 2) { - let byte = parseInt(hex[i + 1] + hex[i], 16); + let byte = parseInt(hex[i] + hex[i + 1], 16); bytes.push(byte); } // read highest bit @@ -65,12 +85,93 @@ function fieldFromHex( return [binable.fromBytes(bytes), paddingBit]; } +function signTransaction(transaction: UnsignedTransaction, privateKey: string, network: NetworkId): SignedRosetta { + let signature: SignatureJson; + if (transaction.payment !== null) { + let payment = unsignedTransactionPaymentToPaymentJson(transaction.payment); + signature = signPayment(payment, privateKey, network); + } + else if (transaction.stakeDelegation !== null) { + let delegation = unsignedTransactionStakeDelegationToDelegationJson(transaction.stakeDelegation); + signature = signStakeDelegation(delegation, privateKey, network); + } + else { + throw Error('signTransaction: Unsupported transaction'); + } + let publicKey = PublicKey.toBase58(PrivateKey.toPublicKey(PrivateKey.fromBase58(privateKey))); + return { + data: transaction, + signature: signatureJsonToHex(signature), + publicKey, + }; +} + +function unsignedTransactionPaymentToPaymentJson(payment: Payment): PaymentJson { + return { + common: { + fee: payment.fee, + feePayer: payment.from, + nonce: payment.nonce, + validUntil: payment.valid_until ?? defaultValidUntil, + memo: payment.memo ?? '', + }, + body: { + receiver: payment.to, + amount: payment.amount, + } + } +} + +function unsignedTransactionStakeDelegationToDelegationJson(delegation: StakeDelegation): DelegationJson { + return { + common: { + feePayer: delegation.delegator, + fee: delegation.fee, + validUntil: delegation.valid_until ?? defaultValidUntil, + memo: delegation.memo ?? '', + nonce: delegation.nonce, + }, + body: { + newDelegate: delegation.new_delegate, + } + } +} + +function verifyTransaction(signedTransaction: SignedRosetta, network: NetworkId): boolean { + if (signedTransaction.data.payment !== null) { + return verifyPayment(unsignedTransactionPaymentToPaymentJson(signedTransaction.data.payment), signatureJsonFromHex(signedTransaction.signature), signedTransaction.publicKey, network); + } else if (signedTransaction.data.stakeDelegation !== null) { + return verifyStakeDelegation(unsignedTransactionStakeDelegationToDelegationJson(signedTransaction.data.stakeDelegation), signatureJsonFromHex(signedTransaction.signature), signedTransaction.publicKey, network); + } + throw Error('verifyTransaction: Unsupported transaction'); +} + +// create a signature for /construction/combine payload +function rosettaCombineSignature(signature: SignedRosetta, signingPayload: any): RosettaSignature { + let publicKey = PublicKey.fromBase58(signature.publicKey); + return { + hex_bytes: signature.signature, + public_key: { + hex_bytes: publicKeyToHex(publicKey), + curve_type: "pallas" + }, + signature_type: "schnorr_poseidon", + signing_payload: signingPayload + } +} + +// create a payload for /construction/combine +function rosettaCombinePayload(unsignedPayload: UnsignedPayload, privateKey: Json.PrivateKey, network: NetworkId) { + let signature = signTransaction(unsignedPayload.unsigned_transaction, privateKey, network); + let signatures = [rosettaCombineSignature(signature, unsignedPayload.payloads[0])]; + return { + network_identifier: { blockchain: "mina", network }, + unsigned_transaction: unsignedPayload.unsigned_transaction, signatures + }; +} + // TODO: clean up this logic, was copied over from OCaml code -function rosettaTransactionToSignedCommand({ - signature, - payment, - stake_delegation, -}: RosettaTransactionJson) { +function rosettaTransactionToSignedCommand({ signature, payment, stake_delegation }: SignedTransaction) { let signatureDecoded = signatureFromHex(signature); let signatureBase58 = Signature.toBase58(signatureDecoded); let [t, nonce] = (() => { @@ -140,24 +241,53 @@ function rosettaTransactionToSignedCommand({ }; } -type RosettaTransactionJson = { +type UnsignedPayload = { + unsigned_transaction: UnsignedTransaction; + payloads: any[]; +} + +type UnsignedTransaction = { + randomOracleInput: string; + signerInput: { + prefix: string[]; + suffix: string[]; + }; + payment: Payment | null; + stakeDelegation: StakeDelegation | null; +} + +type SignedTransaction = { signature: string; - payment: { - to: string; - from: string; - fee: string; - token: string; - nonce: string; - memo: string | null; - amount: string; - valid_until: string | null; - } | null; - stake_delegation: { - delegator: string; - new_delegate: string; - fee: string; - nonce: string; - memo: string | null; - valid_until: string | null; - } | null; -}; + payment: Payment | null; + stake_delegation: StakeDelegation | null; +} + +type RosettaSignature = { + hex_bytes: string; + public_key: { + hex_bytes: string; + curve_type: string; + }; + signature_type: string; + signing_payload: any; +} + +type Payment = { + to: string; + from: string; + fee: string; + token: string; + nonce: string; + memo: string | null; + amount: string; + valid_until: string | null; +} + +type StakeDelegation = { + delegator: string; + new_delegate: string; + fee: string; + nonce: string; + memo: string | null; + valid_until: string | null; +} diff --git a/src/mina-signer/src/types.ts b/src/mina-signer/src/types.ts index 87739e059..6eb7e75c8 100644 --- a/src/mina-signer/src/types.ts +++ b/src/mina-signer/src/types.ts @@ -75,6 +75,9 @@ export type Signed = { data: T; }; +// distinguish from Signed because signature is in hex format +export type SignedRosetta = Signed; + export type SignedAny = SignedLegacy | Signed; export type Group = { diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 9912e6669..5b9491370 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -1,19 +1,45 @@ import Client from '../dist/node/mina-signer/MinaSigner.js'; -import { signatureFromHex, signatureJsonToHex } from '../dist/node/mina-signer/src/rosetta.js'; +import { fieldFromHex, fieldToHex, signatureJsonToHex, signatureJsonFromHex, UnsignedTransaction, publicKeyToHex, signTransaction } from '../dist/node/mina-signer/src/rosetta.js'; +import { PublicKey } from '../dist/node/provable/curve-bigint.js'; +import { Field } from '../dist/node/provable/field-bigint.js'; describe('Rosetta', () => { let client: Client; - const rosettaTnxMockSignature = '389ac7d4077f3d485c1494782870979faa222cd906b25b2687333a92f41e40b925adb08705eddf2a7098e5ac9938498e8a0ce7c70b25ea392f4846b854086d43'; + const rosettaUnsignedTxnString = '{"randomOracleInput":"0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000","signerInput":{"prefix":["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E","33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E","33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"],"suffix":["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480","0000000003800000000000000000000000000000000000000000000000000000","00000000000000000000000000000000000000000000000001DCD65000000000"]},"payment":{"to":"B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X","from":"B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X","fee":"1000000","token":"wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf","nonce":"1","memo":null,"amount":"1000000000","valid_until":null},"stakeDelegation":null}'; + const rosettaUnsignedTxn: UnsignedTransaction = JSON.parse(rosettaUnsignedTxnString); + const rosettaUnsignedPayload = { + unsigned_transaction: rosettaUnsignedTxn, + payloads: [ + { + account_identifier: { + address: "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", + metadata: { + token_id: "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf" + } + }, + hex_bytes: "7B2272616E646F6D4F7261636C65496E707574223A2230303030303030333333453146313443363135354237303644344541313243463730363835423844434433333432413842333641323743433345423631423538373146393231394533334531463134433631353542373036443445413132434637303638354238444344333334324138423336413237434333454236314235383731463932313945333345314631344336313535423730364434454131324346373036383542384443443333343241384233364132374343334542363142353837314639323139453030303030323537303234324630303030303030303030303830303030303030303030303030303043303030303030303746464646464646433030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030453030303030303030303030303030303030313444363737303030303030303030222C227369676E6572496E707574223A7B22707265666978223A5B2233334531463134433631353542373036443445413132434637303638354238444344333334324138423336413237434333454236314235383731463932313945222C2233334531463134433631353542373036443445413132434637303638354238444344333334324138423336413237434333454236314235383731463932313945222C2233334531463134433631353542373036443445413132434637303638354238444344333334324138423336413237434333454236314235383731463932313945225D2C22737566666978223A5B2230303030303030303030303030303037464646464646464330303030303030363030303030303030303030303030303230303030303030303030314538343830222C2230303030303030303033383030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030222C2230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030314443443635303030303030303030225D7D2C227061796D656E74223A7B22746F223A224236327171516F6D43676A614B6861794E3739775771444E73534A4B46615A6A726B754370384B63727433367562586231345848553258222C2266726F6D223A224236327171516F6D43676A614B6861794E3739775771444E73534A4B46615A6A726B754370384B63727433367562586231345848553258222C22666565223A2231303030303030222C22746F6B656E223A22775348563253347158396A46734C6A516F38723142734D4C48325A524B735A7836454A643173626F7A475069654543344A66222C226E6F6E6365223A2231222C226D656D6F223A6E756C6C2C22616D6F756E74223A2231303030303030303030222C2276616C69645F756E74696C223A6E756C6C7D2C227374616B6544656C65676174696F6E223A6E756C6C7D", + signature_type: "schnorr_poseidon" + }] + }; + + const privateKey = 'EKFDpgrxFUU9FH1NF8t6AVC19MdAgxAHS6FSaJCsuhxmtheG9cv3'; + const publicKey = 'B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X'; + // signature using reference `ocaml-signer` using the above payload and private key + const mainnetSignatureHex = '6fbce9ccad07aa99dad2ad8d5415ac24c0b6e6d2d264f65232c0337417b6d839d2271979d2d44161b0763add744f0e4572516ff40609fe337b48d579e50c941b'; + const mainnetSignatureJson = { + field: '26164728085389719244195795314490480006407207982358090253363589956793207995503', + scalar: '12474029284949868513590364568638283159520724895188255555607876437432082180050' + } const signedRosettaTnxMock = ` { - "signature": "${rosettaTnxMockSignature}", + "signature": "${mainnetSignatureHex}", "payment": { - "to": "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", - "from": "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", - "fee": "10000000", + "to": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", + "from": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", + "fee": "1000000", "token": "1", - "nonce": "0", + "nonce": "1", "memo": null, "amount": "1000000000", "valid_until": "4294967295" @@ -21,46 +47,45 @@ describe('Rosetta', () => { "stake_delegation": null }`; - // NOTE: copied from the test-vectors legacy signatures file - // RE: the rosettaTnxMockSignature above does not correctly convert - const legacySignatureJson = { - field: '2290465734865973481454975811990842289349447524565721011257265781466170720513', - scalar: '174718295375042423373378066296864207343460524320417038741346483351503066865', - }; - beforeAll(async () => { client = new Client({ network: 'mainnet' }); }); - it('generates a valid signature from a hex string', () => { - let { r, s } = signatureFromHex(rosettaTnxMockSignature); - expect(r).not.toBeNaN(); - expect(r).not.toBeFalsy(); - expect(s).not.toBeNaN(); - expect(s).not.toBeFalsy(); - - // TODO we could go further and use validate method on the return? - // or leave as is with the assumption that the above hex is known to be valid - - // what would the correct method be to verify? + it('field <-> hex roundtrip', () => { + const field = BigInt(mainnetSignatureJson.field); + const hex = fieldToHex(Field, field); + const field_ = fieldFromHex(Field, hex)[0]; + expect(field_).toEqual(field); }); it('generates a valid hex string from a signature', () => { - // leaving this in place ATM as a comparison to what appears to be simply an incorrect assumption - // i.e -> the mock signature may be testnet vs main or ... + let signatureHex = signatureJsonToHex(mainnetSignatureJson); + expect(signatureHex).toBe(mainnetSignatureHex); + }); - // let signature = signatureFromHex(rosettaTnxMockSignature); - // let hex = signatureToHex(signature); - // fails - // expect(hex).toEqual(rosettaTnxMockSignature); + it('generates a valid signature from a hex string', () => { + let signature = signatureJsonFromHex(mainnetSignatureHex); + expect(signature).toEqual(mainnetSignatureJson); + }); + + it('signs and verifies signature', () => { + let signedTransaction = client.signRosettaTransaction(rosettaUnsignedTxn, privateKey); + expect(client.verifyRosettaTransaction(signedTransaction)).toBeTruthy(); + }); - let hex = signatureJsonToHex(legacySignatureJson); - let sig = signatureFromHex(hex); + it('match signature', () => { + const { signature } = signTransaction(rosettaUnsignedTxn, privateKey, 'mainnet'); + expect(signature).toEqual(mainnetSignatureHex); + }); - expect(sig.r).toEqual(BigInt(legacySignatureJson.field)); - expect(sig.s).toEqual(BigInt(legacySignatureJson.scalar)); + it('verify transaction', () => { + expect(client.verifyRosettaTransaction({ data: rosettaUnsignedTxn, signature: mainnetSignatureHex, publicKey: publicKey })).toBeTruthy(); }); + // it('test user command', () => { + // expect(unsignedTransactionToUserCommand(rosettaUnsignedTxn)).toBeDefined(); + // }); + it('generates a valid rosetta transaction', () => { const signedGraphQLCommand = client.signedRosettaTransactionToSignedCommand(signedRosettaTnxMock); @@ -79,4 +104,25 @@ describe('Rosetta', () => { signedGraphQLCommandJson.data.payload.body[1].amount ); }); + + it('generates valid combine payload', () => { + const combinePayload = client.rosettaCombinePayload(JSON.stringify(rosettaUnsignedPayload), privateKey); + const expectedCombinePayload = { + network_identifier: { blockchain: 'mina', network: 'mainnet' }, + unsigned_transaction: rosettaUnsignedTxn, + signatures: [ + { + hex_bytes: mainnetSignatureHex, + public_key: { + hex_bytes: + publicKeyToHex(PublicKey.fromBase58(publicKey)), + curve_type: 'pallas' + }, + signature_type: 'schnorr_poseidon', + signing_payload: rosettaUnsignedPayload.payloads[0] + } + ] + }; + expect(combinePayload).toBe(JSON.stringify(expectedCombinePayload)); + }); }); From cef9a4d8ebde3d2d641b98ac65eecd62562bbce4 Mon Sep 17 00:00:00 2001 From: ejMina226 <118474890+ejMina226@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:48:59 +0100 Subject: [PATCH 4/6] Code review changes --- src/mina-signer/src/rosetta.ts | 128 +++++++++++++++++--------- src/mina-signer/tests/rosetta.test.ts | 8 +- 2 files changed, 85 insertions(+), 51 deletions(-) diff --git a/src/mina-signer/src/rosetta.ts b/src/mina-signer/src/rosetta.ts index c11f38513..012b23fcb 100644 --- a/src/mina-signer/src/rosetta.ts +++ b/src/mina-signer/src/rosetta.ts @@ -3,10 +3,16 @@ import { PublicKey, PrivateKey, Scalar } from './curve-bigint.js'; import { Field } from './field-bigint.js'; import { Memo } from './memo.js'; import { Signature, SignatureJson } from './signature.js'; -import { DelegationJson, PaymentJson, signPayment, signStakeDelegation, verifyPayment, verifyStakeDelegation } from './sign-legacy.js'; -import { NetworkId, SignedRosetta } from './TSTypes.js'; -import * as Json from './TSTypes.js'; - +import { + DelegationJson, + PaymentJson, + signPayment, + signStakeDelegation, + verifyPayment, + verifyStakeDelegation, +} from './sign-legacy.js'; +import { NetworkId, SignedRosetta } from './types.js'; +import * as Json from './types.js'; export { publicKeyToHex, @@ -65,9 +71,7 @@ function fieldToHex( // set highest bit (which is empty) bytes[bytes.length - 1] |= Number(paddingBit) << 7; // map each byte to a 0-padded hex string of length 2 - return bytes - .map((byte) => byte.toString(16).padStart(2, '0')) - .join(''); + return bytes.map((byte) => byte.toString(16).padStart(2, '0')).join(''); } function fieldFromHex( @@ -85,20 +89,24 @@ function fieldFromHex( return [binable.fromBytes(bytes), paddingBit]; } -function signTransaction(transaction: UnsignedTransaction, privateKey: string, network: NetworkId): SignedRosetta { +function signTransaction( + transaction: UnsignedTransaction, + privateKey: string, + network: NetworkId +): SignedRosetta { let signature: SignatureJson; if (transaction.payment !== null) { - let payment = unsignedTransactionPaymentToPaymentJson(transaction.payment); + let payment = paymentFromRosetta(transaction.payment); signature = signPayment(payment, privateKey, network); - } - else if (transaction.stakeDelegation !== null) { - let delegation = unsignedTransactionStakeDelegationToDelegationJson(transaction.stakeDelegation); + } else if (transaction.stakeDelegation !== null) { + let delegation = delegationFromRosetta(transaction.stakeDelegation); signature = signStakeDelegation(delegation, privateKey, network); - } - else { + } else { throw Error('signTransaction: Unsupported transaction'); } - let publicKey = PublicKey.toBase58(PrivateKey.toPublicKey(PrivateKey.fromBase58(privateKey))); + let publicKey = PublicKey.toBase58( + PrivateKey.toPublicKey(PrivateKey.fromBase58(privateKey)) + ); return { data: transaction, signature: signatureJsonToHex(signature), @@ -106,7 +114,7 @@ function signTransaction(transaction: UnsignedTransaction, privateKey: string, n }; } -function unsignedTransactionPaymentToPaymentJson(payment: Payment): PaymentJson { +function paymentFromRosetta(payment: Payment): PaymentJson { return { common: { fee: payment.fee, @@ -118,11 +126,11 @@ function unsignedTransactionPaymentToPaymentJson(payment: Payment): PaymentJson body: { receiver: payment.to, amount: payment.amount, - } - } + }, + }; } -function unsignedTransactionStakeDelegationToDelegationJson(delegation: StakeDelegation): DelegationJson { +function delegationFromRosetta(delegation: StakeDelegation): DelegationJson { return { common: { feePayer: delegation.delegator, @@ -133,45 +141,77 @@ function unsignedTransactionStakeDelegationToDelegationJson(delegation: StakeDel }, body: { newDelegate: delegation.new_delegate, - } - } + }, + }; } -function verifyTransaction(signedTransaction: SignedRosetta, network: NetworkId): boolean { +function verifyTransaction( + signedTransaction: SignedRosetta, + network: NetworkId +): boolean { if (signedTransaction.data.payment !== null) { - return verifyPayment(unsignedTransactionPaymentToPaymentJson(signedTransaction.data.payment), signatureJsonFromHex(signedTransaction.signature), signedTransaction.publicKey, network); - } else if (signedTransaction.data.stakeDelegation !== null) { - return verifyStakeDelegation(unsignedTransactionStakeDelegationToDelegationJson(signedTransaction.data.stakeDelegation), signatureJsonFromHex(signedTransaction.signature), signedTransaction.publicKey, network); + return verifyPayment( + paymentFromRosetta(signedTransaction.data.payment), + signatureJsonFromHex(signedTransaction.signature), + signedTransaction.publicKey, + network + ); + } + if (signedTransaction.data.stakeDelegation !== null) { + return verifyStakeDelegation( + delegationFromRosetta(signedTransaction.data.stakeDelegation), + signatureJsonFromHex(signedTransaction.signature), + signedTransaction.publicKey, + network + ); } throw Error('verifyTransaction: Unsupported transaction'); } // create a signature for /construction/combine payload -function rosettaCombineSignature(signature: SignedRosetta, signingPayload: any): RosettaSignature { +function rosettaCombineSignature( + signature: SignedRosetta, + signingPayload: unknown +): RosettaSignature { let publicKey = PublicKey.fromBase58(signature.publicKey); return { hex_bytes: signature.signature, public_key: { hex_bytes: publicKeyToHex(publicKey), - curve_type: "pallas" + curve_type: 'pallas', }, - signature_type: "schnorr_poseidon", - signing_payload: signingPayload - } + signature_type: 'schnorr_poseidon', + signing_payload: signingPayload, + }; } // create a payload for /construction/combine -function rosettaCombinePayload(unsignedPayload: UnsignedPayload, privateKey: Json.PrivateKey, network: NetworkId) { - let signature = signTransaction(unsignedPayload.unsigned_transaction, privateKey, network); - let signatures = [rosettaCombineSignature(signature, unsignedPayload.payloads[0])]; +function rosettaCombinePayload( + unsignedPayload: UnsignedPayload, + privateKey: Json.PrivateKey, + network: NetworkId +) { + let signature = signTransaction( + unsignedPayload.unsigned_transaction, + privateKey, + network + ); + let signatures = [ + rosettaCombineSignature(signature, unsignedPayload.payloads[0]), + ]; return { - network_identifier: { blockchain: "mina", network }, - unsigned_transaction: unsignedPayload.unsigned_transaction, signatures + network_identifier: { blockchain: 'mina', network }, + unsigned_transaction: unsignedPayload.unsigned_transaction, + signatures, }; } // TODO: clean up this logic, was copied over from OCaml code -function rosettaTransactionToSignedCommand({ signature, payment, stake_delegation }: SignedTransaction) { +function rosettaTransactionToSignedCommand({ + signature, + payment, + stake_delegation, +}: SignedTransaction) { let signatureDecoded = signatureFromHex(signature); let signatureBase58 = Signature.toBase58(signatureDecoded); let [t, nonce] = (() => { @@ -243,8 +283,8 @@ function rosettaTransactionToSignedCommand({ signature, payment, stake_delegatio type UnsignedPayload = { unsigned_transaction: UnsignedTransaction; - payloads: any[]; -} + payloads: unknown[]; +}; type UnsignedTransaction = { randomOracleInput: string; @@ -254,13 +294,13 @@ type UnsignedTransaction = { }; payment: Payment | null; stakeDelegation: StakeDelegation | null; -} +}; type SignedTransaction = { signature: string; payment: Payment | null; stake_delegation: StakeDelegation | null; -} +}; type RosettaSignature = { hex_bytes: string; @@ -269,8 +309,8 @@ type RosettaSignature = { curve_type: string; }; signature_type: string; - signing_payload: any; -} + signing_payload: unknown; +}; type Payment = { to: string; @@ -281,7 +321,7 @@ type Payment = { memo: string | null; amount: string; valid_until: string | null; -} +}; type StakeDelegation = { delegator: string; @@ -290,4 +330,4 @@ type StakeDelegation = { nonce: string; memo: string | null; valid_until: string | null; -} +}; diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 5b9491370..c31ea8c61 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -6,10 +6,8 @@ import { Field } from '../dist/node/provable/field-bigint.js'; describe('Rosetta', () => { let client: Client; - const rosettaUnsignedTxnString = '{"randomOracleInput":"0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000","signerInput":{"prefix":["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E","33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E","33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"],"suffix":["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480","0000000003800000000000000000000000000000000000000000000000000000","00000000000000000000000000000000000000000000000001DCD65000000000"]},"payment":{"to":"B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X","from":"B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X","fee":"1000000","token":"wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf","nonce":"1","memo":null,"amount":"1000000000","valid_until":null},"stakeDelegation":null}'; - const rosettaUnsignedTxn: UnsignedTransaction = JSON.parse(rosettaUnsignedTxnString); const rosettaUnsignedPayload = { - unsigned_transaction: rosettaUnsignedTxn, + unsigned_transaction: { "randomOracleInput": "0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000", "signerInput": { "prefix": ["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"], "suffix": ["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480", "0000000003800000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000000000000000000001DCD65000000000"] }, "payment": { "to": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "from": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "fee": "1000000", "token": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", "nonce": "1", "memo": null, "amount": "1000000000", "valid_until": null }, "stakeDelegation": null }, payloads: [ { account_identifier: { @@ -82,10 +80,6 @@ describe('Rosetta', () => { expect(client.verifyRosettaTransaction({ data: rosettaUnsignedTxn, signature: mainnetSignatureHex, publicKey: publicKey })).toBeTruthy(); }); - // it('test user command', () => { - // expect(unsignedTransactionToUserCommand(rosettaUnsignedTxn)).toBeDefined(); - // }); - it('generates a valid rosetta transaction', () => { const signedGraphQLCommand = client.signedRosettaTransactionToSignedCommand(signedRosettaTnxMock); From 894a3db239e385134f686aa537e3df4363b74018 Mon Sep 17 00:00:00 2001 From: ejMina226 <118474890+ejMina226@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:59:30 +0100 Subject: [PATCH 5/6] Undo signer change and update imports --- src/mina-signer/tests/rosetta.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index c31ea8c61..cde528b03 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -1,13 +1,14 @@ -import Client from '../dist/node/mina-signer/MinaSigner.js'; +import Client from '../dist/node/mina-signer/mina-signer.js'; import { fieldFromHex, fieldToHex, signatureJsonToHex, signatureJsonFromHex, UnsignedTransaction, publicKeyToHex, signTransaction } from '../dist/node/mina-signer/src/rosetta.js'; -import { PublicKey } from '../dist/node/provable/curve-bigint.js'; -import { Field } from '../dist/node/provable/field-bigint.js'; +import { PublicKey } from '../src/curve-bigint.js'; +import { Field } from '../src/field-bigint.js'; describe('Rosetta', () => { let client: Client; + const rosettaUnsignedTxn: UnsignedTransaction = { "randomOracleInput": "0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000", "signerInput": { "prefix": ["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"], "suffix": ["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480", "0000000003800000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000000000000000000001DCD65000000000"] }, "payment": { "to": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "from": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "fee": "1000000", "token": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", "nonce": "1", "memo": null, "amount": "1000000000", "valid_until": null }, "stakeDelegation": null }; const rosettaUnsignedPayload = { - unsigned_transaction: { "randomOracleInput": "0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000", "signerInput": { "prefix": ["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"], "suffix": ["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480", "0000000003800000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000000000000000000001DCD65000000000"] }, "payment": { "to": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "from": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "fee": "1000000", "token": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", "nonce": "1", "memo": null, "amount": "1000000000", "valid_until": null }, "stakeDelegation": null }, + unsigned_transaction: rosettaUnsignedTxn, payloads: [ { account_identifier: { From d032866ddb41e493bc9bcbc658a632c37dfb4b2b Mon Sep 17 00:00:00 2001 From: ejMina226 <118474890+ejMina226@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:47:13 +0100 Subject: [PATCH 6/6] Fix imports --- src/mina-signer/tests/rosetta.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index cde528b03..2527f526e 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -1,7 +1,7 @@ import Client from '../dist/node/mina-signer/mina-signer.js'; import { fieldFromHex, fieldToHex, signatureJsonToHex, signatureJsonFromHex, UnsignedTransaction, publicKeyToHex, signTransaction } from '../dist/node/mina-signer/src/rosetta.js'; -import { PublicKey } from '../src/curve-bigint.js'; -import { Field } from '../src/field-bigint.js'; +import { PublicKey } from '../dist/node/mina-signer/src/curve-bigint.js'; +import { Field } from '../dist/node/mina-signer/src/field-bigint.js'; describe('Rosetta', () => { let client: Client;