Jelajahi Sumber

contract/money: Initial outline in src/contract/money.

parazyd 3 tahun lalu
induk
melakukan
16178af767

+ 9 - 0
Cargo.lock

@@ -1297,6 +1297,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "darkfi-money-contract"
+version = "0.3.0"
+dependencies = [
+ "darkfi-sdk",
+ "darkfi-serial",
+ "getrandom 0.2.8",
+]
+
 [[package]]
 name = "darkfi-sdk"
 version = "0.3.0"

+ 2 - 0
Cargo.toml

@@ -44,6 +44,8 @@ members = [
     "src/serial/derive",
     "src/serial/derive-internal",
 
+    "src/contract/money",
+
     "example/dchat",
     "example/dao",
 ]

+ 1 - 0
src/contract/README.md

@@ -0,0 +1 @@
+This directory contains native WASM contracts on DarkFi.

+ 2 - 0
src/contract/money/.gitignore

@@ -0,0 +1,2 @@
+money_contract.wasm
+proof/*.zk.bin

+ 18 - 0
src/contract/money/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "darkfi-money-contract"
+version = "0.3.0"
+authors = ["Dyne.org foundation <foundation@dyne.org>"]
+license = "AGPL-3.0-only"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+darkfi-sdk = { path = "../../sdk" }
+darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] }
+
+# We need to disable random using "custom" which makes the crate a noop
+# so the wasm32-unknown-unknown target is enabled.
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+getrandom = { version = "0.2.8", features = ["custom"] }

+ 37 - 0
src/contract/money/Makefile

@@ -0,0 +1,37 @@
+# Cargo binary
+CARGO ?= cargo
+
+# Zkas binary
+ZKAS ?= ../../../zkas
+
+# zkas circuit source files
+ZKAS_SRC = $(shell find proof -type f -name '*.zk')
+
+# wasm source files
+WASM_SRC = \
+	$(shell find src -type f) \
+	$(shell find ../../sdk/src -type f) \
+	$(shell find ../../serial/src -type f)
+
+# zkas circuit bin files
+ZKAS_BIN = $(ZKAS_SRC:=.bin)
+
+# Contract WASM binaries
+WASM_BIN = money_contract.wasm
+
+all: $(ZKAS_BIN) $(WASM_BIN)
+
+$(ZKAS_BIN): $(ZKAS_SRC)
+	$(ZKAS) $(basename $@) -o $@
+
+money_contract.wasm: $(ZKAS_BIN) $(WASM_SRC)
+	$(CARGO) build --release --package darkfi-money-contract --target wasm32-unknown-unknown
+	cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_money_contract.wasm $@
+
+test:
+
+clean:
+	rm -f $(ZKAS_BIN) $(WASM_BIN)
+
+# We always rebuild the wasm no matter what
+.PHONY: $(WASM_BIN) all test clean

+ 66 - 0
src/contract/money/proof/burn.zk

@@ -0,0 +1,66 @@
+constant "Burn" {
+	EcFixedPointShort VALUE_COMMIT_VALUE,
+	EcFixedPoint VALUE_COMMIT_RANDOM,
+	EcFixedPointBase NULLIFIER_K,
+}
+
+contract "Burn" {
+	Base secret,
+	Base serial,
+	Base value,
+	Base token,
+	Base coin_blind,
+	Scalar value_blind,
+	Scalar token_blind,
+	Uint32 leaf_pos,
+	MerklePath path,
+	Base signature_secret,
+}
+
+circuit "Burn" {
+	# Poseidon hash of the nullifier
+	nullifier = poseidon_hash(secret, serial);
+	constrain_instance(nullifier);
+
+	# Pedersen commitment for coin's value
+	vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
+	vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
+	value_commit = ec_add(vcv, vcr);
+	# Since value_commit is a curve point, we fetch its coordinates
+	# and constrain them:
+	value_commit_x = ec_get_x(value_commit);
+	value_commit_y = ec_get_y(value_commit);
+	constrain_instance(value_commit_x);
+	constrain_instance(value_commit_y);
+
+	# Pedersen commitment for coin's token ID
+	tcv = ec_mul_base(token, NULLIFIER_K);
+	tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM);
+	token_commit = ec_add(tcv, tcr);
+	# Since token_commit is also a curve point, we'll do the same
+	# coordinate dance:
+	token_commit_x = ec_get_x(token_commit);
+	token_commit_y = ec_get_y(token_commit);
+	constrain_instance(token_commit_x);
+	constrain_instance(token_commit_y);
+
+	# Coin hash
+	pub = ec_mul_base(secret, NULLIFIER_K);
+	pub_x = ec_get_x(pub);
+	pub_y = ec_get_y(pub);
+	C = poseidon_hash(pub_x, pub_y, value, token, serial, coin_blind);
+
+	# Merkle root
+	root = merkle_root(leaf_pos, path, C);
+	constrain_instance(root);
+
+	# Finally, we derive a public key for the signature and
+	# constrain its coordinates:
+	signature_public = ec_mul_base(signature_secret, NULLIFIER_K);
+	signature_x = ec_get_x(signature_public);
+	signature_y = ec_get_y(signature_public);
+	constrain_instance(signature_x);
+	constrain_instance(signature_y);
+
+	# At this point we've enforced all of our public inputs.
+}

+ 46 - 0
src/contract/money/proof/mint.zk

@@ -0,0 +1,46 @@
+constant "Mint" {
+	EcFixedPointShort VALUE_COMMIT_VALUE,
+	EcFixedPoint VALUE_COMMIT_RANDOM,
+	EcFixedPointBase NULLIFIER_K,
+}
+
+contract "Mint" {
+	Base pub_x,
+	Base pub_y,
+	Base value,
+	Base token,
+	Base serial,
+	Base coin_blind,
+	Scalar value_blind,
+	Scalar token_blind,
+}
+
+circuit "Mint" {
+	# Poseidon hash of the coin
+	C = poseidon_hash(pub_x, pub_y, value, token, serial, coin_blind);
+	constrain_instance(C);
+
+	# Pedersen commitment for coin's value
+	vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
+	vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
+	value_commit = ec_add(vcv, vcr);
+	# Since the value commit is a curve point, we fetch its coordinates
+	# and constrain them:
+	value_commit_x = ec_get_x(value_commit);
+	value_commit_y = ec_get_y(value_commit);
+	constrain_instance(value_commit_x);
+	constrain_instance(value_commit_y);
+
+	# Pedersen commitment for coin's token ID
+	tcv = ec_mul_base(token, NULLIFIER_K);
+	tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM);
+	token_commit = ec_add(tcv, tcr);
+	# Since token_commit is also a curve point, we'll do the same
+	# coordinate dance:
+	token_commit_x = ec_get_x(token_commit);
+	token_commit_y = ec_get_y(token_commit);
+	constrain_instance(token_commit_x);
+	constrain_instance(token_commit_y);
+
+	# At this point we've enforced all of our public inputs.
+}

+ 299 - 0
src/contract/money/src/lib.rs

@@ -0,0 +1,299 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2022 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{ContractId, MerkleNode, MerkleTree, PublicKey},
+    db::{db_contains_key, db_get, db_init, db_lookup, db_set},
+    define_contract,
+    error::{ContractError, ContractResult},
+    merkle::merkle_add,
+    msg,
+    pasta::{arithmetic::CurveAffine, group::Curve, pallas},
+    tx::ContractCall,
+    util::set_return_data,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+/// Functions we allow in this contract
+#[repr(u8)]
+pub enum MoneyFunction {
+    Transfer = 0x00,
+}
+
+impl From<u8> for MoneyFunction {
+    fn from(b: u8) -> Self {
+        match b {
+            0x00 => Self::Transfer,
+            _ => panic!("Invalid function ID: {:#04x?}", b),
+        }
+    }
+}
+
+/// Structures and object definitions
+pub mod state;
+use state::{MoneyTransferParams, MoneyTransferUpdate};
+
+define_contract!(
+    init: init_contract,
+    exec: process_instruction,
+    apply: process_update,
+    metadata: get_metadata
+);
+
+// These are the different sled trees that will be created
+pub const ZKAS_TREE: &str = "zkas";
+pub const COIN_ROOTS_TREE: &str = "coin_roots";
+pub const NULLIFIERS_TREE: &str = "nullifiers";
+pub const INFO_TREE: &str = "info";
+
+// This is a key inside the info tree
+pub const COIN_MERKLE_TREE: &str = "coin_tree";
+pub const FAUCET_PUBKEYS: &str = "faucet_pubkeys";
+
+/// zkas mint contract namespace
+pub const ZKAS_MINT_NS: &str = "Mint";
+/// zkas burn contract namespace
+pub const ZKAS_BURN_NS: &str = "Burn";
+
+/// This function runs when the contract is (re)deployed and initialized.
+fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
+    // The zkas circuits can simply be embedded in the wasm and set up by
+    // the initialization. Note that the tree should then be called "zkas".
+    // The lookups can then be done by `contract_id+zkas+namespace`.
+    let zkas_db = db_init(cid, ZKAS_TREE)?;
+    let mint_bincode = include_bytes!("../proof/mint.zk.bin");
+    let burn_bincode = include_bytes!("../proof/burn.zk.bin");
+
+    /* TODO: Do I really want to make zkas a dependency? Yeah, in the future.
+       For now we take anything.
+    let zkbin = ZkBinary::decode(mint_bincode)?;
+    let mint_namespace = zkbin.namespace.clone();
+    assert_eq!(&mint_namespace, ZKAS_MINT_NS);
+    let zkbin = ZkBinary::decode(burn_bincode)?;
+    let burn_namespace = zkbin.namespace.clone();
+    assert_eq!(&burn_namespace, ZKAS_BURN_NS);
+    db_set(zkas_db, &serialize(&mint_namespace), &mint_bincode[..])?;
+    db_set(zkas_db, &serialize(&burn_namespace), &burn_bincode[..])?;
+    */
+
+    db_set(zkas_db, &serialize(&ZKAS_MINT_NS.to_string()), &mint_bincode[..])?;
+    db_set(zkas_db, &serialize(&ZKAS_BURN_NS.to_string()), &burn_bincode[..])?;
+
+    // Set up a database tree to hold Merkle roots
+    let _ = db_init(cid, COIN_ROOTS_TREE)?;
+
+    // Set up a database tree to hold nullifiers
+    let _ = db_init(cid, NULLIFIERS_TREE)?;
+
+    // Set up a database tree for arbitrary data
+    let info_db = db_init(cid, INFO_TREE)?;
+
+    // Add a Merkle tree to the info db:
+    let coin_tree = MerkleTree::new(100);
+    let mut coin_tree_data = vec![];
+    // TODO: FIXME: What is this write_u32 doing here?
+    coin_tree_data.write_u32(0)?;
+    coin_tree.encode(&mut coin_tree_data)?;
+    db_set(info_db, &serialize(&COIN_MERKLE_TREE.to_string()), &coin_tree_data)?;
+
+    // Whitelisted faucets
+    let faucet_pubkeys: Vec<PublicKey> = vec![];
+    db_set(info_db, &serialize(&FAUCET_PUBKEYS.to_string()), &serialize(&faucet_pubkeys))?;
+
+    Ok(())
+}
+
+/// This function is used by the VM's host to fetch the necessary metadata for
+/// verifying signatures and zk proofs.
+fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult {
+    let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
+    assert!(call_idx < call.len() as u32);
+
+    let self_ = &call[call_idx as usize];
+
+    match MoneyFunction::from(self_.data[0]) {
+        MoneyFunction::Transfer => {
+            let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
+
+            let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
+            let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+            for input in &params.clear_inputs {
+                signature_pubkeys.push(input.signature_public);
+            }
+
+            for input in &params.inputs {
+                let value_coords = input.value_commit.to_affine().coordinates().unwrap();
+                let token_coords = input.token_commit.to_affine().coordinates().unwrap();
+                let (sig_x, sig_y) = input.signature_public.xy();
+
+                zk_public_values.push((
+                    ZKAS_BURN_NS.to_string(),
+                    vec![
+                        input.nullifier.inner(),
+                        *value_coords.x(),
+                        *value_coords.y(),
+                        *token_coords.x(),
+                        *token_coords.y(),
+                        input.merkle_root.inner(),
+                        input.user_data_enc,
+                        sig_x,
+                        sig_y,
+                    ],
+                ));
+
+                signature_pubkeys.push(input.signature_public);
+            }
+
+            for output in &params.outputs {
+                let value_coords = output.value_commit.to_affine().coordinates().unwrap();
+                let token_coords = output.token_commit.to_affine().coordinates().unwrap();
+
+                zk_public_values.push((
+                    ZKAS_MINT_NS.to_string(),
+                    vec![
+                        output.coin.inner(),
+                        *value_coords.x(),
+                        *value_coords.y(),
+                        *token_coords.x(),
+                        *token_coords.y(),
+                    ],
+                ));
+            }
+
+            let mut metadata = vec![];
+            zk_public_values.encode(&mut metadata)?;
+            signature_pubkeys.encode(&mut metadata)?;
+
+            // Using this, we pass the above data to the host.
+            set_return_data(&metadata)?;
+        }
+    };
+
+    Ok(())
+}
+
+/// This function verifies a state transition and produces an
+/// update if everything is successful.
+fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
+    let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
+    assert!(call_idx < call.len() as u32);
+
+    let self_ = &call[call_idx as usize];
+
+    match MoneyFunction::from(self_.data[0]) {
+        MoneyFunction::Transfer => {
+            let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
+
+            let info_db = db_lookup(cid, INFO_TREE)?;
+            let nullifier_db = db_lookup(cid, NULLIFIERS_TREE)?;
+            let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?;
+
+            let Some(faucet_pubkeys) = db_get(info_db, &serialize(&FAUCET_PUBKEYS.to_string()))? else {
+                msg!("[Transfer] Error: Missing faucet pubkeys from info db");
+                return Err(ContractError::Internal);
+            };
+            let faucet_pubkeys: Vec<PublicKey> = deserialize(&faucet_pubkeys)?;
+
+            // State transition for payments
+            msg!("[Transfer] Iterating over clear inputs");
+            for (i, input) in params.clear_inputs.iter().enumerate() {
+                let pk = input.signature_public;
+
+                if !faucet_pubkeys.contains(&pk) {
+                    msg!("[Transfer] Error: Clear input {} has invalid faucet pubkey", i);
+                    return Err(ContractError::Custom(20))
+                }
+            }
+
+            let mut new_coin_roots = vec![];
+            let mut new_nullifiers = vec![];
+
+            msg!("[Transfer] Iterating over anonymous inputs");
+            for (i, input) in params.inputs.iter().enumerate() {
+                // The Merkle root is used to know whether this is a coin that existed
+                // in a previous state.
+                if new_coin_roots.contains(&input.merkle_root) ||
+                    db_contains_key(coin_roots_db, &serialize(&input.merkle_root))?
+                {
+                    msg!("[Transfer] Error: Duplicate Merkle root found in input {}", i);
+                    return Err(ContractError::Custom(21))
+                }
+
+                // The nullifiers should not already exist. It is the double-spend protection.
+                if new_nullifiers.contains(&input.nullifier) ||
+                    db_contains_key(nullifier_db, &serialize(&input.nullifier))?
+                {
+                    msg!("[Transfer] Error: Duplicate nullifier found in input {}", i);
+                    return Err(ContractError::Custom(22))
+                }
+
+                new_coin_roots.push(input.merkle_root);
+                new_nullifiers.push(input.nullifier);
+            }
+
+            // Newly created coins for this transaction are in the outputs.
+            let new_coins = Vec::with_capacity(params.outputs.len());
+            for (i, output) in params.outputs.iter().enumerate() {
+                // TODO: Should we have coins in a sled tree too to check dupes?
+                if new_coins.contains(&output.coin) {
+                    msg!("[Transfer] Error: Duplicate coin found in output {}", i);
+                    return Err(ContractError::Custom(23))
+                }
+            }
+
+            // Create a state update
+            let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
+            let mut update_data = vec![];
+            update_data.write_u8(MoneyFunction::Transfer as u8)?;
+            update.encode(&mut update_data)?;
+            set_return_data(&update_data)?;
+            msg!("[Transfer] State update set!");
+
+            Ok(())
+        }
+    }
+}
+
+fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
+    match MoneyFunction::from(update_data[0]) {
+        MoneyFunction::Transfer => {
+            let update: MoneyTransferUpdate = deserialize(&update_data[1..])?;
+
+            let info_db = db_lookup(cid, INFO_TREE)?;
+            let nullifiers_db = db_lookup(cid, NULLIFIERS_TREE)?;
+            let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?;
+
+            for nullifier in update.nullifiers {
+                db_set(nullifiers_db, &serialize(&nullifier), &[])?;
+            }
+
+            for coin in update.coins {
+                // TODO: merkle_add() should take a list of coins and batch add them for efficiency
+                merkle_add(
+                    info_db,
+                    coin_roots_db,
+                    &serialize(&COIN_MERKLE_TREE.to_string()),
+                    &MerkleNode::from(coin.inner()),
+                )?;
+            }
+
+            Ok(())
+        }
+    }
+}

+ 95 - 0
src/contract/money/src/state.rs

@@ -0,0 +1,95 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2022 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{
+        pedersen::{ValueBlind, ValueCommit},
+        Coin, MerkleNode, Nullifier, PublicKey, TokenId,
+    },
+    pasta::pallas,
+};
+use darkfi_serial::{SerialDecodable, SerialEncodable};
+
+/// Inputs and outputs for a payment
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyTransferParams {
+    /// Clear inputs
+    pub clear_inputs: Vec<ClearInput>,
+    /// Anonymous inputs
+    pub inputs: Vec<Input>,
+    /// Anonymous outputs
+    pub outputs: Vec<Output>,
+}
+
+/// State update produced by a payment
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyTransferUpdate {
+    /// Revealed nullifiers
+    pub nullifiers: Vec<Nullifier>,
+    /// Minted coins
+    pub coins: Vec<Coin>,
+}
+
+/// A transaction's clear input
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct ClearInput {
+    /// Input's value (amount)
+    pub value: u64,
+    /// Input's token ID
+    pub token_id: TokenId,
+    /// Blinding factor for `value`
+    pub value_blind: ValueBlind,
+    /// Blinding factor for `token_id`
+    pub token_blind: ValueBlind,
+    /// Public key for the signature
+    pub signature_public: PublicKey,
+}
+
+/// A transaction's anonymous input
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct Input {
+    /// Pedersen commitment for the input's value
+    pub value_commit: ValueCommit,
+    /// Pedersen commitment for the input's token ID
+    pub token_commit: ValueCommit,
+    /// Revealed nullifier
+    pub nullifier: Nullifier,
+    /// Revealed Merkle root
+    pub merkle_root: MerkleNode,
+    /// spend hook (TODO: document)
+    pub spend_hook: pallas::Base,
+    /// user data enc (TODO: document)
+    pub user_data_enc: pallas::Base,
+    /// Public key for the signature
+    pub signature_public: PublicKey,
+}
+
+/// A transaction's anonymous output
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct Output {
+    /// Pedersen commitment for the output's value
+    pub value_commit: ValueCommit,
+    /// Pedersen commitment for the output's token ID
+    pub token_commit: ValueCommit,
+    /// Minted coin
+    pub coin: Coin,
+    /// The encrypted note ciphertext
+    pub ciphertext: Vec<u8>,
+    /// The ephemeral public key
+    pub ephem_public: PublicKey,
+}

+ 85 - 0
src/sdk/src/crypto/coin.rs

@@ -0,0 +1,85 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2022 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use core::{fmt, str::FromStr};
+use std::io;
+
+use darkfi_serial::{SerialDecodable, SerialEncodable};
+use pasta_curves::{group::ff::PrimeField, pallas};
+
+/// The `Coin` is represented as a base field element.
+#[repr(C)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, SerialEncodable, SerialDecodable)]
+pub struct Coin(pallas::Base);
+
+impl Coin {
+    /// Reference the raw inner base field element
+    pub fn inner(&self) -> pallas::Base {
+        self.0
+    }
+
+    /// Try to create a `Coin` type from the given 32 bytes.
+    /// Returns `Some` if the bytes fit in the base field, and `None` if not.
+    pub fn from_bytes(bytes: [u8; 32]) -> Option<Self> {
+        let n = pallas::Base::from_repr(bytes);
+        match bool::from(n.is_some()) {
+            true => Some(Self(n.unwrap())),
+            false => None,
+        }
+    }
+
+    /// Convert the `Coin` type into 32 raw bytes
+    pub fn to_bytes(&self) -> [u8; 32] {
+        self.0.to_repr()
+    }
+}
+
+impl From<pallas::Base> for Coin {
+    fn from(x: pallas::Base) -> Self {
+        Self(x)
+    }
+}
+
+impl fmt::Display for Coin {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", bs58::encode(self.to_bytes()).into_string())
+    }
+}
+
+impl FromStr for Coin {
+    type Err = io::Error;
+
+    /// Tries to decode a base58 string into a `Coin` type.
+    /// This string is the same string received by calling `Coin::to_string()`.
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let bytes = match bs58::decode(s).into_vec() {
+            Ok(v) => v,
+            Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
+        };
+
+        if bytes.len() != 32 {
+            return Err(io::Error::new(io::ErrorKind::Other, "Length of decoded bytes is not 32"))
+        }
+
+        if let Some(coin) = Self::from_bytes(bytes.try_into().unwrap()) {
+            return Ok(coin)
+        }
+
+        Err(io::Error::new(io::ErrorKind::Other, "Invalid bytes for Coin"))
+    }
+}

+ 4 - 0
src/sdk/src/crypto/mod.rs

@@ -42,6 +42,10 @@ pub use keypair::{Keypair, PublicKey, SecretKey};
 pub mod address;
 pub use address::Address;
 
+/// Coin definitions and methods
+pub mod coin;
+pub use coin::Coin;
+
 /// Contract ID definitions and methods
 pub mod contract_id;
 pub use contract_id::ContractId;