From 6de2786a5a9aaf8fd1ec4796c2ce077d8d460f27 Mon Sep 17 00:00:00 2001 From: Viacheslav Zhygulin Date: Tue, 25 Jun 2024 19:48:51 +0300 Subject: [PATCH] move bitcoin utils to this repo --- script/SetSeed.s.sol | 5 +- src/BTCDepositAddressDeriver.sol | 41 +-- src/Base58.sol | 158 ++++++++++ src/BitcoinNetworkEncoder.sol | 47 +++ src/BitcoinUtils.sol | 454 ++++++++++++++++++++++++++++ test/BTCDepositAddressDeriver.t.sol | 7 +- test/BitcoinUtils_Mainnet.t.sol | 45 +++ test/BitcoinUtils_Regtest.t.sol | 35 +++ test/BitcoinUtils_Testnet.t.sol | 37 +++ 9 files changed, 804 insertions(+), 25 deletions(-) create mode 100644 src/Base58.sol create mode 100644 src/BitcoinNetworkEncoder.sol create mode 100644 src/BitcoinUtils.sol create mode 100644 test/BitcoinUtils_Mainnet.t.sol create mode 100644 test/BitcoinUtils_Regtest.t.sol create mode 100644 test/BitcoinUtils_Testnet.t.sol diff --git a/script/SetSeed.s.sol b/script/SetSeed.s.sol index aae7cf0..04275f8 100644 --- a/script/SetSeed.s.sol +++ b/script/SetSeed.s.sol @@ -9,6 +9,7 @@ import {Tools} from "../src/Tools.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {AddressReaderWriter} from "./AddressReaderWriter.s.sol"; import {console} from "forge-std/console.sol"; +import {BitcoinNetworkEncoder} from "../src/BitcoinNetworkEncoder.sol"; contract SetSeed is Script, AddressReaderWriter { function run() external { @@ -40,9 +41,9 @@ contract SetSeed is Script, AddressReaderWriter { // get network uint _network = vm.envUint("BTC_NETWORK"); - uint8 network = uint8(_network); + BitcoinNetworkEncoder.Network network = BitcoinNetworkEncoder.Network(_network); - console.log("BTC_NETWORK:", network); + console.log("BTC_NETWORK:", _network); BTCDepositAddressDeriver deriver = BTCDepositAddressDeriver( contractAddress diff --git a/src/BTCDepositAddressDeriver.sol b/src/BTCDepositAddressDeriver.sol index 88f2818..2929bbc 100644 --- a/src/BTCDepositAddressDeriver.sol +++ b/src/BTCDepositAddressDeriver.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {Deriver} from "./Deriver.sol"; import {Bech32m} from "./Bech32m.sol"; +import {BitcoinNetworkEncoder} from "./BitcoinNetworkEncoder.sol"; import {console} from "forge-std/console.sol"; error SeedWasNotSetYet(); @@ -43,9 +44,9 @@ contract BTCDepositAddressDeriver { function setSeed( string calldata _btcAddr1, string calldata _btcAddr2, - uint8 _network + BitcoinNetworkEncoder.Network _network ) public virtual { - string memory _hrp = getNetworkPrefix(_network); + string memory _hrp = BitcoinNetworkEncoder.getNetworkPrefix(_network); networkHrp = _hrp; @@ -60,24 +61,24 @@ contract BTCDepositAddressDeriver { } // get address prefix from network type - function getNetworkPrefix( - uint8 _network - ) public pure returns (string memory) { - - string memory _hrp; - - if (_network == 0) { - _hrp = 'tb'; - } else if (_network == 1) { - _hrp = 'bc'; - } else if (_network == 2) { - _hrp = 'brct'; - } else { - _hrp = 'unknown'; - } - - return _hrp; - } + //function getNetworkPrefix( + // uint8 _network + //) public pure returns (string memory) { + + // string memory _hrp; + + // if (_network == 0) { + // _hrp = 'tb'; + // } else if (_network == 1) { + // _hrp = 'bc'; + // } else if (_network == 2) { + // _hrp = 'brct'; + // } else { + // _hrp = 'unknown'; + // } + + // return _hrp; + //} // Derive pubkey's (x,y) coordinates from taproot address function parseBTCTaprootAddress( diff --git a/src/Base58.sol b/src/Base58.sol new file mode 100644 index 0000000..5cf6331 --- /dev/null +++ b/src/Base58.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +bytes constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +/** + * @notice encode is used to encode the given bytes in base58 standard. + * @param data_ raw data, passed in as bytes. + * @return base58 encoded data_, returned as bytes. + */ +function encode(bytes memory data_) pure returns (bytes memory) { + unchecked { + uint256 size = data_.length; + uint256 zeroCount; + while (zeroCount < size && data_[zeroCount] == 0) { + zeroCount++; + } + size = zeroCount + ((size - zeroCount) * 8351) / 6115 + 1; + bytes memory slot = new bytes(size); + uint32 carry; + int256 m; + int256 high = int256(size) - 1; + for (uint256 i = 0; i < data_.length; i++) { + m = int256(size - 1); + for (carry = uint8(data_[i]); m > high || carry != 0; m--) { + carry = carry + 256 * uint8(slot[uint256(m)]); + slot[uint256(m)] = bytes1(uint8(carry % 58)); + carry /= 58; + } + high = m; + } + uint256 n; + for (n = zeroCount; n < size && slot[n] == 0; n++) {} + size = slot.length - (n - zeroCount); + bytes memory out = new bytes(size); + for (uint256 i = 0; i < size; i++) { + uint256 j = i + n - zeroCount; + out[i] = ALPHABET[uint8(slot[j])]; + } + return out; + } +} + +/** + * @notice decode is used to decode the given string in base58 standard. + * @param data_ data encoded with base58, passed in as bytes. + * @return raw data, returned as bytes. + */ +function decode(bytes memory data_) pure returns (bytes memory) { + unchecked { + uint256 zero = 49; + uint256 b58sz = data_.length; + uint256 zcount = 0; + for (uint256 i = 0; i < b58sz && uint8(data_[i]) == zero; i++) { + zcount++; + } + uint256 t; + uint256 c; + bool f; + bytes memory binu = new bytes(2 * (((b58sz * 8351) / 6115) + 1)); + uint32[] memory outi = new uint32[]((b58sz + 3) / 4); + for (uint256 i = 0; i < data_.length; i++) { + bytes1 r = data_[i]; + (c, f) = indexOf(ALPHABET, r); + require(f, "invalid base58 digit"); + for (int256 k = int256(outi.length) - 1; k >= 0; k--) { + t = uint64(outi[uint256(k)]) * 58 + c; + c = t >> 32; + outi[uint256(k)] = uint32(t & 0xffffffff); + } + } + uint64 mask = uint64(b58sz % 4) * 8; + if (mask == 0) { + mask = 32; + } + mask -= 8; + uint256 outLen = 0; + for (uint256 j = 0; j < outi.length; j++) { + while (mask < 32) { + binu[outLen] = bytes1(uint8(outi[j] >> mask)); + outLen++; + if (mask < 8) { + break; + } + mask -= 8; + } + mask = 24; + } + for (uint256 msb = zcount; msb < binu.length; msb++) { + if (binu[msb] > 0) { + return slice(binu, msb - zcount, outLen); + } + } + return slice(binu, 0, outLen); + } +} + +/** + * @notice encodeToString is used to encode the given byte in base58 standard. + * @param data_ raw data, passed in as bytes. + * @return base58 encoded data_, returned as a string. + */ +function encodeToString(bytes memory data_) pure returns (string memory) { + return string(encode(data_)); +} + +/** + * @notice encodeFromString is used to encode the given string in base58 standard. + * @param data_ raw data, passed in as a string. + * @return base58 encoded data_, returned as bytes. + */ +function encodeFromString(string memory data_) pure returns (bytes memory) { + return encode(bytes(data_)); +} + +/** + * @notice decode is used to decode the given string in base58 standard. + * @param data_ data encoded with base58, passed in as string. + * @return raw data, returned as bytes. + */ +function decodeFromString(string memory data_) pure returns (bytes memory) { + return decode(bytes(data_)); +} + +/** + * @notice slice is used to slice the given byte, returns the bytes in the range of [start_, end_) + * @param data_ raw data, passed in as bytes. + * @param start_ start index. + * @param end_ end index. + * @return slice data + */ +function slice(bytes memory data_, uint256 start_, uint256 end_) pure returns (bytes memory) { + unchecked { + bytes memory ret = new bytes(end_ - start_); + for (uint256 i = 0; i < end_ - start_; i++) { + ret[i] = data_[i + start_]; + } + return ret; + } +} + +/** + * @notice indexOf is used to find where char_ appears in data_. + * @param data_ raw data, passed in as bytes. + * @param char_ target byte. + * @return index, and whether the search was successful. + */ +function indexOf(bytes memory data_, bytes1 char_) pure returns (uint256, bool) { + unchecked { + for (uint256 i = 0; i < data_.length; i++) { + if (data_[i] == char_) { + return (i, true); + } + } + return (0, false); + } +} diff --git a/src/BitcoinNetworkEncoder.sol b/src/BitcoinNetworkEncoder.sol new file mode 100644 index 0000000..59b484d --- /dev/null +++ b/src/BitcoinNetworkEncoder.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +library BitcoinNetworkEncoder { + + bytes constant BTC_BECH32_MAINNET_BYTES = hex"626331"; // prefix = bc1 + bytes constant BTC_BECH32_TESTNET_BYTES = hex"746231"; // prefix = tb1 + bytes constant BTC_BECH32_REGTEST_BYTES = hex"6263727431"; // prefix = bcrt1 + + string constant BTC_BECH32_MAINNET = 'bc'; + string constant BTC_BECH32_TESTNET = 'tb'; + string constant BTC_BECH32_REGTEST = 'brct'; + + //NB: don't forget to update `lnbtc_ext.go` when changing this enum! + enum Network { + Mainnet, + Testnet, + Regtest + } + + function getBtcBech32Prefix(Network _network) public pure returns (bytes memory) { + if (_network == Network.Mainnet) { + return BTC_BECH32_MAINNET_BYTES; + } else if (_network == Network.Regtest) { + return BTC_BECH32_REGTEST_BYTES; + } else if (_network == Network.Testnet) { + return BTC_BECH32_TESTNET_BYTES; + } else { + revert("Unknown network type"); + } + } + + function getNetworkPrefix(Network _network) public pure returns (string memory) { + if (_network == Network.Mainnet) { + return BTC_BECH32_MAINNET; + } else if (_network == Network.Testnet) { + return BTC_BECH32_TESTNET; + } else if (_network == Network.Regtest) { + return BTC_BECH32_REGTEST; + } else { + revert("Unknown network type"); + } + } + + +} \ No newline at end of file diff --git a/src/BitcoinUtils.sol b/src/BitcoinUtils.sol new file mode 100644 index 0000000..912e1c2 --- /dev/null +++ b/src/BitcoinUtils.sol @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import "forge-std/console.sol"; + +// TODO: import from submodules instead including in the project? +import "./Base58.sol"; +import "./BitcoinNetworkEncoder.sol"; + +// TODO: make it a library, and auto-link inside go-bindings +contract BitcoinUtils { + // There are currently three invoice address formats in use: + + // P2PKH which begin with the number 1, eg: 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2 + // P2SH type starting with the number 3, eg: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy + // Bech32 type starting with bc1, eg: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq + + // Testnet: + // P2PKH which begin with the number m or n, eg: mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn + // P2SH type starting with the number 2, eg: 2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc + // Bech32 type starting with tb1, eg: tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx + + // Regtest: + // P2PKH which begin with the number m or n, eg: mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn + // P2SH type starting with the number 2, eg: 2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc + // Bech32 type starting with bcrt1, eg: bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx + + //NB: don't forget to update `lnbtc_ext.go` when changing this enum! + //enum Network { + // Mainnet, + // Testnet, + // Regtest + //} + + string constant BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + function BECH32_ALPHABET_MAP(bytes1 char) public view returns (uint8) { + // '{"0":15,"2":10,"3":17,"4":21,"5":20,"6":26,"7":30,"8":7,"9":5,"q":0,"p":1,"z":2,"r":3,"y":4,"x":6,"g":8,"f":9,"t":11,"v":12,"d":13,"w":14,"s":16,"j":18,"n":19,"k":22,"h":23,"c":24,"e":25,"m":27,"u":28,"a":29,"l":31}' + + if (char == bytes1("0")) return 15; + if (char == bytes1("2")) return 10; + if (char == bytes1("3")) return 17; + if (char == bytes1("4")) return 21; + if (char == bytes1("5")) return 20; + if (char == bytes1("6")) return 26; + if (char == bytes1("8")) return 7; + if (char == bytes1("7")) return 30; + if (char == bytes1("9")) return 5; + + if (char == bytes1("q")) return 0; + if (char == bytes1("p")) return 1; + if (char == bytes1("z")) return 2; + if (char == bytes1("r")) return 3; + if (char == bytes1("y")) return 4; + if (char == bytes1("x")) return 6; + if (char == bytes1("g")) return 8; + if (char == bytes1("f")) return 9; + if (char == bytes1("t")) return 11; + if (char == bytes1("v")) return 12; + if (char == bytes1("d")) return 13; + if (char == bytes1("w")) return 14; + if (char == bytes1("s")) return 16; + if (char == bytes1("j")) return 18; + if (char == bytes1("n")) return 19; + if (char == bytes1("k")) return 22; + if (char == bytes1("h")) return 23; + if (char == bytes1("c")) return 24; + if (char == bytes1("e")) return 25; + if (char == bytes1("m")) return 27; + if (char == bytes1("u")) return 28; + if (char == bytes1("a")) return 29; + if (char == bytes1("l")) return 31; + + console.log("Invalid character"); + console.logBytes1(char); + // revert("Invalid character"); + + return type(uint8).max; + } + + // const ALPHABET_MAP: { [key: string]: number } = {}; + // for (let z = 0; z < ALPHABET.length; z++) { + // const x = ALPHABET.charAt(z); + // ALPHABET_MAP[x] = z; + // } + + bytes constant BTC_P2PKH_MAINNET = hex"31"; // prefix = 1 + bytes constant BTC_P2SH_MAINNET = hex"33"; // prefix = 3 + bytes constant BTC_P2PKH_TESTNET = hex"32"; // prefix = 2 + bytes constant BTC_P2SH_TESTNET = hex"6d"; // prefix = m + bytes constant BTC_P2PKH_REGTEST = hex"32"; // prefix = 2 + bytes constant BTC_P2SH_REGTEST = hex"6d"; // prefix = m + + //bytes constant BTC_BECH32_MAINNET = hex"626331"; // prefix = bc1 + //bytes constant BTC_BECH32_REGTEST = hex"6263727431"; // prefix = bcrt1 + //bytes constant BTC_BECH32_TESTNET = hex"746231"; // prefix = tb1 + + function getBtcBase58_P2PKH(BitcoinNetworkEncoder.Network network) public pure returns (bytes memory) { + if (network == BitcoinNetworkEncoder.Network.Mainnet) { + return BTC_P2PKH_MAINNET; + } else if (network == BitcoinNetworkEncoder.Network.Regtest) { + return BTC_P2PKH_REGTEST; + } else if (network == BitcoinNetworkEncoder.Network.Testnet) { + return BTC_P2PKH_TESTNET; + } else { + revert("Unknown network type"); + } + } + + function getBtcBase58_P2SH(BitcoinNetworkEncoder.Network network) public pure returns (bytes memory) { + if (network == BitcoinNetworkEncoder.Network.Mainnet) { + return BTC_P2SH_MAINNET; + } else if (network == BitcoinNetworkEncoder.Network.Regtest) { + return BTC_P2SH_REGTEST; + } else if (network == BitcoinNetworkEncoder.Network.Testnet) { + return BTC_P2SH_TESTNET; + } else { + revert("Unknown network type"); + } + } + + //function getBtcBech32Prefix(Network network) public pure returns (bytes memory) { + // if (network == Network.Mainnet) { + // return BTC_BECH32_MAINNET; + // } else if (network == Network.Regtest) { + // return BTC_BECH32_REGTEST; + // } else if (network == Network.Testnet) { + // return BTC_BECH32_TESTNET; + // } else { + // revert("Unknown network type"); + // } + //} + + function validateBitcoinAddress( + BitcoinNetworkEncoder.Network network, + string calldata BTCAddress + ) public view returns (bool) + { + bytes memory empty; + + if (equalBytes(bytes(BTCAddress), empty)) return false; + + console.log("\nraw address data"); + console.logBytes(bytes(BTCAddress)); + + bytes memory BTC_P2PKH = getBtcBase58_P2PKH(network); + bytes memory BTC_P2SH = getBtcBase58_P2SH(network); + + if (equalBytes(bytes(BTCAddress)[: 1], BTC_P2PKH) || equalBytes(bytes(BTCAddress)[: 1], BTC_P2SH)) { + if (bytes(BTCAddress).length < 26 || bytes(BTCAddress).length > 35 || !alphabetCheck(bytes(BTCAddress))) { + return false; + } + + // check base58 checksum and encoding + return validateBase58Checksum(BTCAddress); + } + + bytes memory prefix = BitcoinNetworkEncoder.getBtcBech32Prefix(network); + if (equalBytes(bytes(BTCAddress)[: prefix.length], prefix)) { + if (network == BitcoinNetworkEncoder.Network.Regtest) { + if (bytes(BTCAddress).length < 43 || bytes(BTCAddress).length > 63) return false; + } else { + if (bytes(BTCAddress).length < 42 || bytes(BTCAddress).length > 62) return false; + } + + // check bech32 checksum and encoding + return validateBech32Checksum(BTCAddress); + } + + return false; + } + + function equalBytes(bytes memory one, bytes memory two) public pure returns (bool) { + if (!(one.length == two.length)) { + return false; + } + for (uint256 i = 0; i < one.length; i++) { + if (!(one[i] == two[i])) { + return false; + } + } + return true; + } + + function alphabetCheck(bytes memory BTCAddress) public pure returns (bool) { + for (uint256 i = 0; i < BTCAddress.length; i++) { + uint8 charCode = uint8(BTCAddress[i]); + bool contains = isLetter(charCode); + if (!contains) return false; + } + + return true; + } + + function isLetter(uint8 charCode) internal pure returns (bool) { + if (charCode == 73 || charCode == 79 || charCode == 108) { + return false; + } + if (charCode >= 49 && charCode <= 57) { + return true; + } + if (charCode >= 65 && charCode <= 90) { + return true; + } + if (charCode >= 97 && charCode <= 122) { + return true; + } + return false; + } + + function validateBech32Checksum(string memory btcAddress) public view returns (bool) { + // TODO: DOESNT SUPPORT TAPROOT ADDRESSES + // from https://github.com/bitcoinjs/bech32/blob/master/src/index.ts + + // function __decode(str: string, LIMIT?: number): Decoded | string { + // LIMIT = LIMIT || 90; + // if (str.length < 8) return str + ' too short'; + // if (str.length > LIMIT) return 'Exceeds length limit'; + + // // don't allow mixed case + // const lowered = str.toLowerCase(); + // const uppered = str.toUpperCase(); + // if (str !== lowered && str !== uppered) return 'Mixed-case string ' + str; + // str = lowered; + + // const split = str.lastIndexOf('1'); + // if (split === -1) return 'No separator character for ' + str; + // if (split === 0) return 'Missing prefix for ' + str; + + // const prefix = str.slice(0, split); + // const wordChars = str.slice(split + 1); + // if (wordChars.length < 6) return 'Data too short'; + + // let chk = prefixChk(prefix); + // if (typeof chk === 'string') return chk; + + // const words = []; + // for (let i = 0; i < wordChars.length; ++i) { + // const c = wordChars.charAt(i); + // const v = ALPHABET_MAP[c]; + // if (v === undefined) return 'Unknown character ' + c; + // chk = polymodStep(chk) ^ v; + + // // not in the checksum? + // if (i + 6 >= wordChars.length) continue; + // words.push(v); + // } + + // if (chk !== ENCODING_CONST) return 'Invalid checksum for ' + str; + // return { prefix, words }; + // } + + console.log("\nvalidate bech32 checksum"); + + console.log("address"); + console.log(btcAddress); + + bytes memory _btcAddress = bytes(btcAddress); + + if (_btcAddress.length < 8) { + console.log("too short"); + return false; + } + + if (_btcAddress.length > 90) { + console.log("too long"); + return false; + } + + // TODO: don't allow mixed case + // bytes memory lowered = bytes(toLower(btcAddress)); + // bytes memory uppered = bytes(toUpper(btcAddress)); + + // if ( + // !equalBytes(_btcAddress, lowered) && + // !equalBytes(_btcAddress, uppered) + // ) { + // console.log("mixed case"); + // return false; + // } + + _btcAddress = bytes(btcAddress); + + uint256 split = 0; + + for (uint256 i = 0; i < _btcAddress.length; i++) { + if (_btcAddress[i] == "1") { + split = i; + break; + } + } + + if (split == 0) { + console.log("no separator"); + return false; + } + + if (split == 1) { + console.log("missing prefix"); + return false; + } + + bytes memory prefix = new bytes(split); + bytes memory wordChars = new bytes(_btcAddress.length - split - 1); + + for (uint256 i = 0; i < split; i++) { + prefix[i] = _btcAddress[i]; + } + + for (uint256 i = 0; i < wordChars.length; i++) { + wordChars[i] = _btcAddress[i + split + 1]; + } + + console.log("prefix"); + console.logBytes(prefix); + + // console.log("wordChars"); + // console.logBytes(wordChars); + + if (wordChars.length < 6) { + console.log("data too short"); + return false; + } + + uint256 chk = prefixChk(bytes(prefix)); + + if (chk == 0) { + console.log("invalid prefix"); + return false; + } + + bytes memory words = new bytes(wordChars.length); + + for (uint256 i = 0; i < wordChars.length; i++) { + bytes1 c = wordChars[i]; + uint8 v = BECH32_ALPHABET_MAP(c); + + // ALPHABET_MAP reverts if the character is not in the map, so this is not needed + if (v == type(uint8).max) { + console.log("unknown character"); + console.log(i); + console.logBytes1(c); + console.log("char", string(abi.encodePacked(c))); + return false; + } + + // console.log("v", v); + // console.log("char", string(abi.encodePacked(c))); + + chk = polymodStep(chk) ^ v; + + // not in the checksum? + if (i + 6 >= wordChars.length) continue; + + words[i] = bytes1(v); + } + + console.log("words"); + console.logBytes(words); + + // ENCODING_CONST is bech32 or bech32m + if (chk != uint256(0x2bc830a3) && chk != 1) { + console.log("invalid checksum", chk); + return false; + } + + console.log("valid checksum", chk); + + return true; + } + + function polymodStep(uint256 pre) public pure returns (uint256) { + uint256 b = pre >> 25; + + return ( + ((pre & 0x1ffffff) << 5) ^ ((b >> 0) & 1 == 1 ? 0x3b6a57b2 : 0) ^ ((b >> 1) & 1 == 1 ? 0x26508e6d : 0) + ^ ((b >> 2) & 1 == 1 ? 0x1ea119fa : 0) ^ ((b >> 3) & 1 == 1 ? 0x3d4233dd : 0) + ^ ((b >> 4) & 1 == 1 ? 0x2a1462b3 : 0) + ); + } + + function prefixChk(bytes memory prefix) public pure returns (uint256) { + uint256 chk = 1; + for (uint256 i = 0; i < bytes(prefix).length; ++i) { + uint256 c = uint8(prefix[i]); + if (c < 33 || c > 126) revert("Invalid prefix"); + + chk = polymodStep(chk) ^ (c >> 5); + } + chk = polymodStep(chk); + + for (uint256 i = 0; i < prefix.length; ++i) { + uint256 v = uint8(prefix[i]); + chk = polymodStep(chk) ^ (v & 0x1f); + } + return chk; + } + + function validateBase58Checksum(string calldata btcAddress) public view returns (bool) { + bytes memory rawData = decodeFromString(btcAddress); + + console.log("validateBase58Checksum"); + + console.log("payload"); + console.logBytes(rawData); + + // raw data is: 1 byte version + 20 bytes of data + 4 bytes of checksum + if (rawData.length != 25) return false; + + // version is 1 byte + + bytes memory version = new bytes(1); + version[0] = rawData[0]; + + console.log("version"); + console.logBytes(version); + + bytes memory payload = new bytes(rawData.length - 1 - 4); + for (uint256 i = 0; i < rawData.length - 1 - 4; i++) { + payload[i] = rawData[i + 1]; + } + + console.log("payload"); + console.logBytes(payload); + + if (payload.length != 20) return false; + + bytes memory checksum = new bytes(4); + for (uint256 i = 0; i < 4; i++) { + checksum[i] = rawData[rawData.length - 4 + i]; + } + + console.log("checksum"); + console.logBytes(checksum); + + bytes32 calculateChecksum = sha256(abi.encodePacked(sha256(abi.encodePacked(version, payload)))); + + console.log("calculated checksum"); + console.logBytes32(calculateChecksum); + + // if (checksum[0] ^ newChecksum[0] | + // checksum[1] ^ newChecksum[1] | + // checksum[2] ^ newChecksum[2] | + // checksum[3] ^ newChecksum[3]) return + + if ( + (checksum[0] ^ calculateChecksum[0]) | (checksum[1] ^ calculateChecksum[1]) + | (checksum[2] ^ calculateChecksum[2]) | (checksum[3] ^ calculateChecksum[3]) != 0 + ) return false; + + return true; + } +} diff --git a/test/BTCDepositAddressDeriver.t.sol b/test/BTCDepositAddressDeriver.t.sol index 3ca2eed..6358a1c 100644 --- a/test/BTCDepositAddressDeriver.t.sol +++ b/test/BTCDepositAddressDeriver.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.24; import {console} from "forge-std/console.sol"; import {Test} from "forge-std/Test.sol"; import {BTCDepositAddressDeriver} from "../src/BTCDepositAddressDeriver.sol"; +import {BitcoinNetworkEncoder} from "../src/BitcoinNetworkEncoder.sol"; contract BTCDepositAddressDeriverTest is Test { @@ -206,7 +207,7 @@ contract BTCDepositAddressDeriverTest is Test { deriver.setSeed( "tb1p7g532zgvuzv8fz3hs02wvn2almqh8qyvz4xdr564nannkxh28kdq62ewy3", "tb1psfpmk6v8cvd8kr4rdda0l8gwyn42v5yfjlqkhnureprgs5tuumkqvdkewz", - 0 + BitcoinNetworkEncoder.Network.Testnet ); assertEq(deriver.wasSeedSet(), true); @@ -241,7 +242,7 @@ contract BTCDepositAddressDeriverTest is Test { deriver.setSeed( "tb1p7g532zgvuzv8fz3hs02wvn2almqh8qyvz4xdr564nannkxh28kdq62ewy3", "tb1psfpmk6v8cvd8kr4rdda0l8gwyn42v5yfjlqkhnureprgs5tuumkqvdkewz", - 0 + BitcoinNetworkEncoder.Network.Testnet ); string memory btcAddress = deriver.getBTCDepositAddress( @@ -257,7 +258,7 @@ contract BTCDepositAddressDeriverTest is Test { deriver.setSeed( "tb1p5z8wl5tu7m0d79vzqqsl9gu0x4fkjug857fusx4fl4kfgwh5j25spa7245", "tb1pfusykjdt46ktwq03d20uqqf94uh9487344wr3q5v9szzsxnjdfks9apcjz", - 0 + BitcoinNetworkEncoder.Network.Testnet ); string memory btcAddress = deriver.getBTCDepositAddress( diff --git a/test/BitcoinUtils_Mainnet.t.sol b/test/BitcoinUtils_Mainnet.t.sol new file mode 100644 index 0000000..fbb195a --- /dev/null +++ b/test/BitcoinUtils_Mainnet.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import "../src/BitcoinUtils.sol"; +import "../src/BitcoinNetworkEncoder.sol"; + +// See also https://en.bitcoin.it/wiki/List_of_address_prefixes + +contract BitcoinUtils_Mainnet_Test is Test { + BitcoinNetworkEncoder.Network private network = BitcoinNetworkEncoder.Network.Mainnet; + BitcoinUtils private utils = new BitcoinUtils(); + + function testValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); + assertTrue(utils.validateBitcoinAddress(network, "15hPYnf4qXCbDBi96DsUPdZ34RyZ5Lou1a")); + } + + function testInvalidAddress() public { + assertFalse(utils.validateBitcoinAddress(network, "")); + assertFalse(utils.validateBitcoinAddress(network, "7SeEnXWPaCCALbVrTnszCVGfRU8cGfx")); + assertFalse(utils.validateBitcoinAddress(network, "j9ywUkWg2fTQrouxxh5rSZhRvrjMkEUfuiKe")); + } + + function testBech32ValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0")); + assertTrue(utils.validateBitcoinAddress(network, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")); + } + + function testBech32InvalidAddress() public { + assertFalse(utils.validateBitcoinAddress(network, "BC1SW50QA3JX3S")); + + // wrong encoding + assertFalse(utils.validateBitcoinAddress(network, "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du")); + + // invalid checksum + assertFalse(utils.validateBitcoinAddress(network, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5")); + + assertFalse(utils.validateBitcoinAddress(network, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7")); + + assertFalse(utils.validateBitcoinAddress(network, "tb1pw508d6qejxtdg4y5r3zarquvzkan")); + } +} diff --git a/test/BitcoinUtils_Regtest.t.sol b/test/BitcoinUtils_Regtest.t.sol new file mode 100644 index 0000000..d94ff4a --- /dev/null +++ b/test/BitcoinUtils_Regtest.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import "../src/BitcoinUtils.sol"; +import "../src/BitcoinNetworkEncoder.sol"; + +// See also https://en.bitcoin.it/wiki/List_of_address_prefixes + +contract BitcoinUtils_Regtest_Test is Test { + BitcoinNetworkEncoder.Network private network = BitcoinNetworkEncoder.Network.Regtest; + BitcoinUtils private utils = new BitcoinUtils(); + + function testValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w")); + assertTrue(utils.validateBitcoinAddress(network, "mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r")); + } + + function testInvalidAddress() public { + assertFalse(utils.validateBitcoinAddress(network, "")); + assertFalse(utils.validateBitcoinAddress(network, "7SeEnXWPaCCALbVrTnszCVGfRU8cGfx")); + assertFalse(utils.validateBitcoinAddress(network, "j9ywUkWg2fTQrouxxh5rSZhRvrjMkEUfuiKe")); + } + + function testBech32ValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "bcrt1qnd2xm45v0uy5nx3qzt28qrhq42w4udrms8sz52")); + } + + function testBech32ValidMainnetAddressIsNotValidForRegtest() public { + assertFalse(utils.validateBitcoinAddress(network, "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0")); + assertFalse(utils.validateBitcoinAddress(network, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")); + } +} diff --git a/test/BitcoinUtils_Testnet.t.sol b/test/BitcoinUtils_Testnet.t.sol new file mode 100644 index 0000000..431bd63 --- /dev/null +++ b/test/BitcoinUtils_Testnet.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import "../src/BitcoinUtils.sol"; +import "../src/BitcoinNetworkEncoder.sol"; + +// See also https://en.bitcoin.it/wiki/List_of_address_prefixes + +contract BitcoinUtils_Testnet_Test is Test { + BitcoinNetworkEncoder.Network private network = BitcoinNetworkEncoder.Network.Testnet; + BitcoinUtils private utils = new BitcoinUtils(); + + function testValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w")); + assertTrue(utils.validateBitcoinAddress(network, "mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r")); + assertTrue(utils.validateBitcoinAddress(network, "2NFPLS6TQVVvic6Nh85PGfcYesbGdm1fjpo")); + } + + function testInvalidAddress() public { + assertFalse(utils.validateBitcoinAddress(network, "")); + assertFalse(utils.validateBitcoinAddress(network, "7SeEnXWPaCCALbVrTnszCVGfRU8cGfx")); + assertFalse(utils.validateBitcoinAddress(network, "j9ywUkWg2fTQrouxxh5rSZhRvrjMkEUfuiKe")); + } + + function testBech32ValidAddress() public { + assertTrue(utils.validateBitcoinAddress(network, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")); + assertTrue(utils.validateBitcoinAddress(network, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")); + } + + function testBech32ValidMainnetAddressIsNotValidForTestnet() public { + assertFalse(utils.validateBitcoinAddress(network, "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0")); + assertFalse(utils.validateBitcoinAddress(network, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")); + } +}