diff --git a/package.json b/package.json index f1d53d0..a9de192 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "cross-fetch": "^4.0.0", "dotenv": "^16.4.5", "jest": "^29.7.0", + "starknet": "^6.11.0", "ts-jest": "^29.2.5" }, "devDependencies": { diff --git a/src/UtuProvider.ts b/src/UtuProvider.ts index a6a7210..9929992 100644 --- a/src/UtuProvider.ts +++ b/src/UtuProvider.ts @@ -1,6 +1,23 @@ import { BitcoinProvider } from "./BitcoinProvider"; import { BlockHeightProof, RegisterBlocksTx } from "@/UtuTypes"; import { BlockHeader } from "./BitcoinTypes"; +import { byteArray } from "starknet"; + +const CONTRACT_ADDRESS = + "0x034838129702a2f071cd8cf9277d2f2f2dac3284c2217d9e2e076624fb5afc2f"; + +// Helper function to convert to little-endian hex +const toLittleEndianHex = (num: number): string => { + return num.toString(16).padStart(8, "0").match(/.{2}/g)!.reverse().join(""); +}; + +// New helper function to serialize hash +const serializedHash = (hash: string): string[] => { + return hash + .match(/.{8}/g)! + .map((chunk) => "0x" + chunk.match(/.{2}/g)!.reverse().join("")) + .reverse(); +}; export interface UtuProviderResult { inclusionProof: string; @@ -25,6 +42,42 @@ export class UtuProvider { const rawTransaction = await this.bitcoinProvider.getRawTransaction( coinbaseTransactionHash ); + // Check if this is a SegWit transaction + const isSegWit = rawTransaction.hex.substring(8, 12) === "0001"; + + let cleanedRawTx = rawTransaction.hex; + + if (isSegWit) { + // Remove marker and flag bytes (0001) + cleanedRawTx = cleanedRawTx.substring(0, 8) + cleanedRawTx.substring(12); + + // Parse transaction components + const txBytes = Buffer.from(cleanedRawTx, "hex"); + let offset = 4; // Skip version + + // Read input count and skip inputs + const numInputs = txBytes[offset]; + offset++; + for (let i = 0; i < numInputs; i++) { + offset += 36; // Previous tx hash (32) + output index (4) + const scriptLen = txBytes[offset]; + offset += 1 + scriptLen; // Script length + script + offset += 4; // Sequence + } + + // Read and skip outputs + const numOutputs = txBytes[offset]; + offset++; + for (let i = 0; i < numOutputs; i++) { + offset += 8; // Value + const scriptLen = txBytes[offset]; + offset += 1 + scriptLen; // Script length + script + } + + // Reconstruct transaction without witness data + cleanedRawTx = + cleanedRawTx.substring(0, offset * 2) + cleanedRawTx.slice(-8); + } const parsePartialMerkleTree = (proofHex: string): string[] => { const proofBytes = Buffer.from(proofHex, "hex"); @@ -58,8 +111,55 @@ export class UtuProvider { return { // because block extends BlockHeader blockHeader: block, - rawCoinbaseTx: rawTransaction.hex, - merkleProof: leftMerkleBranch, + rawCoinbaseTx: cleanedRawTx, + merkleProof: leftMerkleBranch.slice(1), + }; + } + + async getCanonicalChainUpdateTx( + beginHeight: number, + endHeight: number, + proof: boolean + ) { + const contractAddress = CONTRACT_ADDRESS; + const selector = "0x..."; + const lastBlockHash = await this.bitcoinProvider.getBlockHash(endHeight); + + let calldata = [ + "0x" + beginHeight.toString(16), + "0x" + endHeight.toString(16), + ...serializedHash(lastBlockHash), + ]; + + if (proof) { + const proof = await this.getBlockHeightProof(beginHeight); + + const rawCoinbaseTx = serializedHash(proof.rawCoinbaseTx); + // Option::Some + calldata.push("0x1"); + // rawCoinbaseTx is like a hash but we need to specify its length + calldata.push("0x" + rawCoinbaseTx.length.toString(16), ...rawCoinbaseTx); + // a merkleProof is basically an array of hashes (fixed size arrays) + calldata.push( + "0x" + proof.merkleProof.length.toString(16), + ...proof.merkleProof + .map(byteArray.byteArrayFromString) + .flatMap((byteArr) => [ + "0x" + byteArr.data.length.toString(16), + ...byteArr.data.map((word) => "0x" + word.toString(16)), + "0x" + byteArr.pending_word.toString(16), + "0x" + byteArr.pending_word_len.toString(16), + ]) + ); + } else { + // Option::None + calldata.push("0x0"); + } + + return { + contractAddress, + selector, + calldata, }; } @@ -69,8 +169,9 @@ export class UtuProvider { ); return { - contractAddress: "0x...", // Replace with actual contract address - selector: "0x...", // Replace with actual selector + contractAddress: CONTRACT_ADDRESS, + selector: + "0x00afd92eeac2cdc892d6323dd051eaf871b8d21df8933ce111c596038eb3afd3", calldata: [ "0x" + blocks.length.toString(16), ...blockHeaders.flatMap((header) => this.serializeBlockHeader(header)), @@ -95,24 +196,6 @@ export class UtuProvider { } } - // Helper function to convert to little-endian hex - const toLittleEndianHex = (num: number): string => { - return num - .toString(16) - .padStart(8, "0") - .match(/.{2}/g)! - .reverse() - .join(""); - }; - - // New helper function to serialize hash - const serializedHash = (hash: string): string[] => { - return hash - .match(/.{8}/g)! - .map((chunk) => "0x" + chunk.match(/.{2}/g)!.reverse().join("")) - .reverse(); - }; - // Serialize each field const serialized = [ "0x" + toLittleEndianHex(blockHeader.version), diff --git a/src/tests/UtuProvider.test.ts b/src/tests/UtuProvider.test.ts index 19e8275..05b41c1 100644 --- a/src/tests/UtuProvider.test.ts +++ b/src/tests/UtuProvider.test.ts @@ -23,10 +23,9 @@ describe("UtuProvider", () => { expect(proof.blockHeader).toBeDefined(); expect(proof.rawCoinbaseTx).toBe( - "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff1a0300350c0120130909092009092009102cda1492140000000000ffffffff02c09911260000000017a914c3f8f898ae5cab4f4c1d597ecb0f3a81a9b146c3870000000000000000266a24aa21a9ed9fbe517a588ccaca585a868f3cf19cb6897e3c26f3351361fb28ac8509e69a7e0120000000000000000000000000000000000000000000000000000000000000000000000000" + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1a0300350c0120130909092009092009102cda1492140000000000ffffffff02c09911260000000017a914c3f8f898ae5cab4f4c1d597ecb0f3a81a9b146c3870000000000000000266a24aa21a9ed9fbe517a588ccaca585a868f3cf19cb6897e3c26f3351361fb28ac8509e69a7e00000000" ); expect(proof.merkleProof).toEqual([ - "b75ca3106ed100521aa50e3ec267a06431c6319538898b25e1b757a5736f5fb4", "d41f5de48325e79070ccd3a23005f7a3b405f3ce1faa4df09f6d71770497e9d5", "e966899d07c2e59033c073820b2f37a11532c1d11184373c4e558d65dac475e0", "9f43ef264af1c3a4678d2bf5e60cddbd87b97618b1c80bd2b8a7f9b7f3baca68", @@ -42,14 +41,18 @@ describe("UtuProvider", () => { ]); }); - it("should get register blocks tx for a given block hash", async () => { + it("should get register blocks tx for given block hashes", async () => { const blockHash = "00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee"; const registerBlocksTx = await utuProvider.getRegisterBlocksTx([blockHash]); expect(registerBlocksTx).toBeDefined(); - expect(registerBlocksTx.contractAddress).toBe("0x..."); // Replace with actual contract address - expect(registerBlocksTx.selector).toBe("0x..."); // Replace with actual selector + expect(registerBlocksTx.contractAddress).toBe( + "0x034838129702a2f071cd8cf9277d2f2f2dac3284c2217d9e2e076624fb5afc2f" + ); // todo: eplace with actual contract address + expect(registerBlocksTx.selector).toBe( + "0x00afd92eeac2cdc892d6323dd051eaf871b8d21df8933ce111c596038eb3afd3" + ); expect(registerBlocksTx.calldata).toEqual([ "0x1", "0x01000000", @@ -74,4 +77,14 @@ describe("UtuProvider", () => { "0x283e9e70", ]); }); + + it("should get canonical chain update tx for a given block slice", async () => { + const updateCanonicalChainTx = await utuProvider.getCanonicalChainUpdateTx( + 865_698, + 865_699, + true + ); + + // console.log(updateCanonicalChainTx.calldata.join(", ")); + }); });