Skip to content

Commit

Permalink
Document charms-data and charms-sdk
Browse files Browse the repository at this point in the history
Also, some of `charms-spell-checker` and `charms`.
  • Loading branch information
imikushin committed Jan 19, 2025
1 parent 0bfac6d commit e586910
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 15 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Charms are created using _spells_ — special messages added to Bitcoin transact
Install Charms CLI:

```sh
cargo +nightly install charms
export CARGO_TARGET_DIR=$(mktemp -d)/target
cargo install --locked charms --version=0.3.0
```

Create your first app (your own token on Bitcoin):
Expand All @@ -31,7 +32,9 @@ cd ./my-token
ls -l
```

## How To
Read this: [hello world!](./hello-world.md)

## Documentation

Concepts and guides: [charms.dev](https://charms.dev)

Expand All @@ -49,4 +52,4 @@ are, in a way, a generalization of Runes.
The main difference is that Charms are easily programmable (and composable).

---
©️2024 sigmazero
©️2025 sigmazero
3 changes: 3 additions & 0 deletions charms-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`charms_data` provides data types and functions for Charms apps.

`charms_data::util` provides simple CBOR serialization/deserialization functions.
84 changes: 79 additions & 5 deletions charms-data/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ use serde::{
};
pub mod util;

/// Macro to check a condition and return false (early) if it does not hold.
/// This is useful for checking pre-requisite conditions in predicate-type functions.
/// Inspired by the `ensure!` macro from the `anyhow` crate.
/// The function must return a boolean.
/// Example:
/// ```rust
/// use charms_data::check;
///
/// fn b_is_multiple_of_a(a: u32, b: u32) -> bool {
/// check!(a <= b && a != 0); // returns false early if `a` is greater than `b` or `a` is zero
/// match b % a {
/// 0 => true,
/// _ => false,
/// }
/// }
#[macro_export]
macro_rules! check {
($condition:expr) => {
Expand All @@ -26,6 +41,10 @@ macro_rules! check {
};
}

/// Represents a transaction involving Charms.
/// A Charms transaction sits on top of a Bitcoin transaction. Therefore, it transforms a set of
/// input UTXOs into a set of output UTXOs.
/// A Charms transaction may also reference other valid UTXOs that are not being spent or created.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Transaction {
/// Input UTXOs.
Expand All @@ -36,29 +55,40 @@ pub struct Transaction {
pub outs: Vec<Charms>,
}

/// Charm is essentially an app-level UTXO that can carry tokens, NFTs, arbitrary app state.
/// Structurally it is a sorted map of `app -> app_state`
/// Charms are tokens, NFTs or instances of arbitrary app state.
/// This type alias represents a collection of charms.
/// Structurally it is a map of `app -> data`.
pub type Charms = BTreeMap<App, Data>;

/// ID of a UTXO (Unspent Transaction Output) in the underlying ledger system (e.g. Bitcoin).
/// A UTXO ID is a pair of `(transaction ID, index of the output)`.
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct UtxoId(pub TxId, pub u32);

impl UtxoId {
/// Convert to a byte array (of 36 bytes).
pub fn to_bytes(&self) -> [u8; 36] {
let mut bytes = [0u8; 36];
bytes[..32].copy_from_slice(&self.0 .0); // Copy TxId
bytes[32..].copy_from_slice(&self.1.to_le_bytes()); // Copy index as little-endian
bytes
}

/// Create `UtxoId` from a byte array (of 36 bytes).
pub fn from_bytes(bytes: [u8; 36]) -> Self {
let mut txid_bytes = [0u8; 32];
txid_bytes.copy_from_slice(&bytes[..32]);
let index = u32::from_le_bytes(bytes[32..].try_into().unwrap());
UtxoId(TxId(txid_bytes), index)
}

/// Try to create `UtxoId` from a string in the format `txid_hex:index`.
/// Example:
/// ```
/// use charms_data::UtxoId;
/// let utxo_id = UtxoId::from_str("92077a14998b31367efeec5203a00f1080facdb270cbf055f09b66ae0a273c7d:3").unwrap();
/// ```
pub fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
Expand Down Expand Up @@ -145,6 +175,23 @@ impl<'de> Deserialize<'de> for UtxoId {
}
}

/// App represents an application that can be used to create, transform or destroy charms (tokens,
/// NFTs and other instances of app data).
///
/// An app is identified by a single character `tag`, a 32-byte `identity` and a 32-byte `vk`
/// (verification key).
/// The `tag` is a single character that represents the type of the app, with two special values:
/// - `TOKEN` (tag `t`) for tokens,
/// - `NFT` (tag `n`) for NFTs.
///
/// Other values of `tag` are perfectly legal. The above ones are special: tokens and NFTs can be
/// transferred without providing the app's implementation (RISC-V binary).
///
/// The `vk` is a 32-byte byte string (hash) that is used to verify proofs that the app's contract
/// is satisfied (against the certain transaction, additional public input and private input).
///
/// The `identity` is a 32-byte byte string (hash) that uniquely identifies the app among other apps
/// implemented using the same code.
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct App {
Expand Down Expand Up @@ -256,11 +303,21 @@ impl<'de> Deserialize<'de> for App {
}
}

/// ID (hash) of a transaction in the underlying ledger (Bitcoin).
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct TxId(pub [u8; 32]);

impl TxId {
/// Try to create `TxId` from a string of 64 hex characters.
/// Note that string representation of transaction IDs in Bitcoin is reversed, and so is ours
/// (for compatibility).
///
/// Example:
/// ```
/// use charms_data::TxId;
/// let tx_id = TxId::from_str("92077a14998b31367efeec5203a00f1080facdb270cbf055f09b66ae0a273c7d").unwrap();
/// ```
pub fn from_str(s: &str) -> Result<Self> {
ensure!(s.len() == 64, "expected 64 hex characters");
let bytes = hex::decode(s).map_err(|e| anyhow!("invalid txid hex: {}", e))?;
Expand Down Expand Up @@ -342,11 +399,13 @@ impl<'de> Deserialize<'de> for TxId {
}
}

/// 32-byte byte string (e.g. a hash, like SHA256).
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct B32(pub [u8; 32]);

impl B32 {
/// Try to create `B32` from a string of 64 hex characters.
pub fn from_str(s: &str) -> Result<Self> {
ensure!(s.len() == 64, "expected 64 hex characters");
let bytes = hex::decode(s).map_err(|e| anyhow!("invalid hex: {}", e))?;
Expand All @@ -373,6 +432,7 @@ impl fmt::Debug for B32 {
}
}

/// Represents a data value that is guaranteed to be serialized/deserialized to/from CBOR.
#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Data(Value);

Expand All @@ -387,20 +447,25 @@ impl Ord for Data {
}

impl Data {
/// Create an empty data value.
pub fn empty() -> Self {
Self(Value::Null)
}

/// Check if the data value is empty.
pub fn is_empty(&self) -> bool {
self.0.is_null()
}

/// Try to cast to a value of a deserializable type (implementing
/// `serde::de::DeserializeOwned`).
pub fn value<T: DeserializeOwned>(&self) -> Result<T> {
self.0
.deserialized()
.map_err(|e| anyhow!("deserialization error: {}", e))
}

/// Serialize to bytes.
pub fn bytes(&self) -> Vec<u8> {
util::write(&self).expect("serialization should have succeeded")
}
Expand All @@ -427,9 +492,14 @@ impl fmt::Debug for Data {
}
}

/// Special `App.tag` value for fungible tokens. See [`App`] for more details.
pub const TOKEN: char = 't';
/// Special `App.tag` value for non-fungible tokens (NFTs). See [`App`] for more details.
pub const NFT: char = 'n';

/// Check if the provided app's token amounts are balanced in the transaction. This means that the
/// sum of the token amounts in the `tx` inputs is equal to the sum of the token amounts in the `tx`
/// outputs.
pub fn token_amounts_balanced(app: &App, tx: &Transaction) -> bool {
match (
sum_token_amount(app, tx.ins.values()),
Expand All @@ -440,14 +510,16 @@ pub fn token_amounts_balanced(app: &App, tx: &Transaction) -> bool {
}
}

/// Check if the NFT states are preserved in the transaction. This means that the NFTs (created by
/// the provided `app`) in the `tx` inputs are the same as the NFTs in the `tx` outputs.
pub fn nft_state_preserved(app: &App, tx: &Transaction) -> bool {
let nft_states_in = app_state_multiset(app, tx.ins.values());
let nft_states_out = app_state_multiset(app, tx.outs.iter());

nft_states_in == nft_states_out
}

pub fn app_state_multiset<'a>(
fn app_state_multiset<'a>(
app: &App,
strings_of_charms: impl Iterator<Item = &'a Charms>,
) -> BTreeMap<&'a Data, usize> {
Expand All @@ -464,11 +536,13 @@ pub fn app_state_multiset<'a>(
})
}

/// Sum the token amounts in the provided `strings_of_charms`.
pub fn sum_token_amount<'a>(
self_app: &App,
app: &App,
strings_of_charms: impl Iterator<Item = &'a Charms>,
) -> Result<u64> {
strings_of_charms.fold(Ok(0u64), |amount, charms| match charms.get(self_app) {
ensure!(app.tag == TOKEN);
strings_of_charms.fold(Ok(0u64), |amount, charms| match charms.get(app) {
Some(state) => Ok(amount? + state.value::<u64>()?),
None => amount,
})
Expand Down
2 changes: 2 additions & 0 deletions charms-data/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use ciborium_io::Read;
use core::fmt::Debug;
use serde::{de::DeserializeOwned, Serialize};

/// Deserialize a CBOR value from a reader (e.g. `&[u8]` or `std::io::stdin()`).
pub fn read<T, R>(s: R) -> Result<T>
where
T: DeserializeOwned,
Expand All @@ -12,6 +13,7 @@ where
Ok(ciborium::from_reader(s)?)
}

/// Serialize a value to a byte vector as CBOR.
pub fn write<T>(t: &T) -> Result<Vec<u8>>
where
T: Serialize,
Expand Down
46 changes: 46 additions & 0 deletions charms-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
This is the only crate you need to get started coding a Charms app.

## Usage

Run this command to create a new Charms app:

```sh
charms app new my-app
```

It will create a new directory called `my-app` with a basic Charms app template.

It'll have this in `Cargo.toml`:

```toml
[dependencies]
charms-sdk = { version = "0.3.0" }
```

This is how the entire `src/main.rs` looks like:

```rust
#![no_main]
charms_sdk::main!(my_app::app_contract);
```

The most important function in the app is `app_contract` in `src/lib.rs`:

```rust
use charms_sdk::data::{
check, App, Data, Transaction, NFT, TOKEN,
};

pub fn app_contract(app: &App, tx: &Transaction, x: &Data, w: &Data) -> bool {
match app.tag {
NFT => {
check!(nft_contract_satisfied(app, tx, x, w))
}
TOKEN => {
check!(token_contract_satisfied(app, tx, x, w))
}
_ => todo!(),
}
true
}
```
4 changes: 4 additions & 0 deletions charms-spell-checker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
`charms-spell-checker` is not a spelling checker: it's a validator for spells.

It is run inside a zkVM to produce recursive proofs of correctness for spells — metadata on top of transactions that
specifies what charms are created on top of transactions' outputs.
2 changes: 1 addition & 1 deletion charms-spell-checker/src/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub fn main() {
eprintln!("about to commit");

// Commit to the public values of the program.
sp1_zkvm::io::commit(&output);
sp1_zkvm::io::commit_slice(util::write(&output).unwrap().as_slice());
}

pub fn run(input: SpellProverInput) -> (String, NormalizedSpell) {
Expand Down
2 changes: 1 addition & 1 deletion charms-spell-checker/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,6 @@ const V0_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v0/groth16_vk.b

fn to_sp1_pv<T: Serialize>(t: &T) -> SP1PublicValues {
let mut pv = SP1PublicValues::new();
pv.write(t);
pv.write_slice(util::write(t).unwrap().as_slice());
pv
}
2 changes: 1 addition & 1 deletion hello-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Install Charms CLI:
```sh
## important to have this path end with `/target`, otherwise the build will fail (a dependency issue)
export CARGO_TARGET_DIR=$(mktemp -d)/target
cargo install --profile=test --locked --bin charms --version=0.3.0-dev charms
cargo install --locked charms --version=0.3.0
```

## Create an app
Expand Down
Binary file modified src/bin/charms-spell-checker
Binary file not shown.
8 changes: 5 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ pub mod spell;
pub mod tx;
pub mod utils;

/// RISC-V binary compiled from `charms-spell-checker`.
pub const SPELL_CHECKER_BINARY: &[u8] = include_bytes!("./bin/charms-spell-checker");

pub const SPELL_VK: &str = "0x00e6516c2f233068f4480c51d1dfbb45da64cd2e0ba1a058d65e7f64de1a8f4f";
/// Verification key for the `charms-spell-checker` binary.
pub const SPELL_VK: &str = "0x00d42ff3f423b00461bac02ac0071b33f9fb2b81a8239b948a1f7c414763ec2c";

#[cfg(test)]
mod test {
use super::*;
use sp1_sdk::{HashableKey, ProverClient};
use sp1_sdk::{HashableKey, Prover, ProverClient};

#[test]
fn test_spell_vk() {
let client = ProverClient::from_env();
let client = ProverClient::builder().cpu().build();
let (_, vk) = client.setup(SPELL_CHECKER_BINARY);
let s = vk.bytes32();

Expand Down
2 changes: 1 addition & 1 deletion src/spell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use sp1_sdk::{HashableKey, ProverClient, SP1Stdin};
use std::collections::{BTreeMap, BTreeSet};

/// Charm as represented in a spell.
/// Map of `$TICKER: data`
/// Map of `$KEY: data`.
pub type KeyedCharms = BTreeMap<String, Value>;

/// UTXO as represented in a spell.
Expand Down

0 comments on commit e586910

Please sign in to comment.