@rsksmart/btc-transaction-solidity-helper 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Rootstock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,31 @@
1
+ # Bitcoin Transaction Solidity Helper
2
+
3
+ The intention of this library is to make easier to work with Bitcoin transactions in Solidity smart contracts. Since Rootstock extends Bitcoin's capabilities by enabling smart contracts it is important to be able to work with Bitcoin transactions in them.
4
+
5
+ ## Features
6
+
7
+ The features of this library include:
8
+ * Bitcoin transaction output parsing: is able to receive a raw tx and return an array of structs with the tx outputs
9
+ * Bitcoin transaction hashing: is able to receive a raw tx and return its hash
10
+ * Bitcoin transaction output script validation: is able to receive a raw output script, validate that is from a specific type and return a result. E.g. receive a raw null-data script and return the embeded data in it
11
+ * Bitcoin address generation: is able to generate Bitcoin the address from a specific script and also to validate if a given address was generated from a script or not.
12
+
13
+ ### Future features
14
+ These are some features that can increase the library capabilities in the future:
15
+ * Bitcoin transaction input parsing: should be able to receive a raw tx and return an array of structs with the tx inputs
16
+ * Bitcoin transaction creation: utilities for building a raw transaction inside a contract
17
+
18
+ ## Usage
19
+ 1. Run this command to install the contracts
20
+ ```console
21
+ npm install @rsksmart/btc-transaction-solidity-helper
22
+ ```
23
+ 2. Import the library in your contract
24
+ ```solidity
25
+ import "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
26
+ ```
27
+ 3. Use the library. E.g.:
28
+ ```solidity
29
+ BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
30
+ bytes memory btcTxDestination = BtcUtils.parseNullDataScript(outputs[0].pkScript, false);
31
+ ```
@@ -0,0 +1,223 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.18;
3
+
4
+ /**
5
+ * @title BtcUtils
6
+ * @notice This library contains functionality to make easier to work with Bitcoin transactions in Solidity.
7
+ * @notice This library is based in this document:
8
+ * https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
9
+ */
10
+ library BtcUtils {
11
+ uint8 private constant MAX_COMPACT_SIZE_LENGTH = 252;
12
+ uint8 private constant MAX_BYTES_USED_FOR_COMPACT_SIZE = 8;
13
+ uint8 private constant OUTPOINT_SIZE = 36;
14
+ uint8 private constant OUTPUT_VALUE_SIZE = 8;
15
+ uint8 private constant PUBKEY_HASH_SIZE = 20;
16
+ uint8 private constant PUBKEY_HASH_START = 3;
17
+ uint8 private constant CHECK_BYTES_FROM_HASH = 4;
18
+
19
+
20
+ /**
21
+ * @notice This struct contains the information of a tx output separated by fields
22
+ * @notice Its just to have a structured representation of the output
23
+ **/
24
+ struct TxRawOutput {
25
+ uint64 value;
26
+ bytes pkScript;
27
+ uint256 scriptSize;
28
+ uint256 totalSize;
29
+ }
30
+
31
+ /// @notice Parse a raw transaction to get an array of its outputs in a structured representation
32
+ /// @param rawTx the raw transaction
33
+ /// @return An array of `TxRawOutput` with the outputs of the transaction
34
+ function getOutputs(bytes calldata rawTx) public pure returns (TxRawOutput[] memory) {
35
+ uint currentPosition = 4;
36
+
37
+ if (rawTx[4] == 0x00 && rawTx[5] == 0x01) { // if its segwit, skip marker and flag
38
+ currentPosition = 6;
39
+ }
40
+
41
+ (uint64 inputCount, uint16 inputCountSize) = parseCompactSizeInt(currentPosition, rawTx);
42
+ currentPosition += inputCountSize;
43
+
44
+ uint64 scriptLarge;
45
+ uint16 scriptLargeSize;
46
+ for (uint64 i = 0; i < inputCount; i++) {
47
+ currentPosition += OUTPOINT_SIZE;
48
+ (scriptLarge, scriptLargeSize) = parseCompactSizeInt(currentPosition, rawTx);
49
+ currentPosition += scriptLarge + scriptLargeSize + 4;
50
+ }
51
+
52
+ (uint64 outputCount, uint16 outputCountSize) = parseCompactSizeInt(currentPosition, rawTx);
53
+ currentPosition += outputCountSize;
54
+
55
+ TxRawOutput[] memory result = new TxRawOutput[](outputCount);
56
+ for (uint i = 0; i < outputCount; i++) {
57
+ result[i] = extractRawOutput(currentPosition, rawTx);
58
+ currentPosition += result[i].totalSize;
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function extractRawOutput(uint position, bytes memory rawTx) private pure returns (TxRawOutput memory) {
64
+ TxRawOutput memory result;
65
+ result.value = uint64(calculateLittleEndianFragment(position, position + OUTPUT_VALUE_SIZE, rawTx));
66
+ position += OUTPUT_VALUE_SIZE;
67
+
68
+ (uint64 scriptLength, uint16 scriptLengthSize) = parseCompactSizeInt(position, rawTx);
69
+ position += scriptLengthSize;
70
+
71
+ bytes memory pkScript = new bytes(scriptLength);
72
+ for (uint64 i = 0; i < scriptLength; i++) {
73
+ pkScript[i] = rawTx[position + i];
74
+ }
75
+ result.pkScript = pkScript;
76
+ result.scriptSize = scriptLength;
77
+ result.totalSize = OUTPUT_VALUE_SIZE + scriptLength + scriptLengthSize;
78
+ return result;
79
+ }
80
+
81
+ /// @notice Parse a raw pay-to-public-key-hash output script to get the corresponding address
82
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
83
+ /// @param mainnet if the address to generate is from mainnet or testnet
84
+ /// @return The address generated using the receiver's public key hash
85
+ function parsePayToPubKeyHash(bytes calldata outputScript, bool mainnet) public pure returns (bytes memory) {
86
+ require(outputScript.length == 25, "Script has not the required length");
87
+ require(
88
+ outputScript[0] == 0x76 && // OP_DUP
89
+ outputScript[1] == 0xa9 && // OP_HASH160
90
+ outputScript[2] == 0x14 && // pubKeyHashSize, should be always 14 (20B)
91
+ outputScript[23] == 0x88 && // OP_EQUALVERIFY
92
+ outputScript[24] == 0xac, // OP_CHECKSIG
93
+ "Script has not the required structure"
94
+ );
95
+
96
+ bytes memory destinationAddress = new bytes(PUBKEY_HASH_SIZE);
97
+ for(uint8 i = PUBKEY_HASH_START; i < PUBKEY_HASH_SIZE + PUBKEY_HASH_START; i++) {
98
+ destinationAddress[i - PUBKEY_HASH_START] = outputScript[i];
99
+ }
100
+
101
+ uint8 versionByte = mainnet? 0x00 : 0x6f;
102
+ bytes memory result = addVersionByte(bytes1(versionByte), destinationAddress);
103
+
104
+ return result;
105
+ }
106
+
107
+ function addVersionByte(bytes1 versionByte, bytes memory source) private pure returns (bytes memory) {
108
+ bytes memory dataWithVersion = new bytes(source.length + 1);
109
+ dataWithVersion[0] = versionByte;
110
+
111
+ uint8 i;
112
+ for (i = 0; i < source.length; i++) {
113
+ dataWithVersion[i + 1] = source[i];
114
+ }
115
+
116
+ return dataWithVersion;
117
+ }
118
+
119
+ /// @notice Parse a raw null-data output script to get its content
120
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
121
+ /// @return The content embedded inside the script
122
+ function parseNullDataScript(bytes calldata outputScript) public pure returns (bytes memory) {
123
+ require(outputScript.length > 1,"Invalid size");
124
+ require(outputScript[0] == 0x6a, "Not OP_RETURN");
125
+ return outputScript[1:];
126
+ }
127
+
128
+ /// @notice Hash a bitcoin raw transaction to get its id (reversed double sha256)
129
+ /// @param btcTx the transaction to hash
130
+ /// @return The transaction id
131
+ function hashBtcTx(bytes calldata btcTx) public pure returns (bytes32) {
132
+ bytes memory doubleSha256 = abi.encodePacked(sha256(abi.encodePacked(sha256(btcTx))));
133
+ bytes1 aux;
134
+ for (uint i = 0; i < 16; i++) {
135
+ aux = doubleSha256[i];
136
+ doubleSha256[i] = doubleSha256[31 - i];
137
+ doubleSha256[31 - i] = aux;
138
+ }
139
+
140
+ bytes32 result;
141
+ assembly {
142
+ result := mload(add(doubleSha256, 32))
143
+ }
144
+ return result;
145
+ }
146
+
147
+ /// @dev Gets the timestamp of a Bitcoin block header
148
+ /// @param header The block header
149
+ /// @return The timestamp of the block header
150
+ function getBtcBlockTimestamp(bytes memory header) public pure returns (uint256) {
151
+ // bitcoin header is 80 bytes and timestamp is 4 bytes from byte 68 to byte 71 (both inclusive)
152
+ require(header.length == 80, "Invalid header length");
153
+
154
+ return sliceUint32FromLSB(header, 68);
155
+ }
156
+
157
+ // bytes must have at least 28 bytes before the uint32
158
+ function sliceUint32FromLSB(bytes memory bs, uint offset) private pure returns (uint32) {
159
+ require(bs.length >= offset + 4, "Slicing out of range");
160
+
161
+ return
162
+ uint32(uint8(bs[offset])) |
163
+ (uint32(uint8(bs[offset + 1])) << 8) |
164
+ (uint32(uint8(bs[offset + 2])) << 16) |
165
+ (uint32(uint8(bs[offset + 3])) << 24);
166
+ }
167
+
168
+ /// @notice Check if a pay-to-script-hash address belogs to a specific script
169
+ /// @param p2sh the pay-to-script-hash address
170
+ /// @param script the script to check
171
+ /// @param mainnet flag to specify if its a mainnet address
172
+ /// @return Whether the address belongs to the script or not
173
+ function validateP2SHAdress(bytes calldata p2sh, bytes calldata script, bool mainnet) public pure returns (bool) {
174
+ return p2sh.length == 25 && keccak256(p2sh) == keccak256(getP2SHAddressFromScript(script, mainnet));
175
+ }
176
+
177
+ /// @notice Generate a pay-to-script-hash address from a script
178
+ /// @param script the script to generate the address from
179
+ /// @param mainnet flag to specify if the output should be a mainnet address
180
+ /// @return The address generate from the script
181
+ function getP2SHAddressFromScript(bytes calldata script, bool mainnet) public pure returns (bytes memory) {
182
+ bytes20 scriptHash = ripemd160(abi.encodePacked(sha256(script)));
183
+ uint8 versionByte = mainnet ? 0x5 : 0xc4;
184
+ bytes memory versionAndHash = bytes.concat(bytes1(versionByte), scriptHash);
185
+ bytes4 checksum = bytes4(sha256(abi.encodePacked(sha256(versionAndHash))));
186
+ return bytes.concat(versionAndHash, checksum);
187
+ }
188
+
189
+ function parseCompactSizeInt(uint sizePosition, bytes memory array) private pure returns(uint64, uint16) {
190
+ require(array.length > sizePosition, "Size position can't be bigger than array");
191
+ uint8 maxSize = uint8(array[sizePosition]);
192
+ if (maxSize == 0) {
193
+ return (0, 1);
194
+ } else if (maxSize <= MAX_COMPACT_SIZE_LENGTH) {
195
+ return (maxSize, 1);
196
+ }
197
+
198
+ uint compactSizeBytes = 2 ** (maxSize - MAX_COMPACT_SIZE_LENGTH);
199
+ require(compactSizeBytes <= MAX_BYTES_USED_FOR_COMPACT_SIZE, "unsupported compact size length");
200
+
201
+ // the adition of 1 is because the first byte is the indicator of the size and its not part of the number
202
+ uint64 result = uint64(
203
+ calculateLittleEndianFragment(sizePosition + 1, sizePosition + compactSizeBytes + 1, array)
204
+ );
205
+ return (result, uint16(compactSizeBytes) + 1);
206
+ }
207
+
208
+ function calculateLittleEndianFragment(
209
+ uint fragmentStart,
210
+ uint fragmentEnd,
211
+ bytes memory array
212
+ ) private pure returns (uint) {
213
+ require(
214
+ fragmentStart < array.length && fragmentEnd < array.length,
215
+ "Range can't be bigger than array"
216
+ );
217
+ uint result = 0;
218
+ for (uint i = fragmentStart; i < fragmentEnd; i++) {
219
+ result += uint8(array[i]) * uint64(2 ** (8 * (i - (fragmentStart))));
220
+ }
221
+ return result;
222
+ }
223
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rsksmart/btc-transaction-solidity-helper",
3
+ "version": "0.0.3",
4
+ "description": "Solidity library with functions to work with Bitcoin transactions inside smart contracts",
5
+ "main": "contracts",
6
+ "files": [
7
+ "contracts"
8
+ ],
9
+ "scripts": {
10
+ "test": "REPORT_GAS=true npx hardhat test",
11
+ "test:coverage": "npx hardhat coverage",
12
+ "lint": "npx hardhat check",
13
+ "compile": "npx hardhat compile",
14
+ "prepare": "husky install"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/rsksmart/btc-transaction-solidity-helper.git"
19
+ },
20
+ "keywords": [
21
+ "RSK",
22
+ "rootstock",
23
+ "BTC",
24
+ "Bitcoin",
25
+ "solidity",
26
+ "smart-contract",
27
+ "ethereum",
28
+ "blockchain",
29
+ "dapps"
30
+ ],
31
+ "author": "Luis Chavez",
32
+ "license": "ISC",
33
+ "bugs": {
34
+ "url": "https://github.com/rsksmart/btc-transaction-solidity-helper/issues"
35
+ },
36
+ "homepage": "https://github.com/rsksmart/btc-transaction-solidity-helper#readme",
37
+ "devDependencies": {
38
+ "@nomicfoundation/hardhat-toolbox": "^3.0.0",
39
+ "@nomiclabs/hardhat-solhint": "^3.0.1",
40
+ "hardhat": "^2.17.0",
41
+ "husky": "^8.0.3"
42
+ }
43
+ }