Because smart contracts have no knowledge of the outside world, they must rely on oracles to import external data in general. We previo...
Because smart contracts have no knowledge of the outside world, they must rely on oracles to import external data in general. We previously shown two methods for importing data from Oracles, based on the Rabin signature and ECDSA. In this article, we demonstrate how to access a certain form of external data, namely blockchain data (such as block headers and transactions), in the absence of oracles while retaining data integrity. Allowing smart contracts to access on-chain data with minimum trust opens the door to a plethora of different types of smart contracts on Bitcoin.
Headers of Access Blocks
As the name implies, the Bitcoin blockchain is made up of a series of blocks. A block is made up of two components: a block header and transactions.
A Block and Its Header
A block header contains the metadata of the block, with six fields as shown below.
It should be noted that bitcoin headers are a component of Bitcoin's proof-of-work consensus algorithm. In particular, the hash of a serialised block header should not be more than the difficulty target (i.e., the number of leading zeros). Producing a valid block header is extremely expensive due to the trustless nature of proof of work, especially when the difficulty is high. However, it is quite simple to determine whether a given block header is genuine. This is exactly how we import a block header into a smart contract without using any oracles, as seen below.
[import "Util.scrypt"; import "mast.scrypt"; struct BlockHeader { bytes version; Sha256 prevBlockHash; Sha256 merkleRoot; int time; // difficulty target bytes bits; bytes nonce; } // a library to trustlessly access the blockchain: including blockheaders and transactions library Blockchain { // SPV: is a txid in a block static function txInBlock(Sha256 txid, BlockHeader bh, MerklePath merklePath) : bool { return MAST.calMerkleRoot(txid, merklePath) == bh.merkleRoot; } // is block header valid with difficulty meeting target static function isBlockHeaderValid(BlockHeader bh, int blockchainTarget) : bool { int bhHash = blockHeaderHash(bh); int target = bits2Target(bh.bits); // block hash below target and target below blockchain difficulty target return bhHash <= target && target <= blockchainTarget; } // convert difficulty from bits to target static function bits2Target(bytes bits) : int { int exponent = unpack(bits[3 :]); bytes coefficient = bits[: 3]; int leadingZeroBytes = exponent - 3; bytes targetBytes = num2bin(0, leadingZeroBytes) + coefficient; return Util.fromLEUnsigned(targetBytes); } // serialize a block header static function serialize(BlockHeader bh) : bytes { return bh.version + bh.prevBlockHash + bh.merkleRoot + num2bin(bh.time, 4) + bh.bits + bh.nonce; } // block header hash static function blockHeaderHash(BlockHeader bh) : int { bytes bhSerialized = serialize(bh); // hash is reversed return unpack(reverseBytes(hash256(bhSerialized), 32)); } }]
Blockchain Contract
isBlockHeaderValid() at Line 22 checks if a block header is valid. bits2Target() at Line 31 calculates the difficulty target from a compact form (a 4-byte field typically referred to as nBits). We simply hash the block header at Line 23 and make sure it meets the difficulty target at Line 27.
Fake Block Headers
To manage the difficulty of constructing a false block header, we also verify at Line 27 that the difficulty target is no greater than the blockchainTarget parameter. Otherwise, an attacker can easily generate a block header whose hash fulfils the difficulty target within a short period of time (e.g., only has 2 leading zeros). As with many other features of Bitcoin, such as 0-conf, the security of importing block headers in this manner is both economic and technological. In reality, this means that a smart contract that relies on a real block header must not lock more bitcoin than it costs to construct a false header.
Access Transactions
Once a block header is available, we can easily access any transaction in the block. This is because the block header contains the root of the Merkle tree of all the transactions. Similar to SPV, we pass the transaction and its Merkle path into a smart contract and verify it matches the root hash in the block header. txInBlock() at Line 17 demonstrates this.
A Merkle Tree and Merkle Proof
A Case Study: Using Blockchain To Generate Random Numbers
Because the blockchain is both deterministic and visible, it is regarded a difficult problem to create pseudo-random numbers in a secure and fair manner. As a source of entropy, we use blockchain data, notably the nonce field of a block header.
Alice and Bob each put the identical amount of bitcoins into the contract below. Once the contract transaction has been published, it will be mined into a future block. A winner is decided and all locked bitcoins are taken based on the nonce of the block, which is difficult to anticipate and can be viewed as random.
[import "blockchain.scrypt"; /* A trustless psuedo-random number generator using the block containing the deployed contract tx as an entropy source */ contract BlockchainPRNG { PubKey alice; PubKey bob; /* @bh: header of the block containing the contract tx/UTXO @merklePath: Merkle proof for the tx @sig: winner signature @blockchainTarget: difficulty target on mainnet */ public function bet(BlockHeader bh, MerklePath merklePath, Sig sig, int blockchainTarget, SigHashPreimage txPreimage) { require(Tx.checkPreimage(txPreimage)); // get id of previous tx Sha256 prevTxid = Sha256(Util.outpoint(txPreimage)[:32]); // validate block header require(Blockchain.isBlockHeaderValid(bh, blockchainTarget)); // verify previous tx is in the block require(Blockchain.txInBlock(prevTxid, bh, merklePath)); // use block header's nonce's last bit as a psuedo-random number PubKey winner = unpack(bh.nonce) % 2 ? this.alice : this.bob; require(checkSig(sig, winner)); } }]
BlockchainPRNG Contract
Lines 17 and 20 employ the OP PUSH TX method to obtain the transaction txid holding the contract. Line 23 validates the block header, and Line 26 validates that the prior transaction is present. Alice wins if the nonce field is odd; else, Bob wins.
Summary
We demonstrated how to access blockchain data in Bitcoin smart contracts while requiring little trust. This technique is incredibly efficient because a serialised bitcoin header is only 80 bytes long and Merkle proof scales logarithmically (same as SPV).
COMMENTS