diff --git a/src/library/BitcoinOpcodes.sol b/src/library/BitcoinOpcodes.sol index 34728a4..62240a1 100644 --- a/src/library/BitcoinOpcodes.sol +++ b/src/library/BitcoinOpcodes.sol @@ -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; diff --git a/src/library/BtcProof.sol b/src/library/BtcProof.sol index 123b981..a4dbb63 100644 --- a/src/library/BtcProof.sol +++ b/src/library/BtcProof.sol @@ -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. @@ -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); @@ -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. * @@ -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); diff --git a/src/library/BtcScript.sol b/src/library/BtcScript.sol index ea0f8aa..47f2273 100644 --- a/src/library/BtcScript.sol +++ b/src/library/BtcScript.sol @@ -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 ---// @@ -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); + } }