Skip to content

Commit

Permalink
feat: provide a verification path for getting attatched tx data
Browse files Browse the repository at this point in the history
  • Loading branch information
reednaa committed Sep 12, 2024
1 parent aba79f5 commit c5fc43f
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/library/BitcoinOpcodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bytes1 constant LESS_THAN_OP_1 = 0x50;
bytes1 constant OP_1 = 0x51;
bytes1 constant OP_2 = 0x52;
bytes1 constant OP_16 = 0x60;
bytes1 constant OP_RETURN = 0x6a;
bytes1 constant OP_DUB = 0x76;
bytes1 constant OP_EQUAL = 0x87;
bytes1 constant OP_EQUALVERIFY = 0x88;
Expand Down
76 changes: 47 additions & 29 deletions src/library/BtcProof.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ library BtcProof {
* @dev Validates that a given payment appears under a given block hash.
*
* This verifies all of the following:
* 1. Raw transaction contains an output to txOutIx.
* 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.
Expand All @@ -30,11 +29,10 @@ library BtcProof {
*
* Always returns true or reverts with a descriptive reason.
*/
function validateTx(
function subValidate(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txOutIx
) internal pure returns (uint256 sats, bytes memory outputScript) {
BtcTxProof calldata txProof
) internal pure returns (BitcoinTx memory parsedTx) {
// 5. Block header to block hash

bytes32 blockHeaderBlockHash = getBlockHash(txProof.blockHeader);
Expand All @@ -54,22 +52,60 @@ library BtcProof {
if (rawTxId != txProof.txId) revert TxIDMismatch(rawTxId, txProof.txId);

// 1. Finally, validate raw transaction and get relevant values.
BitcoinTx memory parsedTx = parseBitcoinTx(txProof.rawTx);
parsedTx = parseBitcoinTx(txProof.rawTx);
}

/**
* @dev Validates that a given payment appears under a given block hash.
*
* This verifies all of the following:
* 1. Raw transaction contains an output to txOutIx.
*
* The caller must separately verify that the block hash is in the chain.
*
* Always returns true or reverts with a descriptive reason.
*/
function validateTx(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txOutIx
) internal pure returns (uint256 sats, bytes memory outputScript) {
// 1. Finally, validate raw transaction and get relevant values.
BitcoinTx memory parsedTx = subValidate(blockHash, txProof);
BitcoinTxOut memory txo = parsedTx.outputs[txOutIx];

outputScript = txo.script;
sats = txo.valueSats;
}

/**
* @dev Fork of validateTx that also returns the output script of the next output.
* Will revert if no output exists after the specific output (for sats / output script).
* @param returnScript Note that this may not actually be a return script. Please validate that the
* structure is correct.
*/
function validateTxData(
bytes32 blockHash,
BtcTxProof calldata txProof,
uint256 txOutIx
) internal pure returns (uint256 sats, bytes memory outputScript, bytes memory returnScript) {
// 1. Finally, validate raw transaction and get relevant values.
BitcoinTx memory parsedTx = subValidate(blockHash, txProof);
BitcoinTxOut memory txo = parsedTx.outputs[txOutIx];

outputScript = txo.script;
sats = txo.valueSats;
unchecked {
// Load the return script from the next output of the transaction.
returnScript = parsedTx.outputs[txOutIx + 1].script;
}
}

/**
* @dev Validates that a given transfer of ordinal(s) appears 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.
*
Expand All @@ -83,27 +119,9 @@ library BtcProof {
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);
BitcoinTx memory parsedTx = subValidate(blockHash, txProof);
BitcoinTxIn memory txInput = parsedTx.inputs[0];
// Check if correct input transaction is used.
if (txInId != txInput.prevTxID) revert InvalidTxInHash(txInId, txInput.prevTxID);
Expand Down
25 changes: 25 additions & 0 deletions src/library/BtcScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct BitcoinAddress {
* @dev This contract is not intended for on-chain calls.
*/
library BtcScript {
error ScriptTooLong(uint256 length);

//--- Bitcoin Script Decode Helpers ---//

Expand Down Expand Up @@ -203,4 +204,28 @@ library BtcScript {
function scriptP2TR(bytes32 witnessProgram) internal pure returns(bytes memory) {
return bytes.concat(OP_1, PUSH_32, witnessProgram);
}

/**
* @notice Creates the expected OP_RETURN script to embed data onto the Bitcoin blockchain.
* @dev Maximum script length is type(uint32).max. Empty script returns [OP_RETURN, OP_0].
*/
function embedOpReturn(bytes calldata returnScript) internal pure returns(bytes memory) {
uint256 scriptLength = returnScript.length;
// If the script length is 0, there is no valid script that describes that.
// The closest approximation is:
if (scriptLength == 0) return bytes.concat(OP_RETURN, OP_0);
// Pushing between 1 and 75 bytes is done with their respective opcode
// which helpfully is the opcodes 0x01 to 0x4b (75)
if (scriptLength <= 75) return bytes.concat(OP_RETURN, bytes1(uint8(scriptLength)), returnScript);
// If script length is more than than 75, we need to use the longer push codes.
// The first one 0x4c allows us to specify with 1 byte how many bytes to push:
if (scriptLength <= type(uint8).max) bytes.concat(OP_RETURN, OP_PUSHDATA1, bytes1(uint8(scriptLength)), returnScript);
// The next 0x4d allows us to specify with 2 bytes
if (scriptLength <= type(uint16).max) bytes.concat(OP_RETURN, OP_PUSHDATA2, bytes2(uint16(scriptLength)), returnScript);
// The next 0x4e allows us to specify with 4 bytes
if (scriptLength <= type(uint32).max) bytes.concat(OP_RETURN, OP_PUSHDATA4, bytes4(uint32(scriptLength)), returnScript);

// We can't add all script data.
revert ScriptTooLong(scriptLength);
}
}

0 comments on commit c5fc43f

Please sign in to comment.