智能合约对外部世界一无所知,一般必须依靠预言机来导入外部数据。我们之前已经展示了两种基于 Rabin 签名和 ECDSA 从预言机导入数据的方法。在本文中,我们将展示如何在没有预言机的情况下访问特定类型的外部数据,即区块链上的数据(例如区块头和交易),同时仍然保持数据完整性。通过允许智能合约以最少信任原则访问链上数据,将为所有新的比特币智能合约提供无限的机会。
访问区块头
顾名思义,比特币区块链由很多个区块链接而成。一个区块有两部分数据:区块头和交易。
一个块和它的头
区块头包含区块的元数据,有六个字段,如下所示。
比特币区块头
值得注意的是,比特币区块头是比特币工作量证明共识算法的一部分。更具体地说,区块头序列化后的哈希值不应超过难度目标(即前导零的数量)。由于工作量证明无需信任的性质,生成一个有效的区块头的成本非常高,尤其是在难度(difficulty)要求很高的情况下。但是却很容易检查给定的区块头是否有效。这正是我们在不依赖任何预言机的情况下将区块头导入如下所示智能合约的理论前提。
import "util.scrypt";
import "merklePath.scrypt";
struct BlockHeader {
bytes version;
Sha256 prevBlockHash;
Sha256 merkleRoot;
int time;
bytes bits;
int nonce;
}
library Blockchain {
static function txInBlock(Sha256 txid, BlockHeader bh, MerkleProof merkleproof) : bool {
return MerklePath.calMerkleRoot(txid, merkleproof) == bh.merkleRoot;
}
static function isBlockHeaderValid(BlockHeader bh, int blockchainTarget) : bool {
int bhHash = blockHeaderHashAsInt(bh);
int target = bits2Target(bh.bits);
return bhHash <= target && target <= blockchainTarget;
}
static function bits2Target(bytes bits) : int {
int exponent = Util.fromLEUnsigned(bits[3 :]);
int coefficient = Util.fromLEUnsigned(bits[: 3]);
int n = 8 * (exponent - 3);
bytes target = num2bin(coefficient, 32) >> n;
return Util.fromLEUnsigned(target);
}
static function serialize(BlockHeader bh) : bytes {
return bh.version + bh.prevBlockHash + bh.merkleRoot + Util.toLEUnsigned(bh.time, 4) + bh.bits + Util.toLEUnsigned(bh.nonce, 4);
}
static function blockHeaderHash(BlockHeader bh) : Sha256 {
return hash256(serialize(bh));
}
static function blockHeaderHashAsInt(BlockHeader bh) : int {
return Util.fromLEUnsigned(blockHeaderHash(bh));
}
}
在函数 isBlockHeaderValid() 中检查区块头是否有效:函数 bits2Target() 从紧凑形式(通常称为 nBits 的 4 字节字段)计算难度目标;我们需要确保计算出的区块头哈希值满足其中声明的难度目标要求( bhHash <= target )。
假区块头
另外,我们还检查了难度目标不大于 blockchainTarget 参数( target <= blockchainTarget ),以控制产生假区块头的难度(blockchainTarget 越小伪造难度越高)。否则,攻击者可以轻松创建一个区块头,其哈希满足其中声明的难度目标(例如,只有 2 个前导零)。与比特币的许多其他方面(例如零确认)一样,以这种方式导入区块头的安全性是通过经济手段来保障的,而不仅仅是技术性保障。这意味着在实践中,依赖真实区块头的智能合约中锁定的比特币数量,不应超过产生虚假区块头的成本。
访问交易
一旦区块头可用,我们就可以轻松访问区块中的任何交易。这是因为区块头包含所有交易的 Merkle 树根。与 SPV 类似,我们将交易及其 Merkle 路径传递到智能合约中,并验证它与区块头中的根哈希匹配。函数 txInBlock() 演示了这一点。
案例:使用区块链生成随机数
一般来说,在区块链中安全公平地生成伪随机数被认为是一个难题,因为区块链既是确定性的又是透明的。但我们可以利用区块头的 nonce 字段,作为熵的来源。
Alice 和 Bob 都将相同数量的比特币锁定到以下合约中。一旦包含合约的交易被广播,它将被处理并包含在一个未来的区块中。根据区块的 nonce 值(很难预测并且可以被视为随机),确定赢家并拿走所有锁定的比特币。
import "blockchain.scrypt";
contract BlockchainPRNG {
int blockchainTarget;
PubKey alice;
PubKey bob;
public function bet(BlockHeader bh, MerkleProof merkleproof, Sig sig, SigHashPreimage txPreimage) {
require(Tx.checkPreimage(txPreimage));
Sha256 prevTxid = Sha256(Util.outpoint(txPreimage)[:32]);
require(Blockchain.isBlockHeaderValid(bh, this.blockchainTarget));
require(Blockchain.txInBlock(prevTxid, bh, merkleproof));
PubKey winner = bh.nonce % 2 ? this.alice : this.bob;
require(checkSig(sig, winner));
}
}
函数的前两行使用 OP_PUSH_TX 技术获取包含合约的交易的 txid; 第3行 require(Blockchain.isBlockHeaderValid(bh, this.blockchainTarget)) 验证区块头是否合法;第4行 require(Blockchain.txInBlock(prevTxid, bh, merkleproof)) 验证前一个交易是否在该区块中。最后如果 nonce 字段为奇数,则 Alice 获胜;否则,Bob 获胜。
总结
我们已经展示了应用最小信任原则在 sCrypt 智能合约中访问区块链数据。由于序列化的区块头头只有 80 字节长,并且 Merkle 证明按对数缩放,因此该技术非常高效(与 SPV 相同)。我们还展示了一个使用区块链数据生成伪随机数的示例。这只是可能的开始,我们将在以后的文章中探讨。敬请关注。
|