Skip to content

Commit

Permalink
feat: improve seed for NS-Inscriber cli
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jan 5, 2024
1 parent 388a022 commit c36029a
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 59 deletions.
2 changes: 1 addition & 1 deletion crates/ns-indexer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
29 changes: 29 additions & 0 deletions crates/ns-indexer/src/api/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,35 @@ impl NameAPI {
Err(HTTPError::new(404, "not found".to_string()))
}

pub async fn list_best_by_query(
State(app): State<Arc<IndexerAPI>>,
Extension(ctx): Extension<Arc<ReqContext>>,
to: PackObject<()>,
input: Query<QueryName>,
) -> Result<PackObject<SuccessResponse<Vec<String>>>, 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<String> = 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<Arc<IndexerAPI>>,
Extension(ctx): Extension<Arc<ReqContext>>,
Expand Down
2 changes: 1 addition & 1 deletion crates/ns-indexer/src/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions crates/ns-indexer/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub fn new(state: Arc<api::IndexerAPI>) -> 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)),
)
Expand Down
2 changes: 1 addition & 1 deletion crates/ns-inscriber/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
153 changes: 107 additions & 46 deletions crates/ns-inscriber/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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)
Expand All @@ -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<NetworkChecked> =
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 =
Expand All @@ -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()));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String> = names.split(',').map(|n| n.trim().to_string()).collect();
for name in &names {
Expand Down Expand Up @@ -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<String> = names.split(',').map(|n| n.trim().to_string()).collect();
for name in &names {
Expand Down Expand Up @@ -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()?
};

Expand Down Expand Up @@ -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())?;
Expand Down
17 changes: 10 additions & 7 deletions crates/ns-inscriber/src/inscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NetworkChecked>,
) -> anyhow::Result<Txid> {
let amount = unspent_txouts.iter().map(|(_, v)| v.amount).sum::<Amount>();
Expand Down Expand Up @@ -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(),
}]),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -866,7 +869,7 @@ mod tests {
&names,
fee_rate,
&keypair.secret_key(),
&unspent_txs.first().unwrap(),
unspent_txs.first().unwrap(),
)
.await
.unwrap();
Expand Down Expand Up @@ -931,7 +934,7 @@ mod tests {
&names,
fee_rate,
&keypair.secret_key(),
&unspent_txs.first().unwrap(),
unspent_txs.first().unwrap(),
)
.await
.unwrap();
Expand Down
6 changes: 3 additions & 3 deletions crates/ns-inscriber/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ pub fn base64_encode(data: &[u8]) -> String {

pub fn base64url_decode(data: &str) -> anyhow::Result<Vec<u8>> {
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<Vec<u8>> {
general_purpose::STANDARD
.decode(data)
general_purpose::STANDARD_NO_PAD
.decode(data.trim_end_matches('='))
.map_err(anyhow::Error::msg)
}

Expand Down
Loading

0 comments on commit c36029a

Please sign in to comment.