Skip to content

Commit

Permalink
feat: simplify address encoding and decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
reednaa committed Feb 25, 2024
1 parent aae9e26 commit 19b1411
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 77 deletions.
1 change: 1 addition & 0 deletions src/library/BitcoinOpcodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.0;
// Opcodes
bytes1 constant OP_0 = 0x00;
bytes1 constant PUSH_20 = 0x14;
bytes1 constant PUSH_32 = 0x20;
bytes1 constant PUSH_75 = 0x4b;
bytes1 constant OP_PUSHDATA1 = 0x4c;
bytes1 constant OP_PUSHDATA2 = 0x4d;
Expand Down
104 changes: 27 additions & 77 deletions src/library/BtcScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity >=0.8.0;

import "./BitcoinOpcodes.sol";
import { Endian } from "../Endian.sol";

enum AddressType {
UNKNOWN,
Expand All @@ -13,22 +12,13 @@ enum AddressType {
P2TR
}

error BadArgumentLength(uint256 given, uint256 expected);
error WitnessVersionGt16();
error WitnessLengthGt75();
error WrongWitnessVersion();

/**
* @notice A Parsed Script address
*/
struct BitcoinAddress {
AddressType addressType;
/** @dev P2PKH, address hash or P2SH, script hash. Is empty if segwit transaction */
bytes20 legacyAddress;
/** @dev Witness version */
uint8 witnessVersion;
/** @dev Witness Program */
bytes witnessProgram;
bytes32 implementationHash;
}

/**
Expand All @@ -49,31 +39,29 @@ contract BtcScript {
if (firstByte == OP_DUB) {
if (script.length == P2PKH_SCRIPT_LENGTH) {
btcAddress.addressType = AddressType.P2PKH;
btcAddress.legacyAddress = decodeP2PKH(script);
btcAddress.implementationHash = decodeP2PKH(script);
return btcAddress;
}
} else if (firstByte == OP_HASH160) {
if (script.length == P2SH_SCRIPT_LENGTH) {
btcAddress.addressType = AddressType.P2SH;
btcAddress.legacyAddress = decodeP2SH(script);
btcAddress.implementationHash = decodeP2SH(script);
return btcAddress;
}
} else {
// This is likely a segwit transaction. Try decoding the witness program
(int8 version, bytes calldata witPro) = decodeWitnessProgram(script);
(int8 version, uint8 witnessLength, bytes32 witPro) = decodeWitnessProgram(script);
if (version != -1) {
if (version == 0) {
uint256 witnessProgramLength = witPro.length;
if (witnessProgramLength == 20) {
if (witnessLength == 20) {
btcAddress.addressType = AddressType.P2WPKH;
} else if (witnessProgramLength == 32) {
} else if (witnessLength == 32) {
btcAddress.addressType = AddressType.P2WSH;
}
} else if (version == 1) {
btcAddress.addressType = AddressType.P2TR;
}
btcAddress.witnessVersion = uint8(version);
btcAddress.witnessProgram = witPro;
btcAddress.implementationHash = witPro;
return btcAddress;
}
}
Expand Down Expand Up @@ -120,12 +108,13 @@ contract BtcScript {
/**
* @dev Returns the witness program segwit tx.
* @return version The script version, or -1 if verification failed.
* @return hash The witness program, or nothing if verification failed.
* @return witnessLength The length of the witness program. Should either be 20 or 32.
* @return witPro The witness program, or nothing if verification failed.
*/
function decodeWitnessProgram(bytes calldata script)
public
pure
returns (int8 version, bytes calldata)
returns (int8 version, uint8 witnessLength, bytes32 witPro)
{
bytes1 versionBytes1 = script[0];
if (versionBytes1 == OP_0) {
Expand All @@ -135,35 +124,17 @@ contract BtcScript {
version = int8(uint8(versionBytes1)) - int8(uint8(LESS_THAN_OP_1));
}
} else {
return (version = -1, script[0:0]);
return (version = -1, witnessLength = 0, witPro = bytes32(script[0:0]));
}
// Check that the length is given and correct.
uint8 length_byte = uint8(bytes1(script[1]));
// Check if the length is between 1 and 75. If it is more than 75, we need to decode the length in a different want.
// Check if the length is between 1 and 75. If it is more than 75, we need to decode the length in a different way. Currently, only length 20 and 32 are used.
if (1 <= length_byte && length_byte <= 75) {
if (script.length == length_byte + 2) {
return (version, script[2:]);
return (version, witnessLength = length_byte, witPro = bytes32(script[2:]));
}
}
// Currently the spec does not allow for other opcodes but the ones from 1 to 75, so the below is not valid
// witness program pushes.
// } else if (length_byte == uint8(OP_PUSHDATA1)) {
// uint8 length = uint8(bytes1(script[3]));
// if (script.length == length + 3) {
// return (version, script[3:]);
// }
// } else if (length_byte == uint8(OP_PUSHDATA2)) {
// uint16 length = Endian.reverse16(uint16(bytes2(script[3:4])));
// if (script.length == length + 4) {
// return (version, script[4:]);
// }
// } else if (length_byte == uint8(OP_PUSHDATA4)) {
// uint32 length = Endian.reverse32(uint32(bytes4(script[3:6])));
// if (script.length == length + 6) {
// return (version, script[6:]);
// }
// }
return (version = -1, script[0:0]);
return (version = -1, witnessLength = 0, bytes32(script[0:0]));
}


Expand All @@ -175,19 +146,16 @@ contract BtcScript {
*/
function getBitcoinScript(BitcoinAddress calldata btcAddress) external pure returns(bytes memory script) {
// Check if segwit
if (btcAddress.addressType == AddressType.P2PKH) return scriptP2PKH(btcAddress.legacyAddress);
if (btcAddress.addressType == AddressType.P2SH) return scriptP2SH(btcAddress.legacyAddress);
if (btcAddress.addressType == AddressType.P2PKH) return scriptP2PKH(bytes20(btcAddress.implementationHash));
if (btcAddress.addressType == AddressType.P2SH) return scriptP2SH(bytes20(btcAddress.implementationHash));
if (btcAddress.addressType == AddressType.P2WPKH) {
if (btcAddress.witnessVersion != 0) revert WrongWitnessVersion();
return scriptP2WPKH(btcAddress.witnessProgram);
return scriptP2WPKH(bytes20(btcAddress.implementationHash));
}
if (btcAddress.addressType == AddressType.P2SH) {
if (btcAddress.witnessVersion != 0) revert WrongWitnessVersion();
return scriptP2WSH(btcAddress.witnessProgram);
return scriptP2WSH(btcAddress.implementationHash);
}
if (btcAddress.addressType == AddressType.P2TR) {
if (btcAddress.witnessVersion != 1) revert WrongWitnessVersion();
return scriptP2TR(btcAddress.witnessProgram);
return scriptP2TR(btcAddress.implementationHash);
}
}

Expand All @@ -199,38 +167,20 @@ contract BtcScript {

/// @notice Get the associated script out for a P2SH address
function scriptP2SH(bytes20 sHash) public pure returns(bytes memory) {
// OP_HASH, <data 20>, OP_EQUAL
// OP_HASH160, <data 20>, OP_EQUAL
return bytes.concat(OP_HASH160, PUSH_20, sHash, OP_EQUAL);
}

function scriptP2WPKH(bytes calldata pubkeyhash) public pure returns(bytes memory) {
if (pubkeyhash.length != 20) revert BadArgumentLength(pubkeyhash.length, 20);
return scriptWitness(0, pubkeyhash);
}

function scriptP2WSH(bytes calldata witnessScript) public pure returns(bytes memory) {
if (witnessScript.length != 32) revert BadArgumentLength(witnessScript.length, 32);
return scriptWitness(0, witnessScript);
function scriptP2WPKH(bytes20 witnessProgram) public pure returns(bytes memory) {
// OP_0, <data 20>
return bytes.concat(OP_0, PUSH_20, witnessProgram);
}

function scriptP2TR(bytes calldata witnessScript) public pure returns(bytes memory) {
// TODO: Is there a fixed length for taproot?
return scriptWitness(1, witnessScript);
function scriptP2WSH(bytes32 witnessProgram) public pure returns(bytes memory) {
return bytes.concat(OP_0, PUSH_32, witnessProgram);
}

function scriptWitness(uint8 witnessVersion, bytes calldata witnessProgram) public pure returns(bytes memory) {
bytes1 witnessBytes;
// Currently only 2 witness versions exist but this allows for future proofing.
// The number of Bitcoin number opcodes only allow for number 0 through 16.
if (witnessVersion > 16) revert WitnessVersionGt16();
if (witnessVersion == 0) {
witnessBytes = OP_0;
} else {
witnessBytes = bytes1(witnessVersion + uint8(LESS_THAN_OP_1));
}
// The length can't be longer than 75 bytes otherwise we should use another push opcode.
uint8 witnessLength = uint8(witnessProgram.length);
if (witnessLength > 75) revert WitnessLengthGt75();
return bytes.concat(witnessBytes, bytes1(uint8(witnessLength)), witnessProgram);
function scriptP2TR(bytes32 witnessProgram) public pure returns(bytes memory) {
return bytes.concat(OP_0, PUSH_32, witnessProgram);
}
}

0 comments on commit 19b1411

Please sign in to comment.