Added support for public key -> P2TR addresses. Also renamed Public Key to Cryptocurrency Address Operation to Public Key to Bitcoin-Like Address Operation.

This commit is contained in:
David C Goldenberg 2025-05-09 14:34:57 -04:00
parent d745a516cb
commit 5605807778
4 changed files with 155 additions and 48 deletions

View file

@ -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",

View file

@ -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 ###################################################

View file

@ -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;
}
}

View file

@ -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)"]
}
]
},
]);