以太坊侧链跨链转账实现原理之充值交易

《亦来云的侧链侧链白皮书》从理论方面介绍了亦来云的跨链转账原理,本文就从亦来云的以太坊侧链的源码层来看下具体的实现。

跨链转账的过程分为主链到侧链的转账和侧链到主链的转账,这里主链就是指跑ELA的主链,侧链就专指以太坊侧链。
另外,主链到侧链的转账我称为充值交易,侧链到主链的转账称为提现交易。现在就按这两部分来看下具体实现。

充值交易(主链到侧链的转账)

亦来云充值交易是直接基于SPV(Simplified Payment Verification,简单交易验证)来实现的,SPV的原理就不多介绍了,只要了解SPV的数据是可信的,就可以了。

所以只要相应的侧链集成了ELA主链的SPV模块,相应的侧链就可以同步到主链的交易数据。然后,相应侧链再根据相应的数据在自己链上生成一笔转账交易,就可以实现充值交易的过程。(听着是不是很简单^^)。

在亦来云跨链充值交易中,在主链生成的交易称为TX1,在侧链生成的交易称为TX2。过程如下:

1.用户通过钱包在主链从地址 U 向主链上代表侧链的地址 S 转账 n 个 ELA,并在交易中附加上自己在侧链的地址 u,发送到主链上,这个交易标记为 TX1.

2.等待主链挖矿将TX1打包,并成功广播到其他节点。

3.集成在侧链的SPV模块同步到包含TX1的区块.并通知侧链收到充值交易

4.侧链从TX1中解析出目标地址和转账金额,根据自己侧链的交易结构,构造出TX2,给目标地址转账。

5.等待侧链挖矿将TX2打包,并验证区块,出块成功,广播到其他共识节点。

6.等待足够的确认后,用户在钱包上看到的自己的侧链地址 u 入账了 n 个 ETH

过程知道了,我们就按这个过程看源码:

一。先看TX1附加的充值交易的数据结构:

type TransferCrossChainAsset struct {
    CrossChainAddresses []string         //侧链的目标地址
    OutputIndexes       []uint64         //对应tx output的下标值
    CrossChainAmounts   []common.Fixed64 //需要转账的金额
}

上面的结构体附加在TX1的payload字段中,表示此交易是个跨链交易. 具体字段意义看注释就行了。可以看到内容是数组切片,表示可以同时转多个地址。

我们再看下主链创建的TX1的代码

func createCrossChainTransaction(walletPath string, from string, fee common.Fixed64, lockedUntil uint32,
    crossChainOutputs ...*CrossChainOutput) (*types.Transaction, error) {
    // check output
    if len(crossChainOutputs) == 0 {
        return nil, errors.New("invalid transaction target")
    }

    outputs := make([]*OutputInfo, 0)
    perAccountFee := fee / common.Fixed64(len(crossChainOutputs))

    // create payload
    payload := &payload.TransferCrossChainAsset{}
    for index, output := range crossChainOutputs {
        payload.CrossChainAddresses = append(payload.CrossChainAddresses, output.CrossChainAddress)
        payload.OutputIndexes = append(payload.OutputIndexes, uint64(index))
        payload.CrossChainAmounts = append(payload.CrossChainAmounts, *output.Amount-perAccountFee)
        outputs = append(outputs, &OutputInfo{
            Recipient: output.Recipient,
            Amount:    output.Amount,
        })
    }
    

我们主要看下如何创建payload的,也就是TX1要附加的跨链数据,需要注意的是CrossChainAmounts 的数据是每个output的Amount减去平分的Fee得到的。下面在侧链的解析也是要注意的。

二。SPV侦听处理

SPV不过多介绍,需要说明的是SPV可以从主链同步各种类型的交易,如何判断某个交易是转给以太坊侧链的呢?所以需要给SPV一个唯一的地址表示侧链地址,供SPV过滤交易,同时也称为侦听地址。这个地址就根据相应侧链的创世区块的hash生成的。

有了这个侦听地址,并集成SPV模块,我们就可以同步主链的交易了。我们要收到充值交易需要在侧链端实现TransactionListener 这个接口并设置侦听地址给SPV模块。

type listener struct {
    address string   //侦听地址
    service spv.SPVService
}

func (l *listener) Address() string {
    return l.address
}

func (l *listener) Type() core.TxType {
    return core.TransferCrossChainAsset
}

func (l *listener) Flags() uint64 {
    return spv.FlagNotifyInSyncing | spv.FlagNotifyConfirmed
}

func (l *listener) Notify(id common.Uint256, proof bloom.MerkleProof, tx core.Transaction) {
    // Submit transaction receipt
    log.Info("========================================================================================")
    log.Info("mainchain transaction info")
    log.Info("----------------------------------------------------------------------------------------")
    log.Info(string(tx.String()))
    log.Info("----------------------------------------------------------------------------------------")
    savePayloadInfo(tx, l)
    l.service.SubmitTransactionReceipt(id, tx.Hash())// give spv service a receipt, Indicates receipt of notice
}

其中Notify就是我们收到针对本侧链的交易入口。收到这个侦听后,需要调用SubmitTransactionReceipt给SPV模块一个回执,表示侧链端已经收入通知,SPV模块将不会再进行通知

到这里我们已经到了充值交易的第3步--侧链收到充值交易,下面我们看第4步--侧链从TX1中解析出目标地址和转账金额,根据自己侧链的交易结构,构造出TX2,给目标地址转账

1.从TX1中解析出目标地址和转账金额

这部分的内容就是savePayloadInfo(tx, l)

//savePayloadInfo save and send spv perception
func savePayloadInfo(elaTx core.Transaction, l *listener) {
    nr := bytes.NewReader(elaTx.Payload.Data(elaTx.PayloadVersion))
    p := new(payload.TransferCrossChainAsset)
    p.Deserialize(nr, elaTx.PayloadVersion)
    var fees []string
    var address []string
    var outputs []string
    //从交易中解析出 手续费,转账金额,和目标地址,然后存在数据库中.
    for i, amount := range p.CrossChainAmounts {
        fees = append(fees, (elaTx.Outputs[i].Value - amount).String())
        outputs = append(outputs, elaTx.Outputs[i].Value.String())
        address = append(address, p.CrossChainAddresses[i])
    }
    addr := strings.Join(address, ",")
    fee := strings.Join(fees, ",")
    output := strings.Join(outputs, ",")
    if spvTxhash == elaTx.Hash().String() {
        return
    }
    spvTxhash = elaTx.Hash().String()
    err := spvTransactiondb.Put([]byte(elaTx.Hash().String()+"Fee"), []byte(fee))

    if err != nil {
        log.Error("SpvServicedb Put Fee: ", "err", err, "elaHash", elaTx.Hash().String())
    }

    err = spvTransactiondb.Put([]byte(elaTx.Hash().String()+"Address"), []byte(addr))

    if err != nil {
        log.Error("SpvServicedb Put Address: ", "err", err, "elaHash", elaTx.Hash().String())
    }
    err = spvTransactiondb.Put([]byte(elaTx.Hash().String()+"Output"), []byte(output))

    if err != nil {
        log.Error("SpvServicedb Put Output: ", "err", err, "elaHash", elaTx.Hash().String())
    }
    if atomic.LoadInt32(&candSend) == 1 {
        from := GetDefaultSingerAddr()
        IteratorUnTransaction(from)
        f, err := common.StringToFixed64(fees[0])
        if err != nil {
            log.Error("SpvSendTransaction Fee StringToFixed64: ", "err", err, "elaHash", elaTx)
            return

        }
        fe := new(big.Int).SetInt64(f.IntValue())
        y := new(big.Int).SetInt64(rate)
        fee := new(big.Int).Mul(fe, y)
        SendTransaction(from, elaTx.Hash().String(), fee)

    } else {
        UpTransactionIndex(elaTx.Hash().String())
    }
    return
}

代码很简单,这个函数主要功能就是从TX1的payload中解析出需要创建TX2所需要数据,包括手续费,金额,目标账户,其中手续费的解析要注意下:是用TX1的TXOUT的Value减去CrossChainAmounts里的值,和构造TX1的时候相对应。在解析完成后,分别以TX1的hash值为Key的前缀将这三个值存入数据库中,并根据canSend标志来决定是否构造TX2并广播交易,canSend标志是在另一个协程内根据出块的事件通道来设置的。看下面的代码:


//.....省略代码
if spvService, err := spv.NewService(spvCfg,client); err != nil {
        utils.Fatalf("SPV service init error: %v", err)
    } else {
        MinedBlockSub := stack.EventMux().Subscribe(events.MinedBlockEvent{})
        go spv.MinedBroadcastLoop(MinedBlockSub)
        spvService.Start()
        log.Info("Mainchain SPV module started successfully!")
    }
//.....省略代码

//minedBroadcastLoop Mining awareness, eth can initiate a recharge transaction after the block
func MinedBroadcastLoop(minedBlockSub *event.TypeMuxSubscription) {
    var i = 0

    for {
        select {
        case <-minedBlockSub.Chan():
            i++
            if i >= 2 {
                atomic.StoreInt32(&candSend, 1)
                IteratorUnTransaction(GetDefaultSingerAddr())
            }
        case <-time.After(3 * time.Minute):
            i = 0
            atomic.StoreInt32(&candSend, 0)
        }
    }

}

可以看到在启动SPV服务的时候就启动的侦听协程。并且每3分钟将i清0,防止i溢出。也就是本节点每出一个块,根据SPV数据库里的内容构造一次TX2,并发送出去。也就是TX2的构造是由当值节点创建并发送的。
我们就从IteratorUnTransaction开始看:

//IteratorUnTransaction iterates before mining and processes existing spv refill transactions
func IteratorUnTransaction(from ethCommon.Address) {
    muiterator.Lock()
    defer muiterator.Unlock()
    _, ok := blocksigner.Signers[from]  //from地址是否是签名者,也就是当值矿工,只有矿工才有资格发送这笔交易。
    if !ok {
        log.Error("error signers", from.String())
        return
    }

    if atomic.LoadInt32(&candIterator) == 1 {
        return//如果candIterator 为1,表示正在构造TX2,不能重复构造
    }
    atomic.StoreInt32(&candIterator, 1)//设置candIterator 为1
    go func(addr ethCommon.Address) {
        for {
            // 对 candSend 标志做判断.
            if atomic.LoadInt32(&candSend) == 0 {
                break
            }
            index := GetUnTransactionNum(spvTransactiondb, UnTransactionIndex)
            if index == missingNumber {//表示数据库里没内容,不需要构造交易
                break
            }
            seek := GetUnTransactionNum(spvTransactiondb, UnTransactionSeek)
            if seek == missingNumber {//获取需要构造的交易.
                seek = 1
            }
            if seek == index {
                break//表示已经构造过了。
            }
            txHash, err := spvTransactiondb.Get(append([]byte(UnTransaction), encodeUnTransactionNumber(seek)...))
            if err != nil {
                log.Error("get UnTransaction ", "err", err, "seek", seek)
                break
            }
            fee, _, _ := FindOutputFeeAndaddressByTxHash(string(txHash))
            if fee.Uint64() <= 0 {
                break
            }//根据从数据库里取出的txHash,fee构造TX2,并发送交易.
            SendTransaction(from, string(txHash), fee)
            err = spvTransactiondb.Put([]byte(UnTransactionSeek), encodeUnTransactionNumber(seek+1))//更新seek值。
            log.Info(UnTransactionSeek+"put", "seek", seek+1)
            if err != nil {
                log.Error("UnTransactionIndexPutSeek ", err, seek+1)
                break
            }
            err = spvTransactiondb.Delete(append([]byte(UnTransaction), encodeUnTransactionNumber(seek)...))//从数据库中删除已发送的TX1的hash值。
            log.Info(UnTransaction+"delete", "seek", seek)
            if err != nil {
                log.Error("UnTransactionIndexDeleteSeek ", "err", err, "seek", seek)
                break
            }
        }
        atomic.StoreInt32(&candIterator, 0)//要设置candIterator为0.
    }(from)
}

//SendTransaction sends a reload transaction to txpool
func SendTransaction(from ethCommon.Address, elaTx string, fee *big.Int) {
    ethTx, err := ipcClient.StorageAt(context.Background(), ethCommon.Address{}, ethCommon.HexToHash("0x"+elaTx), nil)//根据TX1的hash值获取存储数据,因为TX1的hash值要做为TX2的data字段存在节点上。
    if err != nil {
        log.Error(fmt.Sprintf("IpcClient StorageAt: %v", err))
        return
    }
    h := ethCommon.Hash{}
    if ethCommon.BytesToHash(ethTx) != h {//如果ethTx不为空,表示已经处理过这个交易了.这种情况主要发生在同步到了由其他节点打包的TX2交易。
        log.Warn("Cross-chain transactions have been processed", "elaHash", elaTx)
        return
    }
    data, err := common.HexStringToBytes(elaTx)//将TX1的hash转为data字节.
    if err != nil {
        log.Error("elaTx HexStringToBytes: "+elaTx, "err", err)
        return
    }
    msg := ethereum.CallMsg{From: from, To: &ethCommon.Address{}, Data: data}
    gasLimit, err := ipcClient.EstimateGas(context.Background(), msg)//构造估算GAS MSG
    if err != nil {
        log.Error("IpcClient EstimateGas:", "err", err, "main txhash", elaTx)
        return
    }
    if gasLimit == 0 {
        return
    }

    price := new(big.Int).Quo(fee, new(big.Int).SetUint64(gasLimit))//根据手续费和gas花费,计算gasPrice;
    callmsg := ethereum.TXMsg{From: from, To: &ethCommon.Address{}, Gas: gasLimit, Data: data, GasPrice: price}//构造TX2
    hash, err := ipcClient.SendPublicTransaction(context.Background(), callmsg)//调用RPC发送交易,返回交易hash
    if err != nil {
        log.Error("IpcClient SendPublicTransaction: ", "err", err)
        return
    }
    log.Info("Cross chain Transaction", "elaTx", elaTx, "ethTh", hash.String())
}

代码相应的地方我做了注释,这段代码的意思就是当值超级节点出块的时候,判断spv数库中里是否有充值交易TX1,如果有就取出来,构造TX2交易,并发送出去。当然会有些条件判断,大家看代码和注释。

看到这里,大家可能会有疑问?

1.当值矿工初始并没有ETH,打包交易的手续费从哪里扣的?
2.手续费又是交给谁了?
3.其他节点同步到这个包括TX2交易的区块后,如何验证TX2有效性?
4.转账交易三要素,from:当值矿工,to:空地址,也叫黑洞地址,value:金额。发现这个TX2交易并未设置value?

在上面的代码中,是当值矿工将此MSG发送出去的,并且会将此MSG转为transaction并进入本地交易池.所以在此矿工打包的时候,会从交易池中取出此交易,会在EVM中执行交易,完成交易转换的函数是TransitionDb()

func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
    var (
        evm = st.evm
        // vm errors do not effect consensus and are therefor
        // not assigned to err, except for insufficient balance
        // error.
        vmerr         error
        snapshot      = evm.StateDB.Snapshot()
        blackaddr     common.Address
        blackcontract common.Address
    )

    msg := st.msg
    sender := vm.AccountRef(msg.From())
    contractCreation := msg.To() == nil//是否是布署合约
    txhash := hexutil.Encode(msg.Data())
    //recharge tx
    if len(msg.Data()) == 32 && msg.To() != nil && *msg.To() == blackaddr {
        fee, toaddr, output := spv.FindOutputFeeAndaddressByTxHash(txhash)//从spv模块查看是否有此交易,这也是验证其他节点同步过来的交易的方式。
        completetxhash := evm.StateDB.GetState(blackaddr, common.HexToHash(txhash))
        if toaddr != blackaddr {
        //completetxhash表示是否处理过此交易了。
            if (completetxhash == common.Hash{}) && output.Cmp(fee) > 0 {
            //跨链转账的时候,交易发送者(也是当值矿工)没有ETH,所以为了交易正常执行,先给当值矿工初值一些ETH,具体值可以设置
                st.state.AddBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))
                defer func() {
                //这是函数执行完后的处理.
                    ethfee := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice)
                    //手续费不足,或目标地址有问题,则回滚状态,交易执行失败,否则给当值矿工手续费 
                    if fee.Cmp(new(big.Int)) <= 0 || fee.Cmp(ethfee) < 0 || st.state.GetBalance(toaddr).Cmp(fee) < 0 || vmerr != nil {
                        ret = nil
                        usedGas = 0
                        failed = false
                        if err == nil {
                            err = ErrGasLimitReached
                        }
                        evm.StateDB.RevertToSnapshot(snapshot)
                        return
                    } else {
                        //给当值矿工手续费
                        st.state.AddBalance(st.msg.From(), fee)
                    }

                    //判断是否给预初始值是否增加成功.
                    if st.state.GetBalance(st.msg.From()).Cmp(new(big.Int).SetUint64(evm.ChainConfig().PassBalance)) < 0 {
                        ret = nil
                        usedGas = 0
                        failed = false
                        if err == nil {
                            err = ErrGasLimitReached
                        }
                        evm.StateDB.RevertToSnapshot(snapshot)
                    } else {//TX2执行成功后.给当值矿工的初始ETH减掉
                        st.state.SubBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))
                    }
                }()
            } else {
                return nil, 0, false, ErrMainTxHashPresence
            }
        } else {
            return nil, 0, false, ErrElaToEthAddress
        }
    } else if contractCreation {
        blackcontract = crypto.CreateAddress(sender.Address(), evm.StateDB.GetNonce(sender.Address()))
        //如果布署的合约是我们的提现合约,则也给当值矿工初始一些ETH
        if blackcontract.String() == evm.ChainConfig().BlackContractAddr {
            st.state.AddBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))
            defer func() {
                fromValue := st.state.GetBalance(st.msg.From())
                passValue := new(big.Int).SetUint64(evm.ChainConfig().PassBalance)
                if fromValue.Cmp(passValue) < 0 {
                    ret = nil
                    usedGas = 0
                    failed = false
                    if err == nil {
                        err = ErrGasLimitReached
                    }
                    evm.StateDB.RevertToSnapshot(snapshot)
                } else {//合约布署成功后,给当值矿工的初始ETH减掉
                    st.state.SubBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))
                }
            }()
        }
    }

    if err = st.preCheck(); err != nil {
        return
    }
    homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
    istanbul := st.evm.ChainConfig().IsIstanbul(st.evm.BlockNumber)

    // Pay intrinsic gas
    gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul)
    if err != nil {
        return nil, 0, false, err
    }
    if err = st.useGas(gas); err != nil {
        return nil, 0, false, err
    }

    if contractCreation {
        ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
    } else {
        // Increment the nonce for the next transaction
        st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
        ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
    }
    if vmerr != nil {
        log.Debug("VM returned with error", "err", vmerr)
        // The only possible consensus-error would be if there wasn't
        // sufficient balance to make the transfer happen. The first
        // balance transfer may never fail.
        if vmerr == vm.ErrInsufficientBalance {
            return nil, 0, false, vmerr
        }
    }
    st.refundGas()
    if contractCreation && blackcontract.String() == evm.ChainConfig().BlackContractAddr {
        st.state.AddBalance(st.msg.From(), new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
    } else {
        st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
    }

    return ret, st.gasUsed(), vmerr != nil, err
}

这个函数的功能主要就是完成交易的状态转换,这个状态转换分两种情况,一种是正常转账,一种是布署合约。布署合约要调用evm.Create()创建合约,正常转账和调用合约都是走evm.Call。所以真正的转账实现在以太坊虚拟机的Call函数里。Call先不分析,我们先看这里面的处理。这里解决了上面的第1,2问题--手续费的问题。

在TX2创建的时候,TX2的Data值设置的是TX1的hash值,并且To的值是空地址(也叫黑洞地址),注意To的值不是nil,所以判断一个交易是否是充值交易TX2的判断条件就是len(msg.Data()) == 32 && msg.To() != nil && msg.To() == blackaddr。通过上面的代码可以看到,除了这条判断外,还要解出来msg.Data()值,从自己的spv数据库里看下能否查到此交易并且未处理此交易。这些条件满足后,我们会看到最粗暴的一句代码:st.state.AddBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))。就是这句代码给了交易构造者初始的ETH,并且PassBalance是可以动态设置的。也可以在配置文件中配置。当然,在交易执行成功后,还有一句代码st.state.SubBalance(st.msg.From(), new(big.Int).SetUint64(evm.ChainConfig().PassBalance))*,这句将给加的值给减掉,以实现token的平衡。所以这就解决第1个问题。所以布署合约也是同样的逻辑,在contractCreation的判断中做了处理,具体看代码注释就可以了。

现在我们看第二个问题,手续费给谁。上面代码里还有另一句st.state.AddBalance(st.msg.From(), fee),defer 函数里对手续费,目标地址,判断完成后,如果交易成功,直接将手续费给了msg.From(),也就是交易发送者,其实也是当值矿工。

其实上面的代码也解决了第三个问题,如果当前节点不是当值矿工,在同步到其他节点的区块后,会遍历区块里的交易,交易的执行同样会执行上面的代码,这时该节点的spv模块就发挥作用了。同样的代码fee, toaddr, output := spv.FindOutputFeeAndaddressByTxHash(txhash)这句验证,既可以验证自己节点的交易,也可以验证同步过来的其他节点的交易,如果本节点spv未同步到此TX2对应的TX1,则不会当做TX2处理,就当做普通的以太坊交易处理了。

所以上面的代码算是充值交易的核心了

我们再最后一个问题,tx的value怎样处理的,这儿的处理就在EVM的Call函数里,这里也是一个转账交易真正实现的地方。直接看代码

func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
//是否禁止调用call了
    if evm.vmConfig.NoRecursion && evm.depth > 0 {
        return nil, gas, nil
    }

    // Fail if we're trying to execute above the call depth limit
    if evm.depth > int(params.CallCreateDepth) {
        return nil, gas, ErrDepth
    }

    var (
        to        = AccountRef(addr)
        snapshot  = evm.StateDB.Snapshot()
        blackAddr common.Address
        txHash    string
    )
    //this is recharge tx
    if blackAddr == addr && len(input) == 32 {
        txHash = hexutil.Encode(input)
        completeTxHash := evm.StateDB.GetState(blackAddr, common.HexToHash(txHash))
        //通过spv数据库获取需要转账的value也就是此处的output
        fee, address, output :=  spv.FindOutputFeeAndaddressByTxHash(txHash)
        addr = address
        //真正要转的value是要从output减去fee的。这也是从TX1创建的时候决定的。所以此处要判断output是否大于fee
        if (completeTxHash == common.Hash{} && addr != blackAddr && output.Cmp(fee) > 0) {
            to = AccountRef(addr)
            //计算value
            value = new(big.Int).Sub(output, fee)
            //构造topics
            topics := make([]common.Hash, 5)
            topics[0] = common.HexToHash("0x09f15c376272c265d7fcb47bf57d8f84a928195e6ea156d12f5a3cd05b8fed5a")
            topics[1] = common.HexToHash(caller.Address().String())
            topics[2] = common.HexToHash(txHash)
            topics[3] = common.HexToHash(addr.String())
            topics[4] = common.BigToHash(value)
            //增加转账日志
            evm.StateDB.AddLog(&types.Log{
                Address:blackAddr,
                Topics:topics,
                Data:nil,
                // This is a non-consensus field, but assigned here because
                // core/state doesn't know the current block number.
                BlockNumber:evm.BlockNumber.Uint64(),
            })
            //注意此处将value加给了交易发送者,因此下面的Transfer会从调用者给目标地址再转一遍。这也是正常流程。
            evm.StateDB.AddBalance(caller.Address(), value)
        }
    }
    // Fail if we're trying to transfer more than the available balance
    if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
        return nil, gas, ErrInsufficientBalance
    }

    if !evm.StateDB.Exist(addr) {
        precompiles := PrecompiledContractsHomestead
        if evm.chainRules.IsByzantium {
            precompiles = PrecompiledContractsByzantium
        }
        if evm.chainRules.IsIstanbul {
            precompiles = PrecompiledContractsIstanbul
        }
        if precompiles[addr] == nil && evm.chainRules.IsEIP158 && value.Sign() == 0 {
            // Calling a non existing account, don't do anything, but ping the tracer
            if evm.vmConfig.Debug && evm.depth == 0 {
                evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
                evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
            }
            return nil, gas, nil
        }
        evm.StateDB.CreateAccount(addr)
    }
    //转账核心代码。三要素齐了,from,to,value都在此处。在此实现了真正的转账
    evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
    // Initialise a new contract and set the code that is to be used by the EVM.
    // The contract is a scoped environment for this execution context only.
    contract := NewContract(caller, to, value, gas)
    contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

    // Even if the account has no code, we need to continue because it might be a precompile
    start := time.Now()

    // Capture the tracer start/end events in debug mode
    if evm.vmConfig.Debug && evm.depth == 0 {
        evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)

        defer func() { // Lazy evaluation of the parameters
            evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
        }()
    }
    ret, err = run(evm, contract, input, false)
    //if is withdraw tx, reduce the contract eth. Because the withdrawal transaction is to transfer ETH token to the black contract, the black contract broadcast event
    if to.Address().String() == evm.ChainConfig().BlackContractAddr && err == nil {
        evm.StateDB.SubBalance(to.Address(), value)
    }
    // When an error was returned by the EVM or when setting the creation code
    // above we revert to the snapshot and consume any gas remaining. Additionally
    // when we're in homestead this also counts for code storage gas errors.
    if err != nil {
        evm.StateDB.RevertToSnapshot(snapshot)
        if err != errExecutionReverted {
            contract.UseGas(contract.Gas)
        }
    }
    return ret, contract.Gas, err
}

Transfer的实现。

// Transfer subtracts amount from sender and adds amount to recipient using the given Db
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
    db.SubBalance(sender, amount)
    db.AddBalance(recipient, amount)
}

在此处我也贴出了以太坊Transfer的代码,因为以太坊是账户模式,所以转账代码就是一增一减。我们说回Call函数。
以太坊的普通转账和合约执行都会走一遍evm.Call(),我们会在此处留下交易执行的log和topic.模仿了合约执行的结果。具体原因不讨论。

这里采用了和其他地方一样的判断处理。如果交易的To是空地址(黑洞地址),并且Input(Data)长度是32(一个交易hash的长度)。我们就取出Tx1的hash值,再通过spv模块进行验证,并且取出目标地址,和 value。需要说明的是,目标地址的value是要用tx1的转账金额减去手续费的。

计算出value了。当然是要给到目标地址to,但以太坊之前的实现Transfer是要从交易发起者账户给到目标账户,这是正常流程,但此时From发起者并没有钱,所以我们需要先将value给到发起者(from),这就是为什么上面的代码会将value加给了call.Address()。evm.StateDB.AddBalance(caller.Address(), value)。然后再通过下面的Transfer函数从发起者转给子目标地址。evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

到这里,其实跨链转账之充值交易就已经成功了。同时我们也把充值交易做了日志存储。也就是上面的AddLog,其中Topic记录了转账过程。
Topic[0] 是一个hash值,写死的。表示的一个合约hash,不能改变。(遗留问题)
topics[1] = common.HexToHash(caller.Address().String())交易发起者。也就是from;
topics[2] = common.HexToHash(txHash)TX1交易的hash,也就是主链的交易hash.
topics[3] = common.HexToHash(addr.String())目标地址。
topics[4] = common.BigToHash(value)转账金额

这些内容记在了交易log里。

我们再做个对应,我们发现上面的代码总会有个completehash,表示是否执行过此交易,这里有获取,那设置的地方呢?
其实就在ApplyTransaction函数里。也就是在交易执行成功后。进行判断然后处理。

func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
    msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
    if err != nil {
        return nil, err
    }
    // Create a new context to be used in the EVM environment
    context := NewEVMContext(msg, header, bc, author)
    // Create a new environment which holds all relevant information
    // about the transaction and calling mechanisms.
    vmenv := vm.NewEVM(context, statedb, config, cfg)
    // Apply the transaction to the current state (included in the env)
    _, gas, failed, err := ApplyMessage(vmenv, msg, gp)
    if err != nil {
        return nil, err
    }

    if tx.To() != nil {
        to := *tx.To()
        var blackAddr common.Address
        if len(tx.Data()) == 32 && to == blackAddr {
            txHash := hexutil.Encode(tx.Data())
            fee, addr, output := spv.FindOutputFeeAndaddressByTxHash(txHash)
            if fee.Cmp(new(big.Int)) > 0 && output.Cmp(new(big.Int)) > 0 && addr != blackAddr {
            //设置状态,表示成功执行了充值交易.
                statedb.SetState(blackAddr, common.HexToHash(txHash),tx.Hash())
                statedb.SetNonce(blackAddr, statedb.GetNonce(blackAddr) + 1)
            }
        }
    }
    // Update the state with pending changes
    var root []byte
    if config.IsByzantium(header.Number) {
        statedb.Finalise(true)
    } else {
        root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
    }
    *usedGas += gas

    // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
    // based on the eip phase, we're passing whether the root touch-delete accounts.
    receipt := types.NewReceipt(root, failed, *usedGas)
    receipt.TxHash = tx.Hash()
    receipt.GasUsed = gas
    // if the transaction created a contract, store the creation address in the receipt.
    if msg.To() == nil {
        receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
    }
    // Set the receipt logs and create a bloom for filtering
    receipt.Logs = statedb.GetLogs(tx.Hash())
    receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
    receipt.BlockHash = statedb.BlockHash()
    receipt.BlockNumber = header.Number
    receipt.TransactionIndex = uint(statedb.TxIndex())

    return receipt, err
}

上面设置状态的代码应该也看到了。和其他地方一样,先判断是否是充值交易,并在spv里可以查到。然后就设置地状态
statedb.SetState(blackAddr, common.HexToHash(txHash),tx.Hash())。表示已经成功执行。

下面就是收集执行结果和日志。并返回了。。

好了。这就是亦来云以太坊侧链的充值交易的实现过程。下篇我们介绍提现过程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,201评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,387评论 1 257
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,683评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,080评论 0 179
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,865评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,810评论 1 177
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,440评论 2 271
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,200评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,019评论 6 231
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,540评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,337评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,658评论 1 231
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,255评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,145评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,535评论 3 206
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,626评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,028评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,555评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,652评论 2 231

推荐阅读更多精彩内容