Skip to content

Commit

Permalink
WIP: base implementation for Withdrawal
Browse files Browse the repository at this point in the history
  • Loading branch information
Z4karia committed Aug 30, 2024
1 parent 84b8961 commit a23c84a
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 5 deletions.
84 changes: 83 additions & 1 deletion 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 } from "./types";
import type { Transaction, AcreWithdrawalData } from "./types";
import { log } from "@ledgerhq/logs";

/**
Expand Down Expand Up @@ -313,6 +313,88 @@ export default class BtcNew {
s,
};
}
cleanHexPrefix(hexString: string): string {
return hexString.startsWith("0x") ? hexString.slice(2) : hexString;
}

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

const value = Buffer.alloc(32);
value.writeBigUInt64BE(BigInt(withdrawalData.value), 24);

const dataLength = Buffer.alloc(8);
dataLength.writeUInt32BE(withdrawalData.data.length);

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

const operation = Buffer.alloc(1);
operation.writeUInt8(withdrawalData.operation);

const safeTxGas = Buffer.alloc(32);
safeTxGas.writeBigUInt64BE(BigInt(withdrawalData.safeTxGas));

const baseGas = Buffer.alloc(32);
baseGas.writeBigUInt64BE(BigInt(withdrawalData.baseGas));

const gasPrice = Buffer.alloc(32);
gasPrice.writeBigUInt64BE(BigInt(withdrawalData.gasPrice));

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

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

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]);
}

/**
* 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<{
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"));

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

const v = buf.readUInt8() - 27 - 4;
const r = buf.slice(1, 33).toString("hex");
const s = buf.slice(33, 65).toString("hex");

return {
v,
r,
s,
};
}

/**
* Calculates an output script along with public key and possible redeemScript
Expand Down
31 changes: 31 additions & 0 deletions src/newops/appClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WalletPolicy } from "./policy";
import { createVarint } from "../varint";
import { hashLeaf, Merkle } from "./merkle";
import {log } from "@ledgerhq/logs";
import { AcreWithdrawalData } from "../types";

const CLA_BTC = 0xe1;
const CLA_FRAMEWORK = 0xf8;
Expand Down Expand Up @@ -197,4 +198,34 @@ export class AppClient {

return response.toString("base64");
}

async signWithdrawal(
pathElements: number[],
message: Buffer,
withdrawalData: Buffer
): Promise<string> {
if (pathElements.length > 6) {
throw new Error("Path too long. At most 6 levels allowed.");
}

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));
}

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]),
clientInterpreter,
);

return response.slice(0, 64).toString("hex");
}
}
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ export interface Transaction {
extraData?: Buffer;
}

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

export interface TrustedInput {
trustedInput: boolean;
value: Buffer;
Expand Down
33 changes: 29 additions & 4 deletions tests/newops/BtcNew.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ test("getWalletPublicKey p2tr", async () => {
await testGetWalletPublicKey("m/86'/0'/17'", "tr(@0)");
});

test("getWalletXpub normal path", async () => {
await testGetWalletXpub("m/11'/12'");
await testGetWalletXpub("m/11");
await testGetWalletXpub("m/44'/0'/0'");
test("signWithdrawalRealClient", async () => {
await testSignWithdrawalRealClient();
});

function testPaths(type: StandardPurpose): { ins: string[]; out?: string } {
Expand Down Expand Up @@ -254,6 +252,33 @@ async function testSignMessageRealClient(
console.log('v,r,s: ', result)
}

async function testSignWithdrawalRealClient() {

const transport = await openTransportReplayer(RecordStore.fromString(`
=> e1FFFFFFFF
<= FFFF
`));

const withdrawalData: AcreWithdrawalData = {
to: "1234567890abcdef1234567890abcdef12345678",
value: 2863311530,
data: "0xabcdef",
operation: 0,
safeTxGas: 286331153,
baseGas: 572662306,
gasPrice: 858993459,
gasToken: "0x0000000000000000000000000000000000000000",
refundReceiver: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
nonce: 1,
};
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});
console.log('signed withdrawal:', result);
}

function verifyGetWalletPublicKeyResult(
result: { publicKey: string; bitcoinAddress: string; chainCode: string },
expectedXpub: string,
Expand Down

0 comments on commit a23c84a

Please sign in to comment.