Skip to content

Commit

Permalink
Add commands to mint and verify NFTs (ordinals#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Jun 5, 2022
1 parent c1a9c74 commit 15ed050
Show file tree
Hide file tree
Showing 16 changed files with 976 additions and 66 deletions.
568 changes: 524 additions & 44 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ autotests = false
anyhow = { version = "1.0.56", features = ["backtrace"] }
axum = "0.4.8"
axum-server = "0.3.3"
bitcoin = "0.27.1"
bitcoincore-rpc = "0.14.0"
bech32 = "0.9.0"
bitcoin = "0.28.1"
bitcoin_hashes = "0.10.0"
bitcoincore-rpc = "0.15.0"
chrono = "0.4.19"
clap = { version = "3.1.0", features = ["derive"] }
ctrlc = "3.2.1"
Expand All @@ -26,8 +28,14 @@ jsonrpc = "0.12.1"
lazy_static = "1.4.0"
log = "0.4.14"
ord-lmdb-zero = "0.4.5"
qrcode-generator = "4.1.6"
rayon = "1.5.1"
redb = { version = "0.0.5", optional = true }
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
secp256k1 = { version = "0.22.1", features = ["rand", "rand-std", "global-context"] }
serde = { version = "1.0.137", features = ["derive"] }
serde_json = "1.0.81"
sha2 = "0.10.2"
tokio = { version = "1.17.0", features = ["rt-multi-thread"] }
tower-http = { version = "0.2.5", features = ["cors"] }

Expand All @@ -46,7 +54,7 @@ unindent = "0.1.7"

[[test]]
name = "integration"
path = "tests/integration.rs"
path = "tests/lib.rs"

[[bench]]
name = "index"
Expand Down
4 changes: 2 additions & 2 deletions benches/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use {
bitcoin::{
blockdata::{constants::COIN_VALUE, script},
consensus::Encodable,
Block, BlockHeader, Network, OutPoint, Transaction, TxIn, TxOut,
Block, BlockHeader, Network, OutPoint, Transaction, TxIn, TxOut, Witness,
},
criterion::{Criterion, SamplingMode},
std::{env, fs::File, io::Seek, io::Write, path::Path, process::Command, time::Duration},
Expand Down Expand Up @@ -52,7 +52,7 @@ fn main() -> Result {
previous_output: OutPoint::null(),
script_sig: script::Builder::new().push_scriptint(height).into_script(),
sequence: 0,
witness: vec![],
witness: Witness::new(),
}],
lock_time: 0,
output: vec![TxOut {
Expand Down
12 changes: 11 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ clippy:
bench:
cargo criterion

watch +args='ltest':
watch +args='test':
cargo watch --clear --exec '{{args}}'

install-dev-deps:
Expand All @@ -35,3 +35,13 @@ status:

serve:
python3 -m http.server --directory docs

generate-private-key:
cargo run generate-private-key

generate-paper-wallets:
cat private-keys.txt | cargo run generate-paper-wallets

print-paper-wallet path:
wkhtmltopdf -L 25mm -R 25mm -T 50mm -B 25mm {{path}} wallet.pdf
lp -o sides=two-sided-long-edge wallet.pdf
19 changes: 19 additions & 0 deletions src/decode_bech32.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use super::*;

pub(crate) fn decode_bech32(encoded: &str, expected_hrp: &str) -> Result<Vec<u8>> {
let (hrp, data, variant) = bech32::decode(encoded)?;

if hrp != expected_hrp {
return Err(anyhow!(
"bech32 string should be have `{}` human-readable prefix but starts with `{}`",
expected_hrp,
hrp
));
}

if variant != bech32::Variant::Bech32m {
return Err(anyhow!("bech32 strings must use the bech32m variant",));
}

Ok(Vec::from_base32(&data)?)
}
29 changes: 21 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,35 @@

use {
crate::{
arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index,
options::Options, ordinal::Ordinal, sat_point::SatPoint, subcommand::Subcommand,
arguments::Arguments, bytes::Bytes, decode_bech32::decode_bech32, epoch::Epoch, height::Height,
index::Index, nft::Nft, options::Options, ordinal::Ordinal, sat_point::SatPoint,
subcommand::Subcommand,
},
anyhow::{anyhow, Context, Error},
axum::{extract, http::StatusCode, response::IntoResponse, routing::get, Json, Router},
axum_server::Handle,
bech32::{FromBase32, ToBase32},
bitcoin::{
blockdata::constants::COIN_VALUE, consensus::Decodable, consensus::Encodable, Block, BlockHash,
OutPoint, Transaction, Txid,
blockdata::constants::COIN_VALUE, consensus::Decodable, consensus::Encodable, Address, Block,
BlockHash, Network, OutPoint, Transaction, Txid,
},
bitcoin_hashes::{sha256d, Hash, HashEngine},
chrono::{DateTime, NaiveDateTime, Utc},
clap::Parser,
derive_more::{Display, FromStr},
integer_cbrt::IntegerCubeRoot,
integer_sqrt::IntegerSquareRoot,
lazy_static::lazy_static,
qrcode_generator::QrCodeEcc,
secp256k1::{rand, schnorr::Signature, KeyPair, Secp256k1, SecretKey, XOnlyPublicKey},
serde::{Deserialize, Serialize},
std::{
cmp::Ordering,
collections::VecDeque,
env,
fmt::{self, Display, Formatter},
io,
fs,
io::{self, BufRead, Write},
net::ToSocketAddrs,
ops::{Add, AddAssign, Deref, Sub},
path::PathBuf,
Expand All @@ -48,11 +55,13 @@ use lmdb_database::{Database, WriteTransaction};

mod arguments;
mod bytes;
mod decode_bech32;
mod epoch;
mod height;
mod index;
#[cfg(not(feature = "redb"))]
mod lmdb_database;
mod nft;
mod options;
mod ordinal;
#[cfg(feature = "redb")]
Expand Down Expand Up @@ -86,13 +95,17 @@ fn main() {
})
.expect("Error setting ctrl-c handler");

if let Err(error) = Arguments::parse().run() {
eprintln!("error: {}", error);
if let Err(err) = Arguments::parse().run() {
eprintln!("error: {}", err);
err
.chain()
.skip(1)
.for_each(|cause| eprintln!("because: {}", cause));
if env::var_os("RUST_BACKTRACE")
.map(|val| val == "1")
.unwrap_or_default()
{
eprintln!("{}", error.backtrace());
eprintln!("{}", err.backtrace());
}
process::exit(1);
}
Expand Down
124 changes: 124 additions & 0 deletions src/nft.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use super::*;

const ORDINAL_MESSAGE_PREFIX: &[u8] = b"Ordinal Signed Message:";

pub(crate) struct Nft {
ordinal: Ordinal,
data_hash: sha256d::Hash,
public_key: XOnlyPublicKey,
signature: Signature,
data: Vec<u8>,
}

impl Nft {
const HRP: &'static str = "nft";

pub(crate) fn mint(ordinal: Ordinal, data: &[u8], signing_key_pair: KeyPair) -> Result<Self> {
let data_hash = sha256d::Hash::hash(data);
let public_key = signing_key_pair.public_key();

let mut message = Vec::new();
message.extend_from_slice(&ordinal.n().to_be_bytes());
message.extend_from_slice(&data_hash);
message.extend_from_slice(&public_key.serialize());

let mut engine = sha256d::Hash::engine();
engine.input(ORDINAL_MESSAGE_PREFIX);
engine.input(&message);
let message_hash = secp256k1::Message::from_slice(&sha256d::Hash::from_engine(engine))?;

let signature = signing_key_pair.sign_schnorr(message_hash);
message.extend_from_slice(signature.as_ref());
message.extend_from_slice(data);

Ok(Self {
ordinal,
data_hash,
public_key,
signature,
data: data.into(),
})
}

pub(crate) fn data(&self) -> &[u8] {
&self.data
}

pub(crate) fn encode(&self) -> String {
let mut encoded = Vec::new();
encoded.extend_from_slice(&self.ordinal.n().to_be_bytes());
encoded.extend_from_slice(&self.data_hash);
encoded.extend_from_slice(&self.public_key.serialize());
encoded.extend_from_slice(self.signature.as_ref());
encoded.extend_from_slice(&self.data);
bech32::encode(Self::HRP, encoded.to_base32(), bech32::Variant::Bech32m).unwrap()
}

pub(crate) fn issuer(&self) -> String {
bech32::encode(
"pubkey",
self.public_key.serialize().to_base32(),
bech32::Variant::Bech32m,
)
.unwrap()
}

pub(crate) fn data_hash(&self) -> String {
bech32::encode("data", self.data_hash.to_base32(), bech32::Variant::Bech32m).unwrap()
}

pub(crate) fn ordinal(&self) -> Ordinal {
self.ordinal
}

pub(crate) fn verify(encoded: &str) -> Result<Self> {
let data = decode_bech32(encoded, Self::HRP)?;

let start = 0;

let end = start + 8;
let ordinal = Ordinal(u64::from_be_bytes(data[start..end].try_into()?));
let start = end;

let end = start + sha256d::Hash::LEN;
let expected_hash = &data[start..end];
let start = end;

let end = start + 32;
let public_key = XOnlyPublicKey::from_slice(&data[start..end])?;
let start = end;

let end = start + 64;
let signature = Signature::from_slice(&data[start..end])?;
let start = end;

let data = &data[start..];
let data_hash = sha256d::Hash::hash(data);

if data_hash.as_ref() != expected_hash {
return Err(anyhow!("NFT data hash does not match actual data_hash"));
}

let mut message = Vec::new();
message.extend_from_slice(&ordinal.n().to_be_bytes());
message.extend_from_slice(&data_hash);
message.extend_from_slice(&public_key.serialize());

let mut engine = sha256d::Hash::engine();
engine.input(ORDINAL_MESSAGE_PREFIX);
engine.input(&message);
let message_hash = secp256k1::Message::from_slice(&sha256d::Hash::from_engine(engine))?;

Secp256k1::new()
.verify_schnorr(&signature, &message_hash, &public_key)
.context("Failed to verify NFT signature")?;

Ok(Self {
ordinal,
data_hash,
public_key,
signature,
data: data.into(),
})
}
}
5 changes: 4 additions & 1 deletion src/ordinal.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use super::*;

#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, FromStr)]
#[derive(
Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, FromStr, Deserialize, Serialize,
)]
#[serde(transparent)]
pub(crate) struct Ordinal(pub(crate) u64);

impl Ordinal {
Expand Down
16 changes: 14 additions & 2 deletions src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,54 @@ use super::*;

mod epochs;
mod find;
mod generate_paper_wallets;
mod generate_private_key;
mod index;
mod info;
mod list;
mod mint;
mod name;
mod range;
mod server;
mod supply;
mod traits;
mod verify;

#[derive(Parser)]
pub(crate) enum Subcommand {
Epochs,
Find(find::Find),
GeneratePrivateKey,
GeneratePaperWallets,
Index,
Info,
List(list::List),
Mint(mint::Mint),
Name(name::Name),
Range(range::Range),
Supply,
Server(server::Server),
Info,
Supply,
Traits(traits::Traits),
Verify(verify::Verify),
}

impl Subcommand {
pub(crate) fn run(self, options: Options) -> Result<()> {
match self {
Self::Epochs => epochs::run(),
Self::GeneratePrivateKey => generate_private_key::run(),
Self::GeneratePaperWallets => generate_paper_wallets::run(),
Self::Find(find) => find.run(options),
Self::Index => index::run(options),
Self::List(list) => list.run(options),
Self::Name(name) => name.run(),
Self::Mint(mint) => mint.run(),
Self::Range(range) => range.run(),
Self::Supply => supply::run(),
Self::Server(server) => server.run(options),
Self::Info => info::run(options),
Self::Traits(traits) => traits.run(),
Self::Verify(verify) => verify.run(),
}
}
}
Loading

0 comments on commit 15ed050

Please sign in to comment.