Skip to content

Commit

Permalink
Merge pull request #19 from helix-bridge/xiaoch05-sign-message
Browse files Browse the repository at this point in the history
indexer does signature authentication for relayer messages.
  • Loading branch information
xiaoch05 authored Apr 23, 2024
2 parents dd4e9dc + 1d3890e commit 20c4f44
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 47 deletions.
1 change: 1 addition & 0 deletions src/configure/configure.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface TokenInfo {
swapRate: number;
withdrawLiquidityAmountThreshold: number;
withdrawLiquidityCountThreshold: number;
useDynamicBaseFee: boolean;
}

export interface BridgeInfo {
Expand Down
63 changes: 56 additions & 7 deletions src/dataworker/dataworker.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import axios from "axios";
import { ethers } from "ethers";
import { last } from "lodash";
import {
Erc20Contract,
Expand All @@ -17,6 +18,7 @@ export interface HistoryRecord {
sendTokenAddress: string;
recvToken: string;
sender: string;
relayer: string;
recipient: string;
sendAmount: string;
recvAmount: string;
Expand Down Expand Up @@ -54,6 +56,7 @@ export class DataworkerService implements OnModuleInit {
private readonly statusRefund = 4;
private readonly pendingToConfirmRefund = 5;
private readonly relayGasLimit = BigInt(100000);
private readonly dynamicFeeExpiredTime = 60 * 15; // 15 min

async onModuleInit() {
this.logger.log("data worker started");
Expand Down Expand Up @@ -91,7 +94,7 @@ export class DataworkerService implements OnModuleInit {
token: \"${token.toLowerCase()}\",
order: "${firstPendingOrderBy}",
notsubmited: true
) {id, startTime, sendTokenAddress, recvToken, sender, recipient, sendAmount, recvAmount, fromChain, toChain, reason, fee, requestTxHash, confirmedBlocks, messageNonce}}`;
) {id, startTime, sendTokenAddress, recvToken, sender, relayer, recipient, sendAmount, recvAmount, fromChain, toChain, reason, fee, requestTxHash, confirmedBlocks, messageNonce}}`;
const pendingRecord = await axios
.post(url, {
query,
Expand Down Expand Up @@ -238,27 +241,70 @@ export class DataworkerService implements OnModuleInit {
}
}

async updateConfirmedBlock(url: string, id: string, confirmInfo: string) {
const mutation = `mutation {updateConfirmedBlock( id: \"${id}\", block: \"${confirmInfo}\")}`;
async updateConfirmedBlock(url: string, id: string, relayer: string, confirmInfo: string, wallet: EthereumConnectedWallet) {
const now = Math.floor(Date.now() / 1000);
const signature = await this.signMessage(wallet, confirmInfo, now);
const mutation = `mutation {signConfirmedBlock( id: \"${id}\", relayer: \"${relayer}\" block: \"${confirmInfo}\", timestamp: ${now}, signature: \"${signature}\")}`;
await axios.post(url, {
query: mutation,
variables: null,
});
}

async signMessage(
wallet: EthereumConnectedWallet,
message: string,
timestamp: number
) {
const messageHash = ethers.solidityPackedKeccak256(['uint256', 'string'], [timestamp, message]);
return await wallet.wallet.signMessage(ethers.getBytes(messageHash));
}

async sendHeartBeat(
url: string,
fromChainId: number,
toChainId: number,
relayer: string,
tokenAddress: string,
softTransferLimit: bigint,
version: string
version: string,
wallet: EthereumConnectedWallet
) {
if (version !== "lnv3") {
version = "lnv2";
}
const mutation = `mutation {lnBridgeHeartBeat( version: \"${version}\", fromChainId: \"${fromChainId}\", toChainId: \"${toChainId}\", relayer: \"${relayer}\", tokenAddress: \"${tokenAddress}\", softTransferLimit: \"${softTransferLimit}\")}`;

const now = Math.floor(Date.now() / 1000);
const signature = await this.signMessage(wallet, `${softTransferLimit}`, now);
const mutation = `mutation {signHeartBeat( version: \"${version}\", fromChainId: \"${fromChainId}\", toChainId: \"${toChainId}\", relayer: \"${relayer}\", tokenAddress: \"${tokenAddress}\", softTransferLimit: \"${softTransferLimit}\", timestamp: ${now}, signature: \"${signature}\")}`;
await axios.post(url, {
query: mutation,
variables: null,
});
}

async signDynamicBaseFee(
url: string,
fromChainId: number,
toChainId: number,
relayer: string,
tokenAddress: string,
dynamicFee: bigint,
version: string,
wallet: EthereumConnectedWallet
) {
if (version !== "lnv3") {
version = "lnv2";
}

const now = Math.floor(Date.now() / 1000);
const dynamicFeeExpire = now + this.dynamicFeeExpiredTime;
const messageHash = ethers.solidityPackedKeccak256(['uint112', 'uint64'], [dynamicFee, dynamicFeeExpire]);
const dynamicFeeSignature = await wallet.wallet.signMessage(ethers.getBytes(messageHash));
const message = `${dynamicFee}:${dynamicFeeExpire}:${dynamicFeeSignature}`;
const signature = await this.signMessage(wallet, message, now);

const mutation = `mutation {signDynamicFee( version: \"${version}\", fromChainId: \"${fromChainId}\", toChainId: \"${toChainId}\", relayer: \"${relayer}\", tokenAddress: \"${tokenAddress}\", dynamicFee: \"${dynamicFee}\", dynamicFeeExpire: \"${dynamicFeeExpire}\", dynamicFeeSignature: \"${dynamicFeeSignature}\", timestamp: ${now}, signature: \"${signature}\")}`;
await axios.post(url, {
query: mutation,
variables: null,
Expand All @@ -273,7 +319,8 @@ export class DataworkerService implements OnModuleInit {
fromProvider: EthereumProvider,
toProvider: EthereumProvider,
reorgThreshold: number,
notSupport1559: boolean
notSupport1559: boolean,
wallet
): Promise<ValidInfo> {
// 1. tx must be finalized
const transactionInfo = await fromProvider.checkPendingTransaction(
Expand All @@ -293,7 +340,9 @@ export class DataworkerService implements OnModuleInit {
await this.updateConfirmedBlock(
url,
record.id,
`${confirmedBlock}/${reorgThreshold}`
record.relayer,
`${confirmedBlock}/${reorgThreshold}`,
wallet
);
}
return {
Expand Down
130 changes: 90 additions & 40 deletions src/relayer/relayer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ export class LnProviderInfo {
fromToken: Erc20Contract;
withdrawLiquidityAmountThreshold: number;
withdrawLiquidityCountThreshold: number;
useDynamicBaseFee: boolean;
}

export class LnBridge {
isProcessing: boolean;
fromBridge: BridgeConnectInfo;
toBridge: BridgeConnectInfo;
safeWalletRole: string;
Expand All @@ -72,15 +72,17 @@ export class LnBridge {
export class RelayerService implements OnModuleInit {
private readonly logger = new Logger("relayer");
private readonly scheduleInterval = 10000;
private readonly waitingPendingTime = 12; // 2 minute
private readonly scheduleAdjustFeeInterval = 8640; // 1day
private readonly maxWaitingPendingTimes = 180;
private readonly heartBeatInterval = 6; // 1 minute
private readonly heartBeatInterval = 12; // 2 minute
private readonly withdrawLiqudityInterval = 2160; // 6 hour
private readonly updateDynamicFeeInterval = 60; // 10 min
private chainInfos = new Map();
private lnBridges: LnBridge[];
public store: Store;

private timer = new Map();

constructor(
protected taskService: TasksService,
protected dataworkerService: DataworkerService,
Expand All @@ -93,34 +95,44 @@ export class RelayerService implements OnModuleInit {
this.initConfigure();
this.store = new Store(this.configureService.storePath);
this.chainInfos.forEach((value, key) => {
this.timer.set(key, {
lastAdjustTime: 0,
lastWithdrawLiqudity: 0,
lastUpdateDynamicFeeInterval: 0,
isProcessing: false
});

this.taskService.addScheduleTask(
`${key}-lnbridge-relayer`,
this.scheduleInterval,
async () => {
this.adjustClock(value);
const timer = this.timer.get(key);
if (timer.isProcessing) {
return;
}
timer.isProcessing = true;
for (let item of this.lnBridges.values()) {
if (item.toBridge.chainInfo.chainName !== key) {
continue;
}
if (item.isProcessing) {
return;
}
item.isProcessing = true;
try {
const txPending = await this.relay(
item,
value.lastAdjustTime === 0,
value.lastWithdrawLiqudity === 0
timer.lastAdjustTime === 0,
timer.lastWithdrawLiqudity === 0,
timer.lastUpdateDynamicFeeInterval === 0
);
if (txPending) {
item.isProcessing = false;
timer.isProcessing = false;
this.adjustClock(key);
return;
}
} catch (err) {
this.logger.warn(`relay bridge failed, err: ${err}`);
}
item.isProcessing = false;
}
timer.isProcessing = false;
this.adjustClock(key);
}
);
});
Expand Down Expand Up @@ -154,8 +166,6 @@ export class RelayerService implements OnModuleInit {
tokens: chainInfo.tokens,
txHashCache: "",
checkTimes: 0,
lastAdjustTime: this.scheduleAdjustFeeInterval,
lastWithdrawLiqudity: this.withdrawLiqudityInterval,
},
];
})
Expand Down Expand Up @@ -275,12 +285,12 @@ export class RelayerService implements OnModuleInit {
swapRate: token.swapRate,
withdrawLiquidityAmountThreshold: token.withdrawLiquidityAmountThreshold,
withdrawLiquidityCountThreshold: token.withdrawLiquidityCountThreshold,
useDynamicBaseFee: token.useDynamicBaseFee,
};
})
.filter((item) => item !== null);

return {
isProcessing: false,
safeWalletRole: config.safeWalletRole,
minProfit: config.minProfit,
maxProfit: config.maxProfit,
Expand Down Expand Up @@ -462,18 +472,24 @@ export class RelayerService implements OnModuleInit {
return false;
}

private adjustClock(chainInfo) {
chainInfo.lastAdjustTime += 1;
chainInfo.lastWithdrawLiqudity += 1;
if (chainInfo.lastAdjustTime >= this.scheduleAdjustFeeInterval) {
chainInfo.lastAdjustTime = 0;
private adjustClock(key: string) {
let timer = this.timer.get(key);

timer.lastAdjustTime += 1;
timer.lastWithdrawLiqudity += 1;
timer.lastUpdateDynamicFeeInterval += 1;
if (timer.lastAdjustTime >= this.scheduleAdjustFeeInterval) {
timer.lastAdjustTime = 0;
}
if (timer.lastWithdrawLiqudity >= this.withdrawLiqudityInterval) {
timer.lastWithdrawLiqudity = 0;
}
if (chainInfo.lastWithdrawLiqudity >= this.withdrawLiqudityInterval) {
chainInfo.lastWithdrawLiqudity = 0;
if (timer.lastUpdateDynamicFeeInterval >= this.updateDynamicFeeInterval) {
timer.lastUpdateDynamicFeeInterval = 0;
}
}

async relay(bridge: LnBridge, needAdjustFee: boolean, needWithdrawLiqudity: boolean) {
async relay(bridge: LnBridge, needAdjustFee: boolean, needWithdrawLiqudity: boolean, needUpdateDynamicFee: boolean) {
// checkPending transaction
const toChainInfo = bridge.toBridge.chainInfo;
const fromChainInfo = bridge.fromBridge.chainInfo;
Expand Down Expand Up @@ -509,6 +525,8 @@ export class RelayerService implements OnModuleInit {
);
} catch(e) {
// ignore error
// this time don't send heartbeat
continue;
}
await this.dataworkerService.sendHeartBeat(
this.configureService.indexer,
Expand All @@ -518,6 +536,7 @@ export class RelayerService implements OnModuleInit {
lnProvider.fromAddress,
softTransferLimit,
bridge.bridgeType,
bridge.toWallet,
);
}
}
Expand All @@ -540,17 +559,53 @@ export class RelayerService implements OnModuleInit {
}
}

let nativeFeeUsed = BigInt(0);
// relay for each token configured
for (const lnProvider of bridge.lnProviders) {
if (needAdjustFee) {
let gasPrice = await toChainInfo.provider.feeData(
1,
toChainInfo.notSupport1559
);
const feeUsed = this.dataworkerService.relayFee(gasPrice);
if (lnProvider.useDynamicBaseFee && needUpdateDynamicFee) {
if (nativeFeeUsed <= 0) {
let gasPrice = await toChainInfo.provider.feeData(
1,
toChainInfo.notSupport1559
);
nativeFeeUsed = this.dataworkerService.relayFee(gasPrice);
}
const dynamicBaseFee = nativeFeeUsed * BigInt(lnProvider.swapRate);

let srcDecimals = 18;
if (lnProvider.fromAddress !== zeroAddress) {
srcDecimals = await lnProvider.fromToken.decimals();
}
// native fee decimals = 10**18
function nativeFeeToToken(fee: bigint): bigint {
return (
(fee *
BigInt((lnProvider.swapRate * 100).toFixed()) *
new Any(1, srcDecimals).Number) /
new Ether(100).Number
);
}
const baseFee = nativeFeeToToken(nativeFeeUsed + new Ether(bridge.minProfit).Number);
await this.dataworkerService.signDynamicBaseFee(
this.configureService.indexer,
fromChainInfo.chainId,
toChainInfo.chainId,
lnProvider.relayer,
lnProvider.fromAddress,
baseFee,
bridge.bridgeType,
bridge.toWallet);
} else if (needAdjustFee) {
if (nativeFeeUsed <= 0) {
let gasPrice = await toChainInfo.provider.feeData(
1,
toChainInfo.notSupport1559
);
nativeFeeUsed = this.dataworkerService.relayFee(gasPrice);
}
await this.adjustFee(
bridge,
feeUsed,
nativeFeeUsed,
fromBridgeContract,
fromChainInfo,
toChainInfo,
Expand Down Expand Up @@ -645,7 +700,8 @@ export class RelayerService implements OnModuleInit {
fromChainInfo.provider,
toChainInfo.provider,
bridge.reorgThreshold,
toChainInfo.notSupport1559
toChainInfo.notSupport1559,
bridge.toWallet
);

if (!validInfo.isValid) {
Expand Down Expand Up @@ -765,15 +821,9 @@ export class RelayerService implements OnModuleInit {
await this.dataworkerService.updateConfirmedBlock(
this.configureService.indexer,
record.id,
`${tx.hash}`
);
await this.adjustFee(
bridge,
validInfo.feeUsed,
fromBridgeContract,
fromChainInfo,
toChainInfo,
lnProvider
record.relayer,
`${tx.hash}`,
bridge.toWallet
);
}
return true;
Expand Down

0 comments on commit 20c4f44

Please sign in to comment.