diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index b25cfb66..903e9d12 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -345,7 +345,7 @@ "Extract Segwit Addresses", "Extract Seedphrases", "Deserialize Extended Key", - "Public Key To Cryptocurrency Address", + "Public Key To Bitcoin-Like Address", "To WIF Format", "From WIF Format", "Type Cryptocurrency Artifact", diff --git a/src/core/lib/Bitcoin.mjs b/src/core/lib/Bitcoin.mjs index 45c50a10..0cdb21d7 100644 --- a/src/core/lib/Bitcoin.mjs +++ b/src/core/lib/Bitcoin.mjs @@ -7,10 +7,12 @@ */ import CryptoApi from "crypto-api/src/crypto-api.mjs"; -import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; +import { fromArrayBuffer} from "crypto-api/src/encoder/array-buffer.mjs"; import {toHex} from "crypto-api/src/encoder/hex.mjs"; import Utils from "../Utils.mjs"; import OperationError from "../errors/OperationError.mjs"; +import BigNumber from "bignumber.js"; + /** * Validates the length of the passed in input as one of the allowable lengths. @@ -162,6 +164,57 @@ export function hash160Func(input) { return ripemdHasher.finalize(); } +// Tag Hash defined in https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +/** + * Tag Hash defined in BIP340 https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki + * Hash is defined as SHA256(SHA256(tag) || SHA256(tag) || x) + * @param {*} input + * @returns + */ +export function tweakHash(input) { + const sha256Hasher = CryptoApi.getHasher("sha256"); + sha256Hasher.update("TapTweak"); + const tagHash = sha256Hasher.finalize(); + const sha256Hasher2 = CryptoApi.getHasher("sha256"); + sha256Hasher2.update(tagHash); + sha256Hasher2.update(tagHash); + sha256Hasher2.update(input); + const result = sha256Hasher2.finalize(); + return result; +} + +/** + * Given x, returns the point P(x) where the y-coordinate is even. Fails if x is greater than p-1 or if the point does not exist. + * Since this is mostly going to be used for analysis and not key derivation, failure should be rare but we check anyway. + * @param {*} input + * @returns + */ +export function liftX(input) { + const three = BigNumber(3); + const seven = BigNumber(7); + const one = BigNumber(1); + const four = BigNumber(4); + const two = BigNumber(2); + + const pHex ="0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"; + const p = BigNumber(pHex, 16); + const x = BigNumber("0x" + makeSureIsHex(input), 16); + if (x.comparedTo(p) === 1) { + return -1; + } else { + const temp = x.pow(three, p).plus(seven); + const ySQ = temp.mod(p); + const tempExp = (p.plus(one)).idiv(four); + const y = ySQ.pow(tempExp, p); + if (y.pow(two, p).comparedTo(ySQ) !== 0) { + return -1; + } else { + return input; + } + + } +} + // ################################################ END HELPER HASH FUNCTIONS ################################################### diff --git a/src/core/operations/PublicKeyToP2PKHAddress.mjs b/src/core/operations/PublicKeyToP2PKHAddress.mjs index 19f904e7..af26ec5f 100644 --- a/src/core/operations/PublicKeyToP2PKHAddress.mjs +++ b/src/core/operations/PublicKeyToP2PKHAddress.mjs @@ -9,11 +9,37 @@ import Operation from "../Operation.mjs"; import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; import {toHex} from "crypto-api/src/encoder/hex.mjs"; -import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart, makeSureIsBytes, validatePublicKey} from "../lib/Bitcoin.mjs"; +import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart, makeSureIsBytes, validatePublicKey, tweakHash, liftX, makeSureIsHex} from "../lib/Bitcoin.mjs"; import {encodeProgramToSegwit} from "../lib/Bech32.mjs"; import Utils from "../Utils.mjs"; +import ec from "elliptic"; +/** + * Tweaks the key in compliance with BIP340. Needed for creating P2TR addresses. + * @param {*} input + */ +function tweakKey(input) { + // First EC Context. + const ecContext = ec.ec("secp256k1"); + // We lift the passed in, input, dropping the first byte. + const liftedKey = liftX(makeSureIsHex(input).slice(2,)); + if (liftedKey === -1) + return -1; + // We then run the input through the tweakHash, getting the first tweaked Private Key; + const tweakedKey = tweakHash(makeSureIsBytes(liftedKey)); + // We turn the first private key, into a SECP256k1 Key. + const key = ecContext.keyFromPrivate(makeSureIsHex(tweakedKey)); + + // We take the lifted key, cast it back to a public key + const newKey = "02".concat(makeSureIsHex(liftedKey)); + const ecContext1 = ec.ec("secp256k1"); + const otherKey = ecContext1.keyFromPublic(newKey, "hex"); + + // We add the public keys together and return the result as compressed. + const final = otherKey.getPublic().add(key.getPublic()); + return final.encodeCompressed("hex"); +} /** * Converts a Public Key to a P2PKH Address of the given type. */ @@ -25,9 +51,9 @@ class PublicKeyToP2PKHAddress extends Operation { constructor() { super(); - this.name = "Public Key To Cryptocurrency Address"; + this.name = "Public Key To Bitcoin-Like Address"; this.module = "Default"; - this.description = "Turns a public key into a cryptocurrency address. Can select P2PKH, P2SH-P2WPKH and P2WPKH addresses for Bitcoin and Testnet."; + this.description = "Turns a public key into a Bitcoin-Like cryptocurrency address. Can select P2PKH, P2SH-P2WPKH, P2WPKH and P2TR addresses for Bitcoin and Testnet."; this.inputType = "string"; this.outputType = "string"; this.args = [ @@ -39,7 +65,7 @@ class PublicKeyToP2PKHAddress extends Operation { { "name": "Address Type", "type": "option", - "value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)", "Segwit (P2WPKH bc1 Addresses)"] + "value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)", "Segwit (P2WPKH bc1 Addresses)", "Taproot (P2TR bc1p Addresses)"] } ]; this.checks = [ @@ -66,36 +92,45 @@ class PublicKeyToP2PKHAddress extends Operation { if (validatePublicKey(input) !== "") { return validatePublicKey(input); } - - // We hash the input - const curInput = makeSureIsBytes(input); - const hash160 = toHex(hash160Func(curInput)); - // We do segwit addresses first. - if (args[1] === "Segwit (P2WPKH bc1 Addresses)") { - const redeemScript = hash160; + // P2TR are their own separate case. We handle those first. + if (args[1] === "Taproot (P2TR bc1p Addresses)") { const hrp = getHumanReadablePart(args[0]); - if (hrp !== "") { - return encodeProgramToSegwit(hrp, 0, Utils.convertToByteArray(redeemScript, "hex")); - } else { - return args[0] + " does not support Segwit Addresses."; + const resultKey = tweakKey(input); + if (resultKey === -1) { + return "Error: Bad Public Key to turn into P2TR Address."; } - } - // It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses. - const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]); - // If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte. - let hashRedeemedScript; - if (args[1] === "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)") { - const redeemScript = "0014" + hash160; - hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex")))); + return encodeProgramToSegwit(hrp, 1, Utils.convertToByteArray(resultKey.slice(2,), "hex")); } else { - hashRedeemedScript = versionByte + hash160; - } + // We hash the input + const curInput = makeSureIsBytes(input); + const hash160 = toHex(hash160Func(curInput)); + // We do segwit addresses first. + if (args[1] === "Segwit (P2WPKH bc1 Addresses)") { + const redeemScript = hash160; + const hrp = getHumanReadablePart(args[0]); + if (hrp !== "") { + return encodeProgramToSegwit(hrp, 0, Utils.convertToByteArray(redeemScript, "hex")); + } else { + return args[0] + " does not support Segwit Addresses."; + } + } + // It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses. + const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]); + // If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte. + let hashRedeemedScript; + if (args[1] === "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)") { + const redeemScript = "0014" + hash160; + hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex")))); + } else { + hashRedeemedScript = versionByte + hash160; + } - // We calculate the checksum, convert to Base58 and then we're done! - const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex")))); - const finalString = hashRedeemedScript + checksumHash.slice(0, 8); - const address = base58Encode(Utils.convertToByteArray(finalString, "hex")); - return address; + // We calculate the checksum, convert to Base58 and then we're done! + const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex")))); + const finalString = hashRedeemedScript + checksumHash.slice(0, 8); + const address = base58Encode(Utils.convertToByteArray(finalString, "hex")); + return address; + } } diff --git a/tests/operations/tests/PublicKeyToP2PKHAddress.mjs b/tests/operations/tests/PublicKeyToP2PKHAddress.mjs index 5a0cb7b5..ced9881a 100644 --- a/tests/operations/tests/PublicKeyToP2PKHAddress.mjs +++ b/tests/operations/tests/PublicKeyToP2PKHAddress.mjs @@ -1,5 +1,5 @@ /** - * Public Key to cryptocurrency address tests. + * Public Key To Bitcoin-Like Address tests. * * @author dgoldenberg [virtualcurrency@mitre.org] * @copyright MITRE 2023 @@ -16,7 +16,7 @@ TestRegister.addTests([ expectedOutput: "1MwwHqDj1FAyABeqPeiTTvJQCoCorcuFyP", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2PKH (V1 BTC Addresses)"] }, ], @@ -27,7 +27,7 @@ TestRegister.addTests([ expectedOutput: "18wUwr4Jvor6LG1mvQcfEp1Lx51dYAXZX1", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2PKH (V1 BTC Addresses)"] }, ], @@ -38,7 +38,7 @@ TestRegister.addTests([ expectedOutput: "LPTR2TBuF8vbwWaJdNeCAQemW4SC7q7zJP", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["LTC", "P2PKH (V1 BTC Addresses)"] }, ], @@ -49,7 +49,7 @@ TestRegister.addTests([ expectedOutput: "1BgRqTW8RMmcTRXHymTCVJsn5NVk9U8L9q", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2PKH (V1 BTC Addresses)"] }, ], @@ -60,7 +60,7 @@ TestRegister.addTests([ expectedOutput: "31vhdy8RGhSYZRRGZfqvZHGzVtpcua4cQW", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] }, ], @@ -71,7 +71,7 @@ TestRegister.addTests([ expectedOutput: "3C9wCFwcd36MHVpontDF7zQfKPfRTNg4Fe", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] }, ], @@ -82,7 +82,7 @@ TestRegister.addTests([ expectedOutput: "MMwYiJmkxDKqiP2WWAHMMgkeRt2nLxGqih", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["LTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] }, ], @@ -93,7 +93,7 @@ TestRegister.addTests([ expectedOutput: "bc1qu37uvwyzj23a2dd3x5nd8s77nfskzu3lzkuqfm", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], @@ -104,7 +104,7 @@ TestRegister.addTests([ expectedOutput: "bc1qrjluhfu5qr2780zlvcx3kquckpvuamwqp2sjle", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], @@ -115,7 +115,7 @@ TestRegister.addTests([ expectedOutput: "ltc1qj587punda8h0r4m83k794xseqlnl3az4ktu2zp", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["LTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], @@ -126,7 +126,7 @@ TestRegister.addTests([ expectedOutput: "mmuoeJDuuzeuaii1V6tPK3L5YjaJwjPqUM", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["Testnet", "P2PKH (V1 BTC Addresses)"] }, ], @@ -138,7 +138,7 @@ TestRegister.addTests([ expectedOutput: "Invalid length. We want either 33, 65 (if bytes) or 66, 130 (if hex) but we got: 68", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], @@ -149,7 +149,7 @@ TestRegister.addTests([ expectedOutput: "We have a valid hex string, of reasonable length, (66) but doesn't start with the right value. Correct values are 02, or 03 but we have: 05", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], @@ -160,7 +160,7 @@ TestRegister.addTests([ expectedOutput: "We have a valid hex string of reasonable length, (130) but doesn't start with the right value. Correct values are 04 but we have: 06", recipeConfig: [ { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2PKH (V1 BTC Addresses)"] }, ], @@ -175,9 +175,28 @@ TestRegister.addTests([ "args": ["Auto"] }, { - "op": "Public Key To Cryptocurrency Address", + "op": "Public Key To Bitcoin-Like Address", "args": ["BTC", "P2PKH (V1 BTC Addresses)"] } ], }, + { + name: "Public Key To Address: P2TR (From WIF Key)", + input: "L5R7GAGwrBLcpK4jK1CLDL7VjPifYZZeS1NcixKvrPxXySJWEK9h", + expectedOutput: "bc1ph6py5lduje5urxkqewpaxj8cxlmmc9uxr386e0jgvp9vzsup54dqxpxsn7", + recipeConfig: [ + { + "op": "From WIF Format", + "args": [] + }, + { + "op": "Private EC Key to Public Key", + "args": [true] + }, + { + "op": "Public Key To Bitcoin-Like Address", + "args": ["BTC", "Taproot (P2TR bc1p Addresses)"] + } + ] + }, ]);