diff --git a/offchain-modules/config.json.example b/offchain-modules/config.json.example index 76e9ffe9..74666ab4 100644 --- a/offchain-modules/config.json.example +++ b/offchain-modules/config.json.example @@ -14,6 +14,10 @@ "0x193c96316c6cada898f067004aa07a5d48fcecdb1aa158784f2e8868a4db7ce8", "0x57c246bcb73e5d3409a903c94d46b3b118fdcfb81c5e51d3b0df72e883623fa7" ], + "multiSignHosts": [ + "http://localhost:8090/force-bridge/sign-server/api/v1", + "http://localhost:8091/force-bridge/sign-server/api/v1" + ], "multiSignThreshold": 2, "contractAddress": "0x8326e1d621Cd32752920ed2A44B49bB1a96c7391", "confirmNumber": 1, @@ -57,7 +61,26 @@ "ckb": { "ckbRpcUrl": "http://127.0.0.1:8114", "ckbIndexerUrl": "http://127.0.0.1:8116", - "privateKey": "0xa800c82df5461756ae99b5c6677d019c98cc98c7786b80d7b2e77256e46ea1fe", + "multisigScript": { + "R": 0, + "M": 2, + "publicKeyHashes": [ + "0x40dcec2ef1ffc2340ea13ff4dd9671d2f9787e95", + "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7", + "0x470dcdc5e44064909650113a274b3b36aecb6dc7", + "0xd9a188cc1985a7d4a31f141f4ebb61f241aec182", + "0xebf9befcd8396e88cab8fcb920ab149231658f4b" + ] + }, + "keys": [ + "0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc", + "0x63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d" + ], + "hosts": [ + "http://127.0.0.1:8090/force-bridge/sign-server/api/v1", + "http://127.0.0.1:8091/force-bridge/sign-server/api/v1" + ], + "fromPrivateKey": "0xa800c82df5461756ae99b5c6677d019c98cc98c7786b80d7b2e77256e46ea1fe", "ownerLockHash": "0x49beb8c4c29d06e05452b5d9ea8e86ffd4ea2b614498ba1a0c47890a0ad4f550", "deps": { "bridgeLock": { diff --git a/offchain-modules/package.json b/offchain-modules/package.json index 019f8e4e..29fc9cb0 100644 --- a/offchain-modules/package.json +++ b/offchain-modules/package.json @@ -14,29 +14,32 @@ "build": "lerna run build", "test": "ava", "lint": "run-p check:prettier check:eslint", - "lint-staged": "eslint --fix $(git diff --cached --staged --name-only --relative | grep -E \"(.ts$)\")", + "lint-staged": "npm run build && eslint --fix $(git diff --cached --staged --name-only --relative | grep -E \"(.ts$)\")", "fix": "run-p fix:prettier fix:eslint", "fix:eslint": "eslint --fix --format=pretty 'packages/*/src/**/*.ts'", "fix:prettier": "prettier --write .", "watch:start": "ts-node-dev --respawn --transpile-only ./packages/apps/relayer/index.ts", "check:prettier": "prettier -c .", "check:eslint": "yarn eslint --format=pretty 'packages/*/src/**/*.ts'", - "clean": "rimraf lumos_db dist force-bridge.sqlite", + "clean": "rimraf lumos_db indexer-data dist force-bridge.sqlite", "eth-test": "ts-node ./packages/scripts/src/integration-test/eth.ts", "eos-test": "ts-node ./packages/scripts/src/integration-test/eos.ts", "tron-test": "ts-node ./packages/scripts/src/integration-test/tron.ts", - "prepare-xchain-test": "sleep 2", - "btc-test": "ts-node ./packages/scripts/src/integration-test/btc.ts", "xchain-test": "run-s prepare-xchain-test eth-test", - "integration-test": "run-p -r start xchain-test", - "ci": "run-s clean build deploy integration-test", + "prepare-xchain-test": "sleep 5", + "btc-test": "ts-node ./packages/scripts/src/integration-test/btc.ts", + "integration-test": "run-p -r start sign-server1 sign-server2 xchain-test", + "ci": "run-s clean build deploy init-multisig prepare-xchain-test integration-test", "test:unit": "nyc --silent ava", "cov": "run-s build test:unit", "cov:html": "nyc report --reporter=html", "cov:send": "nyc report --reporter=lcov && codecov", "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", "deploy": "ts-node-dev ./packages/scripts/src/deploy_ckb.ts", - "prepare": "cd .. && husky install offchain-modules/.husky" + "prepare": "cd .. && husky install offchain-modules/.husky", + "init-multisig": "ts-node ./packages/x/src/ckb/tx-helper/multisig/deploy.ts", + "sign-server1": "ts-node ./packages/app-multisign-server/src/index.ts --port 8090 --index 0", + "sign-server2": "ts-node ./packages/app-multisign-server/src/index.ts --port 8091 --index 1" }, "author": "", "license": "MIT", @@ -68,12 +71,18 @@ "tsconfig-paths": "^3.9.0", "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", - "typescript": "^4.2.2" + "typescript": "^4.2.2", + "@types/minimist": "^1.2.1" }, "dependencies": { "@ckb-lumos/base": "^0.16.0", "@ckb-lumos/indexer": "^0.16.0", - "@ckb-lumos/rpc": "^0.16.0", + "@ckb-lumos/rpc": "0.16.0", + "@ckb-lumos/common-scripts": "^0.16.0", + "@ckb-lumos/config-manager": "^0.16.0", + "@ckb-lumos/hd": "^0.16.0", + "@ckb-lumos/helpers": "^0.16.0", + "@ckb-lumos/transaction-manager": "^0.16.0", "@lay2/pw-core": "^0.3.22", "@nervosnetwork/ckb-sdk-core": "^0.39.0", "@types/node": "^15.0.2", @@ -125,5 +134,10 @@ "npm run lint-staged", "git add" ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } } } diff --git a/offchain-modules/packages/app-cli/src/btc.ts b/offchain-modules/packages/app-cli/src/btc.ts index 26fa028a..9805d87b 100644 --- a/offchain-modules/packages/app-cli/src/btc.ts +++ b/offchain-modules/packages/app-cli/src/btc.ts @@ -102,7 +102,7 @@ async function doUnlock( const amount = options.get('amount'); const account = new Account(privateKey); - const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.indexer)); + const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.ckbIndexer)); const burnAmount = new Amount(Unit.fromBTC(amount).toSatoshis(), 0); const burnTx = await generator.burn( await account.getLockscript(), diff --git a/offchain-modules/packages/app-cli/src/eos.ts b/offchain-modules/packages/app-cli/src/eos.ts index 152b708e..fc9071e1 100644 --- a/offchain-modules/packages/app-cli/src/eos.ts +++ b/offchain-modules/packages/app-cli/src/eos.ts @@ -105,7 +105,7 @@ async function doUnlock( } const account = new Account(privateKey); - const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.indexer)); + const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.ckbIndexer)); const burnTx = await generator.burn( await account.getLockscript(), recipientAddress, diff --git a/offchain-modules/packages/app-cli/src/eth.ts b/offchain-modules/packages/app-cli/src/eth.ts index beab72ed..5b504d7c 100644 --- a/offchain-modules/packages/app-cli/src/eth.ts +++ b/offchain-modules/packages/app-cli/src/eth.ts @@ -102,7 +102,7 @@ async function doUnlock( const token = !options.get('asset') ? ETH_ASSET : options.get('asset'); const account = new Account(privateKey); - const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.indexer)); + const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.ckbIndexer)); const burnTx = await generator.burn( await account.getLockscript(), recipientAddress, diff --git a/offchain-modules/packages/app-cli/src/tron.ts b/offchain-modules/packages/app-cli/src/tron.ts index 395d792f..7d66b81b 100644 --- a/offchain-modules/packages/app-cli/src/tron.ts +++ b/offchain-modules/packages/app-cli/src/tron.ts @@ -85,7 +85,7 @@ async function doUnlock( const privateKey = options.get('privateKey'); const account = new Account(privateKey); - const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.indexer)); + const generator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.ckbIndexer)); const burnTx = await generator.burn( await account.getLockscript(), recipientAddress, diff --git a/offchain-modules/packages/app-cli/src/utils.ts b/offchain-modules/packages/app-cli/src/utils.ts index d10958b4..3126afc6 100644 --- a/offchain-modules/packages/app-cli/src/utils.ts +++ b/offchain-modules/packages/app-cli/src/utils.ts @@ -44,7 +44,7 @@ export async function getSudtBalance(address: string, asset: Asset): Promise { + const txSkeleton = payload.txSkeleton; + const sigData = txSkeleton.signingEntries[1].message; + if (rawData !== sigData) { + return new Error(`rawData:${rawData} doesn't match with:${sigData}`); + } + + const createAssets = payload.createAssets; + const ownLockHash = SigServer.getOwnLockHash(); + const bridgeCells = []; + txSkeleton.outputs.forEach((output) => { + if (!output.cell_output.lock) { + return; + } + if (output.cell_output.lock.code_hash === ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash) { + bridgeCells.push(output); + } + }); + if (bridgeCells.length !== createAssets.length) { + return new Error(`create bridge recode length:${bridgeCells.length} doesn't match with:${createAssets.length}`); + } + for (let i = 0; i < createAssets.length; i++) { + const createAsset = createAssets[i]; + let asset; + switch (createAsset.chain) { + case ChainType.BTC: + asset = new BtcAsset(createAsset.asset, ownLockHash); + break; + case ChainType.ETH: + asset = new EthAsset(createAsset.asset, ownLockHash); + break; + case ChainType.TRON: + asset = new TronAsset(createAsset.asset, ownLockHash); + break; + case ChainType.EOS: + asset = new EosAsset(createAsset.asset, ownLockHash); + break; + default: + return Promise.reject(new Error(`chain type:${createAsset.chain} doesn't support`)); + } + + const output = bridgeCells[i]; + const lockScript = output.cell_output.lock; + if (output.data !== '0x') { + return new Error( + `create bridge cell data:${output.data} doesn't match with 0x, asset chain:${createAsset.chain} address:${createAsset.asset}`, + ); + } + if (lockScript.args !== asset.toBridgeLockscriptArgs()) { + return new Error( + `create bridge cell lockScript args:${ + lockScript.args + } doesn't match with ${asset.toBridgeLockscriptArgs()}, asset chain:${createAsset.chain} address:${ + createAsset.asset + }`, + ); + } + if (lockScript.hash_type !== ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType) { + return new Error( + `create bridge cell lockScript hash_type:${lockScript.hash_type} doesn't match with ${ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType}, asset chain:${createAsset.chain} address:${createAsset.asset}`, + ); + } + } + return undefined; +} + +async function verifyMintTx(rawData: string, payload: ckbCollectSignaturesPayload): Promise { + const txSkeleton = payload.txSkeleton; + const sigData = txSkeleton.signingEntries[1].message; + if (rawData !== sigData) { + return new Error(`rawData:${rawData} doesn't match with:${sigData}`); + } + const mintRecords = payload.mintRecords; + + // const mintTxHashes = mintRecords.map((mintRecord) => { + // return mintRecord.id; + // }); + + // const signedTxs = await signedDb.getSignedByRefTxHashes(mintTxHashes); + // if (signedTxs.length != 0) { + // return new Error(`refTxHashes:${mintTxHashes.join(',')} had already signed`); + // } + + const mintCells = []; + txSkeleton.outputs.forEach((output) => { + if (!output.cell_output.type) { + return; + } + if (output.cell_output.type.code_hash === ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash) { + mintCells.push(output); + } + }); + + if (mintRecords.length !== mintCells.length) { + return new Error(`mint recode length:${mintRecords.length} doesn't match with:${mintCells.length}`); + } + + let err: Error; + for (let i = 0; i < mintRecords.length; i++) { + const mintRecord = mintRecords[i]; + if ( + mintRecord.chain === ChainType.BTC || + mintRecord.chain === ChainType.EOS || + mintRecord.chain === ChainType.TRON + ) { + //those chains doesn't verify now + continue; + } + err = await verifyEthMintRecord(mintRecord); + if (err) { + return err; + } + const output = mintCells[i]; + err = await verifyEthMintTx(mintRecord, output); + if (err) { + return err; + } + } + return undefined; +} + +async function verifyEthMintRecord(record: mintRecord): Promise { + let success = false; + const txReceipt = await SigServer.ethProvider.getTransactionReceipt(record.id); + for (const log of txReceipt.logs) { + if (log.address !== ForceBridgeCore.config.eth.contractAddress) { + continue; + } + const parsedLog = SigServer.ethInterface.parseLog(log); + if (parsedLog.topic !== lockTopic) { + continue; + } + const amount = parsedLog.args.lockedAmount.toString(); + if (amount !== record.amount) { + return Promise.reject(new Error(`mint amount:${record.amount} doesn't match with ${amount}`)); + } + const asset = parsedLog.args.token; + if (asset !== record.asset) { + return Promise.reject(new Error(`mint asset:${record.asset} doesn't match with ${asset}`)); + } + const recipientLockscript = uint8ArrayToString(fromHexString(parsedLog.args.recipientLockscript)); + if (recipientLockscript !== record.recipientLockscript) { + return Promise.reject( + new Error(`mint asset:${record.recipientLockscript} doesn't match with ${recipientLockscript}`), + ); + } + success = true; + break; + } + if (!success) { + return Promise.reject(new Error(`cannot found validate log`)); + } + return undefined; +} + +async function verifyEthMintTx(mintRecord: mintRecord, output: Cell): Promise { + const ownLockHash = SigServer.getOwnLockHash(); + const recipient = new Address(mintRecord.recipientLockscript, AddressType.ckb); + const amount = new Amount(mintRecord.amount, 0); + const asset = new EthAsset(mintRecord.asset, ownLockHash); + const recipientLockscript = recipient.toLockScript(); + const bridgeCellLockscript = { + codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, + hashType: ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType, + args: asset.toBridgeLockscriptArgs(), + }; + + const lockScript = output.cell_output.lock; + if (lockScript.code_hash !== recipientLockscript.codeHash) { + return new Error(`lockScript code_hash:${lockScript.code_hash} doesn't match with:${recipientLockscript.codeHash}`); + } + if (lockScript.hash_type !== recipientLockscript.hashType) { + return new Error(`lockScript hash_type:${lockScript.hash_type} doesn't match with:${recipientLockscript.hashType}`); + } + if (lockScript.args !== recipientLockscript.args) { + return new Error(`lockScript args:${lockScript.args} doesn't match with:${recipientLockscript.args}`); + } + + const typeScript = output.cell_output.type; + if (typeScript.code_hash !== ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash) { + return new Error( + `typescript code_hash:${typeScript.code_hash} doesn't match with:${ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash}`, + ); + } + if (typeScript.hash_type !== ForceBridgeCore.config.ckb.deps.sudtType.script.hashType) { + return new Error( + `typescript hash_type:${typeScript.hash_type} doesn't match with:${ForceBridgeCore.config.ckb.deps.sudtType.script.hashType}`, + ); + } + const sudtArgs = ForceBridgeCore.ckb.utils.scriptToHash(bridgeCellLockscript); + if (sudtArgs !== typeScript.args) { + return new Error(`typescript args:${typeScript.args} doesn't with ${sudtArgs}`); + } + + const data = amount.toUInt128LE(); + if (data !== output.data) { + return new Error(`data:${output.data} doesn't with ${data}`); + } + return undefined; +} + +export async function signCkbTx(params: collectSignaturesParams): Promise { + const payload = params.payload as ckbCollectSignaturesPayload; + const txSkeleton = payload.txSkeleton; + let err: Error; + switch (payload.sigType) { + case 'mint': + err = await verifyMintTx(params.rawData, payload); + if (err) { + return Promise.reject(err); + } + break; + case 'create_cell': + err = await verifyCreateCellTx(params.rawData, payload); + if (err) { + return Promise.reject(err); + } + break; + default: + return Promise.reject(new Error(`invalid sigType:${payload.sigType}`)); + } + + const args = minimist(process.argv.slice(2)); + const index = args.index; + const privKey = ForceBridgeCore.config.ckb.keys[index]; + const message = txSkeleton.signingEntries[1].message; + const sig = key.signRecoverable(message, privKey).slice(2); + + // if (payload.sigType === 'mint'){ + // await signedDb.createSigned( + // payload.mintRecords.map((mintRecord) => { + // return { + // sigType: '', + // chain: mintRecord.chain, + // amount: mintRecord.amount, + // asset: mintRecord.asset, + // refTxHash: mintRecord.id, + // txHash: '', + // signature: sig, + // rawData: params.rawData + // }; + // }), + // ); + // } + return sig; +} diff --git a/offchain-modules/packages/app-multisign-server/src/ethSigner.ts b/offchain-modules/packages/app-multisign-server/src/ethSigner.ts new file mode 100644 index 00000000..9d007821 --- /dev/null +++ b/offchain-modules/packages/app-multisign-server/src/ethSigner.ts @@ -0,0 +1,172 @@ +import { RecipientCellData } from '@force-bridge/x/dist/ckb/tx-helper/generated/eth_recipient_cell'; +import { ForceBridgeCore } from '@force-bridge/x/dist/core'; +import { isBurnTx } from '@force-bridge/x/dist/handlers/ckb'; +import { collectSignaturesParams, ethCollectSignaturesPayload } from '@force-bridge/x/dist/multisig/multisig-mgr'; +import { fromHexString, toHexString, uint8ArrayToString } from '@force-bridge/x/dist/utils'; +import { logger } from '@force-bridge/x/dist/utils/logger'; +import { EthUnlockRecord } from '@force-bridge/x/dist/xchain/eth'; + +import { abi } from '@force-bridge/x/dist/xchain/eth/abi/ForceBridge.json'; +import { buildSigRawData } from '@force-bridge/x/dist/xchain/eth/utils'; +import { Amount } from '@lay2/pw-core'; +import { ecsign, toRpcSig } from 'ethereumjs-util'; +import { BigNumber } from 'ethers'; +import minimist from 'minimist'; +import { SigServer } from './sigServer'; + +const UnlockABIFuncName = 'unlock'; + +async function checkDuplicateEthTx(pubKey: string, payload: ethCollectSignaturesPayload) { + const signedDbRecords = await SigServer.signedDb.getSignedByPubkeyAndMsgHash( + pubKey, + payload.unlockRecords.map((record) => { + return record.ckbTxHash; + }), + ); + + if (signedDbRecords.length === 0) { + return; + } + // + // const burnTxs = signedDbRecords.map((r)=>{ + // return r.refTxHash + // }) + // + // logger.info(`checkDuplicateEthTx burnTxs:${burnTxs.join(', ')}. more record info : ${JSON.stringify(payload)}. the sql query info ${JSON.stringify( + // signedDbRecords, + // null, + // 2, + // )}`) + // + // if (!payload.failedTxHash) { + // return Promise.reject( + // `the request params has data that already exists. it must be provide failedTxHash to verify`, + // ); + // } + // + // const failedTxWithReceipt = await SigServer.ethProvider.getTransactionReceipt(payload.failedTxHash); + // if (!failedTxWithReceipt || failedTxWithReceipt.status === 1) { + // return Promise.reject( + // `the tx ${payload.failedTxHash} exec success or is null tx. the tx receipt is ${JSON.stringify( + // failedTxWithReceipt, + // null, + // 2, + // )}`, + // ); + // } + // // Parse abi function params to compare unlock records and nonce + // const failedTx = await SigServer.ethProvider.getTransaction(payload.failedTxHash); + // const iface = new ethers.utils.Interface(abi); + // const contractUnlockParams = iface.decodeFunctionData(UnlockABIFuncName, failedTx.data); + // const unlockResults: EthUnlockRecord[] = contractUnlockParams[0]; + // const nonce: BigNumber = contractUnlockParams[1]; + // // const signature : string = contractUnlockParams[2]; + // if ( + // nonce.toString() !== payload.nonce.toString() || + // !equalsUnlockRecord(unlockResults, payload.unlockRecords) + // ) { + // return Promise.reject( + // `the params which provided do not match the data from chain. provide record: ${JSON.stringify( + // payload.unlockRecords, + // null, + // 2, + // )}, chain record: ${JSON.stringify( + // unlockResults, + // null, + // 2, + // )}. provide nonce : ${payload.nonce.toString()}, chain nonce : ${nonce.toString()}`, + // ); + // } +} + +export async function signEthTx(params: collectSignaturesParams): Promise { + logger.info('signEthTx params: ', JSON.stringify(params, undefined, 2)); + + const args = minimist(process.argv.slice(2)); + const index = args.index; + const privKey = ForceBridgeCore.config.eth.multiSignKeys[index]; + + if (!('domainSeparator' in params.payload)) { + return Promise.reject(`the type of payload params is wrong`); + } + + const payload = params.payload as ethCollectSignaturesPayload; + + // Verify msg hash + const msgHash = buildSigRawData(payload.domainSeparator, payload.typeHash, payload.unlockRecords, payload.nonce); + if (params.rawData !== msgHash) { + return Promise.reject(`the rawData ${params.rawData} does not match the calculated value`); + } + + // Verify unlock records all includes correct transactions + const err = await verifyUnlockRecord(payload.unlockRecords); + if (err) { + return Promise.reject( + `the unlock records ${JSON.stringify(payload.unlockRecords, null, 2)} failed to burn tx verify.`, + ); + } + // Sign the msg hash + const { v, r, s } = ecsign(Buffer.from(params.rawData.slice(2), 'hex'), Buffer.from(privKey.slice(2), 'hex')); + const sigHex = toRpcSig(v, r, s); + + // // Save to data base + // await SigServer.signedDb.createSigned( + // payload.payload.unlockRecords.map((record) => { + // return { + // sigType: 'unlock', + // chain: ChainType.ETH, + // amount: record.amount.toString(), + // asset: record.token, + // refTxHash: record.ckbTxHash, + // txHash: payload.rawData, + // pubkey: pubkey, + // }; + // }), + // ); + return sigHex.slice(2); +} + +async function verifyUnlockRecord(unlockRecords: EthUnlockRecord[]): Promise { + for (const record of unlockRecords) { + const burnTx = await ForceBridgeCore.ckb.rpc.getTransaction(record.ckbTxHash); + if (burnTx.txStatus.status !== 'committed') { + return new Error(`burnTx:${record.ckbTxHash} status:${burnTx.txStatus.status} != committed`); + } + + const recipientData = burnTx.transaction.outputsData[0]; + const cellData = new RecipientCellData(fromHexString(recipientData).buffer); + const assetAddress = uint8ArrayToString(new Uint8Array(cellData.getAsset().raw())); + const amount = Amount.fromUInt128LE(`0x${toHexString(new Uint8Array(cellData.getAmount().raw()))}`).toString(0); + const recipientAddress = uint8ArrayToString(new Uint8Array(cellData.getRecipientAddress().raw())); + + if (assetAddress !== record.token) { + return new Error(`burnTx assetAddress:${assetAddress} != ${record.token}`); + } + if (!BigNumber.from(amount).eq(record.amount)) { + return new Error(`burnTx amount:${amount.toString()} != ${record.amount.toString()}`); + } + if (recipientAddress !== record.recipient) { + return new Error(`burnTx recipientAddress:${recipientAddress} != ${record.recipient}`); + } + + if (!(await isBurnTx(burnTx.transaction, cellData))) { + return new Error(`burnTx:${record.ckbTxHash} is invalidate burnTx`); + } + } + return null; +} + +function equalsUnlockRecord(a, b: EthUnlockRecord[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if ( + a[i].ckbTxHash !== b[i].ckbTxHash || + a[i].token !== b[i].token || + a[i].amount.toString() !== b[i].amount.toString() || + a[i].recipient !== b[i].recipient + ) { + return false; + } + } + return true; +} diff --git a/offchain-modules/packages/app-multisign-server/src/index.ts b/offchain-modules/packages/app-multisign-server/src/index.ts new file mode 100644 index 00000000..f9a3e838 --- /dev/null +++ b/offchain-modules/packages/app-multisign-server/src/index.ts @@ -0,0 +1,62 @@ +import { Config } from '@force-bridge/x/dist/config'; +import { ForceBridgeCore } from '@force-bridge/x/dist/core'; +import { collectSignaturesParams } from '@force-bridge/x/dist/multisig/multisig-mgr'; +import { logger } from '@force-bridge/x/dist/utils/logger'; +import bodyParser from 'body-parser'; +import express from 'express'; +import { JSONRPCServer } from 'json-rpc-2.0'; +import minimist from 'minimist'; +import nconf from 'nconf'; +import { createConnection } from 'typeorm'; +import { signCkbTx } from './ckbSigner'; +import { signEthTx } from './ethSigner'; +import { SigServer } from './sigServer'; +const apiPath = '/force-bridge/sign-server/api/v1'; + +async function main() { + const args = minimist(process.argv.slice(2)); + const configPath = process.env.CONFIG_PATH || './config.json'; + nconf.env().file({ file: configPath }); + const cfg: Config = nconf.get('forceBridge'); + await new ForceBridgeCore().init(cfg); + // const conn = await createConnection(); + let conn; + new SigServer(cfg, conn); + + const server = new JSONRPCServer(); + server.addMethod('signCkbTx', async (params: collectSignaturesParams) => { + return await signCkbTx(params); + }); + server.addMethod('signEthTx', async (payload: collectSignaturesParams) => { + return await signEthTx(payload); + }); + + const app = express(); + app.use(bodyParser.json()); + + app.post(apiPath, (req, res) => { + logger.info('request', req.method, req.body); + const jsonRPCRequest = req.body; + // server.receive takes a JSON-RPC request and returns a promise of a JSON-RPC response. + // Alternatively, you can use server.receiveJSON, which takes JSON string as is (in this case req.body). + server.receive(jsonRPCRequest).then((jsonRPCResponse) => { + if (jsonRPCResponse) { + res.json(jsonRPCResponse); + logger.info('response', jsonRPCResponse); + } else { + // If response is absent, it was a JSON-RPC notification method. + // Respond with no content status (204). + logger.error('response', 204); + res.sendStatus(204); + } + }); + }); + let port = 8080; + if (args.port != undefined) { + port = args.port; + } + app.listen(port); + logger.debug(`rpc server handler started on ${port} 🚀`); +} + +main(); diff --git a/offchain-modules/packages/app-multisign-server/src/sigServer.ts b/offchain-modules/packages/app-multisign-server/src/sigServer.ts new file mode 100644 index 00000000..99c846e5 --- /dev/null +++ b/offchain-modules/packages/app-multisign-server/src/sigServer.ts @@ -0,0 +1,37 @@ +import { getMultisigLock } from '@force-bridge/x/dist/ckb/tx-helper/multisig/multisig_helper'; +import { Config } from '@force-bridge/x/dist/config'; +import { ForceBridgeCore } from '@force-bridge/x/dist/core'; +import { SignedDb } from '@force-bridge/x/dist/db/signed'; +import { abi } from '@force-bridge/x/dist/xchain/eth/abi/ForceBridge.json'; +import { ethers } from 'ethers'; +import { Connection, createConnection } from 'typeorm'; + +export class SigServer { + // static config: SigServerConfig; + // static ckb: typeof CKB; + static ethProvider: ethers.providers.JsonRpcProvider; + static ethInterface: ethers.utils.Interface; + static ownLockHash: string; + static signedDb: SignedDb; + + constructor(config: Config, conn: Connection) { + // SigServer.config = cfg; + // SigServer.ckb = new CKB(cfg.ckb.ckbRpcUrl); + SigServer.ethProvider = new ethers.providers.JsonRpcProvider(config.eth.rpcUrl); + SigServer.ethInterface = new ethers.utils.Interface(abi); + // SigServer.signedDb = new SignedDb(conn); + } + + static getOwnLockHash() { + if (SigServer.ownLockHash) { + return SigServer.ownLockHash; + } + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + SigServer.ownLockHash = ForceBridgeCore.ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); + return SigServer.ownLockHash; + } +} diff --git a/offchain-modules/packages/app-multisign-server/tsconfig.build.json b/offchain-modules/packages/app-multisign-server/tsconfig.build.json new file mode 100644 index 00000000..c07bb8e3 --- /dev/null +++ b/offchain-modules/packages/app-multisign-server/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./dist" + }, + + "include": ["src"] +} diff --git a/offchain-modules/packages/app-multisign-server/tsconfig.json b/offchain-modules/packages/app-multisign-server/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/offchain-modules/packages/app-multisign-server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/offchain-modules/packages/app-rpc-server/src/handler.ts b/offchain-modules/packages/app-rpc-server/src/handler.ts index 1b2587db..667d2470 100644 --- a/offchain-modules/packages/app-rpc-server/src/handler.ts +++ b/offchain-modules/packages/app-rpc-server/src/handler.ts @@ -172,7 +172,7 @@ export class ForceBridgeAPIV1Handler implements API.ForceBridgeAPIV1 { args: fromLockscript.args, hash_type: fromLockscript.hashType, }); - const ckbTxGenerator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.indexer)); + const ckbTxGenerator = new CkbTxGenerator(ForceBridgeCore.ckb, new IndexerCollector(ForceBridgeCore.ckbIndexer)); const burnTx = await ckbTxGenerator.burn(script, payload.recipient, asset, new Amount(amount, 0)); return { network: 'Nervos', @@ -433,7 +433,7 @@ export class ForceBridgeAPIV1Handler implements API.ForceBridgeAPIV1 { hashType: ForceBridgeCore.config.ckb.deps.sudtType.script.hashType, args: value.assetIdent, }; - const collector = new IndexerCollector(ForceBridgeCore.indexer); + const collector = new IndexerCollector(ForceBridgeCore.ckbIndexer); const sudt_amount = await collector.getSUDTBalance( new Script(sudtType.codeHash, sudtType.args, sudtType.hashType), new Script(userScript.codeHash, userScript.args, userScript.hashType as HashType), diff --git a/offchain-modules/packages/scripts/src/demo.ts b/offchain-modules/packages/scripts/src/demo.ts index cd59e8e8..ab962246 100644 --- a/offchain-modules/packages/scripts/src/demo.ts +++ b/offchain-modules/packages/scripts/src/demo.ts @@ -34,6 +34,7 @@ const deploy = async () => { const contractBinLength = BigInt(lockscriptBin.length); console.log({ contractBinLength }); const { secp256k1Dep } = await ckb.loadDeps(); + console.log('secp256k1Dep', JSON.stringify(secp256k1Dep, null, 2)); const lock = { ...secp256k1Dep, args: ARGS }; nconf.set('userLockscript', lock); const cells = await ckb.loadCells({ indexer, CellCollector, lock }); diff --git a/offchain-modules/packages/scripts/src/deploy_ckb.ts b/offchain-modules/packages/scripts/src/deploy_ckb.ts index 6bbf5673..d982062d 100644 --- a/offchain-modules/packages/scripts/src/deploy_ckb.ts +++ b/offchain-modules/packages/scripts/src/deploy_ckb.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-var-requires */ +// todo: remove lumos indexer dep, use collector in packages/ckb/tx-helper/collector import { promises as fs } from 'fs'; import path from 'path'; import { Asset, BtcAsset, ChainType, EosAsset, EthAsset, TronAsset } from '@force-bridge/x/dist/ckb/model/asset'; import { IndexerCollector } from '@force-bridge/x/dist/ckb/tx-helper/collector'; import { CkbTxGenerator } from '@force-bridge/x/dist/ckb/tx-helper/generator'; import { CkbIndexer } from '@force-bridge/x/dist/ckb/tx-helper/indexer'; +import { generateTypeIDScript } from '@force-bridge/x/dist/ckb/tx-helper/multisig/typeid'; import { asyncSleep as sleep, blake2b } from '@force-bridge/x/dist/utils'; import { OutPoint, Script } from '@lay2/pw-core'; import RawTransactionParams from '@nervosnetwork/ckb-sdk-core'; @@ -17,10 +19,10 @@ const configPath = './config.json'; nconf.env().file({ file: configPath }); const CKB_URL = nconf.get('forceBridge:ckb:ckbRpcUrl'); const CKB_IndexerURL = nconf.get('forceBridge:ckb:ckbIndexerUrl'); -const PRI_KEY = nconf.get('forceBridge:ckb:privateKey'); +const PRI_KEY = nconf.get('forceBridge:ckb:fromPrivateKey'); const ckb = new RawTransactionParams(CKB_URL); const ckbIndexer = new CkbIndexer(CKB_URL, CKB_IndexerURL); -const generator = new CkbTxGenerator(ckb, new IndexerCollector(ckbIndexer)); +// const generator = new CkbTxGenerator(ckb, new IndexerCollector(ckbIndexer)); const PUB_KEY = ckb.utils.privateKeyToPublicKey(PRI_KEY); const ARGS = `0x${ckb.utils.blake160(PUB_KEY, 'hex')}`; const ADDRESS = ckb.utils.pubkeyToAddress(PUB_KEY); @@ -89,29 +91,30 @@ function getPreDeployedAssets() { ]; } -async function createBridgeCell(assets: Asset[]) { - const { secp256k1Dep } = await ckb.loadDeps(); - - const lockscript = Script.fromRPC({ - code_hash: secp256k1Dep.codeHash, - args: ARGS, - hash_type: secp256k1Dep.hashType, - }); - - const bridgeLockScripts = []; - for (const asset of assets) { - bridgeLockScripts.push({ - codeHash: nconf.get('forceBridge:ckb:deps:bridgeLock:script:codeHash'), - hashType: 'data', - args: asset.toBridgeLockscriptArgs(), - }); - } - const rawTx = await generator.createBridgeCell(lockscript, bridgeLockScripts); - const signedTx = ckb.signTransaction(PRI_KEY)(rawTx); - const tx_hash = await ckb.rpc.sendTransaction(signedTx); - const txStatus = await waitUntilCommitted(tx_hash); - console.log('pre deploy assets tx status', txStatus); -} +// async function createBridgeCell(assets: Asset[]) { +// const { secp256k1Dep } = await ckb.loadDeps(); +// +// const lockscript = Script.fromRPC({ +// code_hash: secp256k1Dep.codeHash, +// args: ARGS, +// hash_type: secp256k1Dep.hashType, +// }); +// const indexer = new Indexer(ForceBridgeCore.config.ckb.ckbRpcUrl, 'deploy_lumos/'); +// indexer.startForever(); +// let bridgeLockScripts = []; +// for (const asset of assets) { +// bridgeLockScripts.push({ +// codeHash: nconf.get('forceBridge:ckb:deps:bridgeLock:script:codeHash'), +// hashType: 'data', +// args: asset.toBridgeLockscriptArgs(), +// }); +// } +// const rawTx = await generator.createBridgeCell(bridgeLockScripts, indexer); +// const signedTx = ckb.signTransaction(PRI_KEY)(rawTx); +// const tx_hash = await ckb.rpc.sendTransaction(signedTx); +// const txStatus = await waitUntilCommitted(tx_hash); +// console.log('pre deploy assets tx status', txStatus); +// } const deploy = async () => { const lockscriptBin = await fs.readFile(PATH_BRIDGE_LOCKSCRIPT); @@ -143,7 +146,7 @@ const deploy = async () => { const rawTx = ckb.generateRawTransaction({ fromAddress: ADDRESS, toAddress: ADDRESS, - capacity: (contractBinLength + 100n) * 10n ** 8n, + capacity: (contractBinLength + 200n) * 10n ** 8n, fee: 10000000n, safeMode: true, cells: emptyCells, @@ -151,14 +154,14 @@ const deploy = async () => { deps: secp256k1Dep, }); // add sudt - const sudtCodeCellCapacity = (BigInt(sudtBin.length) + 100n) * 10n ** 8n; + const sudtCodeCellCapacity = (BigInt(sudtBin.length) + 200n) * 10n ** 8n; rawTx.outputs.push({ ...rawTx.outputs[0], capacity: `0x${sudtCodeCellCapacity.toString(16)}`, }); rawTx.outputsData.push(utils.bytesToHex(sudtBin)); // add recipient typescript - const recipientTypescriptCodeCellCapacity = (BigInt(recipientTypescriptBin.length) + 100n) * 10n ** 8n; + const recipientTypescriptCodeCellCapacity = (BigInt(recipientTypescriptBin.length) + 200n) * 10n ** 8n; rawTx.outputs.push({ ...rawTx.outputs[0], capacity: `0x${recipientTypescriptCodeCellCapacity.toString(16)}`, @@ -182,7 +185,25 @@ const deploy = async () => { // modify change cell const changeCellCap = BigInt(rawTx.outputs[1].capacity) - sudtCodeCellCapacity - recipientTypescriptCodeCellCapacity; rawTx.outputs[1].capacity = `0x${changeCellCap.toString(16)}`; - // console.dir({ rawTx }, { depth: null }); + const firstInput = { + previous_output: { + tx_hash: rawTx.inputs[0].previousOutput.txHash, + index: rawTx.inputs[0].previousOutput.index, + }, + since: '0x0', + }; + + for (let i = 0; i < rawTx.outputs.length; i++) { + if (i != 1) { + const typeIDScript = generateTypeIDScript(firstInput, `0x${i}`); + rawTx.outputs[i].type = { + codeHash: typeIDScript.code_hash, + hashType: typeIDScript.hash_type, + args: typeIDScript.args, + }; + } + } + console.dir({ rawTx }, { depth: null }); // return const signedTx = ckb.signTransaction(PRI_KEY)(rawTx); @@ -284,9 +305,9 @@ const main = async () => { await setStartTime(); await setOwnerLockHash(); - const assets = getPreDeployedAssets(); + // const assets = getPreDeployedAssets(); await ckbIndexer.waitUntilSync(); - await createBridgeCell(assets); + // await createBridgeCell(assets); await setXChainStartTime(); diff --git a/offchain-modules/packages/scripts/src/integration-test/btc.ts b/offchain-modules/packages/scripts/src/integration-test/btc.ts index 9675ab36..bb1ecf07 100644 --- a/offchain-modules/packages/scripts/src/integration-test/btc.ts +++ b/offchain-modules/packages/scripts/src/integration-test/btc.ts @@ -4,6 +4,7 @@ import { BtcAsset, ChainType } from '@force-bridge/x/dist/ckb/model/asset'; import { IndexerCollector } from '@force-bridge/x/dist/ckb/tx-helper/collector'; import { CkbTxGenerator } from '@force-bridge/x/dist/ckb/tx-helper/generator'; import { CkbIndexer } from '@force-bridge/x/dist/ckb/tx-helper/indexer'; +import { getMultisigLock } from '@force-bridge/x/dist/ckb/tx-helper/multisig/multisig_helper'; import { Config } from '@force-bridge/x/dist/config'; import { ForceBridgeCore } from '@force-bridge/x/dist/core'; import { BtcDb } from '@force-bridge/x/dist/db/btc'; @@ -43,9 +44,7 @@ async function main() { // init bridge force core await new ForceBridgeCore().init(config); - - logger.debug(`config: ${config}`); - const PRI_KEY = ForceBridgeCore.config.ckb.privateKey; + const PRI_KEY = ForceBridgeCore.config.ckb.fromPrivateKey; const client = new RPCClient(config.btc.clientParams); const btcChain = new BTCChain(); @@ -128,7 +127,12 @@ async function main() { // check sudt balance. const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const ownLockHash = ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const asset = new BtcAsset('btc', ownLockHash); const bridgeCellLockscript = { codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, @@ -157,8 +161,6 @@ async function main() { ); const burnAmount = new Amount('100000', 0); - // const account = new Account(PRI_KEY); - // const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); const generator = new CkbTxGenerator(ckb, new IndexerCollector(indexer)); const burnTx = await generator.burn( await account.getLockscript(), diff --git a/offchain-modules/packages/scripts/src/integration-test/eos.ts b/offchain-modules/packages/scripts/src/integration-test/eos.ts index 76c8119d..050f21b3 100644 --- a/offchain-modules/packages/scripts/src/integration-test/eos.ts +++ b/offchain-modules/packages/scripts/src/integration-test/eos.ts @@ -5,6 +5,7 @@ import { IndexerCollector } from '@force-bridge/x/dist/ckb/tx-helper/collector'; import { CkbTxGenerator } from '@force-bridge/x/dist/ckb/tx-helper/generator'; import { CkbIndexer } from '@force-bridge/x/dist/ckb/tx-helper/indexer'; +import { getMultisigLock } from '@force-bridge/x/dist/ckb/tx-helper/multisig/multisig_helper'; import { Config, EosConfig } from '@force-bridge/x/dist/config'; import { ForceBridgeCore } from '@force-bridge/x/dist/core'; import { CkbMint } from '@force-bridge/x/dist/db/entity/CkbMint'; @@ -38,7 +39,7 @@ async function main() { await new ForceBridgeCore().init(conf); const rpcUrl = config.rpcUrl; - const PRI_KEY = ForceBridgeCore.config.ckb.privateKey; + const PRI_KEY = ForceBridgeCore.config.ckb.fromPrivateKey; const lockAccount = 'alice'; const lockAccountPri = ['5KQG4541B1FtDC11gu3NrErWniqTaPHBpmikSztnX8m36sK5px5']; const chain = new EosChain(rpcUrl, new JsSignatureProvider(lockAccountPri)); @@ -115,7 +116,12 @@ async function main() { // check sudt balance. const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const ownLockHash = ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const asset = new EosAsset(lockAsset, ownLockHash); const bridgeCellLockscript = { codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, diff --git a/offchain-modules/packages/scripts/src/integration-test/eth.ts b/offchain-modules/packages/scripts/src/integration-test/eth.ts index a05cf789..8323a433 100644 --- a/offchain-modules/packages/scripts/src/integration-test/eth.ts +++ b/offchain-modules/packages/scripts/src/integration-test/eth.ts @@ -5,6 +5,7 @@ import { IndexerCollector } from '@force-bridge/x/dist/ckb/tx-helper/collector'; import { CkbTxGenerator } from '@force-bridge/x/dist/ckb/tx-helper/generator'; // import {CkbIndexer} from "@force-bridge/x/dist/ckb/tx-helper/indexer"; import { CkbIndexer } from '@force-bridge/x/dist/ckb/tx-helper/indexer'; +import { getMultisigLock } from '@force-bridge/x/dist/ckb/tx-helper/multisig/multisig_helper'; import { Config, EthConfig } from '@force-bridge/x/dist/config'; import { ForceBridgeCore } from '@force-bridge/x/dist/core'; import { CkbDb, EthDb } from '@force-bridge/x/dist/db'; @@ -108,7 +109,12 @@ async function main() { // check sudt balance. const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const ownLockHash = ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const asset = new EthAsset('0x0000000000000000000000000000000000000000', ownLockHash); const bridgeCellLockscript = { codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, @@ -136,7 +142,6 @@ async function main() { const burnAmount = ethers.utils.parseEther('0.01'); if (!sendBurn) { const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); const generator = new CkbTxGenerator(ckb, new IndexerCollector(indexer)); const burnTx = await generator.burn( await account.getLockscript(), diff --git a/offchain-modules/packages/scripts/src/integration-test/tron.ts b/offchain-modules/packages/scripts/src/integration-test/tron.ts index 395e0e50..39c4ac2c 100644 --- a/offchain-modules/packages/scripts/src/integration-test/tron.ts +++ b/offchain-modules/packages/scripts/src/integration-test/tron.ts @@ -4,6 +4,7 @@ import { ChainType, TronAsset } from '@force-bridge/x/dist/ckb/model/asset'; import { IndexerCollector } from '@force-bridge/x/dist/ckb/tx-helper/collector'; import { CkbTxGenerator } from '@force-bridge/x/dist/ckb/tx-helper/generator'; import { CkbIndexer } from '@force-bridge/x/dist/ckb/tx-helper/indexer'; +import { getMultisigLock } from '@force-bridge/x/dist/ckb/tx-helper/multisig/multisig_helper'; import { Config, TronConfig } from '@force-bridge/x/dist/config'; import { ForceBridgeCore } from '@force-bridge/x/dist/core'; import { CkbMint, TronLock, TronUnlock } from '@force-bridge/x/dist/db/model'; @@ -115,7 +116,12 @@ async function main() { const getBalance = async (assetName) => { const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const ownLockHash = ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const asset = new TronAsset(assetName, ownLockHash); const bridgeCellLockscript = { codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, @@ -182,7 +188,12 @@ async function main() { const burnAmount = 1; if (!sendBurn) { const account = new Account(PRI_KEY); - const ownLockHash = ckb.utils.scriptToHash(await account.getLockscript()); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const ownLockHash = ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const generator = new CkbTxGenerator(ckb, new IndexerCollector(indexer)); const burnTx = await generator.burn( await account.getLockscript(), diff --git a/offchain-modules/packages/scripts/src/integration-test/util.ts b/offchain-modules/packages/scripts/src/integration-test/util.ts index c6007af8..7a9f2cba 100644 --- a/offchain-modules/packages/scripts/src/integration-test/util.ts +++ b/offchain-modules/packages/scripts/src/integration-test/util.ts @@ -20,14 +20,19 @@ export async function waitUntilCommitted(ckb, txHash, timeout) { let waitTime = 0; while (true) { const txStatus = await ckb.rpc.getTransaction(txHash); - logger.debug(`tx ${txHash} status: ${txStatus.txStatus.status}, index: ${waitTime}`); - if (txStatus.txStatus.status === 'committed') { - return txStatus; - } - await asyncSleep(1000); - waitTime += 1; - if (waitTime >= timeout) { - return txStatus; + if (txStatus != undefined) { + logger.debug(`tx ${txHash} status: ${txStatus.txStatus.status}, index: ${waitTime}`); + if (txStatus.txStatus.status === 'committed') { + return txStatus; + } + await asyncSleep(1000); + waitTime += 1; + if (waitTime >= timeout) { + return txStatus; + } + } else { + logger.error('failed to call ckb rpc getTransaction'); + await asyncSleep(1000); } } } diff --git a/offchain-modules/packages/scripts/src/test.ts b/offchain-modules/packages/scripts/src/test.ts index a4e76e29..7975918b 100644 --- a/offchain-modules/packages/scripts/src/test.ts +++ b/offchain-modules/packages/scripts/src/test.ts @@ -16,6 +16,10 @@ async function main() { // logger.debug('address', account.address); // const pw = await new PWCore(CKB_URL).init(); // const indexer = new CkbIndexer(CKB_INDEXER_URL, CKB_URL); + // const ckbRpc = new RPC('http://127.0.0.1:8114'); + // // const tx = await ckbRpc.get_transaction('0x3e41080968db4f2b4db5d546ffb886a2e46a4444c57b0653563482de84d6da59'); + // const tx = await ckbRpc.get_live_cell({tx_hash: '0x3e41080968db4f2b4db5d546ffb886a2e46a4444c57b0653563482de84d6da59', index: '0x0'}, false); + // console.dir(tx, {depth: null}) // const collector = new IndexerCollector(indexer); // const generator = new CkbTxGenerator(collector); // const bb = BigNumber.from('0.0001'); diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/generator.ts b/offchain-modules/packages/x/src/ckb/tx-helper/generator.ts index 2de135f3..58dd367a 100644 --- a/offchain-modules/packages/x/src/ckb/tx-helper/generator.ts +++ b/offchain-modules/packages/x/src/ckb/tx-helper/generator.ts @@ -1,13 +1,17 @@ -import { Script as LumosScript } from '@ckb-lumos/base'; +import { Cell, Script as LumosScript } from '@ckb-lumos/base'; +import { common } from '@ckb-lumos/common-scripts'; +import { TransactionSkeleton, TransactionSkeletonType } from '@ckb-lumos/helpers'; +import { CellCollector, Indexer } from '@ckb-lumos/indexer'; import { Address, Amount, Script } from '@lay2/pw-core'; import CKB from '@nervosnetwork/ckb-sdk-core'; import { IndexerCollector } from '../../ckb/tx-helper/collector'; import { SerializeRecipientCellData } from '../../ckb/tx-helper/generated/eth_recipient_cell'; import { ScriptType } from '../../ckb/tx-helper/indexer'; import { ForceBridgeCore } from '../../core'; -import { bigintToSudtAmount, fromHexString, stringToUint8Array, toHexString } from '../../utils'; +import { asyncSleep, bigintToSudtAmount, fromHexString, stringToUint8Array, toHexString } from '../../utils'; import { logger } from '../../utils/logger'; import { Asset } from '../model/asset'; +import { getFromAddr, getMultisigLock } from './multisig/multisig_helper'; export interface MintAssetRecord { asset: Asset; @@ -18,58 +22,120 @@ export interface MintAssetRecord { export class CkbTxGenerator { constructor(private ckb: CKB, private collector: IndexerCollector) {} - async createBridgeCell( - fromLockscript: Script, - bridgeLockscripts: any[], - ): Promise { - logger.debug('createBredgeCell:', bridgeLockscripts); + sudtDep = { + out_point: { + tx_hash: ForceBridgeCore.config.ckb.deps.sudtType.cellDep.outPoint.txHash, + index: ForceBridgeCore.config.ckb.deps.sudtType.cellDep.outPoint.index, + }, + dep_type: ForceBridgeCore.config.ckb.deps.sudtType.cellDep.depType, + }; + + bridgeLockDep = { + out_point: { + tx_hash: ForceBridgeCore.config.ckb.deps.bridgeLock.cellDep.outPoint.txHash, + index: ForceBridgeCore.config.ckb.deps.bridgeLock.cellDep.outPoint.index, + }, + dep_type: ForceBridgeCore.config.ckb.deps.bridgeLock.cellDep.depType, + }; + + async fetchMultisigCell(indexer: Indexer, maxTimes: number): Promise { + const cellCollector = new CellCollector(indexer, { + type: ForceBridgeCore.config.ckb.multisigType, + }); + let index = 0; + while (true) { + if (index > maxTimes) { + throw new Error('failed to fetch multisig cell.'); + } + for await (const cell of cellCollector.collect()) { + if (cell != undefined) { + return cell; + } + } + logger.debug('try to fetch multisig cell: ', index++); + await asyncSleep(1000); + } + } + + async fetchBridgeCell(bridgeLock: LumosScript, indexer: Indexer, maxTimes: number): Promise { + const cellCollector = new CellCollector(indexer, { + lock: bridgeLock, + }); + let index = 0; + while (true) { + if (index > maxTimes) { + throw new Error('failed to fetch bridge cell.'); + } + for await (const cell of cellCollector.collect()) { + if (cell != undefined) { + return cell; + } + } + logger.debug('try to fetch bridge cell: ', index++); + await asyncSleep(1000); + } + } + + async createBridgeCell(scripts: Script[], indexer: Indexer): Promise { + const fromAddress = getFromAddr(); + let txSkeleton = TransactionSkeleton({ cellProvider: indexer }); + const multisig_cell = await this.fetchMultisigCell(indexer, 60); + txSkeleton = await common.setupInputCell(txSkeleton, multisig_cell, ForceBridgeCore.config.ckb.multisigScript); const bridgeCellCapacity = 200n * 10n ** 8n; - const outputsData = []; - const outputBridgeCells = bridgeLockscripts.map((s) => { - outputsData.push('0x'); - return { - lock: s, - capacity: `0x${bridgeCellCapacity.toString(16)}`, + const bridgeOutputs = scripts.map((script) => { + return { + cell_output: { + capacity: `0x${bridgeCellCapacity.toString(16)}`, + lock: { + code_hash: script.codeHash, + hash_type: script.hashType, + args: script.args, + }, + }, + data: '0x', }; }); - let outputs = new Array(0); - outputs = outputs.concat(outputBridgeCells); - const fee = 100000n; - const needSupplyCap = bridgeCellCapacity * BigInt(bridgeLockscripts.length) + fee; - const supplyCapCells = await this.collector.getCellsByLockscriptAndCapacity( - fromLockscript, - new Amount(`0x${needSupplyCap.toString(16)}`, 0), - ); - const inputs = supplyCapCells.map((cell) => { - return { previousOutput: cell.outPoint, since: '0x0' }; + logger.debug('bridgeOutputs:', JSON.stringify(bridgeOutputs, null, 2)); + txSkeleton = txSkeleton.update('outputs', (outputs) => { + return outputs.push(...bridgeOutputs); }); - this.handleChangeCell(supplyCapCells, outputs, outputsData, fromLockscript, fee); - const { secp256k1Dep } = await this.ckb.loadDeps(); - const rawTx = { - version: '0x0', - cellDeps: [ - { - outPoint: secp256k1Dep.outPoint, - depType: secp256k1Dep.depType, - }, - ], - headerDeps: [], - inputs, - outputs, - witnesses: [{ lock: '', inputType: '', outputType: '' }], - outputsData, - }; - logger.debug('createBridgeCell rawTx:', rawTx); - return rawTx as CKBComponents.RawTransactionToSign; + const needCapacity = bridgeCellCapacity * BigInt(scripts.length); + if (needCapacity !== 0n) { + txSkeleton = await common.injectCapacity(txSkeleton, [fromAddress], needCapacity); + } + const feeRate = BigInt(1000); + txSkeleton = await common.payFeeByFeeRate(txSkeleton, [fromAddress], feeRate); + txSkeleton = common.prepareSigningEntries(txSkeleton); + return txSkeleton; } - async mint(userLockscript: Script, records: MintAssetRecord[]): Promise { - logger.debug('start to mint records: ', records); - const bridgeCells = new Array(0); - const outputs = new Array(0); - const outputsData = new Array(0); + async mint(records: MintAssetRecord[], indexer: Indexer): Promise { + const fromAddress = getFromAddr(); + let txSkeleton = TransactionSkeleton({ cellProvider: indexer }); + const multisigCell = await this.fetchMultisigCell(indexer, 60); + txSkeleton = await common.setupInputCell(txSkeleton, multisigCell, ForceBridgeCore.config.ckb.multisigScript); + txSkeleton = txSkeleton.update('cellDeps', (cellDeps) => { + return cellDeps.push(this.sudtDep); + }); + txSkeleton = txSkeleton.update('cellDeps', (cellDeps) => { + return cellDeps.push(this.bridgeLockDep); + }); + + txSkeleton = await this.buildSudtOutput(txSkeleton, records); + txSkeleton = await this.buildBridgeCellOutput(txSkeleton, records, indexer); + + const feeRate = BigInt(1000); + txSkeleton = await common.payFeeByFeeRate(txSkeleton, [fromAddress], feeRate); + txSkeleton = common.prepareSigningEntries(txSkeleton); + return txSkeleton; + } + + async buildSudtOutput( + txSkeleton: TransactionSkeletonType, + records: MintAssetRecord[], + ): Promise { + const fromAddress = getFromAddr(); const sudtCellCapacity = 300n * 10n ** 8n; - const assets = new Array(0); for (const record of records) { if (record.amount.eq(Amount.ZERO)) { continue; @@ -80,88 +146,83 @@ export class CkbTxGenerator { hashType: ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType, args: record.asset.toBridgeLockscriptArgs(), }; - const sudtArgs = this.ckb.utils.scriptToHash(bridgeCellLockscript); - const outputSudtCell = { - lock: recipientLockscript, - type: { - codeHash: ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash, - hashType: ForceBridgeCore.config.ckb.deps.sudtType.script.hashType, - args: sudtArgs, + const outputSudtCell = { + cell_output: { + capacity: `0x${sudtCellCapacity.toString(16)}`, + lock: { + code_hash: recipientLockscript.codeHash, + hash_type: recipientLockscript.hashType, + args: recipientLockscript.args, + }, + type: { + code_hash: ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash, + hash_type: ForceBridgeCore.config.ckb.deps.sudtType.script.hashType, + args: sudtArgs, + }, }, - capacity: `0x${sudtCellCapacity.toString(16)}`, + data: record.amount.toUInt128LE(), }; - outputs.push(outputSudtCell); - outputsData.push(record.amount.toUInt128LE()); + txSkeleton = txSkeleton.update('outputs', (outputs) => { + return outputs.push(outputSudtCell); + }); + } + for (let i = 1; i <= records.length; i++) { + txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { + return fixedEntries.push({ + field: 'outputs', + index: i, + }); + }); + } + const needCapacity = sudtCellCapacity * BigInt(records.length); + if (needCapacity !== 0n) { + txSkeleton = await common.injectCapacity(txSkeleton, [fromAddress], needCapacity); + } + return txSkeleton; + } + async buildBridgeCellOutput( + txSkeleton: TransactionSkeletonType, + records: MintAssetRecord[], + indexer: Indexer, + ): Promise { + const assets = new Array(0); + for (const record of records) { + const bridgeCellLockscript = { + codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, + hashType: ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType, + args: record.asset.toBridgeLockscriptArgs(), + }; if (assets.indexOf(record.asset.toBridgeLockscriptArgs()) != -1) { continue; } assets.push(record.asset.toBridgeLockscriptArgs()); - - const searchKey = { - script: new Script( - bridgeCellLockscript.codeHash, - bridgeCellLockscript.args, - bridgeCellLockscript.hashType, - ).serializeJson() as LumosScript, - script_type: ScriptType.lock, - }; - const cells = await this.collector.indexer.getCells(searchKey); - if (cells.length == 0) { - throw new Error('failed to generate mint tx. the live cell is not found!'); - } - const bridgeCell = cells[0]; - const outputBridgeCell = { - lock: bridgeCellLockscript, - capacity: bridgeCell.capacity, + const bridge_cell = await this.fetchBridgeCell( + { + code_hash: bridgeCellLockscript.codeHash, + hash_type: bridgeCellLockscript.hashType, + args: bridgeCellLockscript.args, + }, + indexer, + 5, + ); + txSkeleton = txSkeleton.update('inputs', (inputs) => { + return inputs.push(bridge_cell); + }); + const outputBridgeCell = { + cell_output: { + capacity: bridge_cell.cell_output.capacity, + lock: bridge_cell.cell_output.lock, + type: bridge_cell.cell_output.type, + }, + data: '0x', }; - outputs.push(outputBridgeCell); - outputsData.push('0x'); - bridgeCells.push(bridgeCell); + txSkeleton = txSkeleton.update('outputs', (outputs) => { + return outputs.push(outputBridgeCell); + }); } - - const fee = 100000n; - const needSupplyCap = sudtCellCapacity * BigInt(records.length) + fee; - const supplyCapCells = await this.collector.getCellsByLockscriptAndCapacity( - userLockscript, - new Amount(`0x${needSupplyCap.toString(16)}`, 0), - ); - const inputCells = supplyCapCells.concat(bridgeCells); - const inputs = inputCells.map((cell) => { - return { previousOutput: cell.outPoint, since: '0x0' }; - }); - this.handleChangeCell(inputCells, outputs, outputsData, userLockscript, fee); - - const { secp256k1Dep } = await this.ckb.loadDeps(); - const cellDeps = [ - { - outPoint: secp256k1Dep.outPoint, - depType: secp256k1Dep.depType, - }, - // sudt dep - { - outPoint: ForceBridgeCore.config.ckb.deps.sudtType.cellDep.outPoint, - depType: ForceBridgeCore.config.ckb.deps.sudtType.cellDep.depType, - }, - // bridge lockscript dep - { - outPoint: ForceBridgeCore.config.ckb.deps.bridgeLock.cellDep.outPoint, - depType: ForceBridgeCore.config.ckb.deps.bridgeLock.cellDep.depType, - }, - ]; - - const rawTx = { - version: '0x0', - cellDeps, - headerDeps: [], - inputs, - outputs, - witnesses: [{ lock: '', inputType: '', outputType: '' }], - outputsData, - }; - logger.debug('generate mint rawTx:', rawTx); - return rawTx as CKBComponents.RawTransactionToSign; + return txSkeleton; } /* @@ -182,6 +243,7 @@ export class CkbTxGenerator { amount: Amount, bridgeFee?: Amount, ): Promise { + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); if (amount.eq(Amount.ZERO)) { throw new Error('amount should larger then zero!'); } @@ -208,7 +270,11 @@ export class CkbTxGenerator { } logger.debug('burn sudtCells: ', sudtCells); let inputCells = sudtCells; - const ownerLockHash = ForceBridgeCore.config.ckb.ownerLockHash; + const ownerLockHash = this.ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); const recipientAddr = fromHexString(toHexString(stringToUint8Array(recipientAddress))).buffer; @@ -305,7 +371,6 @@ export class CkbTxGenerator { depType: ForceBridgeCore.config.ckb.deps.recipientType.cellDep.depType, }, ]; - const rawTx = { version: '0x0', cellDeps, diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/multisig/config.json b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/config.json new file mode 100644 index 00000000..4e2cc047 --- /dev/null +++ b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/config.json @@ -0,0 +1,50 @@ +{ + "__COMMENT__": "It's a devnet config, different devnet has different config. Set env.LUMOS_CONFIG_FILE to this config file (or your own config file) and call `initializeConfig()`, see ./lock.ts#L349-L350 as an example.", + "PREFIX": "ckt", + "SCRIPTS": { + "SECP256K1_BLAKE160": { + "CODE_HASH": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "HASH_TYPE": "type", + "TX_HASH": "0xa777fd1964ffa98a7b0b6c09ff71691705d84d5ed1badfb14271a3a870bdd06b", + "INDEX": "0x0", + "DEP_TYPE": "dep_group", + "SHORT_ID": 0 + }, + "SECP256K1_BLAKE160_MULTISIG": { + "CODE_HASH": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "HASH_TYPE": "type", + "TX_HASH": "0xa777fd1964ffa98a7b0b6c09ff71691705d84d5ed1badfb14271a3a870bdd06b", + "INDEX": "0x1", + "DEP_TYPE": "dep_group", + "SHORT_ID": 1 + }, + "DAO": { + "CODE_HASH": "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + "HASH_TYPE": "type", + "TX_HASH": "0x13c137fdf071c0ab3e6a4c8aaefc16c9bb7b9593b77822b151b18412ecd2ee41", + "INDEX": "0x2", + "DEP_TYPE": "code" + }, + "SUDT": { + "CODE_HASH": "0x48dbf59b4c7ee1547238021b4869bceedf4eea6b43772e5d66ef8865b6ae7212", + "HASH_TYPE": "data", + "TX_HASH": "0x473a05da909427e2b77d3c4226d29a15539b12d9ddf7d4fda4de8a76df18555c", + "INDEX": "0x0", + "DEP_TYPE": "code" + }, + "ANYONE_CAN_PAY": { + "CODE_HASH": "0x636be2afb13c1d0f3478c22c6572b12dd13bd492f648302d5682fffe7e7efdaa", + "HASH_TYPE": "type", + "TX_HASH": "0xc520c895e20ade5b6b0bd90598f8b205afefdc316e38e87fac239ca3f0829378", + "INDEX": "0x0", + "DEP_TYPE": "dep_group" + }, + "PW_LOCK": { + "CODE_HASH": "0xabd63bf842d098c568f32b53191106649a84817288fd0116d4f3ed1b88f0f7e2", + "HASH_TYPE": "type", + "TX_HASH": "0x7441b129b99b4c6ced2016d9b44fcb88544510b426fcdcbf11e9f3c9d0a338dc", + "INDEX": "0x0", + "DEP_TYPE": "dep_group" + } + } +} diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/multisig/deploy.ts b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/deploy.ts new file mode 100644 index 00000000..0628fc77 --- /dev/null +++ b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/deploy.ts @@ -0,0 +1,138 @@ +import { HashType } from '@ckb-lumos/base'; +import { common } from '@ckb-lumos/common-scripts'; +import { key } from '@ckb-lumos/hd'; +import { TransactionSkeleton, sealTransaction, parseAddress, minimalCellCapacity } from '@ckb-lumos/helpers'; +import { Indexer } from '@ckb-lumos/indexer'; +import { RPC } from '@ckb-lumos/rpc'; +import TransactionManager from '@ckb-lumos/transaction-manager'; +import CKB from '@nervosnetwork/ckb-sdk-core'; +import nconf from 'nconf'; +import { Config } from '../../../config'; +import { ForceBridgeCore } from '../../../core'; +import { asyncSleep as sleep } from '../../../utils'; +import { init } from './init_config'; +import { getFromAddr, getMultisigAddr, getMultisigLock } from './multisig_helper'; +import { generateTypeIDScript } from './typeid'; + +const CKB_URL = process.env.CKB_URL || 'http://127.0.0.1:8114'; +init(); + +const acpData = '0x'; +const ckb = new CKB(CKB_URL); +const dataDir = './lumos_db'; +const indexer = new Indexer(CKB_URL, dataDir); +indexer.startForever(); +const transactionManager = new TransactionManager(indexer); + +function getDataOutputCapacity() { + const output = { + cell_output: { + lock: parseAddress(getMultisigAddr(ForceBridgeCore.config.ckb.multisigScript)), + type: { + code_hash: '0x' + '0'.repeat(64), + hash_type: 'type' as HashType, + args: '0x' + '0'.repeat(64), + }, + capacity: '0x0', + }, + data: acpData, + }; + + const min = minimalCellCapacity(output); + return min; +} + +async function deploy() { + const fromPrivateKey = ForceBridgeCore.config.ckb.fromPrivateKey; + const fromAddress = getFromAddr(); + const multisigLockScript = getMultisigLock(ForceBridgeCore.config.ckb.multisigScript); + const multisigAddress = getMultisigAddr(ForceBridgeCore.config.ckb.multisigScript); + + let txSkeleton = TransactionSkeleton({ cellProvider: indexer }); + const capacity = getDataOutputCapacity(); + txSkeleton = await common.transfer(txSkeleton, [fromAddress], multisigAddress, capacity); + const firstOutput = txSkeleton.get('outputs').get(0); + firstOutput.data = acpData; + const firstInput = { + previous_output: txSkeleton.get('inputs').get(0).out_point, + since: '0x0', + }; + const typeIDScript = generateTypeIDScript(firstInput, '0x0'); + firstOutput.cell_output.type = typeIDScript; + txSkeleton = txSkeleton.update('outputs', (outputs) => { + return outputs.set(0, firstOutput); + }); + const feeRate = 1000n; + txSkeleton = await common.payFeeByFeeRate(txSkeleton, [fromAddress], feeRate); + txSkeleton = common.prepareSigningEntries(txSkeleton); + const message = txSkeleton.get('signingEntries').get(0).message; + const content = key.signRecoverable(message, fromPrivateKey); + + const tx = sealTransaction(txSkeleton, [content]); + console.log('tx:', JSON.stringify(tx, null, 2)); + const txHash = await transactionManager.send_transaction(tx); + await waitUntilCommitted(ckb, txHash, 60); + + nconf.set('forceBridge:ckb:multisigType', typeIDScript); + nconf.save(); + + console.log('multi lockscript:', JSON.stringify(multisigLockScript, null, 2)); + process.exit(0); +} + +async function waitUntilCommitted(ckb, txHash, timeout) { + let waitTime = 0; + while (true) { + const txStatus = await ckb.rpc.getTransaction(txHash); + console.log(`tx ${txHash} status: ${txStatus.txStatus.status}, index: ${waitTime}`); + if (txStatus.txStatus.status === 'committed') { + return txStatus; + } + await asyncSleep(1000); + waitTime += 1; + if (waitTime >= timeout) { + return txStatus; + } + } +} + +async function waitUntilSync(): Promise { + const ckbRpc = new RPC(CKB_URL); + const rpcTipNumber = parseInt((await ckbRpc.get_tip_header()).number, 16); + console.log('rpcTipNumber', rpcTipNumber); + const index = 0; + while (true) { + const tip = await indexer.tip(); + console.log('tip', tip); + if (tip == undefined) { + await sleep(1000); + continue; + } + const indexerTipNumber = parseInt((await indexer.tip()).block_number, 16); + console.log('indexerTipNumber', indexerTipNumber); + if (indexerTipNumber >= rpcTipNumber) { + return; + } + console.log(`wait until indexer sync. index: ${index}`); + await sleep(1000); + } +} + +function asyncSleep(ms = 0) { + return new Promise((r) => setTimeout(r, ms)); +} + +const main = async () => { + console.log('\n\n\n---------start init multisig address -----------\n'); + await waitUntilSync(); + const configPath = './config.json'; + nconf.env().file({ file: configPath }); + const config: Config = nconf.get('forceBridge'); + console.log('config: ', config); + await new ForceBridgeCore().init(config); + await deploy(); + console.log('\n\n\n---------end init multisig address -----------\n'); + process.exit(0); +}; + +main(); diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/multisig/init_config.ts b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/init_config.ts new file mode 100644 index 00000000..c429e4d3 --- /dev/null +++ b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/init_config.ts @@ -0,0 +1,14 @@ +import { existsSync } from 'fs'; +import { initializeConfig } from '@ckb-lumos/config-manager'; + +export function init() { + const configFilePath = __dirname + '/config.json'; + if ( + process.env.LUMOS_CONFIG_NAME !== 'LINA' && + process.env.LUMOS_CONFIG_NAME !== 'AGGRON4' && + existsSync(configFilePath) + ) { + process.env.LUMOS_CONFIG_FILE = configFilePath; + } + initializeConfig(); +} diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/multisig/multisig_helper.ts b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/multisig_helper.ts new file mode 100644 index 00000000..eb560ed5 --- /dev/null +++ b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/multisig_helper.ts @@ -0,0 +1,55 @@ +import { HexString } from '@ckb-lumos/base'; +import { serializeMultisigScript, multisigArgs } from '@ckb-lumos/common-scripts/lib/from_info'; +import { getConfig } from '@ckb-lumos/config-manager'; +import { key } from '@ckb-lumos/hd'; +import { generateAddress } from '@ckb-lumos/helpers'; +// import { MultisigItem } from '../config'; +import { MultisigItem } from '../../../config'; +import { ForceBridgeCore } from '../../../core'; +import { init } from './init_config'; + +init(); +const config = getConfig(); +const multisigTemplate = config.SCRIPTS.SECP256K1_BLAKE160_MULTISIG; +if (!multisigTemplate) { + throw new Error('Multisig script template missing!'); +} + +const secpTemplate = getConfig().SCRIPTS.SECP256K1_BLAKE160; + +export function getMultisigLock(multisigScript: MultisigItem) { + const serializedMultisigScript = serializeMultisigScript(multisigScript); + const args = multisigArgs(serializedMultisigScript); + const multisigLockScript = { + code_hash: multisigTemplate.CODE_HASH, + hash_type: multisigTemplate.HASH_TYPE, + args, + }; + return multisigLockScript; +} + +export function getOwnLockHash(multisigScript: MultisigItem): string { + const multisigLockScript = getMultisigLock(multisigScript); + const ownLockHash = ForceBridgeCore.ckb.utils.scriptToHash({ + codeHash: multisigLockScript.code_hash, + hashType: multisigLockScript.hash_type, + args: multisigLockScript.args, + }); + return ownLockHash; +} + +export function getMultisigAddr(multisigScript: MultisigItem): string { + const multisigLockScript = getMultisigLock(multisigScript); + return generateAddress(multisigLockScript); +} + +export function getFromAddr(): string { + const fromPrivateKey = ForceBridgeCore.config.ckb.fromPrivateKey; + const fromBlake160 = key.publicKeyToBlake160(key.privateToPublic(fromPrivateKey as HexString)); + const fromLockScript = { + code_hash: secpTemplate.CODE_HASH, + hash_type: secpTemplate.HASH_TYPE, + args: fromBlake160, + }; + return generateAddress(fromLockScript); +} diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/multisig/typeid.ts b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/typeid.ts new file mode 100644 index 00000000..b723a2b1 --- /dev/null +++ b/offchain-modules/packages/x/src/ckb/tx-helper/multisig/typeid.ts @@ -0,0 +1,36 @@ +import { core, HashType, utils } from '@ckb-lumos/base'; +import { normalizers } from 'ckb-js-toolkit'; + +function toArrayBuffer(buf) { + const ab = new ArrayBuffer(buf.length); + const view = new Uint8Array(ab); + for (let i = 0; i < buf.length; ++i) { + view[i] = buf[i]; + } + return ab; +} + +function toBigUInt64LE(num) { + num = BigInt(num); + const buf = Buffer.alloc(8); + buf.writeBigUInt64LE(num); + return toArrayBuffer(buf); +} + +function generateTypeID(input, outputIndex) { + const s = core.SerializeCellInput(normalizers.NormalizeCellInput(input)); + const i = toBigUInt64LE(outputIndex); + const ckbHasher = new utils.CKBHasher(); + ckbHasher.update(s); + ckbHasher.update(i); + return ckbHasher.digestHex(); +} + +export function generateTypeIDScript(input, outputIndex) { + const args = generateTypeID(input, outputIndex); + return { + code_hash: '0x00000000000000000000000000000000000000000000000000545950455f4944', + hash_type: 'type' as HashType, + args, + }; +} diff --git a/offchain-modules/packages/x/src/ckb/tx-helper/signer.ts b/offchain-modules/packages/x/src/ckb/tx-helper/signer.ts deleted file mode 100644 index 5585b3d2..00000000 --- a/offchain-modules/packages/x/src/ckb/tx-helper/signer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Transaction } from '@lay2/pw-core'; - -export async function sign(tx: Transaction, privateKey: string): Promise { - throw new Error('not implemented'); -} - -export async function signWithMultiKey(tx: Transaction, privateKeys: string[]): Promise { - throw new Error('not implemented'); -} diff --git a/offchain-modules/packages/x/src/config.ts b/offchain-modules/packages/x/src/config.ts index 3460a0ff..9131633d 100644 --- a/offchain-modules/packages/x/src/config.ts +++ b/offchain-modules/packages/x/src/config.ts @@ -17,10 +17,26 @@ export interface ConfigItem { }; } +export interface ScriptItem { + code_hash: string; + hash_type: HashType; + args: string; +} + +export interface MultisigItem { + R: number; + M: number; + publicKeyHashes: string[]; +} + export interface CkbConfig { ckbRpcUrl: string; ckbIndexerUrl: string; - privateKey: string; + fromPrivateKey: string; + keys: string[]; + hosts: string[]; + multisigScript: MultisigItem; + multisigType: ScriptItem; ownerLockHash: string; deps: { bridgeLock: ConfigItem; @@ -36,6 +52,7 @@ export interface EthConfig { contractAddress: string; privateKey: string; multiSignKeys: string[]; + multiSignHosts: string[]; multiSignThreshold: number; confirmNumber: number; startBlockHeight: number; diff --git a/offchain-modules/packages/x/src/core.ts b/offchain-modules/packages/x/src/core.ts index ea19e31f..d89666f8 100644 --- a/offchain-modules/packages/x/src/core.ts +++ b/offchain-modules/packages/x/src/core.ts @@ -8,12 +8,15 @@ import { Config } from './config'; export class ForceBridgeCore { static config: Config; static ckb: CKB; - static indexer: CkbIndexer; + // static indexer: Indexer; + static ckbIndexer: CkbIndexer; async init(config: Config): Promise { ForceBridgeCore.config = config; ForceBridgeCore.ckb = new CKB(config.ckb.ckbRpcUrl); - ForceBridgeCore.indexer = new CkbIndexer(config.ckb.ckbRpcUrl, config.ckb.ckbIndexerUrl); + ForceBridgeCore.ckbIndexer = new CkbIndexer(config.ckb.ckbRpcUrl, config.ckb.ckbIndexerUrl); + // ForceBridgeCore.indexer = new Indexer(config.ckb.ckbRpcUrl, './lumos_db'); + // ForceBridgeCore.indexer.startForever(); return this; } } diff --git a/offchain-modules/packages/x/src/db/entity/SignedTx.ts b/offchain-modules/packages/x/src/db/entity/SignedTx.ts new file mode 100644 index 00000000..eae55181 --- /dev/null +++ b/offchain-modules/packages/x/src/db/entity/SignedTx.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity() +export class SignedTx { + @PrimaryGeneratedColumn() + id: number; + + @Column() + sigType: string; + + @Column() + chain: number; + + @Column() + amount: string; + + @Column() + asset: string; + + @Column() + refTxHash: string; + + @Column() + txHash: string; + + @Column() + singerPubkey: string; + + @CreateDateColumn() + createdAt: string; + + @UpdateDateColumn() + updatedAt: string; +} diff --git a/offchain-modules/packages/x/src/db/model.ts b/offchain-modules/packages/x/src/db/model.ts index 9b7af4ae..b2a375d1 100644 --- a/offchain-modules/packages/x/src/db/model.ts +++ b/offchain-modules/packages/x/src/db/model.ts @@ -24,6 +24,16 @@ export { CkbBurn } from './entity/CkbBurn'; export { TronLock } from './entity/TronLock'; export { TronUnlock } from './entity/TronUnlock'; +export interface ISigned { + sigType: string; + chain: number; + amount: string; + asset: string; + refTxHash: string; + txHash: string; + pubkey: string; +} + export interface ICkbMint { id: string; chain: ChainType; diff --git a/offchain-modules/packages/x/src/db/signed.ts b/offchain-modules/packages/x/src/db/signed.ts new file mode 100644 index 00000000..2beba7b5 --- /dev/null +++ b/offchain-modules/packages/x/src/db/signed.ts @@ -0,0 +1,31 @@ +import { Connection, In, Repository } from 'typeorm'; +import { SignedTx } from './entity/SignedTx'; +import { ISigned } from './model'; + +export class SignedDb { + private signedRepository: Repository; + + constructor(private conn: Connection) { + this.signedRepository = conn.getRepository(SignedTx); + } + + async createSigned(records: ISigned[]): Promise { + const dbRecords = records.map((r) => this.signedRepository.create(r)); + await this.signedRepository.save(dbRecords); + } + + async getSignedByRefTxHashes(refTxHashes: string[]): Promise { + return this.signedRepository + .createQueryBuilder('s') + .where(`refTxHash in (${refTxHashes.join(',')})`) + .getMany(); + } + async getSignedByPubkeyAndMsgHash(pubkey: string, refTxHashes: string[]): Promise { + return this.signedRepository.find({ + where: { + refTxHash: In([refTxHashes]), + singerPubkey: pubkey, + }, + }); + } +} diff --git a/offchain-modules/packages/x/src/handlers/ckb.ts b/offchain-modules/packages/x/src/handlers/ckb.ts index f8ca27cc..bdb8db28 100644 --- a/offchain-modules/packages/x/src/handlers/ckb.ts +++ b/offchain-modules/packages/x/src/handlers/ckb.ts @@ -1,11 +1,13 @@ import { Script as LumosScript } from '@ckb-lumos/base'; import { Address, AddressType, Amount, HashType, Script } from '@lay2/pw-core'; +import { JSONRPCClient } from 'json-rpc-2.0'; +import fetch from 'node-fetch/index'; import { Account } from '../ckb/model/accounts'; import { Asset, BtcAsset, ChainType, EosAsset, EthAsset, TronAsset } from '../ckb/model/asset'; import { IndexerCollector } from '../ckb/tx-helper/collector'; import { RecipientCellData } from '../ckb/tx-helper/generated/eth_recipient_cell'; import { CkbTxGenerator, MintAssetRecord } from '../ckb/tx-helper/generator'; -import { ScriptType } from '../ckb/tx-helper/indexer'; +import { CkbIndexer, ScriptType } from '../ckb/tx-helper/indexer'; import { forceBridgeRole } from '../config'; import { ForceBridgeCore } from '../core'; import { CkbDb } from '../db'; @@ -18,17 +20,39 @@ import TransactionWithStatus = CKBComponents.TransactionWithStatus; import Block = CKBComponents.Block; const lastHandleCkbBlockKey = 'lastHandleCkbBlock'; - +import { serializeMultisigScript } from '@ckb-lumos/common-scripts/lib/secp256k1_blake160_multisig'; +import { Indexer } from '@ckb-lumos/indexer'; +import { sealTransaction } from '@ckb-lumos/helpers'; +import { key } from '@ckb-lumos/hd'; +import TransactionManager from '@ckb-lumos/transaction-manager'; +import { RPC } from '@ckb-lumos/rpc'; +import { getOwnLockHash } from '../ckb/tx-helper/multisig/multisig_helper'; +import { MultiSigMgr } from '../multisig/multisig-mgr'; +import CKB from '@nervosnetwork/ckb-sdk-core'; + +const lumosIndexerData = './indexer-data'; // CKB handler // 1. Listen CKB chain to get new burn events. // 2. Listen database to get new mint events, send tx. export class CkbHandler { private ckb = ForceBridgeCore.ckb; - private indexer = ForceBridgeCore.indexer; - private PRI_KEY = ForceBridgeCore.config.ckb.privateKey; + private indexer: Indexer; + private ckbIndexer = ForceBridgeCore.ckbIndexer; + private transactionManager: TransactionManager; + private multisigMgr: MultiSigMgr; + private PRI_KEY = ForceBridgeCore.config.ckb.fromPrivateKey; private lastHandledBlockHeight: number; private lastHandledBlockHash: string; - constructor(private db: CkbDb, private kvDb, private role: forceBridgeRole) {} + constructor(private db: CkbDb, private kvDb, private role: forceBridgeRole) { + this.indexer = new Indexer(ForceBridgeCore.config.ckb.ckbRpcUrl, lumosIndexerData); + this.indexer.startForever(); + this.transactionManager = new TransactionManager(this.indexer); + this.multisigMgr = new MultiSigMgr( + 'CKB', + ForceBridgeCore.config.ckb.hosts, + ForceBridgeCore.config.ckb.multisigScript.M, + ); + } async getLastHandledBlock(): Promise<{ blockNumber: number; blockHash: string }> { const lastHandledBlock = await this.kvDb.get(lastHandleCkbBlockKey); @@ -169,7 +193,7 @@ export class CkbHandler { } catch (e) { continue; } - if (await this.isBurnTx(tx, cellData)) { + if (await isBurnTx(tx, cellData)) { const burnPreviousTx: TransactionWithStatus = await this.ckb.rpc.getTransaction( tx.inputs[0].previousOutput.txHash, ); @@ -241,7 +265,7 @@ export class CkbHandler { if (firstOutputTypeCodeHash != expectSudtTypeCodeHash) { return false; } - const committeeLockHash = await this.getOwnLockHash(); + const committeeLockHash = getOwnLockHash(ForceBridgeCore.config.ckb.multisigScript); // verify tx input: committee cell. const preHash = tx.inputs[0].previousOutput.txHash; const txPrevious = await this.ckb.rpc.getTransaction(preHash); @@ -257,70 +281,12 @@ export class CkbHandler { return firstInputLockHash === committeeLockHash; } - async isBurnTx(tx: Transaction, cellData: RecipientCellData): Promise { - if (tx.outputs.length < 1) { - return false; - } - const ownLockHash = await this.getOwnLockHash(); - logger.debug('CkbHandler isBurnTx amount: ', toHexString(new Uint8Array(cellData.getAmount().raw()))); - logger.debug( - 'CkbHandler isBurnTx recipient address: ', - toHexString(new Uint8Array(cellData.getRecipientAddress().raw())), - ); - logger.debug('CkbHandler isBurnTx asset: ', toHexString(new Uint8Array(cellData.getAsset().raw()))); - logger.debug('CkbHandler isBurnTx chain: ', cellData.getChain()); - let asset; - const assetAddress = toHexString(new Uint8Array(cellData.getAsset().raw())); - switch (cellData.getChain()) { - case ChainType.BTC: - asset = new BtcAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); - break; - case ChainType.ETH: - asset = new EthAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); - break; - case ChainType.TRON: - asset = new TronAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); - break; - case ChainType.EOS: - asset = new EosAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); - break; - default: - return false; - } - - // verify tx input: sudt cell. - const preHash = tx.inputs[0].previousOutput.txHash; - const txPrevious = await this.ckb.rpc.getTransaction(preHash); - if (txPrevious == null) { - return false; - } - const sudtType = txPrevious.transaction.outputs[Number(tx.inputs[0].previousOutput.index)].type; - const expectType = { - codeHash: ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash, - hashType: ForceBridgeCore.config.ckb.deps.sudtType.script.hashType, - args: this.getBridgeLockHash(asset), - }; - logger.debug('CkbHandler isBurnTx expectType:', expectType); - logger.debug('CkbHandler isBurnTx sudtType:', sudtType); - if (sudtType == null || expectType.codeHash != sudtType.codeHash || expectType.args != sudtType.args) { - return false; - } - - // verify tx output recipientLockscript: recipient cell. - const recipientScript = tx.outputs[0].type; - const expect = ForceBridgeCore.config.ckb.deps.recipientType.script; - logger.debug('recipientScript:', recipientScript); - logger.debug('expect:', expect); - return recipientScript.codeHash == expect.codeHash; - } - async handleMintRecords(): Promise { if (this.role !== 'collector') { return; } - const account = new Account(this.PRI_KEY); - const ownLockHash = await this.getOwnLockHash(); - const generator = new CkbTxGenerator(this.ckb, new IndexerCollector(this.indexer)); + const ownLockHash = getOwnLockHash(ForceBridgeCore.config.ckb.multisigScript); + const generator = new CkbTxGenerator(this.ckb, new IndexerCollector(this.ckbIndexer)); while (true) { const mintRecords = await this.db.getCkbMintRecordsToMint(); if (mintRecords.length == 0) { @@ -330,7 +296,7 @@ export class CkbHandler { } logger.info(`CkbHandler handleMintRecords new mintRecords:${JSON.stringify(mintRecords, null, 2)}`); - await this.indexer.waitUntilSync(); + await this.ckbIndexer.waitUntilSync(); const mintIds = mintRecords .map((ckbMint) => { return ckbMint.id; @@ -344,6 +310,7 @@ export class CkbHandler { `CkbHandler handleMintRecords bridge cell is not exist. do create bridge cell. ownLockHash:${ownLockHash.toString()}`, ); logger.info(`CkbHandler handleMintRecords createBridgeCell newToken:${JSON.stringify(newTokens, null, 2)}`); + await this.waitUntilSync(); await this.createBridgeCell(newTokens, generator); } @@ -352,9 +319,35 @@ export class CkbHandler { r.status = 'pending'; }); await this.db.updateCkbMint(mintRecords); - const rawTx = await generator.mint(await account.getLockscript(), records); - const signedTx = this.ckb.signTransaction(this.PRI_KEY)(rawTx); - const mintTxHash = await this.ckb.rpc.sendTransaction(signedTx); + await this.waitUntilSync(); + const txSkeleton = await generator.mint(records, this.indexer); + logger.info(`mint tx txSkeleton ${JSON.stringify(txSkeleton, null, 2)}`); + const content0 = key.signRecoverable( + txSkeleton.get('signingEntries').get(0).message, + ForceBridgeCore.config.ckb.fromPrivateKey, + ); + let content1 = serializeMultisigScript(ForceBridgeCore.config.ckb.multisigScript); + + const sigs = await this.multisigMgr.collectSignatures({ + rawData: txSkeleton.get('signingEntries').get(1).message, + payload: { + sigType: 'mint', + mintRecords: mintRecords.map((r) => { + return { + id: r.id, + chain: r.chain, + asset: r.asset, + amount: r.amount, + recipientLockscript: r.recipientLockscript, + }; + }), + txSkeleton, + }, + }); + content1 += sigs.join(''); + + const tx = sealTransaction(txSkeleton, [content0, content1]); + const mintTxHash = await this.transactionManager.send_transaction(tx); logger.info( `CkbHandler handleMintRecords Mint Transaction has been sent, ckbTxHash ${mintTxHash}, mintIds:${mintIds}`, ); @@ -440,7 +433,7 @@ export class CkbHandler { ).serializeJson() as LumosScript, script_type: ScriptType.lock, }; - const bridgeCells = await this.indexer.getCells(searchKey); + const bridgeCells = await this.ckbIndexer.getCells(searchKey); if (bridgeCells.length == 0) { newTokens.push(record); } @@ -449,25 +442,53 @@ export class CkbHandler { } async createBridgeCell(newTokens: MintAssetRecord[], generator: CkbTxGenerator) { - const account = new Account(this.PRI_KEY); + const assets = []; const scripts = newTokens.map((r) => { - return { - codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, - hashType: HashType.data, - args: r.asset.toBridgeLockscriptArgs(), - }; + assets.push({ + chain: r.asset.chainType, + asset: r.asset.getAddress(), + }); + return new Script( + ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, + r.asset.toBridgeLockscriptArgs(), + ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType, + ); + }); + + const txSkeleton = await generator.createBridgeCell(scripts, this.indexer); + const message0 = txSkeleton.get('signingEntries').get(0).message; + const content0 = key.signRecoverable(message0, ForceBridgeCore.config.ckb.fromPrivateKey); + let content1 = serializeMultisigScript(ForceBridgeCore.config.ckb.multisigScript); + const sigs = await this.multisigMgr.collectSignatures({ + rawData: txSkeleton.get('signingEntries').get(1).message, + payload: { + sigType: 'create_cell', + createAssets: assets, + txSkeleton, + }, }); - const rawTx = await generator.createBridgeCell(await account.getLockscript(), scripts); - const signedTx = this.ckb.signTransaction(this.PRI_KEY)(rawTx); - const tx_hash = await this.ckb.rpc.sendTransaction(signedTx); - await this.waitUntilCommitted(tx_hash, 60); - await this.indexer.waitUntilSync(); + content1 += sigs.join(''); + + const tx = sealTransaction(txSkeleton, [content0, content1]); + console.log('tx:', JSON.stringify(tx, null, 2)); + const txHash = await this.transactionManager.send_transaction(tx); + await this.waitUntilCommitted(txHash, 60); } - async getOwnLockHash(): Promise { - const account = new Account(this.PRI_KEY); - const ownLockHash = this.ckb.utils.scriptToHash(await account.getLockscript()); - return ownLockHash; + async waitUntilSync(): Promise { + const ckbRpc = new RPC(ForceBridgeCore.config.ckb.ckbRpcUrl); + const rpcTipNumber = parseInt((await ckbRpc.get_tip_header()).number, 16); + logger.debug('rpcTipNumber', rpcTipNumber); + let index = 0; + while (true) { + const indexerTipNumber = parseInt((await this.indexer.tip()).block_number, 16); + logger.debug('indexerTipNumber', indexerTipNumber); + if (indexerTipNumber >= rpcTipNumber) { + return; + } + logger.debug(`wait until indexer sync. index: ${index++}`); + await asyncSleep(1000); + } } getBridgeLockHash(asset: Asset): string { @@ -509,6 +530,65 @@ export class CkbHandler { logger.info('ckb handler started 🚀'); } } +export async function isBurnTx(tx: Transaction, cellData: RecipientCellData): Promise { + if (tx.outputs.length < 1) { + return false; + } + const ownLockHash = getOwnLockHash(ForceBridgeCore.config.ckb.multisigScript); + logger.debug('amount: ', toHexString(new Uint8Array(cellData.getAmount().raw()))); + logger.debug('recipient address: ', toHexString(new Uint8Array(cellData.getRecipientAddress().raw()))); + logger.debug('asset: ', toHexString(new Uint8Array(cellData.getAsset().raw()))); + logger.debug('chain: ', cellData.getChain()); + let asset; + const assetAddress = toHexString(new Uint8Array(cellData.getAsset().raw())); + switch (cellData.getChain()) { + case ChainType.BTC: + asset = new BtcAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); + break; + case ChainType.ETH: + asset = new EthAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); + break; + case ChainType.TRON: + asset = new TronAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); + break; + case ChainType.EOS: + asset = new EosAsset(uint8ArrayToString(fromHexString(assetAddress)), ownLockHash); + break; + default: + return false; + } + + // verify tx input: sudt cell. + const preHash = tx.inputs[0].previousOutput.txHash; + const txPrevious = await ForceBridgeCore.ckb.rpc.getTransaction(preHash); + if (txPrevious == null) { + return false; + } + const sudtType = txPrevious.transaction.outputs[Number(tx.inputs[0].previousOutput.index)].type; + const bridgeCellLockscript = { + codeHash: ForceBridgeCore.config.ckb.deps.bridgeLock.script.codeHash, + hashType: ForceBridgeCore.config.ckb.deps.bridgeLock.script.hashType, + args: asset.toBridgeLockscriptArgs(), + }; + const bridgeLockHash = ForceBridgeCore.ckb.utils.scriptToHash(bridgeCellLockscript); + const expectType = { + codeHash: ForceBridgeCore.config.ckb.deps.sudtType.script.codeHash, + hashType: ForceBridgeCore.config.ckb.deps.sudtType.script.hashType, + args: bridgeLockHash, + }; + logger.debug('expectType:', expectType); + logger.debug('sudtType:', sudtType); + if (sudtType == null || expectType.codeHash != sudtType.codeHash || expectType.args != sudtType.args) { + return false; + } + + // verify tx output recipientLockscript: recipient cell. + const recipientScript = tx.outputs[0].type; + const expect = ForceBridgeCore.config.ckb.deps.recipientType.script; + logger.debug('recipientScript:', recipientScript); + logger.debug('expect:', expect); + return recipientScript.codeHash == expect.codeHash; +} type BurnDbData = { cellData: RecipientCellData; diff --git a/offchain-modules/packages/x/src/handlers/eos.ts b/offchain-modules/packages/x/src/handlers/eos.ts index 5e029b17..fc6162f6 100644 --- a/offchain-modules/packages/x/src/handlers/eos.ts +++ b/offchain-modules/packages/x/src/handlers/eos.ts @@ -324,7 +324,7 @@ export class EosHandler { } await this.processUnLockEvents(todoRecords); } catch (e) { - logger.error('EosHandler watchUnlockEvents error:', e.toString()); + logger.error('EosHandler watchUnlockEvents error:', e); await asyncSleep(3000); } } @@ -431,7 +431,7 @@ export class EosHandler { await this.db.saveEosUnlock(newRecords); } } catch (e) { - logger.error(`EosHandler checkUnlockTxStatus error:${e.toString()}`); + logger.error(`EosHandler checkUnlockTxStatus error:${e}`); await asyncSleep(3000); } } diff --git a/offchain-modules/packages/x/src/handlers/index.ts b/offchain-modules/packages/x/src/handlers/index.ts index b38fa330..f15fa057 100644 --- a/offchain-modules/packages/x/src/handlers/index.ts +++ b/offchain-modules/packages/x/src/handlers/index.ts @@ -24,7 +24,7 @@ export async function startHandlers() { const ckbDb = new CkbDb(conn); const kvDb = new KVDb(conn); if (isCollector) { - ForceBridgeCore.config.ckb.privateKey = parsePrivateKey(ForceBridgeCore.config.ckb.privateKey); + ForceBridgeCore.config.ckb.fromPrivateKey = parsePrivateKey(ForceBridgeCore.config.ckb.fromPrivateKey); } const ckbHandler = new CkbHandler(ckbDb, kvDb, role); ckbHandler.start(); diff --git a/offchain-modules/packages/x/src/multisig/client.ts b/offchain-modules/packages/x/src/multisig/client.ts new file mode 100644 index 00000000..203fd9a5 --- /dev/null +++ b/offchain-modules/packages/x/src/multisig/client.ts @@ -0,0 +1,23 @@ +import { JSONRPCClient } from 'json-rpc-2.0'; +import fetch from 'node-fetch/index'; + +export async function httpRequest(host: string, method: string, params: any): Promise { + const client = new JSONRPCClient((jsonRPCRequest) => + fetch(host, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(jsonRPCRequest), + }).then((response) => { + if (response.status === 200) { + return response.json().then((jsonRPCResponse) => { + client.receive(jsonRPCResponse); + }); + } else if (jsonRPCRequest.id !== undefined) { + return Promise.reject(new Error(response.statusText)); + } + }), + ); + return client.request(method, params); +} diff --git a/offchain-modules/packages/x/src/multisig/index.ts b/offchain-modules/packages/x/src/multisig/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/offchain-modules/packages/x/src/multisig/multisig-mgr.ts b/offchain-modules/packages/x/src/multisig/multisig-mgr.ts new file mode 100644 index 00000000..65a26355 --- /dev/null +++ b/offchain-modules/packages/x/src/multisig/multisig-mgr.ts @@ -0,0 +1,92 @@ +import { TransactionSkeletonType } from '@ckb-lumos/helpers'; +import { logger } from '../utils/logger'; +import { EthUnlockRecord } from '../xchain/eth'; +import { httpRequest } from './client'; + +export interface ethCollectSignaturesPayload { + domainSeparator: string; + typeHash: string; + unlockRecords: EthUnlockRecord[]; + nonce: number; +} + +export type ckbSigType = 'mint' | 'create_cell'; + +export interface mintRecord { + id: string; + chain: number; + asset: string; + amount: string; + recipientLockscript: string; +} + +export interface createAsset { + chain: number; + asset: string; +} + +export interface ckbCollectSignaturesPayload { + sigType: ckbSigType; + mintRecords?: mintRecord[]; + createAssets?: createAsset[]; + txSkeleton: TransactionSkeletonType; +} + +export interface collectSignaturesParams { + rawData: string; + payload: ethCollectSignaturesPayload | ckbCollectSignaturesPayload; + failedTxHash?: string; +} + +export class MultiSigMgr { + private chainType: string; + private sigServerHosts: string[]; + private threshold: number; + constructor(chainType: string, sigServerHosts: string[], threshold: number) { + this.chainType = chainType; + this.sigServerHosts = sigServerHosts; + this.threshold = threshold; + } + + public async collectSignatures(params: collectSignaturesParams): Promise { + logger.info(`collectSignatures rawData:${params.rawData} payload:${JSON.stringify(params.payload, null, 2)}`); + const successSigSvr = []; + const sigs = []; + for (const svrHost of this.sigServerHosts) { + try { + const sig = await this.requestSig(svrHost, params); + sigs.push(sig); + successSigSvr.push(svrHost); + logger.info( + `MultiSigMgr collectSignatures rawData:${params.rawData} sigServer:${svrHost} sig:${sig.toString()}`, + ); + } catch (e) { + logger.error( + `MultiSigMgr collectSignatures rawData:${params.rawData} sigServer:${svrHost}, error:${e.message}`, + ); + } + if (successSigSvr.length === this.threshold) { + logger.info( + `MultiSigMgr collectSignatures success, rawData:${params.rawData} sigServers:${successSigSvr.join(',')}`, + ); + break; + } + } + return sigs; + } + + public async requestSig(host: string, params: collectSignaturesParams): Promise { + let method: string; + switch (this.chainType) { + case 'CKB': + method = 'signCkbTx'; + break; + case 'ETH': + method = 'signEthTx'; + break; + default: + return Promise.reject(new Error(`chain type:${this.chainType} doesn't support`)); + } + return httpRequest(host, method, params); + } +} diff --git a/offchain-modules/packages/x/src/xchain/eth/contract.ts b/offchain-modules/packages/x/src/xchain/eth/contract.ts index 0153ae79..8bebd4cc 100644 --- a/offchain-modules/packages/x/src/xchain/eth/contract.ts +++ b/offchain-modules/packages/x/src/xchain/eth/contract.ts @@ -3,11 +3,20 @@ import { BigNumber, ethers } from 'ethers'; import { EthConfig, forceBridgeRole } from '../../config'; import { ForceBridgeCore } from '../../core'; import { EthUnlock } from '../../db/entity/EthUnlock'; +import { MultiSigMgr } from '../../multisig/multisig-mgr'; import { asyncSleep } from '../../utils'; import { logger } from '../../utils/logger'; import { abi } from './abi/ForceBridge.json'; +import { buildSigRawData } from './utils'; -const lockTopic = ethers.utils.id('Locked(address,address,uint256,bytes,bytes)'); +export const lockTopic = ethers.utils.id('Locked(address,address,uint256,bytes,bytes)'); + +export interface EthUnlockRecord { + token: string; + recipient: string; + amount: BigNumber; + ckbTxHash: string; +} export class EthChain { protected readonly role: forceBridgeRole; @@ -18,6 +27,7 @@ export class EthChain { protected readonly bridge: ethers.Contract; protected readonly wallet: ethers.Wallet; protected readonly multiSignKeys: string[]; + protected readonly multisigMgr: MultiSigMgr; constructor(role: forceBridgeRole) { const config = ForceBridgeCore.config.eth; @@ -29,8 +39,10 @@ export class EthChain { this.iface = new ethers.utils.Interface(abi); if (role === 'collector') { this.wallet = new ethers.Wallet(config.privateKey, this.provider); + logger.debug('address', this.wallet.address); this.bridge = new ethers.Contract(this.bridgeContractAddr, abi, this.provider).connect(this.wallet); this.multiSignKeys = config.multiSignKeys; + this.multisigMgr = new MultiSigMgr('ETH', this.config.multiSignHosts, this.config.multiSignThreshold); } } @@ -85,7 +97,7 @@ export class EthChain { async sendUnlockTxs(records: EthUnlock[]): Promise { logger.debug('contract balance', await this.provider.getBalance(this.bridgeContractAddr)); - const params = records.map((r) => { + const params: EthUnlockRecord[] = records.map((r) => { return { token: r.asset, recipient: r.recipientAddress, @@ -95,50 +107,28 @@ export class EthChain { }); const domainSeparator = await this.bridge.DOMAIN_SEPARATOR(); const typeHash = await this.bridge.UNLOCK_TYPEHASH(); - const nonce = await this.bridge.latestUnlockNonce_(); + const nonce: number = await this.bridge.latestUnlockNonce_(); const signatures = this.signUnlockRecords(domainSeparator, typeHash, params, nonce); logger.debug('sendUnlockTxs params', params); return this.bridge.unlock(params, nonce, signatures); } - private signUnlockRecords(domainSeparator: string, typeHash: string, records, nonce) { - const msg = ethers.utils.keccak256( - ethers.utils.solidityPack( - ['bytes1', 'bytes1', 'bytes32', 'bytes32'], - [ - '0x19', - '0x01', - domainSeparator, - ethers.utils.keccak256( - ethers.utils.defaultAbiCoder.encode( - [ - 'bytes32', - ethers.utils.ParamType.from({ - components: [ - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'amount', type: 'uint256' }, - { name: 'ckbTxHash', type: 'bytes' }, - ], - name: 'records', - type: 'tuple[]', - }), - 'uint256', - ], - [typeHash, records, nonce], - ), - ), - ], - ), - ); - - let signatures = '0x'; - for (let i = 0; i < this.multiSignKeys.length; i++) { - const wallet = new ethers.Wallet(this.multiSignKeys[i], this.provider); - const { v, r, s } = ecsign(Buffer.from(msg.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex')); - const sigHex = toRpcSig(v, r, s); - signatures += sigHex.slice(2); - } - return signatures; + private async signUnlockRecords( + domainSeparator: string, + typeHash: string, + records: EthUnlockRecord[], + nonce: number, + ) { + const rawData = buildSigRawData(domainSeparator, typeHash, records, nonce); + const sigs = await this.multisigMgr.collectSignatures({ + rawData: rawData, + payload: { + domainSeparator: domainSeparator, + typeHash: typeHash, + unlockRecords: records, + nonce: nonce, + }, + }); + return '0x' + sigs.join(''); } } diff --git a/offchain-modules/packages/x/src/xchain/eth/utils.ts b/offchain-modules/packages/x/src/xchain/eth/utils.ts new file mode 100644 index 00000000..bbc4fe45 --- /dev/null +++ b/offchain-modules/packages/x/src/xchain/eth/utils.ts @@ -0,0 +1,32 @@ +import { ethers } from 'ethers'; +export function buildSigRawData(domainSeparator: string, typeHash: string, records, nonce): string { + return ethers.utils.keccak256( + ethers.utils.solidityPack( + ['bytes1', 'bytes1', 'bytes32', 'bytes32'], + [ + '0x19', + '0x01', + domainSeparator, + ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + [ + 'bytes32', + ethers.utils.ParamType.from({ + components: [ + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'ckbTxHash', type: 'bytes' }, + ], + name: 'records', + type: 'tuple[]', + }), + 'uint256', + ], + [typeHash, records, nonce], + ), + ), + ], + ), + ); +} diff --git a/offchain-modules/yarn.lock b/offchain-modules/yarn.lock index 6fa2983a..f8e17338 100644 --- a/offchain-modules/yarn.lock +++ b/offchain-modules/yarn.lock @@ -221,6 +221,47 @@ immutable "^4.0.0-rc.12" js-xxhash "^1.0.4" +"@ckb-lumos/common-scripts@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ckb-lumos/common-scripts/-/common-scripts-0.16.0.tgz#bae2cf74473c4c5c1f2da2d32d77e0934b1f024d" + integrity sha512-LYN0WU6CDn5h40SjBjHH+w57CIwHeBVJ3NPBPRozyFvpVfruJYvVRWzZGCZWNj6Wx2Q0xdLAD2S+UJ+ATWBLUw== + dependencies: + "@ckb-lumos/base" "^0.16.0" + "@ckb-lumos/config-manager" "^0.16.0" + "@ckb-lumos/helpers" "^0.16.0" + "@ckb-lumos/rpc" "^0.16.0" + ckb-js-toolkit "^0.10.2" + immutable "^4.0.0-rc.12" + +"@ckb-lumos/config-manager@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ckb-lumos/config-manager/-/config-manager-0.16.0.tgz#ddff6bf76f9f7b50f5005f8f45120cffc40f1e3e" + integrity sha512-NoctI6I1/SsVxzuwIbl4Qy4CCtmGPQxxTQr53zI5Jo3UAngrlVpj+fDWOjhAcR8r1U7IZRttJmNeye06JU6I/w== + dependencies: + deep-freeze-strict "^1.1.1" + +"@ckb-lumos/hd@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ckb-lumos/hd/-/hd-0.16.0.tgz#f72c7c0f183a9e9b9491fd579bda47e158197bd0" + integrity sha512-tlBtbj6sg1Yb6y7bdJ1Z7IECPRBN8SFrSCOwh0sMPz1ITk3MGir7ezRxPOHyMJTB2BD4XvvbKJZTJ63S1/tYeQ== + dependencies: + "@ckb-lumos/base" "^0.16.0" + bn.js "^5.1.3" + elliptic "^6.5.3" + sha3 "^2.1.3" + uuid "^8.3.0" + +"@ckb-lumos/helpers@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ckb-lumos/helpers/-/helpers-0.16.0.tgz#044e5581ae1e45244881dd011791622135c28374" + integrity sha512-FYNQTHEfkjHmwXetznWrkodXrBAo+Thqp7G4wMU9RkipAUHUxUxBF93+lhKprSUR1jfEmZrIsjikYvLGYGgiFQ== + dependencies: + "@ckb-lumos/base" "^0.16.0" + "@ckb-lumos/config-manager" "^0.16.0" + bech32 "^1.1.4" + ckb-js-toolkit "^0.10.2" + immutable "^4.0.0-rc.12" + "@ckb-lumos/indexer@^0.16.0": version "0.16.0" resolved "https://registry.yarnpkg.com/@ckb-lumos/indexer/-/indexer-0.16.0.tgz#9a73f59d8adee0f4a2275fa846ae077e79671fb1" @@ -234,7 +275,7 @@ request "^2.88.2" xxhash "^0.3.0" -"@ckb-lumos/rpc@^0.16.0": +"@ckb-lumos/rpc@0.16.0", "@ckb-lumos/rpc@^0.16.0": version "0.16.0" resolved "https://registry.yarnpkg.com/@ckb-lumos/rpc/-/rpc-0.16.0.tgz#a0bf587277f1b60fb477169d4252c97a481c4023" integrity sha512-KDxRiPeb6jJt+VAs1P/PWRTgZogBqrB65mp4TLBsr44lcrSVVCTPfb1jG5+3a5P/WrAuGihUbwF86MNLcl8obw== @@ -242,6 +283,16 @@ "@ckb-lumos/base" "^0.16.0" ckb-js-toolkit "^0.10.2" +"@ckb-lumos/transaction-manager@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ckb-lumos/transaction-manager/-/transaction-manager-0.16.0.tgz#b47247ac672fe4b821c772e86e032f001b2e068f" + integrity sha512-ZnCeJH4GIGtuU7kuDqf2lyspk3gbleyFzOWRqsO208MzJy9IpQxTMrczRnIWA/qX/vy3XgZyRUKify/pkH78IQ== + dependencies: + "@ckb-lumos/base" "^0.16.0" + "@ckb-lumos/indexer" "^0.16.0" + ckb-js-toolkit "^0.10.2" + immutable "^4.0.0-rc.12" + "@ckb-lumos/types@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@ckb-lumos/types/-/types-0.2.4.tgz#1733d29a7489af8ddfc297a240249fb3046fda7a" @@ -1769,7 +1820,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== -"@types/minimist@^1.2.0": +"@types/minimist@^1.2.0", "@types/minimist@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== @@ -2603,7 +2654,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2: +bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3: version "5.2.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== @@ -2783,6 +2834,14 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer@6.0.3, buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -2791,14 +2850,6 @@ buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - bufferutil@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b" @@ -3789,6 +3840,11 @@ deep-extend@^0.6.0, deep-extend@~0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-freeze-strict@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" + integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA= + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -9414,6 +9470,13 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sha3@^2.1.3: + version "2.1.4" + resolved "https://registry.yarnpkg.com/sha3/-/sha3-2.1.4.tgz#000fac0fe7c2feac1f48a25e7a31b52a6492cc8f" + integrity sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg== + dependencies: + buffer "6.0.3" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -10738,6 +10801,11 @@ uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"