门限签名
通过(t, n)门限签名,即可将一个秘密(私钥)分成n份,任意 t+1个成员可以通过多轮交互,在不重构私钥的情况下,生成一个有效的签名。
目前主要采用GG18门限签名方案,其密码生成阶段需要5轮交互,签名阶段需要10轮交互。
Anyswap
Anyswap是去中心化的跨链协议,基于Fusion
DCRM技术。Fushion的DCRM(Distributed Control Right Management) 技术主要用来解决跨链的互操作性,主要采用基于MPC的ECDSA和EdDSA门限签名技术, 实现分布式密钥生成和交易签名。
Anyswap 是Fushion的一个由DCRM
节点组成的侧链网络,最开始有5个节点,后续将扩展到10个以上。
技术架构
跨链桥
跨链桥采用的是铸造-销毁模型,以BNB实现BSC -> OEC 跨链为例:
-
用户在BSC上发起转账交易:https://bscscan.com/tx/0x159ca1687828dafc5de4439c4192813911dc54f87f0c8c3935702de63d4182ef
-
mpc地址: 0x63a3d28bb9187809553dd16981c73f498b6b2687在OEC发起
Swapin
交易: https://www.oklink.com/oec/tx/0xCFBC6859EEA694797FA6ED6C4052FC4C017CCF06F32C6791E2BF889A34E39A93
function Swapin(bytes32 txhash, address account, uint256 amount) public onlyAuth returns (bool) {
_mint(account, amount);
emit LogSwapin(txhash, account, amount);
return true;
}
合约信息: https://www.oklink.com/oec/address/0x218c3c3d49d0e7b37aff0d8bb079de36ae61a4c0
反向操作OEC -> BSC:
-
用户在OEC 上发起交易,调用
Swapout
: https://www.oklink.com/oec/address/0x218c3c3d49d0e7b37aff0d8bb079de36ae61a4c0function Swapout(uint256 amount, address bindaddr) public returns (bool) { require(!_vaultOnly, "AnyswapV4ERC20: onlyAuth"); require(bindaddr != address(0), "AnyswapV3ERC20: address(0x0)"); _burn(msg.sender, amount); emit LogSwapout(msg.sender, bindaddr, amount); return true; }
- MPC地址:0x63a3d28bb9187809553dd16981c73f498b6b2687, 在BSC发起转账交易: https://bscscan.com/tx/0x12215f7c44dcacc19e4a6943d03b770dc34db6ea1ce85f9716b0c6b4079507d2
多链路由
多链路由支持原生对原生token的跨链,采用流动性池模型。
源链交易
由用户
发起的交易,调用anySwaputUnderlying
方法:
https://bscscan.com/tx/0x7ffb4944ca3d258966b44c541d798946afe64e833ba7cfbabcc9f18d80ac09af
// Swaps `amount` `token` from this chain to `toChainID` chain with recipient `to` by minting with `underlying`
function anySwapOutUnderlying(address token, address to, uint amount, uint toChainID) external {
TransferHelper.safeTransferFrom(AnyswapV1ERC20(token).underlying(), msg.sender, token, amount);
AnyswapV1ERC20(token).depositVault(amount, msg.sender);
_anySwapOut(msg.sender, token, to, amount, toChainID);
}
源码:https://gist.github.com/zhaojun-sh/0df8429d52ae7d71b6d1ff5e8f0050dc#file-anyswaprouterv4-sol-L239
目标链交易
由MPC
地址发起的交易,调用anySwapInAuto
方法:
https://polygonscan.com/tx/0x0f4aafaa869703525cad458f1c85f850a5ee7d9876a9c208a395bf9df33e2012
// swaps `amount` `token` in `fromChainID` to `to` on this chainID with `to` receiving `underlying` if possible
function anySwapInAuto(bytes32 txs, address token, address to, uint amount, uint fromChainID) external onlyMPC {
_anySwapIn(txs, token, to, amount, fromChainID);
AnyswapV1ERC20 _anyToken = AnyswapV1ERC20(token);
address _underlying = _anyToken.underlying();
if (_underlying != address(0) && IERC20(_underlying).balanceOf(token) >= amount) {
_anyToken.withdrawVault(to, amount, to);
}
}
源码:https://gist.github.com/zhaojun-sh/0df8429d52ae7d71b6d1ff5e8f0050dc#file-anyswaprouterv4-sol-L304
若目标链流动性不足,则采用铸造方式。
更新MPC地址
https://bscscan.com/address/0x2a038e100f8b85df21e4d44121bdbfe0c288a869
https://polygonscan.com/address/0x2a038e100f8b85df21e4d44121bdbfe0c288a869
https://ftmscan.com/address/0x2a038e100f8b85df21e4d44121bdbfe0c288a869
function changeMPC(address newMPC) public onlyMPC returns (bool) {
require(newMPC != address(0), "AnyswapV3Router: address(0x0)");
_oldMPC = mpc();
_newMPC = newMPC;
_newMPCEffectiveTime = block.timestamp + 2*24*3600;
emit LogChangeMPC(_oldMPC, _newMPC, _newMPCEffectiveTime, cID());
return true;
}
安全攻击
2021年7月12日,Anyswap遭黑客攻击,损失超过787万美元。被攻击原因为两个交易采用相同的R值签名,黑客从而可以反推出MPC账户私钥。
对于签名: 选择随机数$k$, 计算$R = kG, r= R_x, s= k^{-1}(z+ rd_A)$, 对于两个采用相同随机数的签名:$(r, s)$ 和 $(r, s')$, 对应的签名消息分别为:$z$ 和 $z'$, 可以得到:
即为可以计算出的私钥。
漏洞修复:历史的签名会保存在数据库中,新生成的签名随机数判断是否重复。
https://github.com/anyswap/CrossChain-Router/commit/ac88cc6861d98645fcdc19d8b985689dd54f5c22
流程分析
ScanJob
CrossChain-Bridge
启动时会开始扫描任务:
// StartScanJob scan job
func StartScanJob(isServer bool) {
srcChainCfg := tokens.SrcBridge.GetChainConfig()
if srcChainCfg.EnableScan && btc.BridgeInstance != nil {
go btc.BridgeInstance.StartChainTransactionScanJob()
if srcChainCfg.EnableScanPool {
go btc.BridgeInstance.StartPoolTransactionScanJob()
}
go btc.BridgeInstance.StartSwapHistoryScanJob()
}
}
连接BTC运行的节点,
// StartChainTransactionScanJob scan job
func (b *Bridge) StartChainTransactionScanJob() {
go b.StartPoolTransactionScanJob() // 扫描mempool 的交易
chainName := b.ChainConfig.BlockChain
log.Infof("[scanchain] start %v scan chain job", chainName)
start, latest := b.getStartAndLatestHeight() //确定起始扫描到和最新的块
_ = tools.UpdateLatestScanInfo(b.IsSrc, start)
log.Infof("[scanchain] start %v scan chain loop from %v latest=%v", chainName, start, latest)
chainCfg := b.GetChainConfig()
confirmations := *chainCfg.Confirmations
stable := start
errorSubject := fmt.Sprintf("[scanchain] get %v block failed", chainName)
scanSubject := fmt.Sprintf("[scanchain] scanned %v block", chainName)
for {
latest := tools.LoopGetLatestBlockNumber(b)
for h := stable + 1; h <= latest; {
blockHash, err := b.GetBlockHash(h) // 从节点获取区块
if err != nil {
log.Error(errorSubject, "height", h, "err", err)
time.Sleep(retryIntervalInScanJob)
continue
}
if scannedBlocks.IsBlockScanned(blockHash) {
h++
continue
}
txids, err := b.GetBlockTxids(blockHash)
if err != nil {
log.Error(errorSubject, "height", h, "blockHash", blockHash, "err", err)
time.Sleep(retryIntervalInScanJob)
continue
}
for _, txid := range txids {
b.processTransaction(txid)
}
scannedBlocks.CacheScannedBlock(blockHash, h)
log.Info(scanSubject, "blockHash", blockHash, "height", h, "txs", len(txids))
h++
}
if stable+confirmations < latest {
stable = latest - confirmations // 对于未固化的块,会重新扫描
_ = tools.UpdateLatestScanInfo(b.IsSrc, stable)
}
time.Sleep(restIntervalInScanJob)
}
}
对于扫描到交易,解析后存储到数据库中。
func (b *Bridge) processSwapin(txid string) {
if tools.IsSwapExist(txid, PairID, "", true) {
return
}
swapInfo, err := b.verifySwapinTx(PairID, txid, true)
tools.RegisterSwapin(txid, []*tokens.TxSwapInfo{swapInfo}, []error{err}) //数据存储
}
存储到mongoDB后的数据为:
// MgoSwap registered swap
type MgoSwap struct {
Key string `bson:"_id"` // txid + pairid + bind
PairID string `bson:"pairid"`
TxID string `bson:"txid"` // 交易id
TxTo string `bson:"txto"`
TxType uint32 `bson:"txtype"` // SwapinTx
Bind string `bson:"bind"` //绑定到目标链的地址
Status SwapStatus `bson:"status"`
InitTime int64 `bson:"inittime"`
Timestamp int64 `bson:"timestamp"`
Memo string `bson:"memo"`
}
SwapJob
启动SwapJob
任务:
// StartSwapJob swap job
func StartSwapJob() {
swapinNonces, swapoutNonces := mongodb.LoadAllSwapNonces()
if tokens.DstNonceSetter != nil {
tokens.DstNonceSetter.InitNonces(swapinNonces)
}
if tokens.SrcNonceSetter != nil {
tokens.SrcNonceSetter.InitNonces(swapoutNonces)
}
for _, pairCfg := range tokens.GetTokenPairsConfig() {
AddSwapJob(pairCfg)
}
mongodb.MgoWaitGroup.Add(2)
go startSwapinSwapJob()
go startSwapoutSwapJob()
}
从MongoDB读取MgoSwap
进行处理:
func processSwap(swap *mongodb.MgoSwap, isSwapin bool) (err error) {
pairID := swap.PairID
txid := swap.TxID
bind := swap.Bind
cacheKey := getSwapCacheKey(isSwapin, txid, bind) // 判断是否已经被处理
if cachedSwapTasks.Contains(cacheKey) {
return errAlreadySwapped
}
res, err := mongodb.FindSwapResult(isSwapin, txid, pairID, bind)
if err != nil {
return err
}
err = preventReswap(res, isSwapin)
if err != nil {
return err
}
dcrmAddress, err := checkSwapResult(res, isSwapin)
if err != nil {
return err
}
logWorker("swap", "start process swap", "pairID", pairID, "txid", txid, "bind", bind, "status", swap.Status, "isSwapin", isSwapin, "value", res.Value)
srcBridge := tokens.GetCrossChainBridge(isSwapin)
// 重复进行验证
swapInfo, err := verifySwapTransaction(srcBridge, pairID, txid, bind, tokens.SwapTxType(swap.TxType))
if err != nil {
return fmt.Errorf("[doSwap] reverify swap failed, %w", err)
}
if swapInfo.Value.String() != res.Value {
return fmt.Errorf("[doSwap] reverify swap value mismatch, in db %v != %v", res.Value, swapInfo.Value)
}
if !strings.EqualFold(swapInfo.Bind, bind) {
return fmt.Errorf("[doSwap] reverify swap bind address mismatch, in db %v != %v", bind, swapInfo.Bind)
}
swapType := getSwapType(isSwapin)
args := &tokens.BuildTxArgs{
SwapInfo: tokens.SwapInfo{
Identifier: params.GetIdentifier(),
PairID: pairID,
SwapID: txid,
SwapType: swapType,
TxType: tokens.SwapTxType(swap.TxType),
Bind: bind,
Reswapping: res.Status == mongodb.Reswapping,
},
From: dcrmAddress,
OriginValue: swapInfo.Value,
}
return dispatchSwapTask(args)
}
构建交易,数据结构为:
// BuildTxArgs struct
type BuildTxArgs struct {
SwapInfo `json:"swapInfo,omitempty"` // 包含Bind 地址
From string `json:"from,omitempty"` //Dcrm 地址
To string `json:"to,omitempty"` //
Value *big.Int `json:"value,omitempty"` // 金额
OriginValue *big.Int `json:"originValue,omitempty"`
SwapValue *big.Int `json:"swapvalue,omitempty"`
Memo string `json:"memo,omitempty"`
Input *[]byte `json:"input,omitempty"`
Extra *AllExtras `json:"extra,omitempty"`
}
通过chan
传递 BuildTxArgs
继续处理任务:
func processSwapTask(swapChan <-chan *tokens.BuildTxArgs, dcrmAddress string, isSwapin bool) {
defer utils.TopWaitGroup.Done()
for {
select {
case <-utils.CleanupChan:
logWorker("doSwap", "stop process swap task", "isSwapin", isSwapin, "dcrmAddress", dcrmAddress)
return
case args := <-swapChan:
if !strings.EqualFold(args.From, dcrmAddress) || args.SwapType != getSwapType(isSwapin) {
logWorkerWarn("doSwap", "ignore swap task as mismatch reason", "isSwapin", isSwapin, "dcrmAddress", dcrmAddress, "args", args)
continue
}
err := doSwap(args)
switch {
case err == nil,
errors.Is(err, errAlreadySwapped):
default:
logWorkerError("doSwap", "process failed", err, "pairID", args.PairID, "txid", args.SwapID, "swapType", args.SwapType.String(), "value", args.OriginValue)
}
}
}
}
开始执行签名过程,若配置文件有DCRM私钥,则直接在本地签名,否则开始进行DCRM MPC 签名:
func doSwap(args *tokens.BuildTxArgs) (err error) {
pairID := args.PairID
txid := args.SwapID
bind := args.Bind
swapType := args.SwapType
isSwapin := swapType == tokens.SwapinType
resBridge := tokens.GetCrossChainBridge(!isSwapin)
cacheKey := getSwapCacheKey(isSwapin, txid, bind)
err = checkAndUpdateProcessSwapTaskCache(cacheKey)
if err != nil {
return err
}
logWorker("doSwap", "add swap cache", "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "value", args.OriginValue)
isCachedSwapProcessed := false
defer func() {
if !isCachedSwapProcessed {
logWorkerError("doSwap", "delete swap cache", err, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "value", args.OriginValue)
cachedSwapTasks.Remove(cacheKey)
}
}()
logWorker("doSwap", "start to process", "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "value", args.OriginValue)
rawTx, err := resBridge.BuildRawTransaction(args)
if err != nil {
logWorkerError("doSwap", "build tx failed", err, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin)
return err
}
swapNonce := args.GetTxNonce()
var signedTx interface{}
var signTxHash string
tokenCfg := resBridge.GetTokenConfig(pairID)
for i := 1; i <= 3; i++ { // with retry
if tokenCfg.GetDcrmAddressPrivateKey() != nil {
signedTx, signTxHash, err = resBridge.SignTransaction(rawTx, pairID) // 直接本地签名
} else {
signedTx, signTxHash, err = resBridge.DcrmSignTransaction(rawTx, args.GetExtraArgs()) // 采用DCRM签名
}
if err == nil {
break
}
logWorkerError("doSwap", "sign tx failed", err, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "signCount", i)
restInJob(retrySignInterval)
}
if err != nil {
return err
}
// recheck reswap before update db
res, err := mongodb.FindSwapResult(isSwapin, txid, pairID, bind)
if err != nil {
return err
}
err = preventReswap(res, isSwapin)
if err != nil {
return err
}
// update database before sending transaction
matchTx := &MatchTx{
SwapTx: signTxHash,
SwapType: swapType,
SwapNonce: swapNonce,
}
if args.SwapValue != nil {
matchTx.SwapValue = args.SwapValue.String()
} else {
matchTx.SwapValue = tokens.CalcSwappedValue(pairID, args.OriginValue, isSwapin).String()
}
err = updateSwapResult(txid, pairID, bind, matchTx)
if err != nil {
logWorkerError("doSwap", "update swap result failed", err, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin)
return err
}
isCachedSwapProcessed = true
err = mongodb.UpdateSwapStatus(isSwapin, txid, pairID, bind, mongodb.TxProcessed, now(), "")
if err != nil {
logWorkerError("doSwap", "update swap status failed", err, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin)
return err
}
txHash, err := sendSignedTransaction(resBridge, signedTx, args) //广播交易,最终调用:eth_sendRawTransaction
if err == nil {
logWorker("doSwap", "send tx success", "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "swapNonce", swapNonce, "txHash", txHash)
if txHash != signTxHash {
logWorkerError("doSwap", "send tx success but with different hash", errSendTxWithDiffHash, "pairID", pairID, "txid", txid, "bind", bind, "isSwapin", isSwapin, "swapNonce", swapNonce, "txHash", txHash, "signTxHash", signTxHash)
_ = replaceSwapResult(txid, pairID, bind, txHash, matchTx.SwapValue, isSwapin)
}
}
return err
}
进行MPC 签名:
// DoSign dcrm sign msgHash with context msgContext
func DoSign(signPubkey string, msgHash, msgContext []string) (keyID string, rsvs []string, err error) {
if !params.IsDcrmEnabled() {
return "", nil, errSignIsDisabled
}
log.Debug("dcrm DoSign", "msgHash", msgHash, "msgContext", msgContext)
if signPubkey == "" {
return "", nil, errSignWithoutPublickey
}
for i := 0; i < retrySignLoop; i++ {
for _, dcrmNode := range allInitiatorNodes { // 参与MPC的节点
if err = pingDcrmNode(dcrmNode); err != nil {
continue
}
signGroupsCount := int64(len(dcrmNode.signGroups))
// randomly pick first subgroup to sign
randIndex, _ := rand.Int(rand.Reader, big.NewInt(signGroupsCount))
startIndex := randIndex.Int64()
i := startIndex
for {
keyID, rsvs, err = doSignImpl(dcrmNode, i, signPubkey, msgHash, msgContext)
if err == nil {
return keyID, rsvs, nil
}
i = (i + 1) % signGroupsCount
if i == startIndex {
break
}
}
}
time.Sleep(2 * time.Second)
}
log.Warn("dcrm DoSign failed", "msgHash", msgHash, "msgContext", msgContext, "err", err)
return "", nil, errDoSignFailed
}
调用MPC 节点进行签名:
// Sign call sign
func Sign(raw, rpcAddr string) (string, error) {
var result DataResultResp
err := httpPostTo(&result, rpcAddr, "sign", raw)
if err != nil {
return "", wrapPostError("sign", err)
}
if result.Status != successStatus {
return "", newWrongStatusError("sign", result.Status, result.Error)
}
return result.Data.Result, nil
}
AcceptSignJob
获取需要进行的签名信息:
func startAcceptProducer() {
i := 0
for {
signInfo, err := dcrm.GetCurNodeSignInfo(maxAcceptSignTimeInterval) //通过接口调用获取签名
if err != nil {
logWorkerError("accept", "getCurNodeSignInfo failed", err)
time.Sleep(retryInterval)
continue
}
i++
if i%7 == 0 {
logWorker("accept", "getCurNodeSignInfo", "count", len(signInfo))
}
for _, info := range signInfo {
if utils.IsCleanuping() {
return
}
if info == nil { // maybe a dcrm RPC problem
continue
}
keyID := info.Key
if cachedAcceptInfos.Contains(keyID) {
logWorkerTrace("accept", "ignore cached accept sign info before dispatch", "keyID", keyID)
continue
}
logWorker("accept", "dispatch accept sign info", "keyID", keyID)
acceptInfoCh <- info // produce //
}
if utils.IsCleanuping() {
return
}
time.Sleep(waitInterval)
}
}
处理签名信息, 确定是否进行签名:
func processAcceptInfo(info *dcrm.SignInfoData) {
defer atomic.AddInt64(&curAcceptRoutines, -1)
keyID := info.Key
if !checkAndUpdateCachedAcceptInfoMap(keyID) {
return
}
isProcessed := false
defer func() {
if !isProcessed {
cachedAcceptInfos.Remove(keyID)
}
}()
args, err := verifySignInfo(info) // 进行签名信息验证,会调用链上全节点的信息
ctx := []interface{}{
"keyID", keyID,
}
if args != nil {
ctx = append(ctx,
"identifier", args.Identifier,
"swaptype", args.SwapType.String(),
"pairID", args.PairID,
"swapID", args.SwapID,
"bind", args.Bind,
)
}
switch {
case errors.Is(err, tokens.ErrTxNotStable),
errors.Is(err, tokens.ErrTxNotFound),
errors.Is(err, tokens.ErrRPCQueryError):
ctx = append(ctx, "err", err)
logWorkerTrace("accept", "ignore sign", ctx...)
return
case errors.Is(err, errIdentifierMismatch):
ctx = append(ctx, "err", err)
logWorkerTrace("accept", "discard sign", ctx...)
isProcessed = true
return
case errors.Is(err, errInitiatorMismatch),
errors.Is(err, errWrongMsgContext),
errors.Is(err, tokens.ErrUnknownPairID),
errors.Is(err, tokens.ErrNoBtcBridge):
ctx = append(ctx, "err", err)
logWorker("accept", "discard sign", ctx...)
isProcessed = true
return
}
agreeResult := acceptAgree
if err != nil {
logWorkerError("accept", "DISAGREE sign", err, ctx...)
agreeResult = acceptDisagree
}
ctx = append(ctx, "result", agreeResult)
res, err := dcrm.DoAcceptSign(keyID, agreeResult, info.MsgHash, info.MsgContext) // 接受签名
if err != nil {
ctx = append(ctx, "rpcResult", res)
logWorkerError("accept", "accept sign job failed", err, ctx...)
} else {
logWorker("accept", "accept sign job finish", ctx...)
isProcessed = true
}
}
接受签名后的处理:
// DoAcceptSign accept sign
func DoAcceptSign(keyID, agreeResult string, msgHash, msgContext []string) (string, error) {
nonce := uint64(0)
data := AcceptData{
TxType: "ACCEPTSIGN",
Key: keyID,
Accept: agreeResult,
MsgHash: msgHash,
//MsgContext: msgContext, // context is verified on top level
TimeStamp: common.NowMilliStr(),
}
payload, err := json.Marshal(data)
if err != nil {
return "", err
}
rawTX, err := BuildDcrmRawTx(nonce, payload, defaultDcrmNode.keyWrapper)
if err != nil {
return "", err
}
return AcceptSign(rawTX)
}
调用acceptSign
接口, 执行MPC签名.
// AcceptSign call acceptSign
func AcceptSign(raw string) (string, error) {
var result DataResultResp
err := httpPost(&result, "acceptSign", raw)
if err != nil {
return "", wrapPostError("acceptSign", err)
}
if result.Status != successStatus {
return "", newWrongStatusError("acceptSign", result.Status, result.Error)
}
return result.Data.Result, nil
}
参考
https://github.com/anyswap/Anyswap-Audit/blob/master/whitepaper/Anyswap-whitepaper.pdf
https://anyswap.exchange/#/router
https://github.com/anyswap/CrossChain-Bridge
https://github.com/anyswap/mBTC
https://anyswap-faq.readthedocs.io/en/latest/
https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
https://anyswap.medium.com/anyswap-multichain-router-v3-exploit-statement-6833f1b7e6fb
网友评论