@rsksmart/btc-transaction-solidity-helper 0.0.3 → 0.2.1

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/Readme.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # Bitcoin Transaction Solidity Helper
2
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/rsksmart/btc-transaction-solidity-helper/badge)](https://scorecard.dev/viewer/?uri=github.com/rsksmart/btc-transaction-solidity-helper)
2
3
 
3
4
  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
 
5
6
  ## Features
6
7
 
7
8
  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 output parsing: is able to receive a raw tx and return an array of structures with the tx outputs
9
10
  * 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 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 embedded data in it
11
12
  * 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.
13
+ * Bitcoin address validation: is able to validate if a Bitcoin address is of a given type or not.
12
14
 
13
15
  ### Future features
14
16
  These are some features that can increase the library capabilities in the future:
@@ -28,4 +30,4 @@ These are some features that can increase the library capabilities in the future
28
30
  ```solidity
29
31
  BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
30
32
  bytes memory btcTxDestination = BtcUtils.parseNullDataScript(outputs[0].pkScript, false);
31
- ```
33
+ ```
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.18;
3
3
 
4
+ import "./OpCodes.sol";
5
+
4
6
  /**
5
7
  * @title BtcUtils
6
8
  * @notice This library contains functionality to make easier to work with Bitcoin transactions in Solidity.
@@ -10,11 +12,27 @@ pragma solidity ^0.8.18;
10
12
  library BtcUtils {
11
13
  uint8 private constant MAX_COMPACT_SIZE_LENGTH = 252;
12
14
  uint8 private constant MAX_BYTES_USED_FOR_COMPACT_SIZE = 8;
15
+
16
+ uint private constant HASH160_SIZE = 20;
17
+ uint private constant SHA256_SIZE = 32;
18
+ uint private constant TAPROOT_PUBKEY_SIZE = 32;
19
+
13
20
  uint8 private constant OUTPOINT_SIZE = 36;
14
21
  uint8 private constant OUTPUT_VALUE_SIZE = 8;
15
- uint8 private constant PUBKEY_HASH_SIZE = 20;
22
+
16
23
  uint8 private constant PUBKEY_HASH_START = 3;
17
- uint8 private constant CHECK_BYTES_FROM_HASH = 4;
24
+ bytes1 private constant PUBKEY_HASH_MAINNET_BYTE = 0x00;
25
+ bytes1 private constant PUBKEY_HASH_TESTNET_BYTE = 0x6f;
26
+
27
+ uint8 private constant SCRIPT_HASH_START = 2;
28
+ bytes1 private constant SCRIPT_HASH_MAINNET_BYTE = 0x05;
29
+ bytes1 private constant SCRIPT_HASH_TESTNET_BYTE = 0xc4;
30
+
31
+ uint private constant BECH32_WORD_SIZE = 5;
32
+ uint private constant BYTE_SIZE = 8;
33
+
34
+ bytes1 private constant WITNESS_VERSION_0 = 0x00;
35
+ bytes1 private constant WITNESS_VERSION_1 = 0x01;
18
36
 
19
37
 
20
38
  /**
@@ -28,6 +46,10 @@ library BtcUtils {
28
46
  uint256 totalSize;
29
47
  }
30
48
 
49
+ function version() external pure returns (string memory) {
50
+ return "0.2.1";
51
+ }
52
+
31
53
  /// @notice Parse a raw transaction to get an array of its outputs in a structured representation
32
54
  /// @param rawTx the raw transaction
33
55
  /// @return An array of `TxRawOutput` with the outputs of the transaction
@@ -61,7 +83,7 @@ library BtcUtils {
61
83
  }
62
84
 
63
85
  function extractRawOutput(uint position, bytes memory rawTx) private pure returns (TxRawOutput memory) {
64
- TxRawOutput memory result;
86
+ TxRawOutput memory result = TxRawOutput(0, "", 0, 0);
65
87
  result.value = uint64(calculateLittleEndianFragment(position, position + OUTPUT_VALUE_SIZE, rawTx));
66
88
  position += OUTPUT_VALUE_SIZE;
67
89
 
@@ -78,42 +100,164 @@ library BtcUtils {
78
100
  return result;
79
101
  }
80
102
 
81
- /// @notice Parse a raw pay-to-public-key-hash output script to get the corresponding address
103
+ /// @notice Parse a raw output script whose type is not known by the consumer. The function will
104
+ /// return the corresponding address if the type of the script is supported or an error if not.
105
+ /// For the addresses that are encoded with base58check the checksum bytes are not included in
106
+ /// the resulting byte array
107
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
108
+ /// @param mainnet if the address to generate is from mainnet or testnet
109
+ /// @return The address bytes regarless of the output script type
110
+ function outputScriptToAddress(bytes calldata outputScript, bool mainnet) public pure returns (bytes memory) {
111
+ if (isP2PKHOutput(outputScript)) {
112
+ return parsePayToPubKeyHash(outputScript, mainnet);
113
+ }
114
+ if (isP2SHOutput(outputScript)) {
115
+ return parsePayToScriptHash(outputScript, mainnet);
116
+ }
117
+ if (isP2WPKHOutput(outputScript)) {
118
+ return parsePayToWitnessPubKeyHash(outputScript);
119
+ }
120
+ if (isP2WSHOutput(outputScript)) {
121
+ return parsePayToWitnessScriptHash(outputScript);
122
+ }
123
+ if (isP2TROutput(outputScript)) {
124
+ return parsePayToTaproot(outputScript);
125
+ }
126
+ revert("Unsupported script type");
127
+ }
128
+
129
+ /// @notice Check if a raw output script is a pay-to-public-key-hash output
130
+ /// @param pkScript the fragment of the raw transaction containing the raw output script
131
+ /// @return Whether the script has a pay-to-public-key-hash output structure or not
132
+ function isP2PKHOutput(bytes memory pkScript) public pure returns (bool) {
133
+ return pkScript.length == 5 + HASH160_SIZE &&
134
+ pkScript[0] == OpCodes.OP_DUP &&
135
+ pkScript[1] == OpCodes.OP_HASH160 &&
136
+ uint8(pkScript[2]) == HASH160_SIZE &&
137
+ pkScript[23] == OpCodes.OP_EQUALVERIFY &&
138
+ pkScript[24] == OpCodes.OP_CHECKSIG;
139
+ }
140
+
141
+ /// @notice Check if a raw output script is a pay-to-script-hash output
142
+ /// @param pkScript the fragment of the raw transaction containing the raw output script
143
+ /// @return Whether the script has a pay-to-script-hash output structure or not
144
+ function isP2SHOutput(bytes memory pkScript) public pure returns (bool) {
145
+ return pkScript.length == 3 + HASH160_SIZE &&
146
+ pkScript[0] == OpCodes.OP_HASH160 &&
147
+ uint8(pkScript[1]) == HASH160_SIZE &&
148
+ pkScript[22] == OpCodes.OP_EQUAL;
149
+ }
150
+
151
+ /// @notice Check if a raw output script is a pay-to-witness-pubkey-hash output
152
+ /// @param pkScript the fragment of the raw transaction containing the raw output script
153
+ /// @return Whether the script has a pay-to-witness-pubkey-hash output structure or not
154
+ function isP2WPKHOutput(bytes memory pkScript) public pure returns (bool) {
155
+ return pkScript.length == 2 + HASH160_SIZE &&
156
+ pkScript[0] == OpCodes.OP_0 &&
157
+ uint8(pkScript[1]) == HASH160_SIZE;
158
+ }
159
+
160
+ /// @notice Check if a raw output script is a pay-to-witness-script-hash output
161
+ /// @param pkScript the fragment of the raw transaction containing the raw output script
162
+ /// @return Whether the script has a pay-to-witness-script-hash output structure or not
163
+ function isP2WSHOutput(bytes memory pkScript) public pure returns (bool) {
164
+ return pkScript.length == 2 + SHA256_SIZE &&
165
+ pkScript[0] == OpCodes.OP_0 &&
166
+ uint8(pkScript[1]) == SHA256_SIZE;
167
+ }
168
+
169
+ /// @notice Check if a raw output script is a pay-to-taproot output
170
+ /// @notice Reference for implementation: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
171
+ /// @param pkScript the fragment of the raw transaction containing the raw output script
172
+ /// @return Whether the script has a pay-to-taproot output structure or not
173
+ function isP2TROutput(bytes memory pkScript) public pure returns (bool) {
174
+ return pkScript.length == 2 + TAPROOT_PUBKEY_SIZE &&
175
+ pkScript[0] == OpCodes.OP_1 &&
176
+ uint8(pkScript[1]) == TAPROOT_PUBKEY_SIZE;
177
+ }
178
+
179
+ /// @notice Parse a raw pay-to-public-key-hash output script to get the corresponding address,
180
+ /// the resulting byte array doesn't include the checksum bytes of the base58check encoding at
181
+ /// the end
82
182
  /// @param outputScript the fragment of the raw transaction containing the raw output script
83
183
  /// @param mainnet if the address to generate is from mainnet or testnet
84
184
  /// @return The address generated using the receiver's public key hash
85
185
  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
- );
186
+ require(isP2PKHOutput(outputScript), "Script hasn't the required structure");
95
187
 
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];
188
+ bytes memory destinationAddress = new bytes(HASH160_SIZE + 1);
189
+ for(uint8 i = PUBKEY_HASH_START; i < HASH160_SIZE + PUBKEY_HASH_START; i++) {
190
+ destinationAddress[i - PUBKEY_HASH_START + 1] = outputScript[i];
99
191
  }
100
192
 
101
- uint8 versionByte = mainnet? 0x00 : 0x6f;
102
- bytes memory result = addVersionByte(bytes1(versionByte), destinationAddress);
193
+ destinationAddress[0] = mainnet? PUBKEY_HASH_MAINNET_BYTE : PUBKEY_HASH_TESTNET_BYTE;
194
+ return destinationAddress;
195
+ }
103
196
 
104
- return result;
197
+ /// @notice Parse a raw pay-to-script-hash output script to get the corresponding address,
198
+ /// the resulting byte array doesn't include the checksum bytes of the base58check encoding at
199
+ /// the end
200
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
201
+ /// @param mainnet if the address to generate is from mainnet or testnet
202
+ /// @return The address generated using the script hash
203
+ function parsePayToScriptHash(bytes calldata outputScript, bool mainnet) public pure returns (bytes memory) {
204
+ require(isP2SHOutput(outputScript), "Script hasn't the required structure");
205
+
206
+ bytes memory destinationAddress = new bytes(HASH160_SIZE + 1);
207
+ for(uint8 i = SCRIPT_HASH_START; i < HASH160_SIZE + SCRIPT_HASH_START; i++) {
208
+ destinationAddress[i - SCRIPT_HASH_START + 1] = outputScript[i];
209
+ }
210
+
211
+ destinationAddress[0] = mainnet? SCRIPT_HASH_MAINNET_BYTE : SCRIPT_HASH_TESTNET_BYTE;
212
+ return destinationAddress;
105
213
  }
106
214
 
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;
215
+ /// @notice Parse a raw pay-to-witness-pubkey-hash output script to get the corresponding address words,
216
+ /// the resulting words are only the data part of the bech32 encoding and doesn't include the HRP
217
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
218
+ /// @return The address bech32 words generated using the pubkey hash
219
+ function parsePayToWitnessPubKeyHash(bytes calldata outputScript) public pure returns (bytes memory) {
220
+ require(isP2WPKHOutput(outputScript), "Script hasn't the required structure");
221
+ uint length = 1 + total5BitWords(HASH160_SIZE);
222
+ bytes memory result = new bytes(length);
223
+ result[0] = WITNESS_VERSION_0;
224
+ bytes memory words = to5BitWords(outputScript[2:]);
225
+ for (uint i = 1; i < length; i++) {
226
+ result[i] = words[i - 1];
227
+ }
228
+ return result;
229
+ }
110
230
 
111
- uint8 i;
112
- for (i = 0; i < source.length; i++) {
113
- dataWithVersion[i + 1] = source[i];
231
+ /// @notice Parse a raw pay-to-witness-script-hash output script to get the corresponding address words,
232
+ /// the resulting words are only the data part of the bech32 encoding and doesn't include the HRP
233
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
234
+ /// @return The address bech32 words generated using the script hash
235
+ function parsePayToWitnessScriptHash(bytes calldata outputScript) public pure returns (bytes memory) {
236
+ require(isP2WSHOutput(outputScript), "Script hasn't the required structure");
237
+ uint length = 1 + total5BitWords(SHA256_SIZE);
238
+ bytes memory result = new bytes(length);
239
+ result[0] = WITNESS_VERSION_0;
240
+ bytes memory words = to5BitWords(outputScript[2:]);
241
+ for (uint i = 1; i < length; i++) {
242
+ result[i] = words[i - 1];
114
243
  }
244
+ return result;
245
+ }
115
246
 
116
- return dataWithVersion;
247
+ /// @notice Parse a raw pay-to-taproot output script to get the corresponding address words,
248
+ /// the resulting words are only the data part of the bech32m encoding and doesn't include the HRP
249
+ /// @param outputScript the fragment of the raw transaction containing the raw output script
250
+ /// @return The address bech32m words generated using the taproot pubkey hash
251
+ function parsePayToTaproot(bytes calldata outputScript) public pure returns (bytes memory) {
252
+ require(isP2TROutput(outputScript), "Script hasn't the required structure");
253
+ uint length = 1 + total5BitWords(TAPROOT_PUBKEY_SIZE);
254
+ bytes memory result = new bytes(length);
255
+ result[0] = WITNESS_VERSION_1;
256
+ bytes memory words = to5BitWords(outputScript[2:]);
257
+ for (uint i = 1; i < length; i++) {
258
+ result[i] = words[i - 1];
259
+ }
260
+ return result;
117
261
  }
118
262
 
119
263
  /// @notice Parse a raw null-data output script to get its content
@@ -121,7 +265,7 @@ library BtcUtils {
121
265
  /// @return The content embedded inside the script
122
266
  function parseNullDataScript(bytes calldata outputScript) public pure returns (bytes memory) {
123
267
  require(outputScript.length > 1,"Invalid size");
124
- require(outputScript[0] == 0x6a, "Not OP_RETURN");
268
+ require(outputScript[0] == OpCodes.OP_RETURN, "Not OP_RETURN");
125
269
  return outputScript[1:];
126
270
  }
127
271
 
@@ -165,7 +309,8 @@ library BtcUtils {
165
309
  (uint32(uint8(bs[offset + 3])) << 24);
166
310
  }
167
311
 
168
- /// @notice Check if a pay-to-script-hash address belogs to a specific script
312
+ /// @notice Check if a pay-to-script-hash address belogs to a specific script, expects the address
313
+ /// bytes to include the 4 checksum bytes at the end
169
314
  /// @param p2sh the pay-to-script-hash address
170
315
  /// @param script the script to check
171
316
  /// @param mainnet flag to specify if its a mainnet address
@@ -174,14 +319,15 @@ library BtcUtils {
174
319
  return p2sh.length == 25 && keccak256(p2sh) == keccak256(getP2SHAddressFromScript(script, mainnet));
175
320
  }
176
321
 
177
- /// @notice Generate a pay-to-script-hash address from a script
322
+ /// @notice Generate a pay-to-script-hash address from a script, the resulting byte array already contains
323
+ /// the 4 checksum bytes at the end of it
178
324
  /// @param script the script to generate the address from
179
325
  /// @param mainnet flag to specify if the output should be a mainnet address
180
326
  /// @return The address generate from the script
181
327
  function getP2SHAddressFromScript(bytes calldata script, bool mainnet) public pure returns (bytes memory) {
182
328
  bytes20 scriptHash = ripemd160(abi.encodePacked(sha256(script)));
183
- uint8 versionByte = mainnet ? 0x5 : 0xc4;
184
- bytes memory versionAndHash = bytes.concat(bytes1(versionByte), scriptHash);
329
+ bytes1 versionByte = mainnet ? SCRIPT_HASH_MAINNET_BYTE : SCRIPT_HASH_TESTNET_BYTE;
330
+ bytes memory versionAndHash = bytes.concat(versionByte, scriptHash);
185
331
  bytes4 checksum = bytes4(sha256(abi.encodePacked(sha256(versionAndHash))));
186
332
  return bytes.concat(versionAndHash, checksum);
187
333
  }
@@ -220,4 +366,36 @@ library BtcUtils {
220
366
  }
221
367
  return result;
222
368
  }
369
+
370
+ /// @notice Referece for implementation: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
371
+ function to5BitWords(bytes memory byteArray) private pure returns(bytes memory) {
372
+ uint8 MAX_VALUE = 31;
373
+
374
+ uint currentValue = 0;
375
+ uint bitCount = 0;
376
+ uint8 resultIndex = 0;
377
+ bytes memory result = new bytes(total5BitWords(byteArray.length));
378
+
379
+ for (uint i = 0; i < byteArray.length; ++i) {
380
+ currentValue = (currentValue << BYTE_SIZE) | uint8(byteArray[i]);
381
+ bitCount += BYTE_SIZE;
382
+ while (bitCount >= BECH32_WORD_SIZE) {
383
+ bitCount -= BECH32_WORD_SIZE;
384
+ // this mask ensures that the result will always have 5 bits
385
+ result[resultIndex] = bytes1(uint8((currentValue >> bitCount) & MAX_VALUE));
386
+ resultIndex++;
387
+ }
388
+ }
389
+
390
+ if (bitCount > 0) {
391
+ result[resultIndex] = bytes1(uint8((currentValue << (BECH32_WORD_SIZE - bitCount)) & MAX_VALUE));
392
+ }
393
+ return result;
394
+ }
395
+
396
+ function total5BitWords(uint numberOfBytes) private pure returns(uint) {
397
+ uint total = (numberOfBytes * BYTE_SIZE) / BECH32_WORD_SIZE;
398
+ bool extraWord = (numberOfBytes * BYTE_SIZE) % BECH32_WORD_SIZE == 0;
399
+ return total + (extraWord? 0 : 1);
400
+ }
223
401
  }
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.18;
3
+
4
+
5
+ library OpCodes {
6
+ bytes1 public constant OP_DUP = 0x76;
7
+ bytes1 public constant OP_HASH160 = 0xa9;
8
+ bytes1 public constant OP_EQUALVERIFY = 0x88;
9
+ bytes1 public constant OP_CHECKSIG = 0xac;
10
+ bytes1 public constant OP_RETURN = 0x6a;
11
+ bytes1 public constant OP_EQUAL = 0x87;
12
+
13
+ bytes1 public constant OP_0 = 0x00;
14
+ bytes1 public constant OP_1 = 0x51;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsksmart/btc-transaction-solidity-helper",
3
- "version": "0.0.3",
3
+ "version": "0.2.1",
4
4
  "description": "Solidity library with functions to work with Bitcoin transactions inside smart contracts",
5
5
  "main": "contracts",
6
6
  "files": [
@@ -11,7 +11,7 @@
11
11
  "test:coverage": "npx hardhat coverage",
12
12
  "lint": "npx hardhat check",
13
13
  "compile": "npx hardhat compile",
14
- "prepare": "husky install"
14
+ "prepare": "pip3 install pre-commit && pre-commit install"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
@@ -37,7 +37,9 @@
37
37
  "devDependencies": {
38
38
  "@nomicfoundation/hardhat-toolbox": "^3.0.0",
39
39
  "@nomiclabs/hardhat-solhint": "^3.0.1",
40
+ "bech32": "^2.0.0",
41
+ "bs58check": "^3.0.1",
40
42
  "hardhat": "^2.17.0",
41
- "husky": "^8.0.3"
43
+ "solidity-coverage": "^0.8.12"
42
44
  }
43
45
  }