Skip to content

Commit

Permalink
Implemented Withdrawal handling
Browse files Browse the repository at this point in the history
Co-authored-by: keiff3r <[email protected]>
  • Loading branch information
Z4karia and keiff3r committed Sep 5, 2024
1 parent a23c84a commit 9ba000a
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 65 deletions.
73 changes: 37 additions & 36 deletions src/BtcNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { extract } from "./newops/psbtExtractor";
import { finalize } from "./newops/psbtFinalizer";
import { psbtIn, PsbtV2 } from "./newops/psbtv2";
import { serializeTransaction } from "./serializeTransaction";
import type { Transaction, AcreWithdrawalData } from "./types";
import type { Transaction, AcreWithdrawalData, AcreWithdrawalDataBuffer } from "./types";
import { log } from "@ledgerhq/logs";

/**
Expand Down Expand Up @@ -317,72 +317,73 @@ export default class BtcNew {
return hexString.startsWith("0x") ? hexString.slice(2) : hexString;
}

formatAcreWithdrawalData(withdrawalData: AcreWithdrawalData): Buffer {
formatAcreWithdrawalData(withdrawalData: AcreWithdrawalData): AcreWithdrawalDataBuffer {
console.log("withdrawalData", withdrawalData);
console.log("dataLength", withdrawalData.data.length);
const to = Buffer.from(this.cleanHexPrefix(withdrawalData.to), "hex").slice(-20);
const to = Buffer.from(this.cleanHexPrefix(withdrawalData.to.toString()), "hex").slice(-20);

let withdrawalValueBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.value), "hex").slice(-32);
const value = Buffer.alloc(32);
value.writeBigUInt64BE(BigInt(withdrawalData.value), 24);

const dataLength = Buffer.alloc(8);
dataLength.writeUInt32BE(withdrawalData.data.length);
withdrawalValueBuffer.copy(value, 32 - withdrawalValueBuffer.length);

const data = Buffer.from(this.cleanHexPrefix(withdrawalData.data), "hex");

const operation = Buffer.alloc(1);
operation.writeUInt8(withdrawalData.operation);
operation.writeUInt8(parseInt(this.cleanHexPrefix(withdrawalData.operation), 16));

let safeTxGasBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.safeTxGas), "hex").slice(-32);
const safeTxGas = Buffer.alloc(32);
safeTxGas.writeBigUInt64BE(BigInt(withdrawalData.safeTxGas));
safeTxGasBuffer.copy(safeTxGas, 32 - safeTxGasBuffer.length);

let baseGasBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.baseGas), "hex").slice(-32);
const baseGas = Buffer.alloc(32);
baseGas.writeBigUInt64BE(BigInt(withdrawalData.baseGas));
baseGasBuffer.copy(baseGas, 32 - baseGasBuffer.length);

let gasPriceBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.gasPrice), "hex").slice(-32);
const gasPrice = Buffer.alloc(32);
gasPrice.writeBigUInt64BE(BigInt(withdrawalData.gasPrice));
gasPriceBuffer.copy(gasPrice, 32 - gasPriceBuffer.length);

let gasTokenBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.gasToken), "hex").slice(-20);
const gasToken = Buffer.alloc(20);
gasTokenBuffer.copy(gasToken, 20 - gasTokenBuffer.length);

const gasToken = Buffer.from(this.cleanHexPrefix(withdrawalData.gasToken.toString()), "hex").slice(-20);

const refundReceiver = Buffer.from(this.cleanHexPrefix(withdrawalData.refundReceiver), "hex").slice(-20);
let refundReceiverBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.refundReceiver), "hex").slice(-20);
const refundReceiver = Buffer.alloc(20);
refundReceiverBuffer.copy(refundReceiver, 20 - refundReceiverBuffer.length);

let nonceBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.nonce), "hex").slice(-32);
const nonce = Buffer.alloc(32);
nonce.writeBigUInt64BE(BigInt(withdrawalData.nonce), 24);

console.log("value", value.toString("hex"));

console.log("to", to.toString("hex").padStart(40, '0'),
"\nvalue", value.toString("hex").padStart(64, '0'),
"\ndataLength", dataLength.toString("hex").padStart(64, '0'),
"\ndata", data.toString("hex"),
"\noperation", operation.toString("hex").padStart(2, '0'),
"\nsafeTxGas", safeTxGas.toString("hex").padStart(64, '0'),
"\nbaseGas", baseGas.toString("hex").padStart(64, '0'),
"\ngasPrice", gasPrice.toString("hex").padStart(64, '0'),
"\ngasToken", gasToken.toString("hex").padStart(40, '0'),
"\nrefundReceiver", refundReceiver.toString("hex").padStart(40, '0'),
"\nnonce", nonce.toString("hex").padStart(64, '0'));

return Buffer.concat([to, value, dataLength, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce]);
nonceBuffer.copy(nonce, 32 - nonceBuffer.length);

return {
to,
value,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
nonce
};
}

/**
* Signs an Acre Withdrawal message with the private key at
* the provided derivation path according to the Bitcoin Signature format
* and returns v, r, s.
*/
async signWithdrawal({ path, messageHex, withdrawalData }: { path: string; messageHex: string; withdrawalData: AcreWithdrawalData }): Promise<{
async signWithdrawal({ path, withdrawalData }: { path: string; withdrawalData: AcreWithdrawalData }): Promise<{
v: number;
r: string;
s: string;
}> {
const pathElements: number[] = pathStringToArray(path);
const message = Buffer.from(messageHex, "hex");
const withdrawalDataBuffer = this.formatAcreWithdrawalData(withdrawalData);
console.log("withdrawalDataBuffer", withdrawalDataBuffer.toString("hex"));
console.log("withdrawalDataBuffer", withdrawalDataBuffer);

// To change after this point
const sig = await this.client.signWithdrawal(pathElements, message, withdrawalDataBuffer);
const sig = await this.client.signWithdrawal(pathElements, withdrawalDataBuffer);
const buf = Buffer.from(sig, "base64");

const v = buf.readUInt8() - 27 - 4;
Expand Down
39 changes: 31 additions & 8 deletions src/newops/appClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { WalletPolicy } from "./policy";
import { createVarint } from "../varint";
import { hashLeaf, Merkle } from "./merkle";
import {log } from "@ledgerhq/logs";
import { AcreWithdrawalData } from "../types";
import { AcreWithdrawalDataBuffer } from "../types";

const CLA_BTC = 0xe1;
const CLA_FRAMEWORK = 0xf8;
Expand All @@ -20,6 +20,7 @@ enum BitcoinIns {
SIGN_PSBT = 0x04,
GET_MASTER_FINGERPRINT = 0x05,
SIGN_MESSAGE = 0x10,
SIGN_WITHDRAW = 0x11
}

enum FrameworkIns {
Expand Down Expand Up @@ -201,8 +202,7 @@ export class AppClient {

async signWithdrawal(
pathElements: number[],
message: Buffer,
withdrawalData: Buffer
withdrawalDataBuffer: AcreWithdrawalDataBuffer
): Promise<string> {
if (pathElements.length > 6) {
throw new Error("Path too long. At most 6 levels allowed.");
Expand All @@ -211,18 +211,41 @@ export class AppClient {
const clientInterpreter = new ClientCommandInterpreter(() => {});

// prepare ClientCommandInterpreter
const nChunks = Math.ceil(message.length / 64);
const chunks: Buffer[] = [];
for (let i = 0; i < nChunks; i++) {
chunks.push(message.subarray(64 * i, 64 * i + 64));

// Chunk 0: to[20] + gasToken[20] + refundReceiver[20]
chunks.push(Buffer.concat([withdrawalDataBuffer.to, withdrawalDataBuffer.gasToken, withdrawalDataBuffer.refundReceiver]));

// Chunk 1: value[32] + safeTxGas[32]
chunks.push(Buffer.concat([withdrawalDataBuffer.value, withdrawalDataBuffer.safeTxGas]));

// Chunk 2: baseGas[32] + gasPrice[32]
chunks.push(Buffer.concat([withdrawalDataBuffer.baseGas, withdrawalDataBuffer.gasPrice]));

// Chunk 3: nonce[32] + operation[1]
chunks.push(Buffer.concat([withdrawalDataBuffer.nonce, withdrawalDataBuffer.operation]));

// Chunk 4: data_selector[4] (the first 4 bytes of data)
chunks.push(withdrawalDataBuffer.data.slice(0, 4));

// Calculate the number of 64-byte chunks needed for the remaining data
const nChunksData = Math.ceil((withdrawalDataBuffer.data.length - 4) / 64);

// Chunk 5 to n: data[64]
for (let i=0; i<nChunksData; i++) {
chunks.push(withdrawalDataBuffer.data.slice(4 + 64*i, 4 + 64*(i+1)));
}

for(let i=0; i<chunks.length; i++) {
console.log("chunks[" + i + "]:", chunks[i].toString("hex"));
}

clientInterpreter.addKnownList(chunks);
const chunksRoot = new Merkle(chunks.map(m => hashLeaf(m))).getRoot();

const response = await this.makeRequest(
BitcoinIns.SIGN_MESSAGE,
Buffer.concat([pathElementsToBuffer(pathElements), createVarint(message.length), chunksRoot]),
BitcoinIns.SIGN_WITHDRAW,
Buffer.concat([pathElementsToBuffer(pathElements), createVarint(nChunksData + 5), chunksRoot]),
clientInterpreter,
);

Expand Down
25 changes: 19 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,28 @@ export interface Transaction {

export interface AcreWithdrawalData {
to: string;
value: number;
value: string;
data: string;
operation: number;
safeTxGas: number;
baseGas: number;
gasPrice: number;
operation: string;
safeTxGas: string;
baseGas: string;
gasPrice: string;
gasToken: string;
refundReceiver: string;
nonce: number;
nonce: string;
}

export interface AcreWithdrawalDataBuffer {
to: Buffer;
value: Buffer;
data: Buffer;
operation: Buffer;
safeTxGas: Buffer;
baseGas: Buffer;
gasPrice: Buffer;
gasToken: Buffer;
refundReceiver: Buffer;
nonce: Buffer;
}

export interface TrustedInput {
Expand Down
27 changes: 12 additions & 15 deletions tests/newops/BtcNew.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ test("getWalletPublicKey p2tr", async () => {

test("signWithdrawalRealClient", async () => {
await testSignWithdrawalRealClient();
});
}, 10 * 60 * 1000);

function testPaths(type: StandardPurpose): { ins: string[]; out?: string } {
const basePath = `m/${type}/1'/0'/`;
Expand Down Expand Up @@ -254,28 +254,25 @@ async function testSignMessageRealClient(

async function testSignWithdrawalRealClient() {

const transport = await openTransportReplayer(RecordStore.fromString(`
=> e1FFFFFFFF
<= FFFF
`));
const transport = new SpeculosTransport("http://localhost:5000")

const withdrawalData: AcreWithdrawalData = {
to: "1234567890abcdef1234567890abcdef12345678",
value: 2863311530,
data: "0xabcdef",
operation: 0,
safeTxGas: 286331153,
baseGas: 572662306,
gasPrice: 858993459,
to: "0xc14972DC5a4443E4f5e89E3655BE48Ee95A795aB",
value: "0x0",
data: "0xcae9ca510000000000000000000000000e781e9d538895ee99bd6e9bf28664942beff32f00000000000000000000000000000000000000000000000000470de4df820000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001000000000000000000000000006083Bde64CCBF08470a1a0dAa9a0281B4951be7C4b5e4623765ec95cfa6e261406d5c446012eff9300000000000000000000000008dcc842b8ed75efe1f222ebdc22d1b06ef35efff6469f708057266816f0595200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000587f579c500000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001a1976a9143c6480044cfafde6dad7f718f76938cc87d0679a88ac000000000000",
operation: "0",
safeTxGas: "0x0",
baseGas: "0x0",
gasPrice: "0x0",
gasToken: "0x0000000000000000000000000000000000000000",
refundReceiver: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
nonce: 1,
refundReceiver: "0x0000000000000000000000000000000000000000",
nonce: "0xC",
};
const client = new AppClient(transport);
const path = "m/44'/0'/0'/0/0";

const btcNew = new BtcNew(client);
const result = await btcNew.signWithdrawal({path: path, messageHex: path, withdrawalData: withdrawalData});
const result = await btcNew.signWithdrawal({path: path, withdrawalData: withdrawalData});
console.log('signed withdrawal:', result);
}

Expand Down
40 changes: 40 additions & 0 deletions tests/speculosTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Transport from "@ledgerhq/hw-transport";
import { log } from "@ledgerhq/logs";
import axios from "axios";

export default class SpeculosTransport extends Transport {
speculosUrl: string;

constructor(speculosUrl: string) {
super();
this.speculosUrl = speculosUrl;
}

async exchange(_apdu: Buffer): Promise<Buffer> {
try {
log("apdu", "=>" + _apdu.toString("hex"));
const response = await axios.post(`${this.speculosUrl}/apdu`, {
data: _apdu.toString("hex"),
});
log("apdu", "<=" + response.data.data);
return Buffer.from(response.data.data, "hex");
} catch (error) {
console.error("Error communicating with Speculos:", error);
throw error;
}
}

setScrambleKey() {
// No need for scrambling in Speculos
}

async close() {
// No cleanup needed for Speculos
}
}

async function createSpeculosTransport(
speculosUrl: string
): Promise<Transport> {
return new SpeculosTransport(speculosUrl);
}

0 comments on commit 9ba000a

Please sign in to comment.