Skip to content

Commit

Permalink
Merge pull request #1604 from o1-labs/rosetta-signature-to-hex-with-f…
Browse files Browse the repository at this point in the history
…ixes

Implement and test signatureToHex for rosetta.ts
  • Loading branch information
mitschabaude committed Apr 22, 2024
2 parents e04216a + d032866 commit 23a9292
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 41 deletions.
50 changes: 43 additions & 7 deletions src/mina-signer/mina-signer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PrivateKey, PublicKey } from './src/curve-bigint.js';
import * as Json from './src/types.js';
import type { SignedLegacy, Signed, NetworkId } from './src/types.js';
import type { SignedLegacy, Signed, NetworkId, SignedRosetta } from './src/types.js';

import {
isPayment,
Expand All @@ -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';

Expand Down Expand Up @@ -210,6 +207,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<Rosetta.UnsignedTransaction> {
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<Rosetta.UnsignedTransaction>): boolean {
return Rosetta.verifyTransaction(signedTransaction, this.network);
}

/**
* Signs a payment transaction using a private key.
*
Expand Down Expand Up @@ -425,10 +448,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.
Expand All @@ -438,7 +474,7 @@ class Client {
*/
publicKeyToRaw(publicKeyBase58: string): string {
let publicKey = PublicKey.fromBase58(publicKeyBase58);
return publicKeyToHex(publicKey);
return Rosetta.publicKeyToHex(publicKey);
}

/**
Expand Down
242 changes: 213 additions & 29 deletions src/mina-signer/src/rosetta.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
import { Binable } from '../../bindings/lib/binable.js';
import { PublicKey, Scalar } from './curve-bigint.js';
import { PublicKey, PrivateKey, Scalar } from './curve-bigint.js';
import { Field } from './field-bigint.js';
import { Memo } from './memo.js';
import { Signature } from './signature.js';
import { Signature, SignatureJson } from './signature.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, rosettaTransactionToSignedCommand };
export {
publicKeyToHex,
signatureFromHex,
signatureJsonFromHex,
signatureToHex,
signatureJsonToHex,
fieldFromHex,
fieldToHex,
rosettaTransactionToSignedCommand,
signTransaction,
verifyTransaction,
rosettaCombineSignature,
rosettaCombinePayload,
UnsignedPayload,
UnsignedTransaction,
SignedTransaction,
};

const defaultValidUntil = '4294967295';

function publicKeyToHex(publicKey: PublicKey) {
return fieldToHex(Field, publicKey.x, !!publicKey.isOdd);
Expand All @@ -20,6 +48,20 @@ 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(Scalar, signature.s);
return `${rHex}${sHex}`;
}

function fieldToHex<T extends Field | Scalar>(
binable: Binable<T>,
x: T,
Expand All @@ -29,11 +71,7 @@ function fieldToHex<T extends Field | Scalar>(
// 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').split('').reverse().join('')
)
.join('');
return bytes.map((byte) => byte.toString(16).padStart(2, '0')).join('');
}

function fieldFromHex<T extends Field | Scalar>(
Expand All @@ -42,7 +80,7 @@ function fieldFromHex<T extends Field | Scalar>(
): [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
Expand All @@ -51,12 +89,129 @@ function fieldFromHex<T extends Field | Scalar>(
return [binable.fromBytes(bytes), paddingBit];
}

function signTransaction(
transaction: UnsignedTransaction,
privateKey: string,
network: NetworkId
): SignedRosetta<UnsignedTransaction> {
let signature: SignatureJson;
if (transaction.payment !== null) {
let payment = paymentFromRosetta(transaction.payment);
signature = signPayment(payment, privateKey, network);
} else if (transaction.stakeDelegation !== null) {
let delegation = delegationFromRosetta(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 paymentFromRosetta(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 delegationFromRosetta(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<UnsignedTransaction>,
network: NetworkId
): boolean {
if (signedTransaction.data.payment !== null) {
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<UnsignedTransaction>,
signingPayload: unknown
): 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) {
}: SignedTransaction) {
let signatureDecoded = signatureFromHex(signature);
let signatureBase58 = Signature.toBase58(signatureDecoded);
let [t, nonce] = (() => {
Expand Down Expand Up @@ -126,24 +281,53 @@ function rosettaTransactionToSignedCommand({
};
}

type RosettaTransactionJson = {
type UnsignedPayload = {
unsigned_transaction: UnsignedTransaction;
payloads: unknown[];
};

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: unknown;
};

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;
};
3 changes: 3 additions & 0 deletions src/mina-signer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export type Signed<T> = {
data: T;
};

// distinguish from Signed because signature is in hex format
export type SignedRosetta<T> = Signed<T>;

export type SignedAny = SignedLegacy<SignableData> | Signed<ZkappCommand>;

export type Group = {
Expand Down
Loading

0 comments on commit 23a9292

Please sign in to comment.