@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 +21 -0
- package/Readme.md +31 -0
- package/contracts/BtcUtils.sol +223 -0
- package/package.json +43 -0
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
|
+
}
|