长安链双花交易防范策略
什么是双花交易
即一笔钱被花了两次或者两次以上,也叫“双重支付”。在数字货币系统中,由于数据的可复制性,使得系统可能存在同一笔数字资产因不当操作被重复使用的情况。
比特币如何解决“双花”问题
比特币作为一个去中心化的点对点电子现金系统,主要依靠未花费的交易输出(unspend transaction output, UTXO)和时间戳来解决“双花”问题。
系统利用数字时间戳保证每个区块按时间顺序相连成区块链,时间戳也为区块链链上每一笔数据打上时间标记。
假设小明将被存在于自己UTXO中的1个BTC同时转账给李雷和韩梅梅,两笔交易仅有一笔会成功完成。为什么呢?因为系统中的节点会选择性地记录先接收到的那笔交易。当两笔交易同时被同一节点记录,根据时间戳的数据,只有先被记录的交易才能被确认成功。
如果小明的两笔转账的时间隔得非常非常近,“小明在12点34分56秒转给李雷1个比特币”、“小明在12点34分57秒转给韩梅梅1个比特币”恰好被两个不同的节点记录,会出现“双花”吗?也不会。这两个节点会将各自挖出包含相关交易的区块同时广播到比特币网络中,此时区块链将出现分叉,剩余节点选择在他们认为的最长链上构建新的区块。最后,率先构建出新区块并成为当前最长链上的交易(通常为6个区块),才能被确认成功。之后的所有节点将在此最长链上构建新的区块。
总结一下,面对“双花问题”,比特币现金系统是这样解决的:
首先,每笔交易都要先确认对应比特币之前的情况,要检查它是否存在于小明的UTXO中。如果不在,那么该交易会被系统拒绝。
如果小明用同一笔UTXO付给李雷和韩梅梅,系统中的节点只确认先接收到的那一笔。
当两笔时间上很接近的交易被不同节点确认,区块链将发生分叉。剩余节点选择在他们认为的最长链上构建新的区块。当其中一笔交易被6个节点确认后,它将成为系统最长链,可以认为这笔交易获得了最终的确认。
什么是UTXO
UTXO 是 unspent transaction outputs(未使用的交易输出)的缩写,每一个比特币其实都是 UTXO,它是比特币的最核心概念之一。
你的比特币就是 UXTO
比特币的挖矿节点获得新区块的挖矿奖励,比如 12.5 个比特币,这时,它的钱包地址得到的就是一个 UTXO,即这个新区块的币基交易(也称创币交易)的输出。币基交易是一个特殊的交易,它没有输入,只有输出。
当甲要把一笔比特币转给乙时,这个过程是把甲的钱包地址中之前的一个 UTXO,用私钥进行签名,发送到乙的地址。这个过程是一个新的交易,而乙得到的是一个新的 UTXO。
这就是为什么有人说在这个世界上根本没有比特币,只有 UTXO,你的地址中的比特币是指没花掉的交易输出。
详看从甲转账给乙的过程
假设甲(Alice)向乙(Bob)转账,则转账过程可以分成三个阶段(见图1):
1) 假设Alice之前通过挖矿获得了 12.5 个比特币,在她的地址中,这些比特币是某个币基交易的 UTXO。
2) Alice 发起一个交易,输入是自己的上一个交易,输出是 Bob 的地址,数量是 12.5 个比特币,Alice 用自己的私钥对交易进行签名。
这里简化了交易过程,只讨论了将上一个交易的输出全部转帐的情况。如果试图转出上一个交易的输出的一部分比特币,则要进行略复杂的处理。
按照比特币系统的设计,比特币交易还要遵循一个原则:每一次交易的输入值都必须全部花掉,不能只花掉部分。比如,我要转出比特币给你的钱包地址中只有 8 个比特币,那么很简单,我发起一个交易,把这 8 个比特币转到你的钱包地址中,我签名确认这个交易。但假如我的钱包地址中有 25 个比特币,那我发起的交易就不是转给你 8 个比特币,然后自己的钱包地址中还剩下 17 个比特币。这时,我发起的交易是:从我的钱包地址中转 8 个比特币给你,同时转 17 个比特币给我的同一地址。
3) 当交易被区块链确认后,Alice 的 UTXO 就变成了 0。而在Bob的地址中就多了一个 UTXO,数量是 12.5。
长安链如何解决“双花”问题
长安链作为联盟链,采用的账户模型。通过数据库表存储数据。长安链主要依靠txid来解决“双花”问题。
在通过java sdk发起一笔交易的时候,sdk默认会采用UUID作为种子生成一个txid,源码如下:
private Payload createPayload(String txId, TxType txType, String contractName, String method, Map<String, byte[]> params, long seq) throws ChainMakerCryptoSuiteException {
if (txId == null || txId.equals("")) {
txId = Utils.generateTxId(ByteString.copyFrom(UUID.randomUUID().toString().getBytes()).concat(ByteString.copyFrom(UUID.randomUUID().toString().getBytes())), this.clientUser.getCryptoSuite());
}
org.chainmaker.pb.common.Request.Payload.Builder payloadBuilder = Payload.newBuilder().setChainId(this.chainId).setTxType(txType).setTxId(txId).setTimestamp(Utils.getCurrentTimeSeconds()).setContractName(contractName).setMethod(method).setSequence(seq);
if (params != null && !params.isEmpty()) {
params.forEach((key, value) -> {
payloadBuilder.addParameters(KeyValuePair.newBuilder().setKey(key).setValue(ByteString.copyFrom(value)).build());
});
}
return payloadBuilder.build();
}
理论上即便用户发起两笔相同的交易,也会生成不同的txid,在执行交易时,始终会执行两次扣减。在联盟链里边有个特殊的数据叫读写集,当同时修改同一账户的值时,会产生读写冲突。长安链的处理策略是:
主要步骤为:
- 将所有交易并行执行,在执行中所有的写操作都在内存缓存中;
- 每笔交易结束时,查看读集中所有的key是否被其他交易修改,若无则将交易写集生效(
ApplyTxSimContext
),若有则将交易重新加入待执行队列;
区块交易执行源码核心方法如下:
故无论是同时发起两笔甚至多笔同一账户的交易都会执行成功。但不会出现“双花”的结果,因为多笔同时执行时,会触发读写集冲突,最先执行的第一笔会先成功,后边的会重新加入执行队列,以此类推。
极端情况下或者自定义txid去试图进行多笔相同txid的同一账户的交易时,始终有一笔会执行成功,另一笔理论上是成功了,但是在交易数据准备落库前,会校验不通过,导致第二笔执行失败。源码如下:
// TxExists returns true if the tx exist, or returns false if none exists.
func (bs *BlockStoreImpl) TxExists(txId string) (bool, error) {
//return bs.blockDB.TxExists(txId)
// bigfilter 未开启则直接查db
//if bs.bigFilterDB == nil {
// return bs.txExistDB.TxExists(txId)
//}
if !bs.storeConfig.EnableBigFilter {
return bs.txExistDB.TxExists(txId)
}
exists, b, err := bs.bigFilterDB.TxExists(txId)
//如果从bigfilter查询出错,直接查db
if err != nil {
bs.logger.Errorf("check tx exist by txid:[%s] in bigfilter error:[%s], we will try check from db",
txId, err)
return bs.txExistDB.TxExists(txId)
}
// 在bigFilter中的cache存在
if exists {
return true, nil
}
// 在bigFilter中的cache不存在,bigFilter存储中也不存在
if !b {
return false, nil
}
// 返回假阳性,查 txExistDB
if b {
return bs.txExistDB.TxExists(txId)
}
return false, nil
}
还没有评论,来说两句吧...