Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow parsing Bitcoin deposit memo with inscription and add E2E test for parsing #2727

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5109776
update e2e tests
bitSmiley Aug 15, 2024
3943574
test
bitSmiley Aug 15, 2024
ffaa63c
only selected tests
bitSmiley Aug 15, 2024
8405c85
add tests
bitSmiley Aug 15, 2024
4aee797
update url
bitSmiley Aug 15, 2024
a7db462
more logs
bitSmiley Aug 15, 2024
73c6695
more logs
bitSmiley Aug 15, 2024
cb60c4e
update
bitSmiley Aug 15, 2024
a43f6ec
logs
bitSmiley Aug 15, 2024
58d51d2
handle json
bitSmiley Aug 15, 2024
cb59e0b
handle json
bitSmiley Aug 15, 2024
a0c0ab1
parse p2tr
bitSmiley Aug 16, 2024
f408186
update memo
bitSmiley Aug 16, 2024
8582d87
update code
bitSmiley Aug 16, 2024
1f70654
add assertion
bitSmiley Aug 16, 2024
508095a
update tss address
bitSmiley Aug 16, 2024
306856d
update code
bitSmiley Aug 16, 2024
475d8e0
update
bitSmiley Aug 16, 2024
d14033b
more logs
bitSmiley Aug 16, 2024
c209bf9
more fixes
bitSmiley Aug 16, 2024
3c47efb
reenabled tests
bitSmiley Aug 16, 2024
e2414d8
Merge branch 'develop' into e2e
bitSmiley Aug 16, 2024
4e7b695
remove unused code
bitSmiley Aug 16, 2024
161d543
Merge branch 'e2e' of bitsmiley-github:bitSmiley/node into e2e
bitSmiley Aug 16, 2024
06bf0bf
Merge branch 'develop' into e2e
lumtis Aug 20, 2024
b56767c
Update cmd/zetae2e/local/bitcoin.go
bitSmiley Aug 21, 2024
2da0d34
Update contrib/localnet/bitcoin-sidecar/js/src/client.ts
bitSmiley Aug 21, 2024
5f3ade1
Update contrib/localnet/bitcoin-sidecar/js/src/client.ts
bitSmiley Aug 21, 2024
77f71c2
Update contrib/localnet/bitcoin-sidecar/js/src/client.ts
bitSmiley Aug 21, 2024
f2bffb2
Update e2e/runner/bitcoin.go
bitSmiley Aug 21, 2024
efcebdc
Update e2e/e2etests/test_extract_bitcoin_inscription_memo.go
bitSmiley Aug 21, 2024
2ed136c
Update e2e/e2etests/test_extract_bitcoin_inscription_memo.go
bitSmiley Aug 21, 2024
bc02de7
Update e2e/e2etests/test_extract_bitcoin_inscription_memo.go
bitSmiley Aug 21, 2024
7bf9b07
Update e2e/runner/bitcoin.go
bitSmiley Aug 21, 2024
7ae9955
Merge branch 'develop' into e2e
bitSmiley Aug 21, 2024
9e498bc
update test order
bitSmiley Aug 21, 2024
4e69799
Merge branch 'e2e' of bitsmiley-github:bitSmiley/node into e2e
bitSmiley Aug 21, 2024
152b0d7
Update e2e/e2etests/test_extract_bitcoin_inscription_memo.go
bitSmiley Aug 21, 2024
b341832
return nil neither both op_ret nor inscription found
bitSmiley Aug 21, 2024
4133914
require not err
bitSmiley Aug 21, 2024
7e394eb
Update e2e/runner/bitcoin.go
bitSmiley Aug 21, 2024
4fd2d19
Update e2e/runner/bitcoin.go
bitSmiley Aug 21, 2024
b49c074
more comments
bitSmiley Aug 21, 2024
7b0ca65
fix lint
bitSmiley Aug 21, 2024
9e38b18
Update changelog.md
bitSmiley Aug 21, 2024
3478967
fix lint
bitSmiley Aug 23, 2024
c7607fb
Merge branch 'e2e' of bitsmiley-github:bitSmiley/node into e2e
bitSmiley Aug 23, 2024
4b4df76
Merge branch 'develop' into e2e
lumtis Sep 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* [2681](https://github.com/zeta-chain/node/pull/2681) - implement `MsgUpdateERC20CustodyPauseStatus` to pause or unpause ERC20 Custody contract (to be used for the migration process for smart contract V2)
* [2644](https://github.com/zeta-chain/node/pull/2644) - add created_timestamp to cctx status
* [2673](https://github.com/zeta-chain/node/pull/2673) - add relayer key importer, encryption and decryption
* [2727](https://github.com/zeta-chain/node/pull/2727) - enable op_return and inscription in memo detection

### Refactor

Expand Down
1 change: 1 addition & 0 deletions cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
e2etests.TestBitcoinWithdrawP2SHName,
e2etests.TestBitcoinWithdrawP2WSHName,
e2etests.TestBitcoinWithdrawRestrictedName,
e2etests.TestExtractBitcoinInscriptionMemoName,
}
ethereumTests := []string{
e2etests.TestEtherWithdrawName,
Expand Down
14 changes: 14 additions & 0 deletions contrib/localnet/bitcoin-sidecar/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM node:18.20.4 as builder

WORKDIR /home/zeta/node

COPY bitcoin-sidecar/js/* .

RUN npm install && npm install typescript -g && tsc
lumtis marked this conversation as resolved.
Show resolved Hide resolved

FROM node:alpine

COPY --from=builder /home/zeta/node/dist ./dist
COPY --from=builder /home/zeta/node/node_modules ./node_modules

CMD ["node", "dist/index.js"]
23 changes: 23 additions & 0 deletions contrib/localnet/bitcoin-sidecar/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "zeta-btc-client",
"version": "0.0.1",
"description": "The Zetachain BTC client",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not introduce nodejs for test cases but write them with existing go tooling.

Regarding update of btcsuite/btcd - I investigated this as as of now it's not trivial because we have a downstream deps of btcd v1:

  • zeta-chain/node. Can be updated
    • zeta-chain/go-tss (thorchain's fork). Can be updated
      • tss-lib (thorchain). It uses github.com/btcsuite/btcd instead of github.com/btcsuite/btcd/v2

Copy link
Contributor

@swift1337 swift1337 Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So probably makes sense to cherry pick missing features from there and implement it natively using zeta's E2E suite.

cc @lumtis @ws4charlie

#2728

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue for the improvement #2900

"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"bip32": "^4.0.0",
"bitcoinjs-lib": "^6.1.6",
"ecpair": "^2.1.0",
"express": "^4.19.2",
"randombytes": "^2.1.0",
"tiny-secp256k1": "^2.2.3"
},
"devDependencies": {
"@types/node": "^20.14.11",
"typescript": "^5.5.3"
}
}
183 changes: 183 additions & 0 deletions contrib/localnet/bitcoin-sidecar/js/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { initEccLib, payments, Psbt } from "bitcoinjs-lib";
import { bitcoin, Network, regtest } from "bitcoinjs-lib/src/networks";
import BIP32Factory, { BIP32Interface } from 'bip32';
import * as ecc from 'tiny-secp256k1';
import randomBytes from "randombytes";
import { ScriptBuilder } from "./script";
import { Taptree } from "bitcoinjs-lib/src/types";
import { toXOnly } from "./util";

const LEAF_VERSION_TAPSCRIPT = 0xc0;

initEccLib(ecc);
const bip32 = BIP32Factory(ecc);
const rng = randomBytes;

/// The evm address type, a 20 bytes hex string
export type Address = String;
lumtis marked this conversation as resolved.
Show resolved Hide resolved
export type BtcAddress = String;

/// The BTC transaction hash returned
export type BtcTxnHash = String;
export interface BtcInput {
txn: BtcTxnHash,
idx: number,
}

/**
* The example client for interacting with ZetaChain in BTC. There are currently two ways
* of calling a smart contract on ZetaChain from BTC:
*
* - Using OP_RETURN
* - Using Witness
*
* The method used is now based on the data size. Within 80 bytes, `OP_RETURN` is used, else
* the data is written to Witness.
*
* This class handles only the case where data is more than 80 bytes.
*/
export class ZetaBtcClient {
/** The BTC network interracting with */
readonly network: Network;

private reveal: RevealTxnBuilder | null;

private constructor(network: Network) {
this.network = network;
}

public static regtest(): ZetaBtcClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to add testnet() as well. Features typically goes to ZetaChain testnet and then mainnet.

return new ZetaBtcClient(regtest);
}

public static mainnet(): ZetaBtcClient {
return new ZetaBtcClient(bitcoin);
}

/**
* Call a target address and passing the data call.
*
* @param address The target zetachain evm address
* @param calldata The calldata that will be invoked on Zetachain
*/
public call(
address: Address,
calldata: Buffer,
): Address {
if (calldata.length <= 80) {
throw Error("Use op return instead");
}

if (address.startsWith("0x")) {
address = address.substring(2);
}

return this.callWithWitness(Buffer.concat([Buffer.from(address, "hex"), calldata]));
}

private callWithWitness(
data: Buffer,
): Address {
const internalKey = bip32.fromSeed(rng(64), this.network);

const leafScript = this.genLeafScript(internalKey.publicKey, data);

const scriptTree: Taptree = { output: leafScript };

const { address: commitAddress } = payments.p2tr({
internalPubkey: toXOnly(internalKey.publicKey),
scriptTree,
network: this.network,
});

this.reveal = new RevealTxnBuilder(internalKey, leafScript, this.network);

return commitAddress;
}

public buildRevealTxn(to: string, commitTxn: BtcInput, commitAmount: number, feeRate: number): Buffer {
if (this.reveal === null) {
throw new Error("commit txn not built yet");
}

this.reveal.with_commit_tx(to, commitTxn, commitAmount, feeRate);
return this.reveal.dump();
}

private genLeafScript(publicKey: Buffer, data: Buffer,): Buffer {
const builder = ScriptBuilder.new(publicKey);
builder.pushData(data);
return builder.build();
}
}

class RevealTxnBuilder {
private psbt: Psbt;
private key: BIP32Interface;
private leafScript: Buffer;
private network: Network

constructor(key: BIP32Interface, leafScript: Buffer, network: Network) {
this.psbt = new Psbt({ network });;
this.key = key;
this.leafScript = leafScript;
this.network = network;
}

public with_commit_tx(to: string, commitTxn: BtcInput, commitAmount: number, feeRate: number): RevealTxnBuilder {
const scriptTree: Taptree = { output: this.leafScript };

const { output, witness } = payments.p2tr({
internalPubkey: toXOnly(this.key.publicKey),
scriptTree,
redeem: {
output: this.leafScript,
redeemVersion: LEAF_VERSION_TAPSCRIPT,
},
network: this.network,
});

this.psbt.addInput({
hash: commitTxn.txn.toString(),
index: commitTxn.idx,
witnessUtxo: { value: commitAmount, script: output! },
tapLeafScript: [
{
leafVersion: LEAF_VERSION_TAPSCRIPT,
script: this.leafScript,
controlBlock: witness![witness!.length - 1],
},
],
});

this.psbt.addOutput({
value: commitAmount - this.estimateFee(to, commitAmount, feeRate),
address: to,
});

this.psbt.signAllInputs(this.key);
this.psbt.finalizeAllInputs();

return this;
}

public dump(): Buffer {
return this.psbt.extractTransaction(true).toBuffer();
}

private estimateFee(to: string, amount: number, feeRate: number): number {
const cloned = this.psbt.clone();

cloned.addOutput({
value: amount,
address: to,
});

// should have a way to avoid signing but just providing mocked signautre
cloned.signAllInputs(this.key);
cloned.finalizeAllInputs();

const size = cloned.extractTransaction().virtualSize();
return size * feeRate;
}
}
38 changes: 38 additions & 0 deletions contrib/localnet/bitcoin-sidecar/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ZetaBtcClient } from "./client";
import express, { Request, Response } from 'express';

const app = express();
const PORT = process.env.PORT || 3000;
let zetaClient = ZetaBtcClient.regtest();

app.use(express.json());

// Middleware to parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));

// Route to handle JSON POST requests
app.post('/commit', (req: Request, res: Response) => {
const memo: string = req.body.memo;
const address = zetaClient.call("", Buffer.from(memo, "hex"));
res.json({ address });
});

// Route to handle URL-encoded POST requests
app.post('/reveal', (req: Request, res: Response) => {
const { txn, idx, amount, feeRate, to } = req.body;
console.log(txn, idx, amount, feeRate);

const rawHex = zetaClient.buildRevealTxn(to,{ txn, idx }, Number(amount), feeRate).toString("hex");
zetaClient = ZetaBtcClient.regtest();
res.json({ rawHex });
});

// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

/**
* curl --request POST --header "Content-Type: application/json" --data '{"memo":"72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c"}' http://localhost:3000/commit
* curl --request POST --header "Content-Type: application/json" --data '{"txn": "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c", "idx": 0, "amount": 1000, "feeRate": 10}' http://localhost:3000/reveal
*/
52 changes: 52 additions & 0 deletions contrib/localnet/bitcoin-sidecar/js/src/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { opcodes, script, Stack } from "bitcoinjs-lib";
import { toXOnly } from "./util";

const MAX_SCRIPT_ELEMENT_SIZE = 520;

/** The tapscript builder for zetaclient spending script */
export class ScriptBuilder {
private script: Stack;

private constructor(initialScript: Stack) {
this.script = initialScript;
}

public static new(publicKey: Buffer): ScriptBuilder {
const stack = [
toXOnly(publicKey),
opcodes.OP_CHECKSIG,
];
return new ScriptBuilder(stack);
}

public pushData(data: Buffer) {
if (data.length <= 80) {
throw new Error("data length should be more than 80 bytes");
}

this.script.push(
opcodes.OP_FALSE,
opcodes.OP_IF
);

const chunks = chunkBuffer(data, MAX_SCRIPT_ELEMENT_SIZE);
for (const chunk of chunks) {
this.script.push(chunk);
}

this.script.push(opcodes.OP_ENDIF);
}

public build(): Buffer {
return script.compile(this.script);
}
}

function chunkBuffer(buffer: Buffer, chunkSize: number): Buffer[] {
const chunks = [];
for (let i = 0; i < buffer.length; i += chunkSize) {
const chunk = buffer.slice(i, i + chunkSize);
chunks.push(chunk);
}
return chunks;
}
1 change: 1 addition & 0 deletions contrib/localnet/bitcoin-sidecar/js/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const toXOnly = pubKey => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
11 changes: 11 additions & 0 deletions contrib/localnet/bitcoin-sidecar/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}
13 changes: 13 additions & 0 deletions contrib/localnet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,19 @@ services:
-rpcauth=smoketest:63acf9b8dccecce914d85ff8c044b78b$$5892f9bbc84f4364e79f0970039f88bdd823f168d4acc76099ab97b14a766a99
-txindex=1

bitcoin-node-sidecar:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a profiles to allow manually setting when this image should be spawned
cc @gartnera in case he has inputs on this

build:
dockerfile: ./bitcoin-sidecar/Dockerfile
container_name: bitcoin-node-sidecar
hostname: bitcoin-node-sidecar
networks:
mynetwork:
ipv4_address: 172.20.0.111
environment:
- PORT=8000
ports:
- "8000:8000"

solana:
image: solana-local:latest
container_name: solana
Expand Down
Loading
Loading