Skip to content

Commit

Permalink
Feature/decode safe transaction (#320)
Browse files Browse the repository at this point in the history
* Add decodeSafeTransaction

* Sign safe tx without utxos

* Add example

* Format

* Remove code example of registration to safe
  • Loading branch information
hundredark authored Jan 9, 2024
1 parent eed03a3 commit f62d061
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 58 deletions.
35 changes: 33 additions & 2 deletions example/safe_multisig.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
const { MixinApi, encodeSafeTransaction, getUnspentOutputsForRecipients, buildSafeTransactionRecipient, buildSafeTransaction, signSafeTransaction } = require('..');
const {
MixinApi,
encodeSafeTransaction,
getUnspentOutputsForRecipients,
buildSafeTransactionRecipient,
buildSafeTransaction,
signSafeTransaction,
decodeSafeTransaction,
} = require('..');
const { v4 } = require('uuid');
const keystore = require('../keystore.json'); // keystore from your bot

Expand Down Expand Up @@ -60,9 +68,32 @@ const main = async () => {
// you can continue to sign this unlocked multisig tx
const index = outputs[0].receivers.sort().findIndex(u => u === keystore.client_id);
// sign safe multisigs with the private key registerd to safe
const signedRaw = signSafeTransaction(tx, utxos, multisig[0].views, safePrivateKey, index);
const signedRaw = signSafeTransaction(tx, multisig[0].views, safePrivateKey, index);
multisig = await client.multisig.signSafeMultisigs(request_id, signedRaw);
console.log(multisig);

// others in the gourp are required to sign the multisigs transaction
otherSign(multisig.request_id);
};

const otherSign = async id => {
const keystore = {
client_id: '',
session_id: '',
pin_token: '',
private_key: '',
};
const privateKey = '';

const client = MixinApi({ keystore });
let multisig = await client.multisig.fetchSafeMultisigs(id);
const tx = decodeSafeTransaction(multisig.raw_transaction);

const index = multisig.senders.sort().findIndex(u => u === keystore.client_id);
// sign safe multisigs with the private key registerd to safe
const signedRaw = signSafeTransaction(tx, multisig.views, privateKey, index);
multisig = await client.multisig.signSafeMultisigs(id, signedRaw);
console.log(multisig);
};

main();
41 changes: 4 additions & 37 deletions example/safe.js → example/safe_tx.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,11 @@
const {
MixinApi,
getED25519KeyPair,
getTipPinUpdateMsg,
base64RawURLDecode,
encodeSafeTransaction,
getUnspentOutputsForRecipients,
buildSafeTransactionRecipient,
buildSafeTransaction,
signSafeTransaction,
} = require('..');
const { MixinApi, encodeSafeTransaction, getUnspentOutputsForRecipients, buildSafeTransactionRecipient, buildSafeTransaction, signSafeTransaction } = require('..');
const { v4 } = require('uuid');
const keystore = require('../keystore.json'); // keystore from your bot

let privateKey = '';

const main = async () => {
const client = MixinApi({ keystore });
let bot = await client.user.profile();

// private key for safe registration
let privateKey = '';
// upgrade to tip pin if haven't
if (!bot.tip_key_base64) {
const keys = getED25519KeyPair();
const pub = base64RawURLDecode(keys.publicKey);
const priv = base64RawURLDecode(keys.privateKey);
const tipPin = priv.toString('hex');
privateKey = tipPin;

const b = getTipPinUpdateMsg(pub, bot.tip_counter + 1);
await client.pin.update(keystore.pin, b);
bot = await client.pin.verifyTipPin(tipPin);
keystore.pin = tipPin; // should update pin in your keystore file too
console.log('new tip pin', tipPin);
}

// register to safe if haven't
// it's convinient to use the same private key as above tipPin
if (!bot.has_safe) {
const resp = await client.safe.register(keystore.client_id, keystore.pin, Buffer.from(privateKey, 'hex'));
console.log(resp);
}

// destination
const members = ['7766b24c-1a03-4c3a-83a3-b4358266875d'];
Expand Down Expand Up @@ -94,7 +61,7 @@ const main = async () => {
console.log(verifiedTx);

// sign safe transaction with the private key registerd to safe
const signedRaw = signSafeTransaction(tx, utxos, verifiedTx[0].views, privateKey);
const signedRaw = signSafeTransaction(tx, verifiedTx[0].views, privateKey);
console.log(signedRaw);
const sendedTx = await client.utxo.sendTransactions([
{
Expand Down
4 changes: 4 additions & 0 deletions src/client/types/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export interface MultisigTransaction {
extra: string;
}

export interface SafeTransaction extends MultisigTransaction {
signatureMap: Record<number, string>[];
}

export interface SafeMultisigsResponse {
type: 'transaction_request';
request_id: string;
Expand Down
66 changes: 56 additions & 10 deletions src/client/utils/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import forge from 'node-forge';
import qs from 'qs';
import { validate, v4 } from 'uuid';
import { FixedNumber } from 'ethers';
import { GhostKey, GhostKeyRequest, MultisigTransaction, PaymentParams, SafeTransactionRecipient, SafeUtxoOutput } from '../types';
import { GhostKey, GhostKeyRequest, MultisigTransaction, PaymentParams, SafeTransaction, SafeTransactionRecipient, SafeUtxoOutput } from '../types';
import { Input, Output } from '../../mvm/types';
import { Encoder, magic } from '../../mvm';
import { Decoder, Encoder, magic } from '../../mvm';
import { base64RawURLEncode } from './base64';
import { TIPBodyForSequencerRegister } from './tip';
import { getPublicFromMainnetAddress, buildMixAddress, parseMixAddress } from './address';
Expand Down Expand Up @@ -158,6 +158,59 @@ export const encodeSafeTransaction = (tx: MultisigTransaction, sigs: Record<numb
return enc.buf.toString('hex');
};

export const decodeSafeTransaction = (raw: string): SafeTransaction => {
const dec = new Decoder(Buffer.from(raw, 'hex'));

const prefix = dec.subarray(0, 2);
if (!prefix.equals(magic)) throw new Error('invalid magic');
dec.read(3);

const version = dec.readByte();
if (version !== TxVersionHashSignature) throw new Error('invalid version');

const asset = dec.subarray(0, 32).toString('hex');
dec.read(32);

const lenInput = dec.readInt();
const inputs = [];
for (let i = 0; i < lenInput; i++) {
inputs.push(dec.decodeInput());
}

const lenOutput = dec.readInt();
const outputs = [];
for (let i = 0; i < lenOutput; i++) {
outputs.push(dec.decodeOutput());
}

const lenRefs = dec.readInt();
const refs = [];
for (let i = 0; i < lenRefs; i++) {
const hash = dec.subarray(0, 32).toString('hex');
dec.read(32);
refs.push(hash);
}

const lenExtra = dec.readUint32();
const extra = dec.subarray(0, lenExtra).toString();
dec.read(lenExtra);

const lenSigs = dec.readInt();
const signatureMap = [];
for (let i = 0; i < lenSigs; i++) {
signatureMap.push(dec.decodeSignature());
}

return {
version,
asset,
extra,
inputs,
outputs,
signatureMap,
};
};

export const buildSafeTransaction = (utxos: SafeUtxoOutput[], rs: SafeTransactionRecipient[], gs: GhostKey[], extra: string) => {
if (utxos.length === 0) throw new Error('empty inputs');
if (Buffer.from(extra).byteLength > 512) throw new Error('extra data is too long');
Expand Down Expand Up @@ -203,7 +256,7 @@ export const buildSafeTransaction = (utxos: SafeUtxoOutput[], rs: SafeTransactio
};
};

export const signSafeTransaction = (tx: MultisigTransaction, utxos: SafeUtxoOutput[], views: string[], privateKey: string) => {
export const signSafeTransaction = (tx: MultisigTransaction, views: string[], privateKey: string, index = 0) => {
const raw = encodeSafeTransaction(tx);
const msg = blake3Hash(Buffer.from(raw, 'hex'));

Expand All @@ -212,19 +265,12 @@ export const signSafeTransaction = (tx: MultisigTransaction, utxos: SafeUtxoOutp

const signaturesMap = [];
for (let i = 0; i < tx.inputs.length; i++) {
const input = tx.inputs[i];
const utxo = utxos[i];
if (!utxo || utxo.transaction_hash !== input.hash || utxo.output_index !== input.index) {
throw new Error(`invalid input: ${input}`);
}
const viewBuffer = Buffer.from(views[i], 'hex');
const x = ed.setCanonicalBytes(viewBuffer);
const t = ed.scalar.add(x, y);
const key = Buffer.from(ed.scalar.toBytes(t));
const sig = ed.sign(msg, key);
const pub = ed.publicFromPrivate(key);
const sigs: Record<number, string> = {};
const index = utxo.keys.findIndex(k => k === pub.toString('hex'));
sigs[index] = sig.toString('hex');
signaturesMap.push(sigs);
}
Expand Down
141 changes: 141 additions & 0 deletions src/mvm/decoder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { utils } from 'ethers';
import { magic } from './encoder';
import { Input, Output } from './types';

export const bytesToInterger = (b: Buffer) => {
let x = 0;
for (let i = 0; i < b.byteLength; i++) {
const byte = b.at(i);
x *= 0x100;
if (byte) x += byte;
}
return x;
};

export class Decoder {
buf: Buffer;

constructor(buf: Buffer) {
this.buf = buf;
}

subarray(start: number, end?: number) {
return this.buf.subarray(start, end);
}

read(offset: number) {
this.buf = this.buf.subarray(offset);
}
Expand All @@ -22,6 +40,18 @@ export class Decoder {
return value;
}

readInt() {
const value = this.buf.readUInt16BE();
this.read(2);
return value;
}

readUint32() {
const value = this.buf.readUInt32BE();
this.read(4);
return value;
}

readUInt64() {
const value = this.buf.readBigUInt64BE();
this.read(8);
Expand All @@ -33,4 +63,115 @@ export class Decoder {
this.read(16);
return value;
}

readInteger() {
const len = this.readInt();
const value = this.buf.subarray(0, len);
this.read(len);
return bytesToInterger(value);
}

decodeInput() {
const hash = this.subarray(0, 32).toString('hex');
this.read(32);
const index = this.readInt();
const input: Input = {
hash,
index,
};

const lenGenesis = this.readInt();
if (lenGenesis > 0) {
input.genesis = this.buf.subarray(0, lenGenesis).toString('hex');
this.read(lenGenesis);
}

const depositPrefix = this.subarray(0, 2);
this.read(2);
if (depositPrefix.equals(magic)) {
const chain = this.subarray(0, 32).toString('hex');
this.read(32);
const asset = this.readBytes();
const transaction = this.readBytes();
const index = this.readUInt64();
const amount = this.readInteger();

input.deposit = {
chain,
asset,
transaction,
index,
amount,
};
}

const mintPrefix = this.subarray(0, 2);
this.read(2);
if (mintPrefix.equals(magic)) {
const group = this.readBytes();
const batch = this.readUInt64();
const amount = this.readInteger();

input.mint = {
group,
batch,
amount,
};
}

return input;
}

decodeOutput() {
const t = this.subarray(0, 2);
this.read(2);
if (t.at(0) !== 0) throw new Error(`invalid output type ${t.at(0)}`);
const type = t.at(1);
const amount = this.readInteger();

const lenKey = this.readInt();
const keys = [];
for (let i = 0; i < lenKey; i++) {
const key = this.subarray(0, 32).toString('hex');
this.read(32);
keys.push(key);
}
const mask = this.subarray(0, 32).toString('hex');
this.read(32);
const lenScript = this.readInt();
const script = this.buf.subarray(0, lenScript).toString('hex');
this.read(lenScript);

const output: Output = {
type,
amount: utils.formatUnits(amount, 8),
keys,
mask,
script,
};

const prefix = this.subarray(0, 2);
this.read(2);
if (prefix.equals(magic)) {
const address = this.readBytes();
const tag = this.readBytes();
output.withdrawal = {
address,
tag,
};
}

return output;
}

decodeSignature() {
const len = this.readInt();
const sigs: Record<number, string> = {};
for (let i = 0; i < len; i++) {
const index = this.readInt();
const sig = this.buf.subarray(0, 64).toString('hex');
sigs[index] = sig;
}
return sigs;
}
}
Loading

0 comments on commit f62d061

Please sign in to comment.