Skip to content

Commit

Permalink
feat: getCanonicalChainUpdateTx
Browse files Browse the repository at this point in the history
  • Loading branch information
Th0rgal committed Oct 25, 2024
1 parent 7c36fed commit 6099482
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 27 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
127 changes: 105 additions & 22 deletions src/UtuProvider.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
Expand Down Expand Up @@ -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,
};
}

Expand All @@ -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)),
Expand All @@ -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),
Expand Down
23 changes: 18 additions & 5 deletions src/tests/UtuProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ describe("UtuProvider", () => {

expect(proof.blockHeader).toBeDefined();
expect(proof.rawCoinbaseTx).toBe(
"010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff1a0300350c0120130909092009092009102cda1492140000000000ffffffff02c09911260000000017a914c3f8f898ae5cab4f4c1d597ecb0f3a81a9b146c3870000000000000000266a24aa21a9ed9fbe517a588ccaca585a868f3cf19cb6897e3c26f3351361fb28ac8509e69a7e0120000000000000000000000000000000000000000000000000000000000000000000000000"
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1a0300350c0120130909092009092009102cda1492140000000000ffffffff02c09911260000000017a914c3f8f898ae5cab4f4c1d597ecb0f3a81a9b146c3870000000000000000266a24aa21a9ed9fbe517a588ccaca585a868f3cf19cb6897e3c26f3351361fb28ac8509e69a7e00000000"
);
expect(proof.merkleProof).toEqual([
"b75ca3106ed100521aa50e3ec267a06431c6319538898b25e1b757a5736f5fb4",
"d41f5de48325e79070ccd3a23005f7a3b405f3ce1faa4df09f6d71770497e9d5",
"e966899d07c2e59033c073820b2f37a11532c1d11184373c4e558d65dac475e0",
"9f43ef264af1c3a4678d2bf5e60cddbd87b97618b1c80bd2b8a7f9b7f3baca68",
Expand All @@ -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",
Expand All @@ -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(", "));
});
});

0 comments on commit 6099482

Please sign in to comment.