From c36029a10f83d9142f6b196d1c39eff7d750e87f Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Fri, 5 Jan 2024 11:30:45 +0800 Subject: [PATCH] feat: improve seed for NS-Inscriber cli --- crates/ns-indexer/Cargo.toml | 2 +- crates/ns-indexer/src/api/name.rs | 29 +++++ crates/ns-indexer/src/indexer.rs | 2 +- crates/ns-indexer/src/router.rs | 4 + crates/ns-inscriber/Cargo.toml | 2 +- crates/ns-inscriber/src/bin/main.rs | 153 ++++++++++++++++++-------- crates/ns-inscriber/src/inscriber.rs | 17 +-- crates/ns-inscriber/src/wallet/mod.rs | 6 +- crates/ns-protocol/src/ns.rs | 10 ++ 9 files changed, 166 insertions(+), 59 deletions(-) diff --git a/crates/ns-indexer/Cargo.toml b/crates/ns-indexer/Cargo.toml index 6b659ce..30fa74b 100644 --- a/crates/ns-indexer/Cargo.toml +++ b/crates/ns-indexer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ns-indexer" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.64" description = "Name & Service Protocol indexer service in Rust" diff --git a/crates/ns-indexer/src/api/name.rs b/crates/ns-indexer/src/api/name.rs index 20f3292..3a8b7c6 100644 --- a/crates/ns-indexer/src/api/name.rs +++ b/crates/ns-indexer/src/api/name.rs @@ -66,6 +66,35 @@ impl NameAPI { Err(HTTPError::new(404, "not found".to_string())) } + pub async fn list_best_by_query( + State(app): State>, + Extension(ctx): Extension>, + to: PackObject<()>, + input: Query, + ) -> Result>>, HTTPError> { + input.validate()?; + + let query = input.name.clone(); + ctx.set_kvs(vec![ + ("action", "list_best_names_by_query".into()), + ("query", query.clone().into()), + ]) + .await; + + let mut names: Vec = Vec::new(); + + { + let best_names_state = app.state.confirming_names.read().await; + for n in best_names_state.keys() { + if n.starts_with(&query) { + names.push(n.clone()); + } + } + } + + Ok(to.with(SuccessResponse::new(names))) + } + pub async fn list_by_query( State(app): State>, Extension(ctx): Extension>, diff --git a/crates/ns-indexer/src/indexer.rs b/crates/ns-indexer/src/indexer.rs index bbb2e61..fd794b3 100644 --- a/crates/ns-indexer/src/indexer.rs +++ b/crates/ns-indexer/src/indexer.rs @@ -25,7 +25,7 @@ use crate::db::{ use crate::envelope::Envelope; use crate::utxo::UTXO; -const ACCEPTED_DISTANCE: u64 = 6; // 6 blocks before the best block +const ACCEPTED_DISTANCE: u64 = 5; // 6 confirmations pub struct IndexerOptions { pub scylla: ScyllaDBOptions, diff --git a/crates/ns-indexer/src/router.rs b/crates/ns-indexer/src/router.rs index 627db75..021914c 100644 --- a/crates/ns-indexer/src/router.rs +++ b/crates/ns-indexer/src/router.rs @@ -34,6 +34,10 @@ pub fn new(state: Arc) -> Router { routing::get(api::InscriptionAPI::list_best), ) .route("/name", routing::get(api::NameAPI::get_best)) + .route( + "/name/list_by_query", + routing::get(api::NameAPI::list_best_by_query), + ) .route("/service", routing::get(api::ServiceAPI::get_best)) .route("/utxo/list", routing::get(api::UtxoAPI::list)), ) diff --git a/crates/ns-inscriber/Cargo.toml b/crates/ns-inscriber/Cargo.toml index 6860fa6..7af3298 100644 --- a/crates/ns-inscriber/Cargo.toml +++ b/crates/ns-inscriber/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ns-inscriber" -version = "0.2.0" +version = "0.3.0" edition = "2021" rust-version = "1.64" description = "Name & Service Protocol inscriber service in Rust" diff --git a/crates/ns-inscriber/src/bin/main.rs b/crates/ns-inscriber/src/bin/main.rs index 6ff89fd..fcba898 100644 --- a/crates/ns-inscriber/src/bin/main.rs +++ b/crates/ns-inscriber/src/bin/main.rs @@ -22,6 +22,7 @@ use ns_inscriber::{ use ns_protocol::ns::{Name, Operation, PublicKeyParams, Service, ThresholdLevel, Value}; const AAD: &[u8; 12] = b"ns-inscriber"; +const TRANSFER_KEY_AAD: &[u8; 20] = b"ns:transfer.cose.key"; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -36,27 +37,43 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { - /// generate a new KEK used to protect other keys + /// Generate a new KEK used to protect other keys NewKEK { /// alias for the new KEK key #[arg(short, long)] alias: String, }, - /// generate a new Secp256k1 seed key that protected by KEK - Secp256k1Seed {}, - /// generate a new Ed25519 seed key that protected by KEK - Ed25519Seed {}, - /// Derive a Secp256k1 key from seed, it is protected by KEK. - /// Options Will be combined to "m/44'/0'/{acc}'/1'/{idx}" (BIP-32/44) + /// Generate a seed secret key that protected by KEK + NewSeed {}, + /// Import a seed secret key and protected it by KEK + ImportSeed { + /// alias for the imported Seed key + #[arg(short, long)] + alias: String, + }, + /// Export the seed key and protected it by a password + ExportSeed { + /// The seed key file name, will be combined to "{key}.cose.key" to read and export + #[arg(long, value_name = "FILE")] + seed: String, + }, + /// Derive a Secp256k1 key from the seed, it is protected by KEK. + /// Options Will be combined to "m/44'/0'/{acc}'/1/{idx}" (BIP-32/44) Secp256k1Derive { + /// The seed key file name, will be combined to "{key}.cose.key" to read as seed key to derive + #[arg(long, value_name = "FILE", default_value = "seed")] + seed: String, #[arg(long, default_value_t = 0)] acc: u32, #[arg(long, default_value_t = 0)] idx: u32, }, - /// Derive a Ed25519 key from seed, it is protected by KEK. - /// Options Will be combined to "m/42'/0'/{acc}'/1'/{idx}" (BIP-32/44) + /// Derive a Ed25519 key from the seed, it is protected by KEK. + /// Options Will be combined to "m/42'/0'/{acc}'/1/{idx}" (BIP-32/44) Ed25519Derive { + /// The seed key file name, will be combined to "{key}.cose.key" to read as seed key to derive + #[arg(long, value_name = "FILE", default_value = "seed")] + seed: String, #[arg(long, default_value_t = 0)] acc: u32, #[arg(long, default_value_t = 0)] @@ -167,12 +184,10 @@ async fn main() -> anyhow::Result<()> { let network = Network::from_core_arg(&std::env::var("BITCOIN_NETWORK").unwrap_or_default()) .unwrap_or(Network::Regtest); - println!("Bitcoin network: {}", network); - match &cli.command { Some(Commands::NewKEK { alias }) => { let mut terminal = Terminal::open()?; - let password = terminal.prompt_sensitive("Enter a password to protect KEK")?; + let password = terminal.prompt_sensitive("Enter a password to protect KEK: ")?; let mkek = hash_256(password.as_bytes()); let kid = if alias.is_empty() { Local::now().to_rfc3339_opts(SecondsFormat::Secs, true) @@ -187,32 +202,88 @@ async fn main() -> anyhow::Result<()> { "Put this new KEK as INSCRIBER_KEK on config file:\n{}", base64url_encode(&data) ); + return Ok(()); } - Some(Commands::Secp256k1Seed {}) => { - let file = keys_path.join("secp256k1-seed.cose.key"); + Some(Commands::NewSeed {}) => { + let file = keys_path.join("seed.cose.key"); if KekEncryptor::key_exists(&file) { println!("{} exists, skipping key generation", file.display()); return Ok(()); } let kek = KekEncryptor::open()?; - let secp = secp256k1::Secp256k1::new(); - let keypair = secp256k1::new_secp256k1(&secp); - let (public_key, _parity) = keypair.x_only_public_key(); - let script_pubkey = ScriptBuf::new_p2tr(&secp, public_key, None); - let address: Address = - Address::from_script(&script_pubkey, network).unwrap(); - let key = Key::secp256k1_from_keypair(&keypair, address.to_string().as_bytes())?; + let signing_key = ed25519::new_ed25519(); + let address = format!("0x{}", hex::encode(signing_key.verifying_key().to_bytes())); + let key = Key::ed25519_from_secret(signing_key.as_bytes(), address.as_bytes())?; + let key_id = key.key_id(); kek.save_key(&file, key)?; - println!("key: {}, address: {}", file.display(), address); + println!( + "New seed key: {}, key id: {}", + file.display(), + String::from_utf8_lossy(&key_id) + ); + return Ok(()); + } + + Some(Commands::ImportSeed { alias }) => { + let kid = if alias.is_empty() { + Local::now().to_rfc3339_opts(SecondsFormat::Secs, true) + ".seed" + } else { + alias.to_owned() + }; + let file = keys_path.join(format!("{kid}.cose.key")); + if KekEncryptor::key_exists(&file) { + println!("{} exists, skipping key generation", file.display()); + return Ok(()); + } + + let key = { + let mut terminal = Terminal::open()?; + let import_key = + terminal.prompt("Enter the seed key (base64url encoded) to import: ")?; + let password = + terminal.prompt_sensitive("Enter the password that protected the seed: ")?; + let kek = hash_256(password.as_bytes()); + let decryptor = Encrypt0::new(kek); + let ciphertext = base64url_decode(import_key.trim())?; + let key = decryptor.decrypt(unwrap_cbor_tag(&ciphertext), TRANSFER_KEY_AAD)?; + Key::from_slice(&key)? + }; + + let kek = KekEncryptor::open()?; + let key_id = key.key_id(); + kek.save_key(&file, key)?; + println!( + "Imported seed key: {}, key id: {}", + file.display(), + String::from_utf8_lossy(&key_id) + ); return Ok(()); } - Some(Commands::Secp256k1Derive { acc, idx }) => { + Some(Commands::ExportSeed { seed }) => { + let key = { + let kek = KekEncryptor::open()?; + kek.read_key(&keys_path.join(format!("{seed}.cose.key")))? + }; + let key_id = key.key_id(); + let mut terminal = Terminal::open()?; + let password = terminal.prompt_sensitive("Enter a password to protect the seed: ")?; + let kek = hash_256(password.as_bytes()); + let encryptor = Encrypt0::new(kek); + let data = encryptor.encrypt(&key.to_vec()?, TRANSFER_KEY_AAD, &key_id)?; + let data = wrap_cbor_tag(&data); + let data = base64url_encode(&data); + + println!("The exported seed key (base64url encoded):\n\n{data}\n\n"); + return Ok(()); + } + + Some(Commands::Secp256k1Derive { seed, acc, idx }) => { let kek = KekEncryptor::open()?; - let seed_key = kek.read_key(&keys_path.join("secp256k1-seed.cose.key"))?; - let kid = format!("m/44'/0'/{acc}'/1'/{idx}'"); + let seed_key = kek.read_key(&keys_path.join(format!("{seed}.cose.key")))?; + let kid = format!("m/44'/0'/{acc}'/1/{idx}"); let path: DerivationPath = kid.parse()?; let secp = secp256k1::Secp256k1::new(); let keypair = @@ -231,26 +302,10 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } - Some(Commands::Ed25519Seed {}) => { - let file = keys_path.join("ed25519-seed.cose.key"); - if KekEncryptor::key_exists(&file) { - println!("{} exists, skipping key generation", file.display()); - return Ok(()); - } - - let kek = KekEncryptor::open()?; - let signing_key = ed25519::new_ed25519(); - let address = format!("0x{}", hex::encode(signing_key.verifying_key().to_bytes())); - let key = Key::ed25519_from_secret(signing_key.as_bytes(), address.as_bytes())?; - kek.save_key(&file, key)?; - println!("key: {}, public key: {}", file.display(), address); - return Ok(()); - } - - Some(Commands::Ed25519Derive { acc, idx }) => { + Some(Commands::Ed25519Derive { seed, acc, idx }) => { let kek = KekEncryptor::open()?; - let seed_key = kek.read_key(&keys_path.join("ed25519-seed.cose.key"))?; - let kid = format!("m/42'/0'/{acc}'/1'/{idx}'"); + let seed_key = kek.read_key(&keys_path.join(format!("{seed}.cose.key")))?; + let kid = format!("m/42'/0'/{acc}'/1/{idx}"); let path: DerivationPath = kid.parse()?; let signing_key = ed25519::derive_ed25519(&seed_key.secret_key()?, &path); let address = format!("0x{}", hex::encode(signing_key.verifying_key().to_bytes())); @@ -383,6 +438,8 @@ async fn main() -> anyhow::Result<()> { amount, fee, }) => { + println!("Bitcoin network: {}", network); + let txid: Txid = txid.parse()?; let to = Address::from_str(to)?.require_network(network)?; let amount = Amount::from_sat(*amount); @@ -429,6 +486,8 @@ async fn main() -> anyhow::Result<()> { } Some(Commands::Preview { fee, key, names }) => { + println!("Bitcoin network: {}", network); + let fee_rate = Amount::from_sat(*fee); let names: Vec = names.split(',').map(|n| n.trim().to_string()).collect(); for name in &names { @@ -504,6 +563,8 @@ async fn main() -> anyhow::Result<()> { key, names, }) => { + println!("Bitcoin network: {}", network); + let fee_rate = Amount::from_sat(*fee); let names: Vec = names.split(',').map(|n| n.trim().to_string()).collect(); for name in &names { @@ -576,7 +637,7 @@ async fn main() -> anyhow::Result<()> { script_pubkey: txout.script_pubkey.clone(), } } else { - let txout: UnspentTxOutJSON = serde_json::from_str(&txout_json)?; + let txout: UnspentTxOutJSON = serde_json::from_str(txout_json)?; txout.to()? }; @@ -609,7 +670,7 @@ impl KekEncryptor { } let mut terminal = Terminal::open()?; - let password = terminal.prompt_sensitive("Enter a password to protect KEK: ")?; + let password = terminal.prompt_sensitive("Enter the password protected KEK: ")?; let mkek = hash_256(password.as_bytes()); let decryptor = Encrypt0::new(mkek); let ciphertext = base64url_decode(kek_str.trim())?; diff --git a/crates/ns-inscriber/src/inscriber.rs b/crates/ns-inscriber/src/inscriber.rs index 116b02d..c4e40be 100644 --- a/crates/ns-inscriber/src/inscriber.rs +++ b/crates/ns-inscriber/src/inscriber.rs @@ -276,7 +276,7 @@ impl Inscriber { pub async fn collect_sats( &self, fee_rate: Amount, - unspent_txouts: &Vec<(SecretKey, UnspentTxOut)>, + unspent_txouts: &[(SecretKey, UnspentTxOut)], to: &Address, ) -> anyhow::Result { let amount = unspent_txouts.iter().map(|(_, v)| v.amount).sum::(); @@ -327,7 +327,7 @@ impl Inscriber { let sighash = sighasher .taproot_key_spend_signature_hash( 0, - &Prevouts::All(&vec![TxOut { + &Prevouts::All(&[TxOut { value: unspent_txout.amount, script_pubkey: unspent_txout.script_pubkey.clone(), }]), @@ -613,9 +613,12 @@ impl Inscriber { let change_value = change_value .checked_sub(reveal_tx_fee) - .ok_or_else(|| anyhow::anyhow!("should compute commit_tx fee"))?; - if change_value <= dust_value { - anyhow::bail!("input value is too small"); + .ok_or_else(|| anyhow::anyhow!("should compute reveal_tx fee"))?; + if change_value < dust_value { + anyhow::bail!( + "input value is too small, need another {} sats", + dust_value - change_value + ); } reveal_tx.output[0].value = change_value; @@ -866,7 +869,7 @@ mod tests { &names, fee_rate, &keypair.secret_key(), - &unspent_txs.first().unwrap(), + unspent_txs.first().unwrap(), ) .await .unwrap(); @@ -931,7 +934,7 @@ mod tests { &names, fee_rate, &keypair.secret_key(), - &unspent_txs.first().unwrap(), + unspent_txs.first().unwrap(), ) .await .unwrap(); diff --git a/crates/ns-inscriber/src/wallet/mod.rs b/crates/ns-inscriber/src/wallet/mod.rs index 355bbb6..0945cf4 100644 --- a/crates/ns-inscriber/src/wallet/mod.rs +++ b/crates/ns-inscriber/src/wallet/mod.rs @@ -25,13 +25,13 @@ pub fn base64_encode(data: &[u8]) -> String { pub fn base64url_decode(data: &str) -> anyhow::Result> { general_purpose::URL_SAFE_NO_PAD - .decode(data) + .decode(data.trim_end_matches('=')) .map_err(anyhow::Error::msg) } pub fn base64_decode(data: &str) -> anyhow::Result> { - general_purpose::STANDARD - .decode(data) + general_purpose::STANDARD_NO_PAD + .decode(data.trim_end_matches('=')) .map_err(anyhow::Error::msg) } diff --git a/crates/ns-protocol/src/ns.rs b/crates/ns-protocol/src/ns.rs index 056486b..0b061b5 100644 --- a/crates/ns-protocol/src/ns.rs +++ b/crates/ns-protocol/src/ns.rs @@ -802,6 +802,16 @@ mod tests { assert_eq!(result, "0123456789abcdefghijklmnopqrstuvwxyz"); } + #[test] + fn check_greek_name() { + for name in &[ + "α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "ι", "κ", "λ", "μ", "ν", "ξ", "ο", "π", "ρ", + "ς", "σ", "τ", "υ", "φ", "χ", "ψ", "ω", "ϕ", "ϵ", + ] { + println!("{} is {}", name, valid_name(name)) + } + } + #[test] fn signature_ser_de() { let sig = Signature(hex!("6b71fd0c8ae2ccc910c39dd20e76653fccca2638b7935f2312e954f5dccd71b209c58ca57e9d4fc2d3c06a57d585dbadf4535abb8a9cf103eeb9b9717d87f201").to_vec());