From 9ba000a61cad1e94ed62cff8674774665308de2e Mon Sep 17 00:00:00 2001 From: Z4karia Date: Thu, 5 Sep 2024 16:43:49 +0200 Subject: [PATCH] Implemented Withdrawal handling Co-authored-by: keiff3r --- src/BtcNew.ts | 73 +++++++++++++++++++------------------ src/newops/appClient.ts | 39 ++++++++++++++++---- src/types.ts | 25 ++++++++++--- tests/newops/BtcNew.test.ts | 27 ++++++-------- tests/speculosTransport.ts | 40 ++++++++++++++++++++ 5 files changed, 139 insertions(+), 65 deletions(-) create mode 100644 tests/speculosTransport.ts diff --git a/src/BtcNew.ts b/src/BtcNew.ts index 3b306fa..02345fe 100644 --- a/src/BtcNew.ts +++ b/src/BtcNew.ts @@ -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"; /** @@ -317,53 +317,56 @@ 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 + }; } /** @@ -371,18 +374,16 @@ export default class BtcNew { * 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; diff --git a/src/newops/appClient.ts b/src/newops/appClient.ts index 45fae5a..fa1d0df 100644 --- a/src/newops/appClient.ts +++ b/src/newops/appClient.ts @@ -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; @@ -20,6 +20,7 @@ enum BitcoinIns { SIGN_PSBT = 0x04, GET_MASTER_FINGERPRINT = 0x05, SIGN_MESSAGE = 0x10, + SIGN_WITHDRAW = 0x11 } enum FrameworkIns { @@ -201,8 +202,7 @@ export class AppClient { async signWithdrawal( pathElements: number[], - message: Buffer, - withdrawalData: Buffer + withdrawalDataBuffer: AcreWithdrawalDataBuffer ): Promise { if (pathElements.length > 6) { throw new Error("Path too long. At most 6 levels allowed."); @@ -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 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, ); diff --git a/src/types.ts b/src/types.ts index dc11943..cc14821 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { diff --git a/tests/newops/BtcNew.test.ts b/tests/newops/BtcNew.test.ts index 19735b5..b8428b5 100644 --- a/tests/newops/BtcNew.test.ts +++ b/tests/newops/BtcNew.test.ts @@ -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'/`; @@ -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); } diff --git a/tests/speculosTransport.ts b/tests/speculosTransport.ts new file mode 100644 index 0000000..da69b6d --- /dev/null +++ b/tests/speculosTransport.ts @@ -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 { + 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 { + return new SpeculosTransport(speculosUrl); +}