Implement basic contract-hash support
Does not do stuff like validating the form of contracts, since this seems like more of an application thing. Does not even distinguish a "nonce", just assumes the contract has whatever uniqueness is needed baked in.
This commit is contained in:
parent
dba71d9253
commit
16e2a3519b
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "bitcoin"
|
name = "bitcoin"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
|
authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
|
||||||
license = "CC0-1.0"
|
license = "CC0-1.0"
|
||||||
homepage = "https://github.com/apoelstra/rust-bitcoin/"
|
homepage = "https://github.com/apoelstra/rust-bitcoin/"
|
||||||
|
@ -24,7 +24,7 @@ num_cpus = "0.2"
|
||||||
rand = "0.3"
|
rand = "0.3"
|
||||||
rust-crypto = "0.2"
|
rust-crypto = "0.2"
|
||||||
rustc-serialize = "0.3"
|
rustc-serialize = "0.3"
|
||||||
secp256k1 = "0.2"
|
secp256k1 = "0.3"
|
||||||
serde = "0.6"
|
serde = "0.6"
|
||||||
serde_json = "0.6"
|
serde_json = "0.6"
|
||||||
time = "0.1"
|
time = "0.1"
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
use std::hash;
|
use std::hash;
|
||||||
use std::char::from_digit;
|
use std::char::from_digit;
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::ops;
|
use std::{fmt, ops};
|
||||||
use serialize::hex::ToHex;
|
use serialize::hex::ToHex;
|
||||||
|
|
||||||
use crypto::digest::Digest;
|
use crypto::digest::Digest;
|
||||||
|
@ -55,6 +55,16 @@ impl Clone for Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::LowerHex for Script {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
try!(f.write_str("Script("));
|
||||||
|
for &ch in self.0.iter() {
|
||||||
|
try!(write!(f, "{:02x}", ch));
|
||||||
|
}
|
||||||
|
f.write_str(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
/// An object which can be used to construct a script piece by piece
|
/// An object which can be used to construct a script piece by piece
|
||||||
pub struct Builder(Vec<u8>);
|
pub struct Builder(Vec<u8>);
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Rust Bitcoin Library
|
||||||
|
// Written in 2014 by
|
||||||
|
// Andrew Poelstra <apoelstra@wpsoftware.net>
|
||||||
|
// To the extent possible under law, the author(s) have dedicated all
|
||||||
|
// copyright and related and neighboring rights to this software to
|
||||||
|
// the public domain worldwide. This software is distributed without
|
||||||
|
// any warranty.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the CC0 Public Domain Dedication
|
||||||
|
// along with this software.
|
||||||
|
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
//! # Addresses
|
||||||
|
//!
|
||||||
|
//! Support for ordinary base58 Bitcoin addresses
|
||||||
|
//!
|
||||||
|
|
||||||
|
use secp256k1::Secp256k1;
|
||||||
|
use secp256k1::key::PublicKey;
|
||||||
|
|
||||||
|
use blockdata::script;
|
||||||
|
use blockdata::opcodes;
|
||||||
|
use network::constants::Network;
|
||||||
|
use util::hash::Hash160;
|
||||||
|
use util::base58::{self, FromBase58, ToBase58};
|
||||||
|
|
||||||
|
/// The method used to produce an address
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Type {
|
||||||
|
/// Standard pay-to-pkhash address
|
||||||
|
PubkeyHash,
|
||||||
|
/// New-fangled P2SH address
|
||||||
|
ScriptHash
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
/// A Bitcoin address
|
||||||
|
pub struct Address {
|
||||||
|
/// The type of the address
|
||||||
|
pub ty: Type,
|
||||||
|
/// The network on which this address is usable
|
||||||
|
pub network: Network,
|
||||||
|
/// The pubkeyhash that this address encodes
|
||||||
|
pub hash: Hash160
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Address {
|
||||||
|
/// Creates an address from a public key
|
||||||
|
#[inline]
|
||||||
|
pub fn from_key(network: Network, pk: &PublicKey, compressed: bool) -> Address {
|
||||||
|
let secp = Secp256k1::without_caps();
|
||||||
|
Address {
|
||||||
|
ty: Type::PubkeyHash,
|
||||||
|
network: network,
|
||||||
|
hash: Hash160::from_data(&pk.serialize_vec(&secp, compressed)[..])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a script pubkey spending to this address
|
||||||
|
#[inline]
|
||||||
|
pub fn script_pubkey(&self) -> script::Script {
|
||||||
|
let mut script = script::Builder::new();
|
||||||
|
match self.ty {
|
||||||
|
Type::PubkeyHash => {
|
||||||
|
script.push_opcode(opcodes::All::OP_DUP);
|
||||||
|
script.push_opcode(opcodes::All::OP_HASH160);
|
||||||
|
script.push_slice(&self.hash[..]);
|
||||||
|
script.push_opcode(opcodes::All::OP_EQUALVERIFY);
|
||||||
|
script.push_opcode(opcodes::All::OP_CHECKSIG);
|
||||||
|
}
|
||||||
|
Type::ScriptHash => {
|
||||||
|
script.push_opcode(opcodes::All::OP_HASH160);
|
||||||
|
script.push_slice(&self.hash[..]);
|
||||||
|
script.push_opcode(opcodes::All::OP_EQUAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script.into_script()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToBase58 for Address {
|
||||||
|
fn base58_layout(&self) -> Vec<u8> {
|
||||||
|
let mut ret = vec![
|
||||||
|
match (self.network, self.ty) {
|
||||||
|
(Network::Bitcoin, Type::PubkeyHash) => 0,
|
||||||
|
(Network::Bitcoin, Type::ScriptHash) => 5,
|
||||||
|
(Network::Testnet, Type::PubkeyHash) => 111,
|
||||||
|
(Network::Testnet, Type::ScriptHash) => 196
|
||||||
|
}
|
||||||
|
];
|
||||||
|
ret.extend(self.hash[..].iter().cloned());
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromBase58 for Address {
|
||||||
|
fn from_base58_layout(data: Vec<u8>) -> Result<Address, base58::Error> {
|
||||||
|
if data.len() != 21 {
|
||||||
|
return Err(base58::Error::InvalidLength(data.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (network, ty) = match data[0] {
|
||||||
|
0 => (Network::Bitcoin, Type::PubkeyHash),
|
||||||
|
5 => (Network::Bitcoin, Type::ScriptHash),
|
||||||
|
111 => (Network::Testnet, Type::PubkeyHash),
|
||||||
|
196 => (Network::Testnet, Type::ScriptHash),
|
||||||
|
x => { return Err(base58::Error::InvalidVersion(vec![x])); }
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Address {
|
||||||
|
ty: ty,
|
||||||
|
network: network,
|
||||||
|
hash: Hash160::from(&data[1..])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::fmt::Debug for Address {
|
||||||
|
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||||
|
write!(f, "{}", self.to_base58check())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use secp256k1::Secp256k1;
|
||||||
|
use secp256k1::key::PublicKey;
|
||||||
|
use serialize::hex::FromHex;
|
||||||
|
|
||||||
|
use blockdata::script::Script;
|
||||||
|
use network::constants::Network::{Bitcoin, Testnet};
|
||||||
|
use util::hash::Hash160;
|
||||||
|
use util::base58::{FromBase58, ToBase58};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! hex (($hex:expr) => ($hex.from_hex().unwrap()));
|
||||||
|
macro_rules! hex_key (($secp:expr, $hex:expr) => (PublicKey::from_slice($secp, &hex!($hex)).unwrap()));
|
||||||
|
macro_rules! hex_script (($hex:expr) => (Script::from(hex!($hex))));
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_p2pkh_address_58() {
|
||||||
|
let addr = Address {
|
||||||
|
ty: Type::PubkeyHash,
|
||||||
|
network: Bitcoin,
|
||||||
|
hash: Hash160::from(&"162c5ea71c0b23f5b9022ef047c4a86470a5b070".from_hex().unwrap()[..])
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(addr.script_pubkey(), hex_script!("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac"));
|
||||||
|
assert_eq!(&addr.to_base58check(), "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM");
|
||||||
|
assert_eq!(FromBase58::from_base58check("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM"), Ok(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_p2pkh_from_key() {
|
||||||
|
let secp = Secp256k1::without_caps();
|
||||||
|
|
||||||
|
let key = hex_key!(&secp, "048d5141948c1702e8c95f438815794b87f706a8d4cd2bffad1dc1570971032c9b6042a0431ded2478b5c9cf2d81c124a5e57347a3c63ef0e7716cf54d613ba183");
|
||||||
|
let addr = Address::from_key(Bitcoin, &key, false);
|
||||||
|
assert_eq!(&addr.to_base58check(), "1QJVDzdqb1VpbDK7uDeyVXy9mR27CJiyhY");
|
||||||
|
|
||||||
|
let key = hex_key!(&secp, &"03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f");
|
||||||
|
let addr = Address::from_key(Testnet, &key, true);
|
||||||
|
assert_eq!(&addr.to_base58check(), "mqkhEMH6NCeYjFybv7pvFC22MFeaNT9AQC");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_p2sh_address_58() {
|
||||||
|
let addr = Address {
|
||||||
|
ty: Type::ScriptHash,
|
||||||
|
network: Bitcoin,
|
||||||
|
hash: Hash160::from(&"162c5ea71c0b23f5b9022ef047c4a86470a5b070".from_hex().unwrap()[..])
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(addr.script_pubkey(), hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087"));
|
||||||
|
assert_eq!(&addr.to_base58check(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k");
|
||||||
|
assert_eq!(FromBase58::from_base58check("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k"), Ok(addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Rust Bitcoin Library
|
||||||
|
// Written in 2015 by
|
||||||
|
// Andrew Poelstra <apoelstra@wpsoftware.net>
|
||||||
|
//
|
||||||
|
// To the extent possible under law, the author(s) have dedicated all
|
||||||
|
// copyright and related and neighboring rights to this software to
|
||||||
|
// the public domain worldwide. This software is distributed without
|
||||||
|
// any warranty.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the CC0 Public Domain Dedication
|
||||||
|
// along with this software.
|
||||||
|
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
//! # Pay-to-contract-hash supporte
|
||||||
|
//! See Appendix A of the Blockstream sidechains whitepaper
|
||||||
|
//! at http://blockstream.com/sidechains.pdf for details of
|
||||||
|
//! what this does.
|
||||||
|
|
||||||
|
use secp256k1::{self, ContextFlag, Secp256k1};
|
||||||
|
use secp256k1::key::{PublicKey, SecretKey};
|
||||||
|
use blockdata::{opcodes, script};
|
||||||
|
use crypto::{hmac, sha2};
|
||||||
|
use crypto::mac::Mac;
|
||||||
|
|
||||||
|
use network::constants::Network;
|
||||||
|
use util::{address, hash};
|
||||||
|
|
||||||
|
/// Encoding of "pubkey here" in script; from bitcoin core `src/script/script.h`
|
||||||
|
static PUBKEY: u8 = 0xFE;
|
||||||
|
|
||||||
|
/// A contract-hash error
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Contract hashed to an out-of-range value (this is basically impossible
|
||||||
|
/// and much more likely suggests memory corruption or hardware failure)
|
||||||
|
BadTweak(secp256k1::Error),
|
||||||
|
/// Other secp256k1 related error
|
||||||
|
Secp(secp256k1::Error),
|
||||||
|
/// Did not have enough keys to instantiate a script template
|
||||||
|
TooFewKeys(usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An element of a script template
|
||||||
|
enum TemplateElement {
|
||||||
|
Op(opcodes::All),
|
||||||
|
Key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A script template
|
||||||
|
pub struct Template(Vec<TemplateElement>);
|
||||||
|
|
||||||
|
impl Template {
|
||||||
|
/// Instantiate a template
|
||||||
|
pub fn to_script(&self, keys: &[PublicKey]) -> Result<script::Script, Error> {
|
||||||
|
let secp = Secp256k1::with_caps(ContextFlag::None);
|
||||||
|
let mut key_index = 0;
|
||||||
|
let mut ret = script::Builder::new();
|
||||||
|
for elem in &self.0 {
|
||||||
|
match *elem {
|
||||||
|
TemplateElement::Op(opcode) => ret.push_opcode(opcode),
|
||||||
|
TemplateElement::Key => {
|
||||||
|
if key_index == keys.len() {
|
||||||
|
return Err(Error::TooFewKeys(key_index));
|
||||||
|
}
|
||||||
|
ret.push_slice(&keys[key_index].serialize_vec(&secp, true)[..]);
|
||||||
|
key_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ret.into_script())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a [u8]> for Template {
|
||||||
|
fn from(slice: &'a [u8]) -> Template {
|
||||||
|
Template(slice.iter().map(|&byte| {
|
||||||
|
if byte == PUBKEY {
|
||||||
|
TemplateElement::Key
|
||||||
|
} else {
|
||||||
|
TemplateElement::Op(opcodes::All::from(byte))
|
||||||
|
}
|
||||||
|
}).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tweak keys using some arbitrary data
|
||||||
|
pub fn tweak_keys(secp: &Secp256k1, keys: &[PublicKey], contract: &[u8]) -> Result<Vec<PublicKey>, Error> {
|
||||||
|
let mut ret = Vec::with_capacity(keys.len());
|
||||||
|
for mut key in keys.iter().cloned() {
|
||||||
|
let mut hmac_raw = [0; 32];
|
||||||
|
let mut hmac = hmac::Hmac::new(sha2::Sha256::new(), &key.serialize_vec(&secp, true));
|
||||||
|
hmac.input(contract);
|
||||||
|
hmac.raw_result(&mut hmac_raw);
|
||||||
|
let hmac_sk = try!(SecretKey::from_slice(&secp, &hmac_raw).map_err(Error::BadTweak));
|
||||||
|
try!(key.add_exp_assign(&secp, &hmac_sk).map_err(Error::Secp));
|
||||||
|
ret.push(key);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a contract, template and key set and runs through all the steps
|
||||||
|
pub fn create_address(secp: &Secp256k1,
|
||||||
|
network: Network,
|
||||||
|
contract: &[u8],
|
||||||
|
keys: &[PublicKey],
|
||||||
|
template: &Template)
|
||||||
|
-> Result<address::Address, Error> {
|
||||||
|
let keys = try!(tweak_keys(secp, keys, contract));
|
||||||
|
let script = try!(template.to_script(&keys));
|
||||||
|
Ok(address::Address {
|
||||||
|
network: network,
|
||||||
|
ty: address::Type::ScriptHash,
|
||||||
|
hash: hash::Hash160::from_data(&script[..])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use secp256k1::Secp256k1;
|
||||||
|
use secp256k1::key::PublicKey;
|
||||||
|
use serialize::hex::FromHex;
|
||||||
|
|
||||||
|
use network::constants::Network;
|
||||||
|
use util::base58::ToBase58;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! hex (($hex:expr) => ($hex.from_hex().unwrap()));
|
||||||
|
macro_rules! hex_key (($secp:expr, $hex:expr) => (PublicKey::from_slice($secp, &hex!($hex)).unwrap()));
|
||||||
|
macro_rules! alpha_template(() => (Template::from(&hex!("55fefefefefefefe57AE")[..])));
|
||||||
|
macro_rules! alpha_keys(($secp:expr) => (
|
||||||
|
&[hex_key!($secp, "0269992fb441ae56968e5b77d46a3e53b69f136444ae65a94041fc937bdb28d933"),
|
||||||
|
hex_key!($secp, "021df31471281d4478df85bfce08a10aab82601dca949a79950f8ddf7002bd915a"),
|
||||||
|
hex_key!($secp, "02174c82021492c2c6dfcbfa4187d10d38bed06afb7fdcd72c880179fddd641ea1"),
|
||||||
|
hex_key!($secp, "033f96e43d72c33327b6a4631ccaa6ea07f0b106c88b9dc71c9000bb6044d5e88a"),
|
||||||
|
hex_key!($secp, "0313d8748790f2a86fb524579b46ce3c68fedd58d2a738716249a9f7d5458a15c2"),
|
||||||
|
hex_key!($secp, "030b632eeb079eb83648886122a04c7bf6d98ab5dfb94cf353ee3e9382a4c2fab0"),
|
||||||
|
hex_key!($secp, "02fb54a7fcaa73c307cfd70f3fa66a2e4247a71858ca731396343ad30c7c4009ce")]
|
||||||
|
));
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanity() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
let keys = alpha_keys!(&secp);
|
||||||
|
// This is the first withdraw ever, in alpha a94f95cc47b444c10449c0eed51d895e4970560c4a1a9d15d46124858abc3afe
|
||||||
|
let contract = hex!("5032534894ffbf32c1f1c0d3089b27c98fd991d5d7329ebd7d711223e2cde5a9417a1fa3e852c576");
|
||||||
|
|
||||||
|
let addr = create_address(&secp, Network::Testnet, &contract, keys, &alpha_template!()).unwrap();
|
||||||
|
assert_eq!(addr.to_base58check(), "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,10 @@ impl ::std::fmt::Debug for Sha256dHash {
|
||||||
pub struct Ripemd160Hash([u8; 20]);
|
pub struct Ripemd160Hash([u8; 20]);
|
||||||
impl_array_newtype!(Ripemd160Hash, u8, 20);
|
impl_array_newtype!(Ripemd160Hash, u8, 20);
|
||||||
|
|
||||||
|
/// A Bitcoin hash160, 20-bytes, computed from x as RIPEMD160(SHA256(x))
|
||||||
|
pub struct Hash160([u8; 20]);
|
||||||
|
impl_array_newtype!(Hash160, u8, 20);
|
||||||
|
|
||||||
/// A 32-bit hash obtained by truncating a real hash
|
/// A 32-bit hash obtained by truncating a real hash
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Hash32((u8, u8, u8, u8));
|
pub struct Hash32((u8, u8, u8, u8));
|
||||||
|
@ -68,8 +72,23 @@ impl Ripemd160Hash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash160 {
|
||||||
|
/// Create a hash by hashing some data
|
||||||
|
pub fn from_data(data: &[u8]) -> Hash160 {
|
||||||
|
let mut tmp = [0; 32];
|
||||||
|
let mut ret = [0; 20];
|
||||||
|
let mut sha2 = Sha256::new();
|
||||||
|
let mut rmd = Ripemd160::new();
|
||||||
|
sha2.input(data);
|
||||||
|
sha2.result(&mut tmp);
|
||||||
|
rmd.input(&tmp);
|
||||||
|
rmd.result(&mut ret);
|
||||||
|
Hash160(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This doesn't make much sense to me, but is implicit behaviour
|
// This doesn't make much sense to me, but is implicit behaviour
|
||||||
// in the C++ reference client
|
// in the C++ reference client, so we need it for consensus.
|
||||||
impl Default for Sha256dHash {
|
impl Default for Sha256dHash {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn default() -> Sha256dHash { Sha256dHash([0u8; 32]) }
|
fn default() -> Sha256dHash { Sha256dHash([0u8; 32]) }
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
//!
|
//!
|
||||||
//! Functions needed by all parts of the Bitcoin library
|
//! Functions needed by all parts of the Bitcoin library
|
||||||
|
|
||||||
|
pub mod address;
|
||||||
pub mod base58;
|
pub mod base58;
|
||||||
|
pub mod contracthash;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod iter;
|
pub mod iter;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
|
|
Loading…
Reference in New Issue