Skip to content

Commit

Permalink
Nostr binding type script implementation (#3)
Browse files Browse the repository at this point in the history
* Nostr binding type script implementation

* Update doc
  • Loading branch information
XuJiandong authored Jun 24, 2024
1 parent f7a0803 commit 2e102d2
Show file tree
Hide file tree
Showing 13 changed files with 535 additions and 1,254 deletions.
775 changes: 10 additions & 765 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions contracts/nostr-binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ckb-hash = {version = "0.114.0", default-features = false, features = ["ckb-contract"] }
ckb-std = "0.15"
hex = { version = "0.4", default-features = false, features = ["alloc"]}
nostr = {version = "0.29.0", default-features = false, features = ["alloc"]}
ckb-nostr-utils = { path = "../ckb-nostr-utils", version = "0.1.0" }
blake2b-ref = "0.3.1"

[build-dependencies]
ckb-gen-types = "0.114.0"
23 changes: 0 additions & 23 deletions contracts/nostr-binding/build.rs

This file was deleted.

5 changes: 1 addition & 4 deletions contracts/nostr-binding/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
pub const CELL_TYPE_ID_TAG_NAME: &str = "cell_type_id";

pub const ASSET_META_KIND: u16 = 23332;
pub const ASSET_MINT_KIND: u16 = 23333;
pub const GLOBAL_UNIQUE_ID_TAG_NAME: &str = "ckb_global_unique_id";
44 changes: 25 additions & 19 deletions contracts/nostr-binding/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
use ckb_nostr_utils::error::Error as NostrError;
use ckb_std::error::SysError;

#[cfg(test)]
extern crate alloc;

#[repr(i8)]
pub enum Error {
IndexOutOfBound = 1,
ItemMissing,
LengthNotEnough,
Encoding,
// [Auth]
// Add customized errors here...
AuthFail,
// [type_id]
// There can only be at most one input and at most one output type ID cell
InvalidTypeIDCellNum,
// Type id does not match args
TypeIDNotMatch,
// Length of type id is incorrect
ArgsLengthNotEnough,
// [nostr]
InvalidEventLength,
NotAssetOwnerToMint,
AssetEventIdNotMatch,
InvalidAssetEventKind,
InvalidAssetMetaEventKind,
ValidationFail,
WitnessNotExisting,
WrongArgsLength,
InvalidPublicKey,
InvalidEventId,
InvalidSignatureFormat,
UnknownKey,
Json,
GlobalUniqueIdNotFound,
TooManyTypeIdCell,
TypeIdNotMatch,
}

impl From<SysError> for Error {
Expand All @@ -38,3 +31,16 @@ impl From<SysError> for Error {
}
}
}

impl From<NostrError> for Error {
fn from(err: NostrError) -> Self {
match err {
NostrError::InvalidPublicKey => Self::InvalidPublicKey,
NostrError::InvalidEventId => Self::InvalidEventId,
NostrError::ValidationFail => Self::ValidationFail,
NostrError::InvalidSignatureFormat => Self::InvalidSignatureFormat,
NostrError::UnknownKey(_) => Self::UnknownKey,
NostrError::Json(_) => Self::Json,
}
}
}
221 changes: 33 additions & 188 deletions contracts/nostr-binding/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,222 +1,67 @@
#![no_std]
#![cfg_attr(not(test), no_main)]

#[cfg(test)]
extern crate alloc;
#![no_main]

mod config;
mod error;
mod type_id;
mod util;

use alloc::vec::Vec;
use ckb_std::debug;
#[cfg(not(test))]
use ckb_nostr_utils::event::Event;
use ckb_std::default_alloc;
#[cfg(not(test))]
ckb_std::entry!(program_entry);
#[cfg(not(test))]
default_alloc!();
default_alloc!(4 * 1024, 1024 * 1024, 64);

use alloc::format;
use alloc::{ffi::CString, string::ToString};
use ckb_std::{
ckb_constants::Source,
ckb_types::{bytes::Bytes, core::ScriptHashType, prelude::Unpack},
high_level::{exec_cell, load_script, load_witness_args},
high_level::{load_script, load_witness_args},
};
use config::ASSET_META_KIND;
use hex::encode;

use ckb_hash::blake2b_256;
use config::GLOBAL_UNIQUE_ID_TAG_NAME;
use error::Error;
use nostr::Event;
use nostr::JsonUtil;
use type_id::{load_type_id_from_script_args, validate_type_id};
use util::get_asset_event_cell_type_id;

use crate::config::ASSET_MINT_KIND;
use crate::type_id::has_type_id_cell;
use crate::util::get_first_tag_value;

include!(concat!(env!("OUT_DIR"), "/auth_code_hash.rs"));
use type_id::{has_type_id_cell, validate_type_id};

pub fn program_entry() -> i8 {
match auth() {
match entry() {
Ok(_) => 0,
Err(err) => err as i8,
}
}

fn auth() -> Result<(), Error> {
let type_id = load_type_id_from_script_args(32)?;
validate_type_id(type_id)?;
fn entry() -> Result<(), Error> {
let script = load_script()?;
let args = script.as_reader().args();
if args.len() != 64 {
return Err(Error::WrongArgsLength);
}
let mut event_id = [0; 32];
event_id.copy_from_slice(&args.raw_data()[0..32]);
let mut global_unique_id = [0; 32];
global_unique_id.copy_from_slice(&args.raw_data()[32..]);

validate_type_id(global_unique_id)?;

if !has_type_id_cell(0, Source::GroupInput) {
// build a new binding cell
// need to verify nostr asset Event

// read nostr event from witness
// mint a new binding cell
let witness_args = load_witness_args(0, Source::GroupOutput)?;
let witness = witness_args
.output_type()
.to_opt()
.ok_or(Error::InvalidTypeIDCellNum)?
.ok_or(Error::WitnessNotExisting)?
.raw_data();
let events_bytes = witness.to_vec();
let events = decode_events(events_bytes);

if !events.len().eq(&2) {
return Err(Error::InvalidEventLength);
let event = Event::from_json(witness.as_ref())?;
event.verify_id()?;
if &event_id != event.id().as_bytes() {
return Err(Error::InvalidEventId);
}

let mint_event = events[0].clone();
let asset_meta_event = events[1].clone();

let mint_pubkey = mint_event.pubkey.to_bytes();
let asset_meta_pubkey = asset_meta_event.pubkey.to_bytes();
let asset_meta_event_id = asset_meta_event.id.to_bytes();

validate_mint_event(mint_event.clone())?;
validate_asset_metadata_event(asset_meta_event)?;

// check if mint_event public key is same with asset meta event
if !mint_pubkey.eq(&asset_meta_pubkey) {
return Err(Error::NotAssetOwnerToMint);
let global_unique_id_hex = hex::encode(global_unique_id);
let found = event.tags().into_iter().any(|e| {
let e = e.as_vec();
e.len() == 2 && e[0] == GLOBAL_UNIQUE_ID_TAG_NAME && e[1] == global_unique_id_hex
});
if !found {
return Err(Error::GlobalUniqueIdNotFound);
}

// check if mint_event e tag is same with event_id from asset_meta_event
let asset_event_id = get_first_tag_value(mint_event, nostr::Alphabet::E);
if !encode(asset_meta_event_id).eq(&asset_event_id) {
return Err(Error::AssetEventIdNotMatch);
}
}

Ok(())
}

pub fn validate_mint_event(event: Event) -> Result<(), Error> {
event.verify_id().unwrap();

let kind = event.clone().kind.as_u32();
if !kind.eq(&(ASSET_MINT_KIND as u32)) {
return Err(Error::InvalidAssetEventKind);
}

// todo: check pow

// check if event tag type id is equal to script type id
let cell_type_id = get_asset_event_cell_type_id(event.clone());
let type_id = load_type_id_from_script_args(32)?;
let script_type_id = encode(type_id);
if !script_type_id.eq(&cell_type_id) {
return Err(Error::TypeIDNotMatch);
}

// check if event id is equal to script event id
let script_event_id = load_event_id_from_script_args()?;
if !script_event_id.eq(event.id.as_bytes()) {
return Err(Error::AssetEventIdNotMatch);
}

validate_event_signature(event.clone())?;

Ok(())
}

pub fn validate_asset_metadata_event(event: Event) -> Result<(), Error> {
event.verify_id().unwrap();

let kind = event.clone().kind.as_u32();
if !kind.eq(&(ASSET_META_KIND as u32)) {
return Err(Error::InvalidAssetMetaEventKind);
event.verify_signature()?;
}

validate_event_signature(event.clone())?;

Ok(())
}

pub fn validate_event_signature(event: Event) -> Result<(), Error> {
let sig = event.signature();
let signature = sig.as_ref();
let message = event.id.as_bytes();
let public_key = event.pubkey.to_bytes();
let mut signature_auth = [0u8; 96];
signature_auth[..32].copy_from_slice(&public_key);
signature_auth[32..].copy_from_slice(signature.as_ref());

let mut pubkey_hash = [0u8; 20];
let args = blake2b_256(public_key);
pubkey_hash.copy_from_slice(&args[0..20]);

// AuthAlgorithmIdSchnorr = 7
let algorithm_id_str = CString::new(format!("{:02X?}", 7u8)).unwrap();
let signature_str = CString::new(encode(signature_auth).to_string()).unwrap();
let message_str = CString::new(encode(message).to_string()).unwrap();
let pubkey_hash_str = CString::new(encode(pubkey_hash).to_string()).unwrap();

let args = [
algorithm_id_str.as_c_str(),
signature_str.as_c_str(),
message_str.as_c_str(),
pubkey_hash_str.as_c_str(),
];

exec_cell(&AUTH_CODE_HASH, ScriptHashType::Data1, &args).map_err(|_| Error::AuthFail)?;
Ok(())
}

pub fn load_event_id_from_script_args() -> Result<[u8; 32], Error> {
let mut script_event_id = [0u8; 32];
let script = load_script()?;
let args: Bytes = script.args().unpack();
script_event_id.copy_from_slice(&args[0..32]);
Ok(script_event_id)
}

// witness format:
// total_event_count(1 byte, le) + first_event_length(8 bytes, le) + first_event_content + second_event_length(8 bytes, le)....
pub fn decode_events(data: Vec<u8>) -> Vec<Event> {
// Ensure we have at least 1 byte for the total number of events
if data.is_empty() {
debug!("Not enough data to decode events.");
panic!("Not enough data to decode events.");
}

let mut cursor = 1; // Start after the first byte (total number of events)
let mut events = Vec::new();

// Get the total number of events
let total_events = data[0] as usize;

// Iterate over each event
for _ in 0..total_events {
// Ensure we have enough bytes to read the event length
if data.len() < cursor + 8 {
debug!("Not enough data to decode event length.");
panic!("Not enough data to decode events.");
}

// Get the length of the current event
let event_length_bytes: [u8; 8] = data[cursor..cursor + 8].try_into().unwrap();
let event_length = u64::from_le_bytes(event_length_bytes) as usize;

cursor += 8; // Move the cursor to the start of the event data

// Ensure we have enough bytes to read the event data
if data.len() < cursor + event_length {
debug!("Not enough data to decode event.");
panic!("Not enough data to decode events.");
}

// Extract the event data
let event_data = &data[cursor..cursor + event_length].to_vec();
let event = Event::from_json(event_data).unwrap();
events.push(event);

cursor += event_length; // Move the cursor to the next event length
}

events
}
Loading

0 comments on commit 2e102d2

Please sign in to comment.