《亦来云的侧链侧链白皮书》从理论方面介绍了亦来云的跨链转账原理,本文就从亦来云的以太坊侧链的源码层来看下具体的实现。
跨链转账的过程分为主链到侧链的转账和侧链到主链的转账,这里主链就是指跑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: ðCommon.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: ðCommon.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())。表示已经成功执行。
下面就是收集执行结果和日志。并返回了。。
好了。这就是亦来云以太坊侧链的充值交易的实现过程。下篇我们介绍提现过程。