-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: enable parsing Bitcoin deposit memo with inscription; deposit f…
…ee improvement (#2768) * allow parsing Bitcoin memo iwth inscription; improve Bitcoin depositor fee * add changelog entry * corrected PR# and function comment typos * replace GetRecentBlockhash with GetLatestBlockhash; add check on Vsize; misc * test: cherry pick E2E bitcoin test for inscription for 19.1.2 (#2769) * test: cherry pick E2E bitcoin test for inscription * fix dockerfile --------- Co-authored-by: Lucas Bertrand <[email protected]>
- Loading branch information
1 parent
d85e61f
commit bc9f419
Showing
23 changed files
with
1,019 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
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 { | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.