#![allow(dead_code)]
use bitcoin::hashes::{sha256, Hash, HashEngine};
use bitcoin::secp256k1::{PublicKey, Scalar, SecretKey, XOnlyPublicKey};
use sidestr_core::block::secp;
use crate::error::{Error, Result};
const TWEAK_RANGE: &str = "a tweak is a scalar in [1, n-1]";
const SECRET_RANGE: &str = "a secret is a scalar in [1, n-1]";
const NOT_A_POINT: &str = "not a compressed secp256k1 point (02/03 + x, on the curve)";
fn bytes32(text: &str) -> Option<[u8; 32]> {
if text.len() != 64 {
return None;
}
let mut out = [0u8; 32];
hex::decode_to_slice(text, &mut out).ok()?;
Some(out)
}
pub(crate) fn scalar(t: &str) -> Result<Scalar> {
let b = bytes32(t).ok_or(Error::Key(TWEAK_RANGE))?;
scalar_of(b)
}
pub(crate) fn scalar_of(b: [u8; 32]) -> Result<Scalar> {
if b == [0u8; 32] {
return Err(Error::Key(TWEAK_RANGE));
}
Scalar::from_be_bytes(b).map_err(|_| Error::Key(TWEAK_RANGE))
}
pub(crate) fn secret(d: &str) -> Result<SecretKey> {
let b = bytes32(d).ok_or(Error::Key(SECRET_RANGE))?;
SecretKey::from_slice(&b).map_err(|_| Error::Key(SECRET_RANGE))
}
pub(crate) fn is_point(p: &str) -> bool {
parse_point(p).is_some()
}
fn parse_point(p: &str) -> Option<PublicKey> {
let b = p.as_bytes();
let shaped = b.len() == 66
&& b[0] == b'0'
&& (b[1] == b'2' || b[1] == b'3')
&& b[2..]
.iter()
.all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f'));
if !shaped {
return None;
}
PublicKey::from_slice(&hex::decode(p).ok()?).ok()
}
pub(crate) fn point(p: &str) -> Result<PublicKey> {
parse_point(p).ok_or(Error::Key(NOT_A_POINT))
}
fn hex_point(p: &PublicKey) -> String {
hex::encode(p.serialize())
}
fn hex_secret(d: &SecretKey) -> String {
hex::encode(d.secret_bytes())
}
fn is_odd(p: &PublicKey) -> bool {
p.serialize()[0] == 0x03
}
pub(crate) fn point_of(d: &SecretKey) -> PublicKey {
PublicKey::from_secret_key(secp(), d)
}
pub(crate) fn public_key(d: &str) -> Result<String> {
Ok(hex_point(&point_of(&secret(d)?)))
}
pub(crate) fn normalized(d: &SecretKey) -> SecretKey {
if is_odd(&point_of(d)) {
d.negate()
} else {
*d
}
}
pub(crate) fn normalize(d: &str) -> Result<String> {
let s = secret(d)?;
Ok(if is_odd(&point_of(&s)) {
hex_secret(&s.negate())
} else {
d.to_lowercase()
})
}
pub(crate) fn signing_key(d: &str) -> Result<String> {
normalize(d)
}
pub(crate) fn base_point(id: &str) -> Result<String> {
let s = crate::account::js_trim(id).to_lowercase();
let is_hex = |t: &str| t.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f'));
let bare = s.strip_prefix("did:nostr:").unwrap_or(&s);
if bare.len() == 64 && is_hex(bare) {
let x = hex::decode(bare).expect("checked hex");
if XOnlyPublicKey::from_slice(&x).is_err() {
return Err(Error::Key("that x is not on the curve"));
}
let p = format!("02{bare}");
point(&p)?;
return Ok(p);
}
if let Some(mk) = s.strip_prefix("fe701") {
if mk.len() == 66 && (mk.starts_with("02") || mk.starts_with("03")) && is_hex(mk) {
point(mk)?;
return Ok(mk.to_owned());
}
}
if s.len() == 66 && (s.starts_with("02") || s.starts_with("03")) && is_hex(&s) {
point(&s)?;
return Ok(s);
}
Err(Error::Key(
"not a did:nostr identifier, a did:nostr Multikey, or a compressed point",
))
}
pub(crate) fn x_only(p: &str) -> Result<String> {
point(p)?;
Ok(p[2..].to_owned())
}
pub(crate) fn did(p: &str) -> Result<String> {
Ok(format!("did:nostr:{}", x_only(p)?))
}
pub(crate) fn multikey(p: &str) -> Result<String> {
Ok(format!("fe701{}", hex_point(&point(p)?)))
}
pub(crate) fn negate(p: &str) -> Result<String> {
Ok(hex_point(&point(p)?.negate(secp())))
}
pub(crate) fn tweaked_secret(d: &SecretKey, t: &Scalar) -> Result<SecretKey> {
d.add_tweak(t)
.map_err(|_| Error::Key("the tweak cancels the secret"))
}
pub(crate) fn tweaked_point(p: &PublicKey, t: &Scalar) -> Result<PublicKey> {
p.add_exp_tweak(secp(), t)
.map_err(|_| Error::Key("the tweak lands on infinity"))
}
pub(crate) fn tweak_secret(d: &str, t: &str) -> Result<String> {
let d = secret(d)?;
Ok(hex_secret(&tweaked_secret(&d, &scalar(t)?)?))
}
pub(crate) fn tweak_point(p: &str, t: &str) -> Result<String> {
let p = point(p)?;
Ok(hex_point(&tweaked_point(&p, &scalar(t)?)?))
}
pub(crate) fn chain_points(p: &str, tweaks: &[&str]) -> Result<Vec<String>> {
let mut out = vec![hex_point(&point(p)?)];
for t in tweaks {
let next = tweak_point(out.last().expect("non-empty"), t)?;
out.push(next);
}
Ok(out)
}
pub(crate) fn chain_secrets(d: &str, tweaks: &[&str]) -> Result<Vec<String>> {
secret(d)?;
let mut out = vec![d.to_lowercase()];
for t in tweaks {
let next = tweak_secret(out.last().expect("non-empty"), t)?;
out.push(next);
}
Ok(out)
}
pub(crate) fn tagged_hash(tag: &str, chunks: &[&[u8]]) -> [u8; 32] {
let t = sha256::Hash::hash(tag.as_bytes()).to_byte_array();
let mut e = sha256::Hash::engine();
e.input(&t);
e.input(&t);
for c in chunks {
e.input(c);
}
sha256::Hash::from_engine(e).to_byte_array()
}
pub(crate) fn reduce(b: [u8; 32]) -> Option<Scalar> {
if let Ok(s) = Scalar::from_be_bytes(b) {
return (b != [0u8; 32]).then_some(s);
}
let mut hi = [0u8; 32];
hi[16..].copy_from_slice(&b[..16]);
let mut lo = [0u8; 32];
lo[16..].copy_from_slice(&b[16..]);
let mut two128 = [0u8; 32];
two128[15] = 1;
let hi = SecretKey::from_slice(&hi).ok()?;
let shifted = hi
.mul_tweak(&Scalar::from_be_bytes(two128).expect("2^128 < n"))
.ok()?;
let sum = shifted
.add_tweak(&Scalar::from_be_bytes(lo).expect("below 2^128"))
.ok()?;
Some(Scalar::from(sum))
}
pub(crate) fn tagged_scalar(tag: &str, chunks: &[&[u8]]) -> Result<String> {
let s = reduce(tagged_hash(tag, chunks))
.ok_or(Error::Key("the tweak is zero: refuse this data"))?;
Ok(hex::encode(s.to_be_bytes()))
}
pub(crate) fn tap_tweak(p: &str, h32: Option<&[u8; 32]>) -> Result<String> {
let x = hex::decode(x_only(p)?).expect("hex point");
match h32 {
Some(h) => tagged_scalar("TapTweak", &[&x, h]),
None => tagged_scalar("TapTweak", &[&x]),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
const VECTORS: &str = include_str!("../tests/vectors/keys-vectors.json");
const N_HEX: &str = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141";
fn strs(v: &Value) -> Vec<&str> {
v.as_array()
.unwrap()
.iter()
.map(|s| s.as_str().unwrap())
.collect()
}
#[test]
fn keys_vectors_json_both_cases() {
let v: Value = serde_json::from_str(VECTORS).unwrap();
let cases = v["cases"].as_object().unwrap();
assert_eq!(cases.len(), 2);
for (name, c) in cases {
let s = |k: &str| c[k].as_str().unwrap();
let d = s("secret");
let p = public_key(d).unwrap();
assert_eq!(p, s("point"), "{name}");
assert_eq!(did(&p).unwrap(), s("did"), "{name}");
assert_eq!(multikey(&p).unwrap(), s("multikey"), "{name}");
let n = normalize(d).unwrap();
assert_eq!(n, s("normalized"), "{name}");
assert_eq!(public_key(&n).unwrap(), s("normalizedPoint"), "{name}");
let tws = strs(&c["tweaks"]);
assert_eq!(
tagged_scalar("keys-vectors", &[&[0xca, 0xfe]]).unwrap(),
tws[2]
);
let secrets = chain_secrets(&n, &tws).unwrap();
assert_eq!(&secrets[1..], strs(&c["chainFromSecret"]), "{name}");
let base = base_point(&did(&p).unwrap()).unwrap();
let points = chain_points(&base, &tws).unwrap();
assert_eq!(&points[1..], strs(&c["chainPoints"]), "{name}");
let outs: Vec<String> = points[1..].iter().map(|q| x_only(q).unwrap()).collect();
assert_eq!(outs, strs(&c["outputs"]), "{name}");
let signing: Vec<String> = secrets[1..]
.iter()
.map(|q| signing_key(q).unwrap())
.collect();
assert_eq!(signing, strs(&c["signingKeys"]), "{name}");
for (q, sk) in points.iter().zip(&secrets) {
assert_eq!(q, &public_key(sk).unwrap(), "{name}");
}
}
assert_eq!(&cases["oddSecret"]["point"].as_str().unwrap()[..2], "03");
}
#[test]
fn reading_keys_as_keys_test_reads_them() {
let p = public_key(&"11".repeat(32)).unwrap();
let x = x_only(&p).unwrap();
assert_eq!(
base_point(&format!("did:nostr:{x}")).unwrap(),
format!("02{x}")
);
assert_eq!(base_point(&x.to_uppercase()).unwrap(), format!("02{x}"));
assert_eq!(
base_point(&format!("fe70103{x}")).unwrap(),
format!("03{x}")
);
assert_eq!(base_point(&p).unwrap(), p);
assert_eq!(
multikey(
&base_point(
"did:nostr:124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2"
)
.unwrap()
)
.unwrap(),
"fe70102124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2"
);
assert_eq!(
base_point(&"00".repeat(32)),
Err(Error::Key("that x is not on the curve"))
);
for bad in [format!("04{x}"), x[2..].to_owned(), "npub1abc".into()] {
assert!(matches!(base_point(&bad), Err(Error::Key(m)) if m.starts_with("not a")));
}
assert_eq!(x_only(&negate(&p).unwrap()).unwrap(), x);
assert_ne!(negate(&p).unwrap(), p);
}
#[test]
fn limits_in_keys_mjs_words() {
let p = public_key(&"11".repeat(32)).unwrap();
assert_eq!(
tweak_point(&p, &"00".repeat(32)),
Err(Error::Key(TWEAK_RANGE))
);
assert_eq!(tweak_point(&p, N_HEX), Err(Error::Key(TWEAK_RANGE)));
assert_eq!(public_key(&"00".repeat(32)), Err(Error::Key(SECRET_RANGE)));
let d = "11".repeat(32);
let minus = hex_secret(&secret(&d).unwrap().negate());
assert_eq!(
tweak_secret(&d, &minus),
Err(Error::Key("the tweak cancels the secret"))
);
}
#[test]
fn reduction_mod_n_at_and_above_n() {
let n: [u8; 32] = hex::decode(N_HEX).unwrap().try_into().unwrap();
assert_eq!(reduce(n), None);
assert_eq!(reduce([0u8; 32]), None);
let mut n5 = n;
n5[31] += 5;
let mut five = [0u8; 32];
five[31] = 5;
assert_eq!(reduce(n5).unwrap().to_be_bytes(), five);
let want = hex::decode("000000000000000000000000000000014551231950b75fc4402da1732fc9bebe")
.unwrap();
assert_eq!(reduce([0xff; 32]).unwrap().to_be_bytes().to_vec(), want);
assert_eq!(reduce(five).unwrap().to_be_bytes(), five);
}
#[test]
fn d_plus_t_is_p_plus_tg_for_both_parities() {
let mut odd = 0;
for i in 1u8..=40 {
let d = tagged_hash("test/keys", &[&[i]]);
let t = tagged_hash("test/tweak", &[&[i]]);
let (d, t) = (hex::encode(d), hex::encode(t));
let p = public_key(&d).unwrap();
if p.starts_with("03") {
odd += 1;
}
assert_eq!(
tweak_point(&p, &t).unwrap(),
public_key(&tweak_secret(&d, &t).unwrap()).unwrap()
);
let n = normalize(&d).unwrap();
assert!(public_key(&n).unwrap().starts_with("02"));
assert_eq!(
base_point(&did(&p).unwrap()).unwrap(),
public_key(&n).unwrap()
);
}
assert!(odd > 5 && odd < 35, "{odd}");
}
}