Skip to content

Commit

Permalink
feat: allow verifying an ordinal transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
reednaa committed Feb 24, 2024
1 parent dc6b646 commit 55c6f67
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/BtcTxVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ contract BtcTxVerifier is IBtcTxVerifier {
bytes32 blockHash = mirror.getBlockHash(blockNum);

if(
!BtcProof.validateScriptMatch(
!BtcProof.validateExactOut(
blockHash,
inclusionProof,
txOutIx,
Expand Down
69 changes: 68 additions & 1 deletion src/library/BtcProof.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ error AmountMismatch(uint256 txoSats, uint256 expected);
error TxIDMismatch(bytes32 rawTxId, bytes32 txProofId);
error BlockHashMismatch(bytes32 blockHeader, bytes32 givenBlockHash);

error InvalidTxInHash(uint256 expected, uint256 actual);
error InvalidTxInIndex(uint32 expected, uint32 actual);

// BtcProof provides functions to prove things about Bitcoin transactions.
// Verifies merkle inclusion proofs, transaction IDs, and payment details.
library BtcProof {
Expand All @@ -27,7 +30,7 @@ library BtcProof {
*
* Always returns true or reverts with a descriptive reason.
*/
function validateScriptMatch(
function validateExactOut(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txOutIx,
Expand Down Expand Up @@ -68,6 +71,70 @@ library BtcProof {
return true;
}

/**
* @dev Validates that a given transfer of ordinal(s) under a given block hash..
*
* This verifies all of the following:
* 1. Raw transaction contains a specific input (at index 0) that pays more than X to specific output (at index 0).
* 2. Raw transaction hashes to the given transaction ID.
* 3. Transaction ID appears under transaction root (Merkle proof).
* 4. Transaction root is part of the block header.
* 5. Block header hashes to a given block hash.
*
* The caller must separately verify that the block hash is in the chain.
*
* Always returns true or reverts with a descriptive reason.
*/
function validateOrdinalTransfer(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txInId,
uint32 txInPrevTxIndex,
bytes calldata outputScript,
uint256 satoshisExpected
) internal pure returns (bool) {
// 5. Block header to block hash

bytes32 blockHeaderBlockHash = getBlockHash(txProof.blockHeader);
if (blockHeaderBlockHash != blockHash) revert BlockHashMismatch(blockHeaderBlockHash, blockHash);

// 4. and 3. Transaction ID included in block
bytes32 blockTxRoot = getBlockTxMerkleRoot(txProof.blockHeader);
bytes32 txRoot = getTxMerkleRoot(
txProof.txId,
txProof.txIndex,
txProof.txMerkleProof
);
if (blockTxRoot != txRoot) revert TxMerkleRootMismatch(blockTxRoot, txRoot);

// 2. Raw transaction to TxID
bytes32 rawTxId = getTxID(txProof.rawTx);
if (rawTxId != txProof.txId) revert TxIDMismatch(rawTxId, txProof.txId);

// 1. Finally, validate raw transaction correctly transfers the ordinal(s).
// Parse transaction
BitcoinTx memory parsedTx = parseBitcoinTx(txProof.rawTx);
BitcoinTxIn memory txInput = parsedTx.inputs[0];
// Check if correct input transaction is used.
if (txInId != txInput.prevTxID) revert InvalidTxInHash(txInId, txInput.prevTxID);
// Check if correct index of that transaction is used.
if (txInPrevTxIndex == txInput.prevTxIndex) revert InvalidTxInIndex(txInPrevTxIndex, txInput.prevTxIndex);

BitcoinTxOut memory txo = parsedTx.outputs[0];
// if the length are less than 32, then use bytes32 to compare.
if (txo.script.length <= 32 && outputScript.length <= 32) {
if (bytes32(txo.script) != bytes32(outputScript)) revert ScriptMismatch(txo.script, outputScript);
} else {
if (keccak256(txo.script) != keccak256(outputScript)) revert ScriptMismatch(txo.script, outputScript);
}
// We allow for sending more because of the dust limit which may cause problems.
if (txo.valueSats < satoshisExpected) revert AmountMismatch(txo.valueSats, satoshisExpected);

// We've verified that blockHash contains a transaction with correct script
// that sends at least satoshisExpected to the given hash.
return true;
}

/**
* @dev Compute a block hash given a block header.
*/
Expand Down
18 changes: 9 additions & 9 deletions test/BtcProof.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import "forge-std/Test.sol";
import { BtcProof, BtcTxProof, BitcoinTx, TxMerkleRootMismatch, ScriptMismatch, AmountMismatch, TxIDMismatch, BlockHashMismatch } from "../src/library/BtcProof.sol";

contract MockBtcProof {
function validateScriptMatch(
function validateExactOut(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txOutIx,
bytes calldata outputScript,
uint256 satoshisExpected
) external pure returns (bool) {
return BtcProof.validateScriptMatch(blockHash, txProof, txOutIx, outputScript, satoshisExpected);
return BtcProof.validateExactOut(blockHash, txProof, txOutIx, outputScript, satoshisExpected);
}

function getBlockHash(bytes calldata blockHeader)
Expand Down Expand Up @@ -301,7 +301,7 @@ contract BtcProofTest is DSTest {
bytes memory destScript = hex"a914ae2f3d4b06579b62574d6178c10c882b9150374087";

// Should succeed
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash736000,
BtcTxProof(header736000, txId736, 1, txProof736, tx736),
0,
Expand All @@ -311,7 +311,7 @@ contract BtcProofTest is DSTest {

// Make each argument invalid, one at a time.
vm.expectRevert(abi.encodeWithSelector(BlockHashMismatch.selector, 0x00000000000000000002d52d9816a419b45f1f0efe9a9df4f7b64161e508323d, 0x00000000000000000000135a8473d7d3a3b091c928246c65ce2a396dd2a5ca9a));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash717695,
BtcTxProof(header736000, txId736, 1, txProof736, tx736),
0,
Expand All @@ -321,7 +321,7 @@ contract BtcProofTest is DSTest {

// - Bad tx proof (doesn't match root)
vm.expectRevert(abi.encodeWithSelector(TxMerkleRootMismatch.selector, 0xf8aec519bcd878c9713dc8153a72fd62e3667c5ade70d8d0415584b8528d79ca, 0x31b669b35884e22c31b286ed8949007609db6cb50afe8b6e6e649e62cc24e19c));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash717695,
BtcTxProof(headerGood, txId736, 1, txProof736, tx736),
0,
Expand All @@ -331,7 +331,7 @@ contract BtcProofTest is DSTest {

// - Wrong tx index
vm.expectRevert(abi.encodeWithSelector(TxMerkleRootMismatch.selector, 0x31b669b35884e22c31b286ed8949007609db6cb50afe8b6e6e649e62cc24e19c, 0x28ce7a513419e3d298d4cac4ce4e7b2ede283c56f4faf3d99801bc3585b29387));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash736000,
BtcTxProof(header736000, txId736, 2, txProof736, tx736),
0,
Expand All @@ -341,7 +341,7 @@ contract BtcProofTest is DSTest {

// - Wrong tx output index
vm.expectRevert(abi.encodeWithSelector(ScriptMismatch.selector, hex"a91415ecf89e95eb07fbc351b3f7f4c54406f7ee5c1087", hex"a914ae2f3d4b06579b62574d6178c10c882b9150374087"));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash736000,
BtcTxProof(header736000, txId736, 1, txProof736, tx736),
1,
Expand All @@ -351,7 +351,7 @@ contract BtcProofTest is DSTest {

// - Wrong dest script hash
vm.expectRevert(abi.encodeWithSelector(ScriptMismatch.selector, destScript, hex"abcd"));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash736000,
BtcTxProof(header736000, txId736, 1, txProof736, tx736),
0,
Expand All @@ -361,7 +361,7 @@ contract BtcProofTest is DSTest {

// - Wrong amount, off by one satoshi
vm.expectRevert(abi.encodeWithSelector(AmountMismatch.selector, 25200000, 25200001));
BtcProofUtils.validateScriptMatch(
BtcProofUtils.validateExactOut(
blockHash736000,
BtcTxProof(header736000, txId736, 1, txProof736, tx736),
0,
Expand Down

0 comments on commit 55c6f67

Please sign in to comment.