diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e0f47d..efce261 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,14 +14,16 @@ jobs: - name: Setup Environment run: | rustup update stable + rustup toolchain install nightly rustup target add wasm32-unknown-unknown + rustup component add rustfmt --toolchain nightly - name: Build for wasm # Check if the library target compiles. This will still allow for using # non-wasm functionality in tests and benchmarks but guarantees that # consumers of the library can use it to generate wasm bindings. run: cargo check --target wasm32-unknown-unknown --lib - name: Rustfmt - run: cargo fmt --all -- --check + run: cargo +nightly fmt --all -- --check - name: Clippy run: cargo clippy -- -D warnings - name: Test diff --git a/.gitignore b/.gitignore index ccb5166..b7363bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.vscode \ No newline at end of file +.vscode +.DS_Store \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..0c205e8 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +newline_style="Unix" +reorder_imports=true +# only supported in nightly +imports_granularity="Module" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f32f819..6923ec4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,27 +19,27 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstyle", "clap_lex", @@ -184,15 +184,15 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -311,6 +311,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -434,18 +444,18 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "log" @@ -459,6 +469,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -470,9 +486,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" @@ -518,29 +534,35 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -597,9 +619,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -609,9 +631,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -620,15 +642,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -707,7 +729,7 @@ dependencies = [ ] [[package]] -name = "sia_core" +name = "sia_sdk" version = "0.0.1" dependencies = [ "base64", @@ -722,7 +744,18 @@ dependencies = [ "serde-big-array", "serde_json", "sha2", + "sia_sdk_derive", "thiserror", + "time", +] + +[[package]] +name = "sia_sdk_derive" +version = "0.0.1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -763,24 +796,54 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -814,9 +877,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -851,9 +914,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -862,9 +925,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -877,9 +940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -887,9 +950,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -900,15 +963,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -1007,9 +1070,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" -version = "0.6.6" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", @@ -1017,9 +1080,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.6.6" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 175bccd..60ab684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,35 +1,6 @@ -[package] -name = "sia_core" -version = "0.0.1" -edition = "2021" -repository = "https://github.com/SiaFoundation/core-rs" -license = "MIT" -description = "Low-level SDK for interacting with the Sia decentralized storage network" -authors = ["The Sia Foundation"] -categories = ["cryptography::cryptocurrencies"] -keywords = ["sia", "decentralized", "blockchain", "depin", "storage"] - -[lib] -name = "sia_core" -path = "src/lib.rs" - -[dependencies] -base64 = "0.22.1" -bip39 = "2.1.0" -blake2b_simd = "1.0.2" -ed25519-dalek = "2.1.1" -hex = "0.4.3" -rayon = "1.10.0" -serde = { version = "1.0.214", features = ["derive"] } -serde-big-array = "0.5.1" -serde_json = "1.0.132" -sha2 = "0.10.8" -thiserror = "1.0.67" - -[dev-dependencies] -rand = "0.8.5" -criterion = { version = "0.5" } - -[[bench]] -name = "merkle_root" -harness = false +[workspace] +resolver = "2" +members = [ + "sia", + "sia_sdk_derive", +] \ No newline at end of file diff --git a/sia/Cargo.toml b/sia/Cargo.toml new file mode 100644 index 0000000..671485a --- /dev/null +++ b/sia/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "sia_sdk" +version = "0.0.1" +edition = "2021" +repository = "https://github.com/SiaFoundation/sia-sdk-rs" +license = "MIT" +description = "Low-level SDK for interacting with the Sia decentralized storage network" +authors = ["The Sia Foundation"] +categories = ["cryptography::cryptocurrencies"] +keywords = ["sia", "decentralized", "blockchain", "depin", "storage"] + +[lib] +name = "sia" +path = "src/lib.rs" + +[dependencies] +base64 = "0.22.1" +bip39 = "2.1.0" +blake2b_simd = "1.0.2" +ed25519-dalek = "2.1.1" +hex = "0.4.3" +rayon = "1.10.0" +serde = { version = "1.0.213", features = ["derive"] } +serde-big-array = "0.5.1" +serde_json = "1.0.132" +sha2 = "0.10.8" +sia_sdk_derive = { version = "0.0.1", path = "../sia_sdk_derive" } +thiserror = "1.0.65" +time = {version = "0.3.36", features = ["serde"] } + +[dev-dependencies] +rand = "0.8.5" +criterion = { version = "0.5" } + +[[bench]] +name = "merkle_root" +harness = false diff --git a/benches/merkle_root.rs b/sia/benches/merkle_root.rs similarity index 81% rename from benches/merkle_root.rs rename to sia/benches/merkle_root.rs index 06ea113..7407f63 100644 --- a/benches/merkle_root.rs +++ b/sia/benches/merkle_root.rs @@ -4,7 +4,7 @@ use std::hint::black_box; fn criterion_benchmark(c: &mut Criterion) { let sector = [0u8; 1 << 22]; c.bench_function("sector_root", |b| { - b.iter(|| sia_core::rhp::sector_root(black_box(§or))) + b.iter(|| sia::rhp::sector_root(black_box(§or))) }); } diff --git a/src/common.rs b/sia/src/common.rs similarity index 66% rename from src/common.rs rename to sia/src/common.rs index 0005376..705269a 100644 --- a/src/common.rs +++ b/sia/src/common.rs @@ -1,79 +1,18 @@ +use crate::encoding::{ + SiaDecodable, SiaDecode, SiaEncodable, SiaEncode, V1SiaDecodable, V1SiaDecode, V1SiaEncodable, + V1SiaEncode, +}; use blake2b_simd::Params; use core::fmt; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -pub struct ChainIndex { - pub height: u64, - pub id: [u8; 32], -} - -impl fmt::Display for ChainIndex { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}", self.height, hex::encode(self.id)) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct Leaf([u8; 64]); - -impl From<[u8; 64]> for Leaf { - fn from(data: [u8; 64]) -> Self { - Leaf(data) - } -} - -impl fmt::Display for Leaf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", hex::encode(self.0)) - } -} - -impl Serialize for Leaf { - fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize(&self.to_string(), serializer) - } else { - #[derive(Serialize)] - struct BinaryLeaf { - #[serde(with = "serde_big_array::BigArray")] - data: [u8; 64], - } - BinaryLeaf { data: self.0 }.serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for Leaf { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let data = hex::decode(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?; - if data.len() != 64 { - return Err(serde::de::Error::custom("invalid length")); - } - Ok(Leaf(data.try_into().unwrap())) - } else { - #[derive(Deserialize)] - struct BinaryLeaf { - #[serde(with = "serde_big_array::BigArray")] - data: [u8; 64], - } - let leaf = BinaryLeaf::deserialize(deserializer)?; - Ok(Leaf(leaf.data)) - } - } -} - // Macro to implement types used as identifiers which are 32 byte hashes and are // serialized with a prefix #[macro_export] macro_rules! ImplHashID { - ($name:ident, $prefix:expr) => { - #[derive(Debug, Clone, Copy, PartialEq)] + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, SiaEncode, SiaDecode, V1SiaEncode, V1SiaDecode)] pub struct $name([u8; 32]); impl serde::Serialize for $name { @@ -91,38 +30,32 @@ macro_rules! ImplHashID { where D: serde::Deserializer<'de>, { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - $name::parse_string(&s) - .map_err(|e| serde::de::Error::custom(format!("{:?}", e))) - } else { - let data = <[u8; 32]>::deserialize(deserializer)?; - Ok($name(data)) - } + let s = String::deserialize(deserializer)?; + $name::parse_string(&s).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) } } impl $name { // Example method that might be used in serialization/deserialization - pub fn parse_string(s: &str) -> Result { + pub fn parse_string(s: &str) -> Result { let s = match s.split_once(':') { Some((_prefix, suffix)) => suffix, None => s, }; if s.len() != 64 { - return Err(HexParseError::InvalidLength); + return Err($crate::HexParseError::InvalidLength); } let mut data = [0u8; 32]; - hex::decode_to_slice(s, &mut data).map_err(HexParseError::HexError)?; + hex::decode_to_slice(s, &mut data).map_err($crate::HexParseError::HexError)?; Ok($name(data)) } } - impl fmt::Display for $name { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}", $prefix, hex::encode(self.0)) + impl core::fmt::Display for $name { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", hex::encode(self.0)) } } @@ -166,7 +99,56 @@ macro_rules! ImplHashID { }; } -ImplHashID!(Hash256, "h"); +ImplHashID!(Hash256); +ImplHashID!(BlockID); + +#[derive(Debug, PartialEq, SiaEncode, SiaDecode, Serialize, Deserialize)] + +pub struct ChainIndex { + pub height: u64, + pub id: BlockID, +} + +impl fmt::Display for ChainIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.height, hex::encode(self.id)) + } +} + +#[derive(Debug, PartialEq, Clone, SiaEncode, V1SiaEncode, SiaDecode, V1SiaDecode)] +pub struct Leaf([u8; 64]); + +impl From<[u8; 64]> for Leaf { + fn from(data: [u8; 64]) -> Self { + Leaf(data) + } +} + +impl fmt::Display for Leaf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl Serialize for Leaf { + fn serialize(&self, serializer: S) -> Result { + String::serialize(&self.to_string(), serializer) + } +} + +impl<'de> Deserialize<'de> for Leaf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let data = hex::decode(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?; + if data.len() != 64 { + return Err(serde::de::Error::custom("invalid length")); + } + Ok(Leaf(data.try_into().unwrap())) + } +} /// encapsulates the various errors that can occur when parsing a Sia object /// from a string @@ -180,7 +162,7 @@ pub enum HexParseError { } /// An address that can be used to receive UTXOs -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, SiaEncode, V1SiaEncode, SiaDecode, V1SiaDecode)] pub struct Address([u8; 32]); impl<'de> Deserialize<'de> for Address { @@ -188,13 +170,8 @@ impl<'de> Deserialize<'de> for Address { where D: serde::Deserializer<'de>, { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - Address::parse_string(&s).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) - } else { - let data = <[u8; 32]>::deserialize(deserializer)?; - Ok(Address(data)) - } + let s = String::deserialize(deserializer)?; + Address::parse_string(&s).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) } } @@ -203,11 +180,7 @@ impl Serialize for Address { where S: serde::Serializer, { - if serializer.is_human_readable() { - self.to_string().serialize(serializer) - } else { - self.0.serialize(serializer) - } + self.to_string().serialize(serializer) } } @@ -217,11 +190,6 @@ impl Address { } pub fn parse_string(s: &str) -> Result { - let s = match s.split_once(':') { - Some((_prefix, suffix)) => suffix, - None => s, - }; - if s.len() != 76 { return Err(HexParseError::InvalidLength); } @@ -276,14 +244,13 @@ impl fmt::Display for Address { .finalize(); buf[32..].copy_from_slice(&h.as_bytes()[..6]); - write!(f, "addr:{}", hex::encode(buf)) + write!(f, "{}", hex::encode(buf)) } } #[cfg(test)] mod tests { use super::*; - use crate::encoding::{from_reader, to_bytes}; #[test] fn test_serialize_hash256() { @@ -291,15 +258,16 @@ mod tests { let hash = Hash256(hex::decode(hash_str).unwrap().try_into().unwrap()); // binary - let hash_serialized = to_bytes(&hash).unwrap(); - let hash_deserialized: Hash256 = from_reader(&mut &hash_serialized[..]).unwrap(); - assert_eq!(hash_serialized, hex::decode(hash_str).unwrap()); // serialize + let mut hash_serialized: Vec = Vec::new(); + hash.encode(&mut hash_serialized).unwrap(); + assert_eq!(hash_serialized, hex::decode(hash_str).unwrap()); + let hash_deserialized = Hash256::decode(&mut &hash_serialized[..]).unwrap(); assert_eq!(hash_deserialized, hash); // deserialize // json let hash_serialized = serde_json::to_string(&hash).unwrap(); let hash_deserialized: Hash256 = serde_json::from_str(&hash_serialized).unwrap(); - assert_eq!(hash_serialized, format!("\"h:{0}\"", hash_str)); // serialize + assert_eq!(hash_serialized, format!("\"{0}\"", hash_str)); // serialize assert_eq!(hash_deserialized, hash); // deserialize } @@ -310,18 +278,16 @@ mod tests { let address = Address(hex::decode(addr_str).unwrap().try_into().unwrap()); // binary - let addr_serialized = to_bytes(&address).unwrap(); - let addr_deserialized: Address = from_reader(&mut &addr_serialized[..]).unwrap(); + let mut addr_serialized: Vec = Vec::new(); + address.encode(&mut addr_serialized).unwrap(); assert_eq!(addr_serialized, hex::decode(addr_str).unwrap()); // serialize + let addr_deserialized = Address::decode(&mut &addr_serialized[..]).unwrap(); assert_eq!(addr_deserialized, address); // deserialize // json let addr_serialized = serde_json::to_string(&address).unwrap(); let addr_deserialized: Address = serde_json::from_str(&addr_serialized).unwrap(); - assert_eq!( - addr_serialized, - format!("\"addr:{0}{1}\"", addr_str, checksum) - ); // serialize + assert_eq!(addr_serialized, format!("\"{0}{1}\"", addr_str, checksum)); // serialize assert_eq!(addr_deserialized, address); // deserialize } } diff --git a/src/currency.rs b/sia/src/currency.rs similarity index 59% rename from src/currency.rs rename to sia/src/currency.rs index 6628bef..eb24716 100644 --- a/src/currency.rs +++ b/sia/src/currency.rs @@ -1,9 +1,12 @@ use core::num::ParseIntError; use core::ops::{Add, Deref, DerefMut, Div, Mul, Sub}; +use std::io::Write; use std::iter::Sum; use serde::{Deserialize, Serialize}; +use crate::encoding::{self, SiaDecodable, SiaEncodable, V1SiaDecodable, V1SiaEncodable}; + // I miss untyped constants const SIACOIN_PRECISION_I32: i32 = 24; const SIACOIN_PRECISION_U32: u32 = 24; @@ -17,34 +20,53 @@ pub struct Currency(u128); // to decide on what to do about mixing v1 and v2 currencies. impl Serialize for Currency { fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize(&self.to_string(), serializer) - } else { - let currency_buf = self.to_be_bytes(); - let i = currency_buf - .iter() - .enumerate() - .find(|&(_index, &value)| value != 0) - .map_or(16, |(index, _value)| index); // 16 if all bytes are 0 - currency_buf[i..].serialize(serializer) - } + serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for Currency { fn deserialize>(deserializer: D) -> Result { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - Currency::parse_string(&s).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) - } else { - let data = Vec::deserialize(deserializer)?; - if data.is_empty() || data.len() > 16 { - return Err(serde::de::Error::custom("invalid currency length")); - } - let mut buf = [0; 16]; - buf[16 - data.len()..].copy_from_slice(&data); - Ok(Currency(u128::from_be_bytes(buf))) + let s = String::deserialize(deserializer)?; + Currency::parse_string(&s).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) + } +} + +impl V1SiaEncodable for Currency { + fn encode_v1(&self, w: &mut W) -> encoding::Result<()> { + let currency_buf = self.to_be_bytes(); + let i = currency_buf + .iter() + .enumerate() + .find(|&(_index, &value)| value != 0) + .map_or(16, |(index, _value)| index); // 16 if all bytes are 0 + currency_buf[i..].encode_v1(w) + } +} + +impl V1SiaDecodable for Currency { + fn decode_v1(r: &mut R) -> encoding::Result { + let len = usize::decode_v1(r)?; + if len > 16 { + return Err(encoding::Error::InvalidLength); } + let mut buf = [0u8; 16]; + r.read_exact(&mut buf[16 - len..])?; + Ok(Currency(u128::from_be_bytes(buf))) + } +} + +impl SiaEncodable for Currency { + fn encode(&self, w: &mut W) -> encoding::Result<()> { + w.write_all(&self.0.to_le_bytes())?; + Ok(()) + } +} + +impl SiaDecodable for Currency { + fn decode(r: &mut R) -> encoding::Result { + let mut buf = [0u8; 16]; + r.read_exact(&mut buf)?; + Ok(Currency(u128::from_le_bytes(buf))) } } @@ -63,6 +85,13 @@ impl DerefMut for Currency { } } +impl TryInto for Currency { + type Error = core::num::TryFromIntError; + fn try_into(self) -> Result { + self.0.try_into() + } +} + // Implement AsRef as well to be able to implicitly obtain a &u128 from a Currency as well. impl AsRef for Currency where @@ -220,137 +249,10 @@ impl From for CurrencyParseError { } } -/*impl fmt::Display for Currency { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.0 == 0 { - return f.write_str("0 SC"); - } - - let value_string = self.0.to_string(); - let mut u = (value_string.len() - 1) / 3; - if u < 4 { - return write!(f, "{} H", value_string); - } else if u > 12 { - u = 12; - } - - let mant = &value_string[..value_string.len() - 3 * u]; - let frac = value_string[value_string.len()-u*3..].trim_end_matches('0'); - let unit = match u-4 { - 0 => "pS", - 1 => "nS", - 2 => "uS", - 3 => "mS", - 4 => "SC", - 5 => "KS", - 6 => "MS", - 7 => "GS", - 8 => "TS", - _ => panic!("unexpected unit") - }; - - if frac.is_empty() { - return write!(f, "{} {}", mant, unit); - } - write!(f, "{}.{} {}", mant, frac, unit) - } -} - -impl FromStr for Currency { - type Err = CurrencyParseError; - - fn from_str(s: &str) -> Result { - let i = s.find(|c: char| !c.is_digit(10) && c != '.').unwrap_or(s.len()); - let (value, unit) = s.split_at(i); - let unit = unit.trim(); - - if unit.is_empty() || unit == "H" { - let value = value.parse::()?; - return Ok(Currency::new(value)) - } - - let scaling_factor: i32 = match unit { - "pS" => -12, - "nS" => -9, - "uS" => -6, - "mS" => -3, - "SC" => 0, - "KS" => 3, - "MS" => 6, - "GS" => 9, - "TS" => 12, - &_ => return Err(CurrencyParseError::InvalidUnit(unit.to_string())), - }; - - let parts: Vec<&str> = value.split('.').collect(); - if parts.len() > 2 { - return Err(CurrencyParseError::InvalidFormat("too many decimal points".to_string())) - } - - let integer_part = parts[0].parse::().map_err(|_| CurrencyParseError::InvalidFormat("invalid integer part".to_string()))?; - let fraction_part = if parts.len() == 2 { - parts[1].parse::().map_err(|_| CurrencyParseError::InvalidFormat("invalid integer part".to_string()))? - } else { - 0 - }; - - let frac_digits = parts.get(1).map_or(0, |frac| frac.len() as i32); - let integer = integer_part * 10u128.pow((SIACOIN_PRECISION_I32 + scaling_factor) as u32); - let fraction = fraction_part * 10u128.pow((SIACOIN_PRECISION_I32 - frac_digits + scaling_factor) as u32); - - Ok(Currency::new(integer+fraction)) - } -}*/ - #[cfg(test)] mod tests { - use crate::encoding::{from_reader, to_bytes}; - use super::*; - /* #[test] - fn test_display() { - let test_cases = vec![ - (Currency::new(1), "1 H"), - (Currency::new(100), "100 H"), - (Currency::new(1001), "1001 H"), - (Currency::new(10000), "10000 H"), - (Currency::siacoins(1)/Currency::new(1_000_000_000_000), "1 pS"), - (Currency::siacoins(151212312)/Currency::new(1_000_000_000_000), "151.212312 uS"), - (Currency::siacoins(1)/Currency::new(2), "500 mS"), - (Currency::siacoins(1), "1 SC"), - (Currency::siacoins(10), "10 SC"), - (Currency::siacoins(100), "100 SC"), - (Currency::siacoins(1000), "1 KS"), - (Currency::siacoins(10000), "10 KS"), - (Currency::siacoins(u16::MAX as u64), "65.535 KS"), - (Currency::siacoins(10_0000), "100 KS"), - (Currency::siacoins(1_000_000), "1 MS"), - (Currency::siacoins(10_000_000), "10 MS"), - (Currency::siacoins(100_000_000), "100 MS"), - (Currency::siacoins(1_000_000_000), "1 GS"), - (Currency::siacoins(u32::MAX as u64), "4.294967295 GS"), - (Currency::siacoins(10_000_000_000), "10 GS"), - (Currency::siacoins(100_000_000_000), "100 GS"), - (Currency::siacoins(1_000_000_000_000), "1 TS"), - (Currency::siacoins(10_000_000_000_000), "10 TS"), - (Currency::siacoins(100_000_000_000_000), "100 TS"), - (Currency::siacoins(10) - Currency::new(1), "9.999999999999999999999999 SC"), - (Currency::new(50_587_566_000_000_000_000_000_000),"50.587566 SC"), - (Currency::new(2529378333356156158367), "2.529378333356156158367 mS"), - (Currency::new(u128::MAX), "340.282366920938463463374607431768211455 TS"), - ]; - - for (currency, expected) in test_cases { - assert_eq!(currency.to_string(), expected); - } - }*/ - - #[test] - fn test_json_serialize_currency() { - assert_eq!(serde_json::to_string(&Currency::new(1)).unwrap(), "\"1\"") - } - #[test] fn test_from_str() { let test_cases = vec![ @@ -404,7 +306,7 @@ mod tests { } #[test] - fn test_encode() { + fn test_encode_v1() { let test_cases = vec![ (Currency::new(0), vec![0, 0, 0, 0, 0, 0, 0, 0]), (Currency::new(10000), vec![2, 0, 0, 0, 0, 0, 0, 0, 39, 16]), @@ -430,28 +332,22 @@ mod tests { ]; for (currency, expected) in test_cases { - let bytes = to_bytes(¤cy).unwrap(); - assert_eq!(bytes, expected, "failed for {:?}", currency); + let mut serialized_currency = Vec::new(); + currency + .encode_v1(&mut serialized_currency) + .unwrap_or_else(|e| panic!("failed to encode: {:?}", e)); + assert_eq!(serialized_currency, expected, "failed for {:?}", currency); + let deserialized_currency = Currency::decode_v1(&mut &serialized_currency[..]) + .unwrap_or_else(|e| panic!("failed to decode: {:?}", e)); + assert_eq!(deserialized_currency, currency, "failed for {:?}", currency); } } #[test] - fn test_serialize_currency() { + fn test_json_serialize_currency() { let currency_num = 120282366920938463463374607431768211455; let currency = Currency::new(currency_num); - // binary - let currency_serialized = to_bytes(¤cy).unwrap(); - let currency_deserialized: Currency = from_reader(&mut ¤cy_serialized[..]).unwrap(); - assert_eq!( - currency_serialized, - u64::to_le_bytes(16) // prefix - .into_iter() - .chain(u128::to_be_bytes(currency_num)) // number - .collect::>() - ); - assert_eq!(currency_deserialized, currency); - // json let currency_serialized = serde_json::to_string(¤cy).unwrap(); let currency_deserialized: Currency = serde_json::from_str(¤cy_serialized).unwrap(); diff --git a/sia/src/encoding.rs b/sia/src/encoding.rs new file mode 100644 index 0000000..b38ad11 --- /dev/null +++ b/sia/src/encoding.rs @@ -0,0 +1,8 @@ +mod v1; +mod v2; + +pub use sia_sdk_derive::{SiaDecode, SiaEncode}; +pub use v2::{Error, Result, SiaDecodable, SiaEncodable}; + +pub use sia_sdk_derive::{V1SiaDecode, V1SiaEncode}; +pub use v1::{V1SiaDecodable, V1SiaEncodable}; diff --git a/sia/src/encoding/v1.rs b/sia/src/encoding/v1.rs new file mode 100644 index 0000000..59ece0d --- /dev/null +++ b/sia/src/encoding/v1.rs @@ -0,0 +1,257 @@ +use crate::encoding::{Error, Result}; +use std::io::{Read, Write}; + +pub trait V1SiaEncodable { + fn encode_v1(&self, w: &mut W) -> Result<()>; +} + +pub trait V1SiaDecodable: Sized { + fn decode_v1(r: &mut R) -> Result; +} + +impl V1SiaEncodable for u8 { + fn encode_v1(&self, w: &mut W) -> Result<()> { + w.write_all(&[*self])?; + Ok(()) + } +} + +impl V1SiaDecodable for u8 { + fn decode_v1(r: &mut R) -> Result { + let mut buf = [0; 1]; + r.read_exact(&mut buf)?; + Ok(buf[0]) + } +} + +impl V1SiaEncodable for bool { + fn encode_v1(&self, w: &mut W) -> Result<()> { + (*self as u8).encode_v1(w) + } +} + +impl V1SiaDecodable for bool { + fn decode_v1(r: &mut R) -> Result { + let v = u8::decode_v1(r)?; + match v { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::InvalidValue), + } + } +} + +impl V1SiaEncodable for [T] { + fn encode_v1(&self, w: &mut W) -> Result<()> { + self.len().encode_v1(w)?; + for item in self { + item.encode_v1(w)?; + } + Ok(()) + } +} + +impl V1SiaEncodable for Option { + fn encode_v1(&self, w: &mut W) -> Result<()> { + match self { + Some(v) => { + true.encode_v1(w)?; + v.encode_v1(w) + } + None => false.encode_v1(w), + } + } +} + +impl V1SiaDecodable for Option { + fn decode_v1(r: &mut R) -> Result { + let has_value = bool::decode_v1(r)?; + if has_value { + Ok(Some(T::decode_v1(r)?)) + } else { + Ok(None) + } + } +} + +macro_rules! impl_sia_numeric { + ($($t:ty),*) => { + $( + impl V1SiaEncodable for $t { + fn encode_v1(&self, w: &mut W) -> Result<()> { + w.write_all(&(*self as u64).to_le_bytes())?; + Ok(()) + } + } + + impl V1SiaDecodable for $t { + fn decode_v1(r: &mut R) -> Result { + let mut buf = [0u8; 8]; + r.read_exact(&mut buf)?; + Ok(u64::from_le_bytes(buf) as Self) + } + } + )* + } +} + +impl_sia_numeric!(u16, u32, usize, i16, i32, i64, u64); + +impl V1SiaEncodable for Vec +where + T: V1SiaEncodable, +{ + fn encode_v1(&self, w: &mut W) -> Result<()> { + self.len().encode_v1(w)?; + for item in self { + item.encode_v1(w)?; + } + Ok(()) + } +} + +impl V1SiaDecodable for Vec +where + T: V1SiaDecodable, +{ + fn decode_v1(r: &mut R) -> Result { + let len = usize::decode_v1(r)?; + let mut vec = Vec::with_capacity(len); + for _ in 0..len { + vec.push(T::decode_v1(r)?); + } + Ok(vec) + } +} + +impl V1SiaEncodable for String { + fn encode_v1(&self, w: &mut W) -> Result<()> { + self.as_bytes().encode_v1(w) + } +} + +impl V1SiaDecodable for String { + fn decode_v1(r: &mut R) -> Result { + let buf = Vec::::decode_v1(r)?; + String::from_utf8(buf).map_err(|_| Error::InvalidLength) + } +} + +impl V1SiaEncodable for [u8; N] { + fn encode_v1(&self, w: &mut W) -> Result<()> { + w.write_all(self)?; + Ok(()) + } +} + +impl V1SiaDecodable for [u8; N] { + fn decode_v1(r: &mut R) -> Result { + let mut arr = [0u8; N]; + r.read_exact(&mut arr)?; + Ok(arr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_roundtrip( + value: T, + expected_bytes: Vec, + ) { + let mut encoded_bytes = Vec::new(); + value + .encode_v1(&mut encoded_bytes) + .unwrap_or_else(|e| panic!("failed to encode: {:?}", e)); + + assert_eq!( + encoded_bytes, expected_bytes, + "encoding mismatch for {:?}", + value + ); + + let mut bytes = &expected_bytes[..]; + let decoded = + T::decode_v1(&mut bytes).unwrap_or_else(|e| panic!("failed to decode: {:?}", e)); + assert_eq!(decoded, value, "decoding mismatch for {:?}", value); + + assert_eq!(bytes.len(), 0, "leftover bytes for {:?}", value); + } + + #[test] + fn test_numerics() { + test_roundtrip(1u8, vec![1]); + test_roundtrip(2u16, vec![2, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(3u32, vec![3, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(4u64, vec![4, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(5usize, vec![5, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(-1i16, vec![255, 255, 255, 255, 255, 255, 255, 255]); + test_roundtrip(-2i32, vec![254, 255, 255, 255, 255, 255, 255, 255]); + test_roundtrip(-3i64, vec![253, 255, 255, 255, 255, 255, 255, 255]); + } + + #[test] + fn test_strings() { + test_roundtrip( + "hello".to_string(), + vec![ + 5, 0, 0, 0, 0, 0, 0, 0, // length prefix + 104, 101, 108, 108, 111, // "hello" + ], + ); + test_roundtrip( + "".to_string(), + vec![0, 0, 0, 0, 0, 0, 0, 0], // empty string length + ); + } + + #[test] + fn test_fixed_arrays() { + test_roundtrip([1u8, 2u8, 3u8], vec![1, 2, 3]); + test_roundtrip([0u8; 4], vec![0, 0, 0, 0]); + } + + #[test] + fn test_vectors() { + test_roundtrip( + vec![1u8, 2u8, 3u8], + vec![ + 3, 0, 0, 0, 0, 0, 0, 0, // length prefix + 1, 2, 3, // values + ], + ); + test_roundtrip( + vec![100u64, 200u64], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // length prefix + 100, 0, 0, 0, 0, 0, 0, 0, // 100u64 + 200, 0, 0, 0, 0, 0, 0, 0, // 200u64 + ], + ); + test_roundtrip( + vec!["a".to_string(), "bc".to_string()], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // vector length + 1, 0, 0, 0, 0, 0, 0, 0, // first string length + 97, // "a" + 2, 0, 0, 0, 0, 0, 0, 0, // second string length + 98, 99, // "bc" + ], + ); + } + + #[test] + fn test_nested() { + test_roundtrip( + vec![vec![1u8, 2u8], vec![3u8, 4u8]], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // outer vec length + 2, 0, 0, 0, 0, 0, 0, 0, // first inner vec length + 1, 2, // first inner vec contents + 2, 0, 0, 0, 0, 0, 0, 0, // second inner vec length + 3, 4, // second inner vec contents + ], + ); + } +} diff --git a/sia/src/encoding/v2.rs b/sia/src/encoding/v2.rs new file mode 100644 index 0000000..300536b --- /dev/null +++ b/sia/src/encoding/v2.rs @@ -0,0 +1,272 @@ +use std::io::{self, Read, Write}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("Invalid timestamp")] + InvalidTimestamp, + #[error("Invalid length")] + InvalidLength, + #[error("Invalid value")] + InvalidValue, + #[error("Custom error: {0}")] + Custom(String), +} + +pub type Result = std::result::Result; + +pub trait SiaEncodable { + fn encode(&self, w: &mut W) -> Result<()>; +} + +pub trait SiaDecodable: Sized { + fn decode(r: &mut R) -> Result; +} + +impl SiaEncodable for u8 { + fn encode(&self, w: &mut W) -> Result<()> { + w.write_all(&[*self])?; + Ok(()) + } +} + +impl SiaDecodable for u8 { + fn decode(r: &mut R) -> Result { + let mut buf = [0; 1]; + r.read_exact(&mut buf)?; + Ok(buf[0]) + } +} + +impl SiaEncodable for bool { + fn encode(&self, w: &mut W) -> Result<()> { + (*self as u8).encode(w) + } +} + +impl SiaDecodable for bool { + fn decode(r: &mut R) -> Result { + let v = u8::decode(r)?; + match v { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::InvalidValue), + } + } +} + +impl SiaEncodable for [T] { + fn encode(&self, w: &mut W) -> Result<()> { + self.len().encode(w)?; + for item in self { + item.encode(w)?; + } + Ok(()) + } +} + +impl SiaEncodable for Option { + fn encode(&self, w: &mut W) -> Result<()> { + match self { + Some(v) => { + true.encode(w)?; + v.encode(w) + } + None => false.encode(w), + } + } +} + +impl SiaDecodable for Option { + fn decode(r: &mut R) -> Result { + let has_value = bool::decode(r)?; + if has_value { + Ok(Some(T::decode(r)?)) + } else { + Ok(None) + } + } +} + +macro_rules! impl_sia_numeric { + ($($t:ty),*) => { + $( + impl SiaEncodable for $t { + fn encode(&self, w: &mut W) -> Result<()> { + w.write_all(&(*self as u64).to_le_bytes())?; + Ok(()) + } + } + + impl SiaDecodable for $t { + fn decode(r: &mut R) -> Result { + let mut buf = [0u8; 8]; + r.read_exact(&mut buf)?; + Ok(u64::from_le_bytes(buf) as Self) + } + } + )* + } +} + +impl_sia_numeric!(u16, u32, usize, i16, i32, i64, u64); + +impl SiaEncodable for Vec +where + T: SiaEncodable, +{ + fn encode(&self, w: &mut W) -> Result<()> { + self.len().encode(w)?; + for item in self { + item.encode(w)?; + } + Ok(()) + } +} + +impl SiaDecodable for Vec +where + T: SiaDecodable, +{ + fn decode(r: &mut R) -> Result { + let len = usize::decode(r)?; + let mut vec = Vec::with_capacity(len); + for _ in 0..len { + vec.push(T::decode(r)?); + } + Ok(vec) + } +} + +impl SiaEncodable for String { + fn encode(&self, w: &mut W) -> Result<()> { + self.as_bytes().encode(w) + } +} + +impl SiaDecodable for String { + fn decode(r: &mut R) -> Result { + let buf = Vec::::decode(r)?; + String::from_utf8(buf).map_err(|_| Error::InvalidLength) + } +} + +impl SiaEncodable for [u8; N] { + fn encode(&self, w: &mut W) -> Result<()> { + w.write_all(self)?; + Ok(()) + } +} + +impl SiaDecodable for [u8; N] { + fn decode(r: &mut R) -> Result { + let mut arr = [0u8; N]; + r.read_exact(&mut arr)?; + Ok(arr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_roundtrip( + value: T, + expected_bytes: Vec, + ) { + let mut encoded_bytes = Vec::new(); + value + .encode(&mut encoded_bytes) + .unwrap_or_else(|e| panic!("failed to encode: {:?}", e)); + + assert_eq!( + encoded_bytes, expected_bytes, + "encoding mismatch for {:?}", + value + ); + + let mut bytes = &expected_bytes[..]; + let decoded = T::decode(&mut bytes).unwrap_or_else(|e| panic!("failed to decode: {:?}", e)); + assert_eq!(decoded, value, "decoding mismatch for {:?}", value); + + assert_eq!(bytes.len(), 0, "leftover bytes for {:?}", value); + } + + #[test] + fn test_numerics() { + test_roundtrip(1u8, vec![1]); + test_roundtrip(2u16, vec![2, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(3u32, vec![3, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(4u64, vec![4, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(5usize, vec![5, 0, 0, 0, 0, 0, 0, 0]); + test_roundtrip(-1i16, vec![255, 255, 255, 255, 255, 255, 255, 255]); + test_roundtrip(-2i32, vec![254, 255, 255, 255, 255, 255, 255, 255]); + test_roundtrip(-3i64, vec![253, 255, 255, 255, 255, 255, 255, 255]); + } + + #[test] + fn test_strings() { + test_roundtrip( + "hello".to_string(), + vec![ + 5, 0, 0, 0, 0, 0, 0, 0, // length prefix + 104, 101, 108, 108, 111, // "hello" + ], + ); + test_roundtrip( + "".to_string(), + vec![0, 0, 0, 0, 0, 0, 0, 0], // empty string length + ); + } + + #[test] + fn test_fixed_arrays() { + test_roundtrip([1u8, 2u8, 3u8], vec![1, 2, 3]); + test_roundtrip([0u8; 4], vec![0, 0, 0, 0]); + } + + #[test] + fn test_vectors() { + test_roundtrip( + vec![1u8, 2u8, 3u8], + vec![ + 3, 0, 0, 0, 0, 0, 0, 0, // length prefix + 1, 2, 3, // values + ], + ); + test_roundtrip( + vec![100u64, 200u64], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // length prefix + 100, 0, 0, 0, 0, 0, 0, 0, // 100u64 + 200, 0, 0, 0, 0, 0, 0, 0, // 200u64 + ], + ); + test_roundtrip( + vec!["a".to_string(), "bc".to_string()], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // vector length + 1, 0, 0, 0, 0, 0, 0, 0, // first string length + 97, // "a" + 2, 0, 0, 0, 0, 0, 0, 0, // second string length + 98, 99, // "bc" + ], + ); + } + + #[test] + fn test_nested() { + test_roundtrip( + vec![vec![1u8, 2u8], vec![3u8, 4u8]], + vec![ + 2, 0, 0, 0, 0, 0, 0, 0, // outer vec length + 2, 0, 0, 0, 0, 0, 0, 0, // first inner vec length + 1, 2, // first inner vec contents + 2, 0, 0, 0, 0, 0, 0, 0, // second inner vec length + 3, 4, // second inner vec contents + ], + ); + } +} diff --git a/src/lib.rs b/sia/src/lib.rs similarity index 100% rename from src/lib.rs rename to sia/src/lib.rs diff --git a/src/merkle.rs b/sia/src/merkle.rs similarity index 100% rename from src/merkle.rs rename to sia/src/merkle.rs diff --git a/src/rhp.rs b/sia/src/rhp.rs similarity index 97% rename from src/rhp.rs rename to sia/src/rhp.rs index 3f7b41f..b411e5f 100644 --- a/src/rhp.rs +++ b/sia/src/rhp.rs @@ -85,7 +85,7 @@ mod tests { assert_eq!( root, Hash256::parse_string( - "h:50ed59cecd5ed3ca9e65cec0797202091dbba45272dafa3faa4e27064eedd52c" + "50ed59cecd5ed3ca9e65cec0797202091dbba45272dafa3faa4e27064eedd52c" ) .unwrap() ); diff --git a/src/seed.rs b/sia/src/seed.rs similarity index 100% rename from src/seed.rs rename to sia/src/seed.rs diff --git a/src/signing.rs b/sia/src/signing.rs similarity index 60% rename from src/signing.rs rename to sia/src/signing.rs index 32a6bd6..c576a12 100644 --- a/src/signing.rs +++ b/sia/src/signing.rs @@ -1,13 +1,14 @@ use core::fmt; use std::time::SystemTime; +use crate::encoding::{SiaDecodable, SiaDecode, SiaEncodable, SiaEncode}; use crate::{ChainIndex, Hash256, HexParseError}; -use base64::prelude::*; use ed25519_dalek::{Signature as ED25519Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use serde::{de::Error, Deserialize, Serialize}; +use serde::de::Error; +use serde::{Deserialize, Serialize}; /// An ed25519 public key that can be used to verify a signature -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, SiaEncode, SiaDecode)] pub struct PublicKey([u8; 32]); impl PublicKey { @@ -16,14 +17,10 @@ impl PublicKey { impl Serialize for PublicKey { fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize( - &format!("{}{}", Self::PREFIX, &self.to_string()), - serializer, - ) - } else { - <[u8; 32]>::serialize(&self.0, serializer) - } + String::serialize( + &format!("{}{}", Self::PREFIX, &self.to_string()), + serializer, + ) } } @@ -32,18 +29,14 @@ impl<'de> Deserialize<'de> for PublicKey { where D: serde::Deserializer<'de>, { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let s = s.strip_prefix(Self::PREFIX).ok_or(Error::custom(format!( - "key must have prefix '{}'", - Self::PREFIX - )))?; - let mut pk = [0; 32]; - hex::decode_to_slice(s, &mut pk).map_err(|e| Error::custom(format!("{:?}", e)))?; - Ok(Self::new(pk)) - } else { - Ok(PublicKey(<[u8; 32]>::deserialize(deserializer)?)) - } + let s = String::deserialize(deserializer)?; + let s = s.strip_prefix(Self::PREFIX).ok_or(Error::custom(format!( + "key must have prefix '{}'", + Self::PREFIX + )))?; + let mut pk = [0; 32]; + hex::decode_to_slice(s, &mut pk).map_err(|e| Error::custom(format!("{:?}", e)))?; + Ok(Self::new(pk)) } } @@ -118,16 +111,12 @@ impl Drop for PrivateKey { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, SiaEncode, SiaDecode)] pub struct Signature([u8; 64]); impl Serialize for Signature { fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize(&BASE64_STANDARD.encode(self.0), serializer) - } else { - <[u8]>::serialize(&self.0, serializer) // prefixed with length - } + String::serialize(&hex::encode(self.0), serializer) } } @@ -136,22 +125,12 @@ impl<'de> Deserialize<'de> for Signature { where D: serde::Deserializer<'de>, { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let sig = BASE64_STANDARD - .decode(s) - .map_err(|e| D::Error::custom(format!("{:?}", e)))?; - if sig.len() != 64 { - return Err(D::Error::custom("Invalid signature length")); - } - Ok(Signature(sig.try_into().unwrap())) - } else { - let data = >::deserialize(deserializer)?; - if data.len() != 64 { - return Err(D::Error::custom("Invalid signature length")); - } - Ok(Signature(data.try_into().unwrap())) + let buf = hex::decode(String::deserialize(deserializer)?) + .map_err(|e| D::Error::custom(format!("{:?}", e)))?; + if buf.len() != 64 { + return Err(D::Error::custom("Invalid signature length")); } + Ok(Signature(buf.try_into().unwrap())) } } @@ -165,11 +144,6 @@ impl Signature { } pub fn parse_string(s: &str) -> Result { - let s = match s.split_once(':') { - Some((_prefix, suffix)) => suffix, - None => s, - }; - let data = hex::decode(s).map_err(HexParseError::HexError)?; if data.len() != 64 { return Err(HexParseError::InvalidLength); @@ -195,9 +169,10 @@ impl AsRef<[u8; 64]> for Signature { impl fmt::Display for Signature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "sig:{}", hex::encode(self.0)) + write!(f, "{}", hex::encode(self.0)) } } + #[derive(Default, Debug)] pub struct NetworkHardforks { pub asic_height: u64, @@ -243,16 +218,13 @@ impl SigningState { mod tests { use std::vec; - use crate::{ - encoding::{from_reader, to_bytes}, - transactions::{ - CoveredFields, FileContract, FileContractID, FileContractRevision, SiacoinInput, - SiacoinOutput, SiafundInput, SiafundOutput, StorageProof, Transaction, - TransactionSignature, - }, - unlock_conditions::UnlockConditions, - Address, Currency, Leaf, + use crate::transactions::{ + CoveredFields, FileContract, FileContractID, FileContractRevision, SiacoinInput, + SiacoinOutput, SiafundInput, SiafundOutput, StorageProof, Transaction, + TransactionSignature, }; + use crate::unlock_conditions::UnlockConditions; + use crate::{Address, Currency, Leaf}; use super::*; @@ -262,10 +234,11 @@ mod tests { let public_key = PublicKey::new(hex::decode(public_key_str).unwrap().try_into().unwrap()); // binary - let public_key_serialized = to_bytes(&public_key).unwrap(); - let public_key_deserialized: PublicKey = - from_reader(&mut &public_key_serialized[..]).unwrap(); + let mut public_key_serialized = Vec::new(); + public_key.encode(&mut public_key_serialized).unwrap(); assert_eq!(public_key_serialized, hex::decode(public_key_str).unwrap()); + let public_key_deserialized = + PublicKey::decode(&mut public_key_serialized.as_slice()).unwrap(); assert_eq!(public_key_deserialized, public_key); // json @@ -336,9 +309,8 @@ mod tests { claim_address: Address::from([0u8; 32]), }], siafund_outputs: vec![SiafundOutput { - value: Currency::new(0), + value: 0, address: Address::from([0u8; 32]), - claim_start: Currency::new(0), }], miner_fees: vec![Currency::new(0)], arbitrary_data: vec![vec![1, 2, 3]], @@ -376,108 +348,58 @@ mod tests { struct TestCase { height: u64, whole_transaction: bool, - signature: Signature, + sig_string: String, } let test_cases = [ TestCase { height: 1, whole_transaction: true, - signature: Signature([ - 234, 238, 234, 82, 152, 120, 252, 134, 26, 199, 122, 110, 100, 234, 63, 174, - 163, 91, 8, 4, 246, 85, 241, 247, 166, 72, 109, 221, 91, 16, 83, 6, 33, 236, - 123, 143, 220, 242, 58, 214, 111, 116, 5, 171, 35, 69, 214, 59, 87, 13, 56, 85, - 84, 6, 147, 151, 116, 98, 170, 70, 97, 103, 199, 4, - ]), + sig_string: "eaeeea529878fc861ac77a6e64ea3faea35b0804f655f1f7a6486ddd5b10530621ec7b8fdcf23ad66f7405ab2345d63b570d3855540693977462aa466167c704".to_string(), }, TestCase { height: 10, whole_transaction: true, - signature: Signature([ - 80, 194, 168, 49, 197, 125, 113, 187, 180, 205, 217, 28, 80, 170, 17, 159, 117, - 95, 245, 44, 133, 182, 149, 37, 77, 245, 228, 243, 251, 102, 25, 224, 181, 18, - 13, 31, 180, 96, 209, 73, 214, 13, 21, 228, 255, 248, 29, 107, 5, 186, 71, 10, - 176, 249, 214, 61, 188, 210, 216, 182, 75, 104, 129, 13, - ]), + sig_string: "50c2a831c57d71bbb4cdd91c50aa119f755ff52c85b695254df5e4f3fb6619e0b5120d1fb460d149d60d15e4fff81d6b05ba470ab0f9d63dbcd2d8b64b68810d".to_string(), }, - TestCase { + TestCase { height: 100, whole_transaction: true, - signature: Signature([ - 237, 185, 228, 3, 94, 93, 25, 75, 28, 201, 74, 170, 19, 48, 133, 12, 239, 114, - 143, 204, 146, 209, 29, 76, 12, 232, 214, 93, 122, 108, 232, 26, 218, 138, 57, - 227, 223, 58, 0, 82, 238, 69, 44, 52, 164, 63, 43, 19, 33, 123, 86, 122, 116, - 172, 10, 48, 238, 132, 227, 129, 133, 77, 148, 6, - ]), + sig_string: "edb9e4035e5d194b1cc94aaa1330850cef728fcc92d11d4c0ce8d65d7a6ce81ada8a39e3df3a0052ee452c34a43f2b13217b567a74ac0a30ee84e381854d9406".to_string(), }, TestCase { height: 1000, whole_transaction: true, - signature: Signature([ - 152, 254, 143, 177, 91, 215, 136, 166, 244, 60, 242, 79, 141, 227, 181, 117, - 86, 224, 29, 102, 248, 125, 210, 100, 157, 127, 248, 193, 226, 212, 138, 0, - 116, 104, 166, 150, 90, 149, 204, 211, 219, 144, 113, 40, 25, 126, 31, 14, 55, - 115, 85, 147, 23, 109, 97, 12, 202, 133, 151, 23, 234, 194, 253, 5, - ]), + sig_string: "98fe8fb15bd788a6f43cf24f8de3b57556e01d66f87dd2649d7ff8c1e2d48a007468a6965a95ccd3db907128197e1f0e37735593176d610cca859717eac2fd05".to_string(), }, TestCase { height: 10000, whole_transaction: true, - signature: Signature([ - 152, 254, 143, 177, 91, 215, 136, 166, 244, 60, 242, 79, 141, 227, 181, 117, - 86, 224, 29, 102, 248, 125, 210, 100, 157, 127, 248, 193, 226, 212, 138, 0, - 116, 104, 166, 150, 90, 149, 204, 211, 219, 144, 113, 40, 25, 126, 31, 14, 55, - 115, 85, 147, 23, 109, 97, 12, 202, 133, 151, 23, 234, 194, 253, 5, - ]), + sig_string: "98fe8fb15bd788a6f43cf24f8de3b57556e01d66f87dd2649d7ff8c1e2d48a007468a6965a95ccd3db907128197e1f0e37735593176d610cca859717eac2fd05".to_string(), }, TestCase { height: 1, whole_transaction: false, - signature: Signature([ - 181, 144, 210, 1, 156, 166, 8, 49, 142, 181, 56, 101, 211, 105, 252, 11, 201, - 110, 98, 25, 71, 131, 107, 123, 234, 40, 142, 178, 115, 198, 205, 108, 60, 26, - 9, 127, 170, 98, 99, 107, 25, 113, 138, 180, 229, 195, 37, 183, 36, 178, 210, - 21, 98, 217, 114, 185, 112, 100, 170, 121, 104, 207, 182, 1, - ]), + sig_string: "1d2f0cda9aafbe3ac87b0facf7fdf40c322b7413291fe63c3971f962755fe71c35e638a56eb3a26199a5dbc09244d8a2e4311fc263b34b793772e95e2b663b01".to_string(), }, TestCase { height: 10, whole_transaction: false, - signature: Signature([ - 136, 111, 242, 99, 13, 112, 234, 124, 181, 21, 23, 158, 192, 18, 187, 33, 149, - 13, 192, 196, 133, 226, 125, 225, 116, 234, 56, 179, 135, 166, 182, 9, 44, 41, - 122, 186, 233, 10, 113, 89, 3, 132, 97, 222, 23, 35, 106, 32, 233, 220, 194, - 83, 58, 200, 141, 187, 33, 205, 178, 98, 147, 149, 253, 9, - ]), + sig_string: "b0c3b86c36db9200ef6fecd31442652af277aa17859e895d8bf9ce517e93d1765a46eb13790aa6c06fa03df6d2be9032eaa965cccf79223c1fc7e8d4b8e3eb0a".to_string(), }, TestCase { height: 100, whole_transaction: false, - signature: Signature([ - 172, 255, 46, 255, 7, 203, 157, 222, 3, 90, 1, 63, 126, 149, 142, 90, 159, 179, - 94, 24, 159, 89, 48, 110, 9, 85, 249, 161, 129, 235, 104, 65, 116, 106, 139, - 241, 96, 111, 111, 185, 55, 111, 170, 177, 133, 225, 68, 113, 143, 119, 243, - 71, 130, 112, 179, 17, 20, 191, 89, 133, 69, 15, 137, 8, - ]), + sig_string: "c6e1f4fee7b9c1a141d6780e61af883a669127f5fa7569a7b69d1b88d434c0336690075b0a5048aba3be2b81751430f0229a3e6e7fcdd3c080ac7435a0c4500d".to_string(), }, TestCase { height: 1000, whole_transaction: false, - signature: Signature([ - 154, 185, 87, 199, 88, 179, 54, 250, 4, 244, 56, 175, 57, 117, 40, 183, 17, - 139, 220, 120, 68, 57, 5, 235, 114, 61, 246, 246, 67, 158, 110, 232, 5, 255, - 139, 236, 235, 76, 156, 218, 108, 110, 250, 96, 172, 78, 13, 143, 186, 221, - 207, 49, 14, 156, 193, 27, 182, 239, 101, 152, 215, 249, 55, 8, - ]), + sig_string: "c1547a6f8193329f00dd3409ef3f3d2df961cf606075eef28fe2d87ff446baa3ac7ae65e1773e40c5bef5f1d9452a6ec3d78579dea20b544c5f8f672ee96130f".to_string(), }, TestCase { height: 10000, whole_transaction: false, - signature: Signature([ - 154, 185, 87, 199, 88, 179, 54, 250, 4, 244, 56, 175, 57, 117, 40, 183, 17, - 139, 220, 120, 68, 57, 5, 235, 114, 61, 246, 246, 67, 158, 110, 232, 5, 255, - 139, 236, 235, 76, 156, 218, 108, 110, 250, 96, 172, 78, 13, 143, 186, 221, - 207, 49, 14, 156, 193, 27, 182, 239, 101, 152, 215, 249, 55, 8, - ]), + sig_string: "c1547a6f8193329f00dd3409ef3f3d2df961cf606075eef28fe2d87ff446baa3ac7ae65e1773e40c5bef5f1d9452a6ec3d78579dea20b544c5f8f672ee96130f".to_string(), }, ]; @@ -487,10 +409,7 @@ mod tests { // covered fields are either the whole transaction or all fields let covered_fields = if tc.whole_transaction { - CoveredFields { - whole_transaction: true, - ..Default::default() - } + CoveredFields::whole_transaction() } else { CoveredFields { whole_transaction: false, @@ -511,7 +430,12 @@ mod tests { let signature = unsigned_transaction .sign(&state, &covered_fields, Hash256::default(), 1, 100, &key) .unwrap(); - assert_eq!(signature.signature, tc.signature); + assert_eq!( + hex::encode(signature.signature.clone()), + tc.sig_string, + "height: {}", + tc.height + ); // manually build the sig_hash and check the signature let sig_hash = if tc.whole_transaction { @@ -523,9 +447,12 @@ mod tests { .partial_sig_hash(&state, &covered_fields) .unwrap() }; - assert!(key - .public_key() - .verify(sig_hash.as_ref(), &signature.signature)); + let sig = Signature::new(signature.signature.try_into().unwrap()); + assert!( + key.public_key().verify(sig_hash.as_ref(), &sig), + "height: {}", + tc.height + ); } } } diff --git a/src/specifier.rs b/sia/src/specifier.rs similarity index 78% rename from src/specifier.rs rename to sia/src/specifier.rs index 3a5395c..c468337 100644 --- a/src/specifier.rs +++ b/sia/src/specifier.rs @@ -1,10 +1,24 @@ +use crate::encoding::{ + SiaDecodable, SiaDecode, SiaEncodable, SiaEncode, V1SiaDecodable, V1SiaDecode, V1SiaEncodable, + V1SiaEncode, +}; use core::{fmt, str}; - use serde::{Deserialize, Serialize}; pub const SPECIFIER_SIZE: usize = 16; -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + SiaEncode, + V1SiaEncode, + V1SiaDecode, + SiaDecode, +)] pub struct Specifier([u8; SPECIFIER_SIZE]); impl Specifier { @@ -17,6 +31,17 @@ impl Specifier { } } +impl From<&str> for Specifier { + fn from(s: &str) -> Self { + let mut buf = [0; SPECIFIER_SIZE]; + let src = s.as_bytes(); + let len = src.len(); + let index = len.min(SPECIFIER_SIZE); + buf[..index].copy_from_slice(&src[..index]); + Self(buf) + } +} + impl fmt::Display for Specifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // get the last non-zero byte or 0 if all bytes are 0 @@ -48,7 +73,6 @@ macro_rules! specifier { $crate::specifier::Specifier::new(buf) }}; } - pub(crate) use specifier; #[cfg(test)] diff --git a/sia/src/spendpolicy.rs b/sia/src/spendpolicy.rs new file mode 100644 index 0000000..407cc6a --- /dev/null +++ b/sia/src/spendpolicy.rs @@ -0,0 +1,624 @@ +use crate::encoding::{self, SiaDecodable, SiaDecode, SiaEncodable, SiaEncode}; +use crate::signing::{PublicKey, Signature}; +#[allow(deprecated)] +use crate::unlock_conditions::UnlockConditions; +use crate::{Address, Hash256}; +use blake2b_simd::Params; +use core::fmt; +use serde::de::{self, MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use thiserror::Error; +use time::OffsetDateTime; + +const POLICY_ABOVE_PREFIX: u8 = 1; +const POLICY_AFTER_PREFIX: u8 = 2; +const POLICY_PUBLIC_KEY_PREFIX: u8 = 3; +const POLICY_HASH_PREFIX: u8 = 4; +const POLICY_THRESHOLD_PREFIX: u8 = 5; +const POLICY_OPAQUE_PREFIX: u8 = 6; +#[deprecated] +const POLICY_UNLOCK_CONDITIONS_PREFIX: u8 = 7; + +const POLICY_ABOVE_STR: &str = "above"; +const POLICY_AFTER_STR: &str = "after"; +const POLICY_PUBLIC_KEY_STR: &str = "pk"; +const POLICY_HASH_STR: &str = "h"; +const POLICY_THRESHOLD_STR: &str = "thresh"; +const POLICY_OPAQUE_STR: &str = "opaque"; +#[deprecated] +const POLICY_UNLOCK_CONDITIONS_STR: &str = "uc"; + +#[derive(Debug, PartialEq, Error)] +pub enum ValidationError { + #[error("opaque policy")] + OpaquePolicy, + #[error("invalid policy")] + InvalidPolicy, + #[error("invalid signature")] + InvalidSignature, + #[error("invalid preimage")] + InvalidPreimage, + #[error("invalid height")] + InvalidHeight, + #[error("invalid timestamp")] + InvalidTimestamp, + #[error("missing signature")] + MissingSignature, + #[error("missing preimage")] + MissingPreimage, + #[error("threshold not met")] + ThresholdNotMet, +} + +/// A spend policy is a condition or set of conditions that must be met in +/// order to spend a UTXO. +#[derive(Debug, PartialEq, Clone)] +pub enum SpendPolicy { + /// A policy that is only valid after a block height + Above(u64), + /// A policy that is only valid after a timestamp + After(OffsetDateTime), + /// A policy that requires a valid signature from an ed25519 key pair + PublicKey(PublicKey), + /// A policy that requires a valid SHA256 hash preimage + Hash(Hash256), + /// A threshold policy that requires n-of-m sub-policies to be met + Threshold(u8, Vec), + /// An opaque policy that is not directly spendable + Opaque(Address), + + /// A set of v1 unlock conditions for compatibility with v1 transactions + #[deprecated] + UnlockConditions(UnlockConditions), +} + +impl SpendPolicy { + fn type_prefix(&self) -> u8 { + match self { + SpendPolicy::Above(_) => POLICY_ABOVE_PREFIX, + SpendPolicy::After(_) => POLICY_AFTER_PREFIX, + SpendPolicy::PublicKey(_) => POLICY_PUBLIC_KEY_PREFIX, + SpendPolicy::Hash(_) => POLICY_HASH_PREFIX, + SpendPolicy::Threshold(_, _) => POLICY_THRESHOLD_PREFIX, + SpendPolicy::Opaque(_) => POLICY_OPAQUE_PREFIX, + #[allow(deprecated)] + SpendPolicy::UnlockConditions(_) => POLICY_UNLOCK_CONDITIONS_PREFIX, + } + } + + fn type_str(&self) -> &str { + match self { + SpendPolicy::Above(_) => POLICY_ABOVE_STR, + SpendPolicy::After(_) => POLICY_AFTER_STR, + SpendPolicy::PublicKey(_) => POLICY_PUBLIC_KEY_STR, + SpendPolicy::Hash(_) => POLICY_HASH_STR, + SpendPolicy::Threshold(_, _) => POLICY_THRESHOLD_STR, + SpendPolicy::Opaque(_) => POLICY_OPAQUE_STR, + #[allow(deprecated)] + SpendPolicy::UnlockConditions(_) => POLICY_UNLOCK_CONDITIONS_STR, + } + } + + /// Create a policy that is only valid after a certain block height + pub fn above(height: u64) -> Self { + Self::Above(height) + } + + /// Create a policy that is only valid after a certain timestamp + pub fn after(timestamp: OffsetDateTime) -> Self { + Self::After(timestamp) + } + + /// Create a policy that requires a valid signature from a public key + pub fn public_key(pk: PublicKey) -> Self { + Self::PublicKey(pk) + } + + /// Create a policy that requires a hash preimage + pub fn hash(hash: Hash256) -> Self { + Self::Hash(hash) + } + + /// Create a threshold policy with n-of-m sub-policies + pub fn threshold(n: u8, policies: Vec) -> Self { + for policy in policies.iter() { + #[allow(deprecated)] + if let SpendPolicy::UnlockConditions(_) = policy { + panic!("UnlockConditions are not allowed in a threshold policy"); + } + } + Self::Threshold(n, policies) + } + + /// Create a v1 unlock conditions policy for compatibility with v1 + /// transactions. + #[deprecated] + pub fn unlock_conditions(uc: UnlockConditions) -> Self { + #[allow(deprecated)] + Self::UnlockConditions(uc) + } + + /// Returns the address of the policy. This is a hash of the policy that + /// can be used to receive funds. + pub fn address(&self) -> Address { + #[allow(deprecated)] + if let SpendPolicy::UnlockConditions(uc) = self { + return uc.address(); + } else if let SpendPolicy::Opaque(addr) = self { + return addr.clone(); + } + + let mut state = Params::new().hash_length(32).to_state(); + state.update("sia/address|".as_bytes()); + + if let SpendPolicy::Threshold(n, of) = self { + let mut opaque = Vec::with_capacity(of.len()); + for policy in of { + opaque.push(SpendPolicy::Opaque(policy.address())) + } + SpendPolicy::Threshold(*n, opaque) + .encode(&mut state) + .unwrap(); + } else { + self.encode(&mut state).unwrap(); + } + Address::from(state.finalize().as_bytes()) + } +} + +impl<'de> Deserialize<'de> for SpendPolicy { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SpendPolicyVisitor; + + impl<'de> Visitor<'de> for SpendPolicyVisitor { + type Value = SpendPolicy; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a spend policy") + } + + // json encoding + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut policy_type: Option = None; + let mut policy_value: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "type" => { + policy_type = Some(map.next_value()?); + } + "policy" => { + policy_value = Some(map.next_value()?); + } + _ => return Err(de::Error::unknown_field(&key, &["type", "policy"])), + } + } + + let policy_type = policy_type.ok_or_else(|| de::Error::missing_field("type"))?; + let policy_value = + policy_value.ok_or_else(|| de::Error::missing_field("policy"))?; + + match policy_type.as_str() { + POLICY_ABOVE_STR => { + let height = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + Ok(SpendPolicy::Above(height)) + } + POLICY_AFTER_STR => { + let unix_seconds: u64 = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + let timestamp = OffsetDateTime::from_unix_timestamp(unix_seconds as i64) + .map_err(|_| de::Error::custom("invalid timestamp"))?; + Ok(SpendPolicy::After(timestamp)) + } + POLICY_PUBLIC_KEY_STR => { + let pk: PublicKey = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + Ok(SpendPolicy::PublicKey(pk)) + } + POLICY_HASH_STR => { + let hash: Hash256 = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + Ok(SpendPolicy::Hash(hash)) + } + POLICY_THRESHOLD_STR => { + #[derive(Deserialize)] + struct ThreshPolicy { + n: u8, + of: Vec, + } + let thresh: ThreshPolicy = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + Ok(SpendPolicy::Threshold(thresh.n, thresh.of)) + } + POLICY_OPAQUE_STR => { + let addr: Address = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + Ok(SpendPolicy::Opaque(addr)) + } + #[allow(deprecated)] + POLICY_UNLOCK_CONDITIONS_STR => { + #[allow(deprecated)] + let uc: UnlockConditions = + serde_json::from_value(policy_value).map_err(de::Error::custom)?; + #[allow(deprecated)] + Ok(SpendPolicy::UnlockConditions(uc)) + } + _ => Err(de::Error::unknown_variant( + &policy_type, + #[allow(deprecated)] + &[ + POLICY_ABOVE_STR, + POLICY_AFTER_STR, + POLICY_PUBLIC_KEY_STR, + POLICY_HASH_STR, + POLICY_THRESHOLD_STR, + POLICY_OPAQUE_STR, + POLICY_UNLOCK_CONDITIONS_STR, + ], + )), + } + } + } + + deserializer.deserialize_map(SpendPolicyVisitor) + } +} + +impl Serialize for SpendPolicy { + fn serialize(&self, serializer: S) -> Result { + let mut state = serializer.serialize_struct("SpendPolicy", 2)?; + state.serialize_field("type", self.type_str())?; + match self { + SpendPolicy::Above(height) => { + state.serialize_field("policy", height)?; + } + SpendPolicy::After(time) => { + let unix_seconds = time.unix_timestamp() as u64; + state.serialize_field("policy", &unix_seconds)?; + } + SpendPolicy::PublicKey(pk) => { + state.serialize_field("policy", &pk)?; + } + SpendPolicy::Hash(hash) => { + state.serialize_field("policy", &hash)?; + } + SpendPolicy::Threshold(n, policies) => { + state.serialize_field( + "policy", + &json!({ + "n": n, + "of": policies, + }), + )?; + } + SpendPolicy::Opaque(addr) => { + state.serialize_field("policy", addr)?; + } + #[allow(deprecated)] + SpendPolicy::UnlockConditions(uc) => { + state.serialize_field("policy", uc)?; + } + } + state.end() + } +} + +impl SiaEncodable for SpendPolicy { + fn encode(&self, w: &mut W) -> encoding::Result<()> { + // helper to recursively encode policies + fn encode_policy( + policy: &SpendPolicy, + w: &mut W, + ) -> encoding::Result<()> { + w.write_all(&[policy.type_prefix()])?; + match policy { + SpendPolicy::Above(height) => height.encode(w), + SpendPolicy::After(time) => (time.unix_timestamp() as u64).encode(w), + SpendPolicy::PublicKey(pk) => pk.encode(w), + SpendPolicy::Hash(hash) => hash.encode(w), + SpendPolicy::Threshold(of, policies) => { + of.encode(w)?; + (policies.len() as u8).encode(w)?; + for policy in policies { + encode_policy(policy, w)?; + } + Ok(()) + } + SpendPolicy::Opaque(addr) => addr.encode(w), + #[allow(deprecated)] + SpendPolicy::UnlockConditions(uc) => uc.encode(w), + } + } + 1u8.encode(w)?; + encode_policy(self, w) + } +} + +impl SiaDecodable for SpendPolicy { + fn decode(r: &mut R) -> encoding::Result { + // helper to recursively decode policies + fn decode_policy(r: &mut R) -> encoding::Result { + let policy_type = u8::decode(r)?; + match policy_type { + POLICY_ABOVE_PREFIX => Ok(SpendPolicy::Above(u64::decode(r)?)), + POLICY_AFTER_PREFIX => { + let unix_seconds = u64::decode(r)?; + let timestamp: OffsetDateTime = + OffsetDateTime::from_unix_timestamp(unix_seconds as i64).map_err(|_| { + encoding::Error::Custom("invalid timestamp".to_string()) + })?; + Ok(SpendPolicy::After(timestamp)) + } + POLICY_PUBLIC_KEY_PREFIX => Ok(SpendPolicy::PublicKey(PublicKey::decode(r)?)), + POLICY_HASH_PREFIX => Ok(SpendPolicy::Hash(Hash256::decode(r)?)), + POLICY_THRESHOLD_PREFIX => { + let of: u8 = u8::decode(r)?; + let n = u8::decode(r)?; + let mut policies = Vec::with_capacity(n as usize); + while policies.len() < n as usize { + policies.push(decode_policy(r)?); + } + Ok(SpendPolicy::Threshold(of, policies)) + } + POLICY_OPAQUE_PREFIX => Ok(SpendPolicy::Opaque(Address::decode(r)?)), + #[allow(deprecated)] + POLICY_UNLOCK_CONDITIONS_PREFIX => { + Ok(SpendPolicy::UnlockConditions(UnlockConditions::decode(r)?)) + } + _ => Err(encoding::Error::Custom("invalid policy type".to_string())), + } + } + let policy_version = u8::decode(r)?; + if policy_version != 1 { + return Err(encoding::Error::Custom( + "invalid policy version".to_string(), + )); + } + decode_policy(r) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, SiaEncode, SiaDecode)] +/// A policy that has been satisfied by a set of preimages and signatures. +pub struct SatisfiedPolicy { + pub policy: SpendPolicy, + pub preimages: Vec>, + pub signatures: Vec, +} + +impl SatisfiedPolicy { + /// Create a new satisfied policy from a policy, preimages, and signatures. + pub fn new(policy: SpendPolicy, preimages: Vec>, signatures: Vec) -> Self { + Self { + policy, + preimages, + signatures, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_address() { + let test_cases = vec![ + ( + SpendPolicy::PublicKey(PublicKey::new([ + 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ])), + "55a7793237722c6df8222fd512063cb74228085ef1805c5184713648c159b919ac792fbad0e1", + ), + ( + SpendPolicy::Above(100), + "c2fba9b9607c800e80d9284ed0fb9a55737ba1bbd67311d0d9242dd6376bed0c6ee355e814fa", + ), + ( + SpendPolicy::After(OffsetDateTime::from_unix_timestamp(1433600000).unwrap()), + "5bdb96e33ffdf72619ad38bee57ad4db9eb242aeb2ee32020ba16179af5d46d501bd2011806b", + ), + ( + SpendPolicy::Hash(Hash256::from([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ])), + "1cc0fc4cde659333cf7e61971cc5025c5a6b4759c9d1c1d438227c3eb57d841512d4cd4ce620", + ), + ( + SpendPolicy::Threshold( + 2, + vec![ + SpendPolicy::PublicKey(PublicKey::new([ + 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ])), + SpendPolicy::Above(100), + SpendPolicy::Threshold( + 2, + vec![ + SpendPolicy::PublicKey(PublicKey::new([0; 32])), + SpendPolicy::After( + OffsetDateTime::from_unix_timestamp(1433600000).unwrap(), + ), + ], + ), + ], + ), + "30f516630280059c25ae92f3bf3c451be258ecd3249c43906e3d9dd9e86f2dc00ef5eeffc2c4", + ), + ]; + + for (policy, expected) in test_cases { + assert_eq!(policy.address().to_string(), expected); + } + } + + #[test] + fn test_opaque_policy() { + let test_cases = vec![ + SpendPolicy::above(100), + SpendPolicy::after(OffsetDateTime::from_unix_timestamp(100).unwrap()), + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::hash(Hash256::from([0; 32])), + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::above(100), + ], + ), + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::above(100), + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::after(OffsetDateTime::from_unix_timestamp(100).unwrap()), + ], + ), + SpendPolicy::PublicKey(PublicKey::new([1; 32])), + ], + ), + ]; + + for (i, policy) in test_cases.into_iter().enumerate() { + let policy = policy.clone(); + let address = policy.address(); + let expected_address = address.to_string(); + let opaque = SpendPolicy::Opaque(address); + assert_eq!( + opaque.address().to_string(), + expected_address, + "test case {}", + i + ); + + if let SpendPolicy::Threshold(n, of) = policy { + // test that the address of opaque threshold policies is the + // same as the address of normal threshold policies + for j in 0..of.len() { + let mut of = of.clone(); + of[j] = SpendPolicy::Opaque(of[j].address()); + let opaque_policy = SpendPolicy::threshold(n, of); + + assert_eq!( + opaque_policy.address().to_string(), + expected_address, + "test case {}-{}", + i, + j + ); + } + } + } + } + + #[test] + fn test_policy_encoding() { + let test_cases = vec![ + ( + SpendPolicy::above(100), + "{\"type\":\"above\",\"policy\":100}", + "01016400000000000000", + ), + ( + SpendPolicy::after(OffsetDateTime::from_unix_timestamp(100).unwrap()), + "{\"type\":\"after\",\"policy\":100}", + "01026400000000000000" + ), + ( + SpendPolicy::public_key(PublicKey::new([1; 32])), + "{\"type\":\"pk\",\"policy\":\"ed25519:0101010101010101010101010101010101010101010101010101010101010101\"}", + "01030101010101010101010101010101010101010101010101010101010101010101", + ), + ( + SpendPolicy::hash(Hash256::from([0; 32])), + "{\"type\":\"h\",\"policy\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "01040000000000000000000000000000000000000000000000000000000000000000", + ), + ( + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::above(100), + ], + ), + "{\"type\":\"thresh\",\"policy\":{\"n\":2,\"of\":[{\"policy\":\"ed25519:0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"pk\"},{\"policy\":100,\"type\":\"above\"}]}}", + "01050202030000000000000000000000000000000000000000000000000000000000000000016400000000000000", + ), + ( + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::above(100), + SpendPolicy::threshold( + 2, + vec![ + SpendPolicy::public_key(PublicKey::new([0; 32])), + SpendPolicy::after(OffsetDateTime::from_unix_timestamp(100).unwrap()), + ], + ), + SpendPolicy::PublicKey(PublicKey::new([0; 32])), + ], + ), + "{\"type\":\"thresh\",\"policy\":{\"n\":2,\"of\":[{\"policy\":\"ed25519:0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"pk\"},{\"policy\":100,\"type\":\"above\"},{\"policy\":{\"n\":2,\"of\":[{\"policy\":\"ed25519:0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"pk\"},{\"policy\":100,\"type\":\"after\"}]},\"type\":\"thresh\"},{\"policy\":\"ed25519:0000000000000000000000000000000000000000000000000000000000000000\",\"type\":\"pk\"}]}}", + "01050204030000000000000000000000000000000000000000000000000000000000000000016400000000000000050202030000000000000000000000000000000000000000000000000000000000000000026400000000000000030000000000000000000000000000000000000000000000000000000000000000", + ), + ( + #[allow(deprecated)] + SpendPolicy::UnlockConditions(UnlockConditions { + timelock: 100, + signatures_required: 2, + public_keys: vec![ + PublicKey::new([0; 32]).into(), + PublicKey::new([1; 32]).into(), + ], + }), + "{\"type\":\"uc\",\"policy\":{\"timelock\":100,\"publicKeys\":[\"ed25519:0000000000000000000000000000000000000000000000000000000000000000\",\"ed25519:0101010101010101010101010101010101010101010101010101010101010101\"],\"signaturesRequired\":2}}", + "010764000000000000000200000000000000656432353531390000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000065643235353139000000000000000000200000000000000001010101010101010101010101010101010101010101010101010101010101010200000000000000", + ) + ]; + + for (i, (policy, json, binary)) in test_cases.iter().enumerate() { + let serialized_json = serde_json::to_string(&policy) + .unwrap_or_else(|e| panic!("failed to serialize json in test case {}: {}", i, e)); + assert_eq!(serialized_json, *json, "test case {}", i); + let deserialized_json: SpendPolicy = serde_json::from_str(json) + .unwrap_or_else(|e| panic!("failed to deserialize json in test case {}: {}", i, e)); + assert_eq!(deserialized_json, *policy, "test case {}", i); + + let mut serialized_binary = Vec::new(); + policy + .encode(&mut serialized_binary) + .unwrap_or_else(|e| panic!("failed to serialize binary in test case {}: {}", i, e)); + assert_eq!( + hex::encode(serialized_binary.clone()), + *binary, + "test case {}", + i + ); + + let deserialized_binary = SpendPolicy::decode(&mut &serialized_binary[..]) + .unwrap_or_else(|e| { + panic!("failed to deserialize binary in test case {}: {}", i, e) + }); + assert_eq!(deserialized_binary, *policy, "test case {}", i); + } + } +} diff --git a/sia/src/transactions.rs b/sia/src/transactions.rs new file mode 100644 index 0000000..0d36056 --- /dev/null +++ b/sia/src/transactions.rs @@ -0,0 +1,938 @@ +use crate::encoding::{ + SiaDecodable, SiaDecode, SiaEncodable, SiaEncode, V1SiaDecodable, V1SiaDecode, V1SiaEncodable, + V1SiaEncode, +}; +use crate::signing::{PrivateKey, SigningState}; +use crate::specifier::{specifier, Specifier}; +use crate::unlock_conditions::UnlockConditions; +use crate::{encoding, Address, Currency, Hash256, ImplHashID, Leaf}; +use blake2b_simd::Params; +use serde::{Deserialize, Serialize}; + +ImplHashID!(SiacoinOutputID); +ImplHashID!(SiafundOutputID); +ImplHashID!(FileContractID); +ImplHashID!(TransactionID); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct SiacoinInput { + #[serde(rename = "parentID")] + pub parent_id: SiacoinOutputID, + pub unlock_conditions: UnlockConditions, +} + +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, SiaEncode, SiaDecode, V1SiaEncode, V1SiaDecode, +)] +#[serde(rename_all = "camelCase")] +pub struct SiacoinOutput { + pub value: Currency, + pub address: Address, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct SiafundInput { + #[serde(rename = "parentID")] + pub parent_id: SiafundOutputID, + pub unlock_conditions: UnlockConditions, + pub claim_address: Address, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SiaEncode, SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct SiafundOutput { + pub value: u64, + pub address: Address, +} + +impl V1SiaEncodable for SiafundOutput { + fn encode_v1(&self, w: &mut W) -> encoding::Result<()> { + Currency::new(self.value as u128).encode_v1(w)?; + self.address.encode_v1(w)?; + Currency::new(0).encode_v1(w) // siad encodes a "claim start," but transactions its an error if it's non-zero. + } +} + +impl V1SiaDecodable for SiafundOutput { + fn decode_v1(r: &mut R) -> encoding::Result { + let se = SiafundOutput { + value: Currency::decode_v1(r)? + .try_into() + .map_err(|_| encoding::Error::Custom("invalid value".to_string()))?, + address: Address::decode_v1(r)?, + }; + Currency::decode_v1(r)?; // ignore claim start + Ok(se) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct FileContract { + #[serde(rename = "filesize")] + pub file_size: u64, + pub file_merkle_root: Hash256, + pub window_start: u64, + pub window_end: u64, + pub payout: Currency, + pub valid_proof_outputs: Vec, + pub missed_proof_outputs: Vec, + pub unlock_hash: Hash256, + pub revision_number: u64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct FileContractRevision { + #[serde(rename = "parentID")] + pub parent_id: FileContractID, + pub unlock_conditions: UnlockConditions, + pub revision_number: u64, + #[serde(rename = "filesize")] + pub file_size: u64, + pub file_merkle_root: Hash256, + pub window_start: u64, + pub window_end: u64, + pub valid_proof_outputs: Vec, + pub missed_proof_outputs: Vec, + pub unlock_hash: Hash256, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct StorageProof { + #[serde(rename = "parentID")] + pub parent_id: FileContractID, + pub leaf: Leaf, + pub proof: Vec, +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct CoveredFields { + pub whole_transaction: bool, + pub siacoin_inputs: Vec, + pub siacoin_outputs: Vec, + pub file_contracts: Vec, + pub file_contract_revisions: Vec, + pub storage_proofs: Vec, + pub siafund_inputs: Vec, + pub siafund_outputs: Vec, + pub miner_fees: Vec, + pub arbitrary_data: Vec, + pub signatures: Vec, +} + +impl CoveredFields { + pub fn whole_transaction() -> Self { + CoveredFields { + whole_transaction: true, + ..Default::default() + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct TransactionSignature { + #[serde(rename = "parentID")] + pub parent_id: Hash256, + pub public_key_index: u64, + pub timelock: u64, + pub covered_fields: CoveredFields, + #[serde(with = "base64")] + pub signature: Vec, +} + +#[derive(Default, Debug, PartialEq, Serialize, Deserialize, V1SiaEncode, V1SiaDecode)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + pub siacoin_inputs: Vec, + pub siacoin_outputs: Vec, + pub file_contracts: Vec, + pub file_contract_revisions: Vec, + pub storage_proofs: Vec, + pub siafund_inputs: Vec, + pub siafund_outputs: Vec, + pub miner_fees: Vec, + pub arbitrary_data: Vec>, + pub signatures: Vec, +} + +impl Transaction { + const SIACOIN_OUTPUT_ID_PREFIX: Specifier = specifier!("siacoin output"); + const SIAFUND_OUTPUT_ID_PREFIX: Specifier = specifier!("siafund output"); + + pub fn encode_no_sigs(&self, w: &mut W) -> Result<(), encoding::Error> { + self.siacoin_inputs.encode_v1(w)?; + self.siacoin_outputs.encode_v1(w)?; + self.file_contracts.encode_v1(w)?; + self.file_contract_revisions.encode_v1(w)?; + self.storage_proofs.encode_v1(w)?; + self.siafund_inputs.encode_v1(w)?; + self.siafund_outputs.encode_v1(w)?; + self.miner_fees.encode_v1(w)?; + self.arbitrary_data.encode_v1(w) + } + + pub(crate) fn whole_sig_hash( + &self, + chain: &SigningState, + parent_id: &Hash256, + public_key_index: u64, + timelock: u64, + covered_sigs: &Vec, + ) -> Result { + let mut state = Params::new().hash_length(32).to_state(); + + state.update(&(self.siacoin_inputs.len() as u64).to_le_bytes()); + for input in self.siacoin_inputs.iter() { + state.update(chain.replay_prefix()); + input.encode_v1(&mut state)?; + } + + self.siacoin_outputs.encode_v1(&mut state)?; + self.file_contracts.encode_v1(&mut state)?; + self.file_contract_revisions.encode_v1(&mut state)?; + self.storage_proofs.encode_v1(&mut state)?; + + state.update(&(self.siafund_inputs.len() as u64).to_le_bytes()); + for input in self.siafund_inputs.iter() { + state.update(chain.replay_prefix()); + input.encode_v1(&mut state)?; + } + + self.siafund_outputs.encode_v1(&mut state)?; + self.miner_fees.encode_v1(&mut state)?; + self.arbitrary_data.encode_v1(&mut state)?; + + parent_id.encode_v1(&mut state)?; + public_key_index.encode_v1(&mut state)?; + timelock.encode_v1(&mut state)?; + + for &i in covered_sigs { + if i >= self.signatures.len() { + return Err(encoding::Error::Custom( + "signatures index out of bounds".to_string(), + )); + } + self.signatures[i].encode_v1(&mut state)?; + } + + Ok(state.finalize().into()) + } + + pub(crate) fn partial_sig_hash( + &self, + chain: &SigningState, + covered_fields: &CoveredFields, + ) -> Result { + let mut state = Params::new().hash_length(32).to_state(); + + for &i in covered_fields.siacoin_inputs.iter() { + if i >= self.siacoin_inputs.len() { + return Err(encoding::Error::Custom( + "siacoin_inputs index out of bounds".to_string(), + )); + } + state.update(chain.replay_prefix()); + self.siacoin_inputs[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.siacoin_outputs.iter() { + if i >= self.siacoin_outputs.len() { + return Err(encoding::Error::Custom( + "siacoin_outputs index out of bounds".to_string(), + )); + } + self.siacoin_outputs[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.file_contracts.iter() { + if i >= self.file_contracts.len() { + return Err(encoding::Error::Custom( + "file_contracts index out of bounds".to_string(), + )); + } + self.file_contracts[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.file_contract_revisions.iter() { + if i >= self.file_contract_revisions.len() { + return Err(encoding::Error::Custom( + "file_contract_revisions index out of bounds".to_string(), + )); + } + self.file_contract_revisions[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.storage_proofs.iter() { + if i >= self.storage_proofs.len() { + return Err(encoding::Error::Custom( + "storage_proofs index out of bounds".to_string(), + )); + } + self.storage_proofs[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.siafund_inputs.iter() { + if i >= self.siafund_inputs.len() { + return Err(encoding::Error::Custom( + "siafund_inputs index out of bounds".to_string(), + )); + } + state.update(chain.replay_prefix()); + self.siafund_inputs[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.siafund_outputs.iter() { + if i >= self.siafund_outputs.len() { + return Err(encoding::Error::Custom( + "siafund_outputs index out of bounds".to_string(), + )); + } + self.siafund_outputs[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.miner_fees.iter() { + if i >= self.miner_fees.len() { + return Err(encoding::Error::Custom( + "miner_fees index out of bounds".to_string(), + )); + } + self.miner_fees[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.arbitrary_data.iter() { + if i >= self.arbitrary_data.len() { + return Err(encoding::Error::Custom( + "arbitrary_data index out of bounds".to_string(), + )); + } + self.arbitrary_data[i].encode_v1(&mut state)?; + } + + for &i in covered_fields.signatures.iter() { + if i >= self.signatures.len() { + return Err(encoding::Error::Custom( + "signatures index out of bounds".to_string(), + )); + } + self.signatures[i].encode_v1(&mut state)?; + } + Ok(state.finalize().into()) + } + + pub fn sign( + &self, + state: &SigningState, + covered_fields: &CoveredFields, + parent_id: Hash256, + public_key_index: u64, + timelock: u64, + private_key: &PrivateKey, + ) -> Result { + let sig_hash = if covered_fields.whole_transaction { + self.whole_sig_hash( + state, + &parent_id, + public_key_index, + timelock, + &covered_fields.signatures, + ) + } else { + self.partial_sig_hash(state, covered_fields) + }?; + + Ok(TransactionSignature { + parent_id, + public_key_index, + timelock, + covered_fields: covered_fields.clone(), + signature: private_key.sign(sig_hash.as_ref()).data().to_vec(), + }) + } + + pub fn id(&self) -> TransactionID { + let mut state = Params::new().hash_length(32).to_state(); + self.encode_no_sigs(&mut state).unwrap(); + let hash = state.finalize(); + hash.into() + } + + pub fn siacoin_output_id(&self, i: usize) -> SiacoinOutputID { + let mut state = Params::new().hash_length(32).to_state(); + + state.update(Self::SIACOIN_OUTPUT_ID_PREFIX.as_bytes()); + self.encode_no_sigs(&mut state).unwrap(); + + let h = state.update(&i.to_le_bytes()).finalize(); + SiacoinOutputID::from(h) + } + + pub fn siafund_output_id(&self, i: usize) -> SiafundOutputID { + let mut state = Params::new().hash_length(32).to_state(); + + state.update(Self::SIAFUND_OUTPUT_ID_PREFIX.as_bytes()); + self.encode_no_sigs(&mut state).unwrap(); + + let h = state.update(&i.to_le_bytes()).finalize(); + SiafundOutputID::from(h) + } +} + +// Create a helper module for base64 serialization +mod base64 { + use base64::engine::general_purpose::STANDARD; + use base64::Engine; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(v: &[u8], s: S) -> Result { + let base64 = STANDARD.encode(v); + s.serialize_str(&base64) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let base64 = String::deserialize(d)?; + STANDARD + .decode(base64.as_bytes()) + .map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signing::{NetworkHardforks, PublicKey, Signature}; + use crate::{BlockID, ChainIndex}; + use serde::de::DeserializeOwned; + use std::fmt::Debug; + use std::time::SystemTime; + use std::vec; + + /// test_serialize_json is a helper to test serialization and deserialization of a struct to and from JSON. + fn test_serialize_json( + obj: &S, + json_str: &str, + ) { + let serialized = serde_json::to_string(&obj).unwrap(); + assert_eq!(serialized, json_str); + let deserialized: S = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, *obj); + } + + /// test_serialize_v1 is a helper to test serialization and deserialization of a struct to and from Sia's + /// custom binary encoding. + fn test_serialize_v1( + obj: &S, + hex_binary: &str, + ) { + let mut serialized = Vec::new(); + obj.encode_v1(&mut serialized).unwrap(); + assert_eq!(hex::encode(serialized.clone()), hex_binary); + let deserialized = S::decode_v1(&mut &serialized[..]).unwrap(); + assert_eq!(deserialized, *obj); + } + + /// test_serialize is a helper to test serialization and deserialization of a struct to and from Sia's + /// custom binary encoding. + fn test_serialize( + obj: &S, + hex_binary: &str, + ) { + let mut serialized = Vec::new(); + obj.encode(&mut serialized).unwrap(); + assert_eq!(hex::encode(serialized.clone()), hex_binary); + let deserialized = S::decode(&mut &serialized[..]).unwrap(); + assert_eq!(deserialized, *obj); + } + + #[test] + fn test_serialize_covered_fields() { + let mut cf = CoveredFields::default(); + cf.siacoin_inputs.push(1); + cf.siacoin_outputs.push(2); + cf.siacoin_outputs.push(3); + + let binary_str = "000100000000000000010000000000000002000000000000000200000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + test_serialize_v1(&cf, binary_str); + + let json_str = "{\"wholeTransaction\":false,\"siacoinInputs\":[1],\"siacoinOutputs\":[2,3],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]}"; + test_serialize_json(&cf, json_str); + } + + #[test] + fn test_serialize_siacoin_input() { + let siacoin_input = SiacoinInput { + parent_id: SiacoinOutputID::parse_string( + "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", + ) + .unwrap(), + unlock_conditions: UnlockConditions::new( + 123, + vec![PublicKey::new([ + 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, + 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, + 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, + ]) + .into()], + 1, + ), + }; + + let binary_str = hex::encode([ + 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, 158, + 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 123, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, + 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, 253, 16, 121, 168, 198, 200, 123, 71, 218, 29, + 86, 126, 53, 185, 114, 52, 153, 60, 40, 140, 26, 208, 219, 29, 28, 225, 182, 1, 0, 0, + 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&siacoin_input, binary_str.as_str()); + + let json_str = "{\"parentID\":\"b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1}}"; + test_serialize_json(&siacoin_input, json_str); + } + + #[test] + fn test_serialize_siafund_input() { + let siafund_input = SiafundInput { + parent_id: SiafundOutputID::parse_string( + "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", + ) + .unwrap(), + unlock_conditions: UnlockConditions::new( + 123, + vec![PublicKey::new([ + 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, + 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, + 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, + ]) + .into()], + 1, + ), + claim_address: Address::new( + hex::decode("8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8c") + .unwrap() + .try_into() + .unwrap(), + ), + }; + + // binary + let binary_str = hex::encode([ + 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, 158, + 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 123, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, + 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, 253, 16, 121, 168, 198, 200, 123, 71, 218, 29, + 86, 126, 53, 185, 114, 52, 153, 60, 40, 140, 26, 208, 219, 29, 28, 225, 182, 1, 0, 0, + 0, 0, 0, 0, 0, 143, 180, 156, 207, 23, 223, 220, 201, 82, 109, 236, 110, 232, 165, 204, + 162, 15, 248, 36, 115, 2, 5, 61, 55, 119, 65, 11, 155, 4, 148, 186, 140, + ]); + test_serialize_v1(&siafund_input, binary_str.as_str()); + + let json_str = "{\"parentID\":\"b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1},\"claimAddress\":\"8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8cdf32abee86f0\"}"; + test_serialize_json(&siafund_input, json_str); + } + + #[test] + fn test_serialize_siacoin_output() { + let addr_str = + "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69"; + let output = SiacoinOutput { + value: Currency::new(67856467336433871), + address: Address::parse_string(addr_str).unwrap(), + }; + + let v1_binary_str = hex::encode([ + 7, 0, 0, 0, 0, 0, 0, 0, 241, 19, 24, 247, 77, 16, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&output, v1_binary_str.as_str()); + + let v2_binary_str = "cf104df71813f10000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + test_serialize(&output, v2_binary_str); + + let json_str = format!( + "{{\"value\":\"67856467336433871\",\"address\":\"{}\"}}", + addr_str + ); + test_serialize_json(&output, json_str.as_str()); + } + + #[test] + fn test_serialize_transaction_signature() { + let signature = TransactionSignature { + parent_id: Hash256::parse_string( + "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", + ) + .unwrap(), + public_key_index: 1, + timelock: 2, + covered_fields: CoveredFields { + whole_transaction: true, + ..Default::default() + }, + signature: Signature::new([3u8; 64]).data().to_vec(), + }; + + let binary_str = hex::encode([ + 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, 158, + 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 64, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + ]); + test_serialize_v1(&signature, binary_str.as_str()); + + let json_str = "{\"parentID\":\"b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"publicKeyIndex\":1,\"timelock\":2,\"coveredFields\":{\"wholeTransaction\":true,\"siacoinInputs\":[],\"siacoinOutputs\":[],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]},\"signature\":\"AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw==\"}"; + test_serialize_json(&signature, json_str); + } + + #[test] + fn test_serialize_siafund_output() { + let addr_str = + "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69"; + let output = SiafundOutput { + value: 67856467336433871, + address: Address::parse_string(addr_str).unwrap(), + }; + + let v1_binary_str = hex::encode([ + 7, 0, 0, 0, 0, 0, 0, 0, 241, 19, 24, 247, 77, 16, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&output, v1_binary_str.as_str()); + + let v2_binary_str = + "cf104df71813f1000000000000000000000000000000000000000000000000000000000000000000"; + test_serialize(&output, v2_binary_str); + + let json_str = format!( + "{{\"value\":67856467336433871,\"address\":\"{}\"}}", + addr_str + ); + test_serialize_json(&output, json_str.as_str()); + } + + #[test] + fn test_serialize_filecontract() { + let contract = FileContract { + file_size: 1, + file_merkle_root: Hash256::from([ + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + window_start: 2, + window_end: 3, + payout: Currency::new(456), + valid_proof_outputs: vec![SiacoinOutput { + value: Currency::new(789), + address: Address::new([ + 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]), + }], + missed_proof_outputs: vec![SiacoinOutput { + value: Currency::new(101112), + address: Address::new([ + 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]), + }], + unlock_hash: Hash256::from([ + 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + revision_number: 4, + }; + + let binary_str = hex::encode([ + 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, + 0, 0, 0, 0, 0, 0, 1, 200, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 21, 2, 2, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 138, 248, 3, 3, 3, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, + 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&contract, binary_str.as_str()); + + let json_str = "{\"filesize\":1,\"fileMerkleRoot\":\"0101010000000000000000000000000000000000000000000000000000000000\",\"windowStart\":2,\"windowEnd\":3,\"payout\":\"456\",\"validProofOutputs\":[{\"value\":\"789\",\"address\":\"02020200000000000000000000000000000000000000000000000000000000008749787b31db\"}],\"missedProofOutputs\":[{\"value\":\"101112\",\"address\":\"0303030000000000000000000000000000000000000000000000000000000000c596d559a239\"}],\"unlockHash\":\"0404040000000000000000000000000000000000000000000000000000000000\",\"revisionNumber\":4}"; + test_serialize_json(&contract, json_str); + } + + #[test] + fn test_serialize_filecontract_revision() { + let revision = FileContractRevision { + parent_id: FileContractID::from([ + 9, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + file_size: 1, + file_merkle_root: Hash256::from([ + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + window_start: 2, + window_end: 3, + valid_proof_outputs: vec![SiacoinOutput { + value: Currency::new(789), + address: Address::new([ + 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]), + }], + missed_proof_outputs: vec![SiacoinOutput { + value: Currency::new(789), + address: Address::new([ + 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]), + }], + unlock_conditions: UnlockConditions::new( + 123, + vec![PublicKey::new([ + 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, + 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, + 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, + ]) + .into()], + 1, + ), + unlock_hash: Hash256::from([ + 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + revision_number: 4, + }; + + let binary_str = hex::encode([ + 9, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, 49, + 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, 253, 16, + 121, 168, 198, 200, 123, 71, 218, 29, 86, 126, 53, 185, 114, 52, 153, 60, 40, 140, 26, + 208, 219, 29, 28, 225, 182, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 21, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, + 0, 0, 3, 21, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&revision, binary_str.as_str()); + + let json_str = "{\"parentID\":\"0908070000000000000000000000000000000000000000000000000000000000\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1},\"revisionNumber\":4,\"filesize\":1,\"fileMerkleRoot\":\"0101010000000000000000000000000000000000000000000000000000000000\",\"windowStart\":2,\"windowEnd\":3,\"validProofOutputs\":[{\"value\":\"789\",\"address\":\"02020200000000000000000000000000000000000000000000000000000000008749787b31db\"}],\"missedProofOutputs\":[{\"value\":\"789\",\"address\":\"0303030000000000000000000000000000000000000000000000000000000000c596d559a239\"}],\"unlockHash\":\"0404040000000000000000000000000000000000000000000000000000000000\"}"; + test_serialize_json(&revision, json_str); + } + + #[test] + fn test_serialize_storage_proof() { + let storage_proof = StorageProof { + parent_id: FileContractID::parse_string( + "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", + ) + .unwrap(), + leaf: Leaf::from([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + ]), + proof: vec![ + Hash256::parse_string( + "0102030000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + Hash256::parse_string( + "0405060000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + ], + }; + + let binary_str = hex::encode([ + 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, 158, + 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&storage_proof, binary_str.as_str()); + + let json_str = "{\"parentID\":\"b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"leaf\":\"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40\",\"proof\":[\"0102030000000000000000000000000000000000000000000000000000000000\",\"0405060000000000000000000000000000000000000000000000000000000000\"]}"; + test_serialize_json(&storage_proof, json_str); + } + + #[test] + fn test_transaction_id() { + let txn = Transaction::default(); + let id = txn.id(); + assert_eq!( + hex::encode(id), + "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24" + ); + } + + #[test] + fn test_whole_sig_hash() { + let state = SigningState { + index: ChainIndex { + height: 0, + id: BlockID::default(), + }, + median_timestamp: SystemTime::now(), + hardforks: NetworkHardforks { + asic_height: 0, + foundation_height: 0, + v2_allow_height: 1000, + v2_require_height: 1000, + }, + }; + let pk = PrivateKey::from_seed(&[ + 136, 215, 58, 248, 45, 30, 78, 97, 128, 111, 82, 204, 43, 233, 223, 111, 110, 29, 73, + 157, 52, 25, 242, 96, 131, 16, 187, 22, 232, 107, 17, 205, + ]); + let test_cases = vec![ + ( + Transaction { + siacoin_inputs: vec![ + SiacoinInput{ + parent_id: SiacoinOutputID::from([32,11,215,36,166,174,135,0,92,215,179,18,74,229,52,154,221,194,213,216,219,47,225,205,251,84,248,2,69,252,37,117]), + unlock_conditions: UnlockConditions::standard_unlock_conditions(pk.public_key()), + } + ], + siacoin_outputs: vec![ + SiacoinOutput{ + value: Currency::new(67856467336433871), + address: Address::parse_string("000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69").unwrap(), + } + ], + file_contracts: Vec::new(), + file_contract_revisions: Vec::new(), + storage_proofs: Vec::new(), + siafund_inputs: Vec::new(), + siafund_outputs: Vec::new(), + miner_fees: Vec::new(), + arbitrary_data: Vec::new(), + signatures: Vec::new(), + }, + "a4b1855c546db7ec902237f730717faae96187db8ce9fe139504323a639f731e" + ) + ]; + + for (txn, expected) in test_cases { + let sig_hash = txn + .whole_sig_hash( + &state, + &Hash256::from(>::into( + txn.siacoin_inputs[0].parent_id, + )), + 0, + 0, + &vec![], + ) + .expect("expect tranasction to hash"); + + assert_eq!(sig_hash.to_string(), expected) + } + } + + #[test] + fn test_transaction_sign() { + let state = SigningState { + index: ChainIndex { + height: 0, + id: BlockID::default(), + }, + median_timestamp: SystemTime::now(), + hardforks: NetworkHardforks { + asic_height: 0, + foundation_height: 0, + v2_allow_height: 1000, + v2_require_height: 1000, + }, + }; + let pk = PrivateKey::from_seed(&[ + 136, 215, 58, 248, 45, 30, 78, 97, 128, 111, 82, 204, 43, 233, 223, 111, 110, 29, 73, + 157, 52, 25, 242, 96, 131, 16, 187, 22, 232, 107, 17, 205, + ]); + let test_cases = vec![ + ( + Transaction { + siacoin_inputs: vec![ + SiacoinInput{ + parent_id: SiacoinOutputID::from([32,11,215,36,166,174,135,0,92,215,179,18,74,229,52,154,221,194,213,216,219,47,225,205,251,84,248,2,69,252,37,117]), + unlock_conditions: UnlockConditions::standard_unlock_conditions(pk.public_key()), + } + ], + siacoin_outputs: vec![ + SiacoinOutput{ + value: Currency::new(67856467336433871), + address: Address::parse_string("000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69").unwrap(), + } + ], + file_contracts: Vec::new(), + file_contract_revisions: Vec::new(), + storage_proofs: Vec::new(), + siafund_inputs: Vec::new(), + siafund_outputs: Vec::new(), + miner_fees: Vec::new(), + arbitrary_data: Vec::new(), + signatures: Vec::new(), + }, + "7a5db98318b5ecad2954d41ba2084c908823ebf4000b95543f352478066a0d04bf4829e3e6d086b42ff9d943f68981c479798fd42bf6f63dac254f4294a37609" + ) + ]; + + for (txn, expected) in test_cases { + let sig = txn + .sign( + &state, + &CoveredFields::whole_transaction(), + Hash256::from(>::into( + txn.siacoin_inputs[0].parent_id, + )), + 0, + 0, + &pk, + ) + .expect(""); + + assert_eq!(hex::encode(sig.signature), expected) + } + } + + #[test] + fn test_serialize_transaction() { + let transaction = Transaction { + siacoin_inputs: Vec::new(), + siacoin_outputs: Vec::new(), + file_contracts: Vec::new(), + file_contract_revisions: Vec::new(), + storage_proofs: Vec::new(), + siafund_inputs: Vec::new(), + siafund_outputs: Vec::new(), + miner_fees: Vec::new(), + arbitrary_data: Vec::new(), + signatures: Vec::new(), + }; + let binary_str = hex::encode([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + test_serialize_v1(&transaction, binary_str.as_str()); + + let json_str = "{\"siacoinInputs\":[],\"siacoinOutputs\":[],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]}"; + test_serialize_json(&transaction, json_str); + } +} diff --git a/src/unlock_conditions.rs b/sia/src/unlock_conditions.rs similarity index 61% rename from src/unlock_conditions.rs rename to sia/src/unlock_conditions.rs index 7664b9f..861259e 100644 --- a/src/unlock_conditions.rs +++ b/sia/src/unlock_conditions.rs @@ -1,32 +1,31 @@ -use crate::encoding::to_writer; +use crate::encoding::{ + SiaDecodable, SiaDecode, SiaEncodable, SiaEncode, V1SiaDecodable, V1SiaDecode, V1SiaEncodable, + V1SiaEncode, +}; use crate::merkle::{Accumulator, LEAF_HASH_PREFIX}; use crate::signing::PublicKey; use crate::specifier::{specifier, Specifier}; -use crate::Address; -use crate::HexParseError; +use crate::{Address, HexParseError}; use blake2b_simd::Params; -#[deprecated] use core::fmt; use serde::de::Error; use serde::{Deserialize, Serialize}; +pub const ALGORITHM_ED25519: Specifier = specifier!["ed25519"]; + /// A generic public key that can be used to spend a utxo or revise a file /// contract /// /// Currently only supports ed25519 keys -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, SiaEncode, V1SiaEncode, SiaDecode, V1SiaDecode)] pub struct UnlockKey { - algorithm: Algorithm, - public_key: PublicKey, + pub algorithm: Specifier, + pub key: Vec, } impl Serialize for UnlockKey { fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize(&self.to_string(), serializer) - } else { - <(Algorithm, &[u8])>::serialize(&(self.algorithm, self.public_key.as_ref()), serializer) - } + String::serialize(&self.to_string(), serializer) } } @@ -39,124 +38,43 @@ impl<'de> Deserialize<'de> for UnlockKey { let s = String::deserialize(deserializer)?; UnlockKey::parse_string(&s).map_err(|e| Error::custom(format!("{:?}", e))) } else { - let (algorithm, raw_key) = <(Algorithm, Vec)>::deserialize(deserializer)?; - Ok(Self { - algorithm, - public_key: PublicKey::new( - raw_key - .try_into() - .map_err(|e| Error::custom(format!("Invalid key: {:?}", e)))?, - ), - }) + let (algorithm, key) = <(Specifier, Vec)>::deserialize(deserializer)?; + Ok(Self { algorithm, key }) } } } impl fmt::Display for UnlockKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}", self.algorithm, self.public_key) - } -} - -/// An enum representing algorithms supported for signing and verifying -/// a v1 unlock key -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Algorithm(Specifier); - -impl Algorithm { - const ED25519_SPECIFIER: Specifier = specifier!("ed25519"); - - /// Returns the corresponding Specifier for the Algorithm - pub fn as_specifier(&self) -> Specifier { - self.0 - } - - pub fn ed25519() -> Algorithm { - Algorithm(Self::ED25519_SPECIFIER) - } -} - -impl fmt::Display for Algorithm { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.as_specifier().fmt(f) - } -} - -impl Serialize for Algorithm { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let spec: Specifier = self.as_specifier(); - if serializer.is_human_readable() { - String::serialize(&self.to_string(), serializer) - } else { - spec.serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for Algorithm { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "ed25519" => Ok(Algorithm::ed25519()), - _ => Err(Error::custom("Invalid algorithm")), - } - } else { - let spec = Specifier::deserialize(deserializer)?; - match spec { - Self::ED25519_SPECIFIER => Ok(Algorithm::ed25519()), - _ => Err(Error::custom("Invalid algorithm")), - } - } + write!(f, "{}:{}", self.algorithm, hex::encode(self.key.as_slice())) } } impl UnlockKey { - /// Creates a new UnlockKey - pub fn new(algorithm: Algorithm, public_key: PublicKey) -> UnlockKey { - UnlockKey { - algorithm, - public_key, - } - } - /// Parses an UnlockKey from a string /// The string should be in the format "algorithm:public_key" pub fn parse_string(s: &str) -> Result { let (prefix, key_str) = s.split_once(':').ok_or(HexParseError::MissingPrefix)?; - let algorithm = match prefix { - "ed25519" => Algorithm::ed25519(), - _ => return Err(HexParseError::InvalidPrefix), - }; - - let mut data = [0u8; 32]; - hex::decode_to_slice(key_str, &mut data).map_err(HexParseError::HexError)?; Ok(UnlockKey { - algorithm, - public_key: PublicKey::new(data), + algorithm: Specifier::from(prefix), + key: hex::decode(key_str).map_err(HexParseError::HexError)?, }) } - - // Returns the public key of the UnlockKey - pub fn public_key(&self) -> PublicKey { - self.public_key - } } impl From for UnlockKey { fn from(val: PublicKey) -> Self { - UnlockKey::new(Algorithm::ed25519(), val) + UnlockKey { + algorithm: ALGORITHM_ED25519, + key: val.as_ref().to_vec(), + } } } // specifies the conditions for spending an output or revising a file contract. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive( + Debug, PartialEq, Clone, Serialize, Deserialize, SiaEncode, SiaDecode, V1SiaEncode, V1SiaDecode, +)] #[serde(rename_all = "camelCase")] pub struct UnlockConditions { pub timelock: u64, @@ -180,16 +98,17 @@ impl UnlockConditions { pub fn standard_unlock_conditions(public_key: PublicKey) -> UnlockConditions { UnlockConditions { timelock: 0, - public_keys: vec![UnlockKey::new(Algorithm::ed25519(), public_key)], + public_keys: vec![public_key.into()], signatures_required: 1, } } pub fn address(&self) -> Address { let mut acc = Accumulator::new(); + let mut p = Params::new(); + p.hash_length(32); - let h = Params::new() - .hash_length(32) + let h = p .to_state() .update(LEAF_HASH_PREFIX) .update(&self.timelock.to_le_bytes()) @@ -200,9 +119,9 @@ impl UnlockConditions { acc.add_leaf(&leaf); for key in self.public_keys.iter() { - let mut state = Params::new().hash_length(32).to_state(); + let mut state = p.to_state(); state.update(LEAF_HASH_PREFIX); - to_writer(&mut state, key).unwrap(); + key.encode(&mut state).unwrap(); let h = state.finalize(); let mut leaf = [0u8; 32]; @@ -210,8 +129,7 @@ impl UnlockConditions { acc.add_leaf(&leaf); } - let h = Params::new() - .hash_length(32) + let h = p .to_state() .update(LEAF_HASH_PREFIX) .update(&self.signatures_required.to_le_bytes()) @@ -228,42 +146,20 @@ impl UnlockConditions { #[cfg(test)] mod tests { use super::*; - use crate::encoding::{from_reader, to_bytes}; use crate::seed::Seed; - #[test] - fn test_sia_serialize_algorithm() { - let algorithm = Algorithm::ed25519(); - let bytes = to_bytes(&algorithm).unwrap(); - let expected: [u8; 16] = [ - b'e', b'd', b'2', b'5', b'5', b'1', b'9', 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - assert_eq!(bytes, expected); - } - - #[test] - fn test_json_serialize_algorithm() { - assert_eq!( - serde_json::to_string(&Algorithm::ed25519()).unwrap(), - "\"ed25519\"" - ) - } - #[test] fn test_serialize_unlock_key() { - let unlock_key = UnlockKey::new( - Algorithm::ed25519(), - PublicKey::new([ - 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, 0xda, - 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, 0xd0, 0xdb, - 0x1d, 0x1c, 0xe1, 0xb6, - ]), - ); + let unlock_key: UnlockKey = PublicKey::new([ + 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, 0xda, + 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, 0xd0, 0xdb, + 0x1d, 0x1c, 0xe1, 0xb6, + ]) + .into(); // binary - let unlock_key_serialized = to_bytes(&unlock_key).unwrap(); - let unlock_key_deserialized: UnlockKey = - from_reader(&mut &unlock_key_serialized[..]).unwrap(); + let mut unlock_key_serialized: Vec = Vec::new(); + unlock_key.encode(&mut unlock_key_serialized).unwrap(); assert_eq!( unlock_key_serialized, [ @@ -273,7 +169,7 @@ mod tests { 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6 ] ); - assert_eq!(unlock_key_deserialized, unlock_key); + //assert_eq!(unlock_key_deserialized, unlock_key); // json let unlock_key_serialized = serde_json::to_string(&unlock_key).unwrap(); @@ -290,21 +186,21 @@ mod tests { fn test_serialize_unlock_conditions() { let unlock_conditions = UnlockConditions::new( 123, - vec![UnlockKey::new( - Algorithm::ed25519(), - PublicKey::new([ - 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, - 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, - 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, - ]), - )], + vec![PublicKey::new([ + 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, 0x47, 0xda, + 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, 0x8c, 0x1a, 0xd0, 0xdb, + 0x1d, 0x1c, 0xe1, 0xb6, + ]) + .into()], 1, ); // binary - let unlock_conditions_serialized = to_bytes(&unlock_conditions).unwrap(); - let unlock_conditions_deserialized: UnlockConditions = - from_reader(&mut &unlock_conditions_serialized[..]).unwrap(); + let mut unlock_conditions_serialized: Vec = Vec::new(); + unlock_conditions + .encode(&mut unlock_conditions_serialized) + .unwrap(); + assert_eq!( unlock_conditions_serialized, [ @@ -314,7 +210,7 @@ mod tests { 26, 208, 219, 29, 28, 225, 182, 1, 0, 0, 0, 0, 0, 0, 0 ] ); - assert_eq!(unlock_conditions_deserialized, unlock_conditions); + //assert_eq!(unlock_conditions_deserialized, unlock_conditions); // json let unlock_conditions_serialized = serde_json::to_string(&unlock_conditions).unwrap(); @@ -418,37 +314,37 @@ mod tests { .unwrap(), ), ( - "addr:c4850dbcddb9dfac6f44007ec58fe824bc58e3de2432de478f3e53f7965c2afd7ea651b6c2bf", + "c4850dbcddb9dfac6f44007ec58fe824bc58e3de2432de478f3e53f7965c2afd7ea651b6c2bf", hex::decode("6f5c23f8797f93d3d3c689fe1a3f5d9a1fbf326a7a6ea51fecbeaa9aba46f180") .unwrap(), ), ( - "addr:6a8f4f1d5a7405aa24cb1fb2a3c1dcaae74175c712002627289b5cd9dd887088afe605460abd", + "6a8f4f1d5a7405aa24cb1fb2a3c1dcaae74175c712002627289b5cd9dd887088afe605460abd", hex::decode("45f12760f6005a93cece248f5ec78adf15f9d29dafe397c8c28fefc72781d6fb") .unwrap(), ), ( - "addr:e464b9b1c9282d8edeed5832b95405761db6dacf6a156fc9119a396bdc8f8892815c7dce20fd", + "e464b9b1c9282d8edeed5832b95405761db6dacf6a156fc9119a396bdc8f8892815c7dce20fd", hex::decode("1c12d17a2a8b2c25950872f312d5d0758f07d8357c98897fc472565a44b3d1f1") .unwrap(), ), ( - "addr:9ae839af434aa13de6e8baa280541716811dcbaa33165fea5e9bad0c33998c10f16fcac4f214", + "9ae839af434aa13de6e8baa280541716811dcbaa33165fea5e9bad0c33998c10f16fcac4f214", hex::decode("686d28bf7e4b4cadf759994caed1e52092e12c11cef257a265b50402dbd70c3b") .unwrap(), ), ( - "addr:e92722d80103af9574f19a6cf72aab424335927eb7da022455f53314e3587dc8ece40d254981", + "e92722d80103af9574f19a6cf72aab424335927eb7da022455f53314e3587dc8ece40d254981", hex::decode("b2e9ddef40897219a997ae7af277a5550cc10c54e793b6d2146de94df3bd552b") .unwrap(), ), ( - "addr:e2a02510f242f35e46b8840d8da42c087ea906b09d8e454c734663650236977da0362dd2ab43", + "e2a02510f242f35e46b8840d8da42c087ea906b09d8e454c734663650236977da0362dd2ab43", hex::decode("4f756e475a706cdcec8eb1c02b21a591e0c0450cc0408ae8aec82ae97f634ecf") .unwrap(), ), ( - "addr:8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8cdf32abee86f0", + "8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8cdf32abee86f0", hex::decode("cd46b523d2ee92f205a00726d8544094bb4fe58142ecffd20ea32b37b6e6bfc3") .unwrap(), ), @@ -462,8 +358,8 @@ mod tests { assert_eq!(addr, expected); // test string round-trip - if !expected_str.starts_with("addr:") { - assert_eq!(addr.to_string(), "addr:".to_string() + expected_str) + if !expected_str.starts_with("") { + assert_eq!(addr.to_string(), "".to_string() + expected_str) } else { assert_eq!(addr.to_string(), expected_str) } @@ -475,20 +371,98 @@ mod tests { const PHRASE: &str = "song renew capable taxi follow sword more hybrid laptop dance unfair poem"; let test_addresses = vec![ - (0, Address::parse_string("addr:16e09f8dc8a100a03ba1f9503e4035661738d1bea0b6cdc9bb012d3cd25edaacfd780909e550").unwrap()), - (1, Address::parse_string("addr:cb016a7018485325fa299bc247113e3792dbea27ee08d2bb57a16cb0804fa449d3a91ee647a1").unwrap()), - (2, Address::parse_string("addr:5eb70f141387df1e2ecd434b22be50bff57a6e08484f3890fe4415a6d323b5e9e758b4f79b34").unwrap()), - (3, Address::parse_string("addr:c3bc7bc1431460ed2556874cb63714760120125da758ebbd78198534cb3d25774352fdbb3e8b").unwrap()), - (4, Address::parse_string("addr:ebc7eae02ecf76e3ba7312bab6b6f71e9d255801a3a3b83f7cc26bd520b2c27a511cd8604e4b").unwrap()), - (5, Address::parse_string("addr:fce241a44b944b10f414782dd35f5d96b92aec3d6da92a45ae44b7dc8cfb4b4ba97a34ce7032").unwrap()), - (6, Address::parse_string("addr:36d253e7c3af2213eccaf0a61c6d24be8668f72af6e773463f3c41efc8bb70f2b353b90de9dd").unwrap()), - (7, Address::parse_string("addr:c8f85375fb264428c86594863440f856db1da4614d75f4a30e3d9db3dfc88af6995128c6a845").unwrap()), - (8, Address::parse_string("addr:85ef2ba14ee464060570b16bddaac91353961e7545067ccdf868a0ece305f00d2c08ec6844c6").unwrap()), - (9, Address::parse_string("addr:9dcf644245eba91e7ea70c47ccadf479e6834c1c1221335e7246e0a6bc40e18362c4faa760b8").unwrap()), - (4294967295, Address::parse_string("addr:a906891f0c524fd272a905aa5dd7018c69e5d68222385cbd9d5292f38f021ce4bf00953a0659").unwrap()), - (4294967296, Address::parse_string("addr:b6ab338e624a304add7afe205361ac71821b87559a3b9c5b3735eaafa914eed533613a0af7fa").unwrap()), - (18446744073709551615, Address::parse_string("addr:832d0e8b5f967677d812d75559c373d930ad16eb90c31c29982a190bb7db9edf9438fd827938").unwrap()), - ]; + ( + 0, + Address::parse_string( + "16e09f8dc8a100a03ba1f9503e4035661738d1bea0b6cdc9bb012d3cd25edaacfd780909e550", + ) + .unwrap(), + ), + ( + 1, + Address::parse_string( + "cb016a7018485325fa299bc247113e3792dbea27ee08d2bb57a16cb0804fa449d3a91ee647a1", + ) + .unwrap(), + ), + ( + 2, + Address::parse_string( + "5eb70f141387df1e2ecd434b22be50bff57a6e08484f3890fe4415a6d323b5e9e758b4f79b34", + ) + .unwrap(), + ), + ( + 3, + Address::parse_string( + "c3bc7bc1431460ed2556874cb63714760120125da758ebbd78198534cb3d25774352fdbb3e8b", + ) + .unwrap(), + ), + ( + 4, + Address::parse_string( + "ebc7eae02ecf76e3ba7312bab6b6f71e9d255801a3a3b83f7cc26bd520b2c27a511cd8604e4b", + ) + .unwrap(), + ), + ( + 5, + Address::parse_string( + "fce241a44b944b10f414782dd35f5d96b92aec3d6da92a45ae44b7dc8cfb4b4ba97a34ce7032", + ) + .unwrap(), + ), + ( + 6, + Address::parse_string( + "36d253e7c3af2213eccaf0a61c6d24be8668f72af6e773463f3c41efc8bb70f2b353b90de9dd", + ) + .unwrap(), + ), + ( + 7, + Address::parse_string( + "c8f85375fb264428c86594863440f856db1da4614d75f4a30e3d9db3dfc88af6995128c6a845", + ) + .unwrap(), + ), + ( + 8, + Address::parse_string( + "85ef2ba14ee464060570b16bddaac91353961e7545067ccdf868a0ece305f00d2c08ec6844c6", + ) + .unwrap(), + ), + ( + 9, + Address::parse_string( + "9dcf644245eba91e7ea70c47ccadf479e6834c1c1221335e7246e0a6bc40e18362c4faa760b8", + ) + .unwrap(), + ), + ( + 4294967295, + Address::parse_string( + "a906891f0c524fd272a905aa5dd7018c69e5d68222385cbd9d5292f38f021ce4bf00953a0659", + ) + .unwrap(), + ), + ( + 4294967296, + Address::parse_string( + "b6ab338e624a304add7afe205361ac71821b87559a3b9c5b3735eaafa914eed533613a0af7fa", + ) + .unwrap(), + ), + ( + 18446744073709551615, + Address::parse_string( + "832d0e8b5f967677d812d75559c373d930ad16eb90c31c29982a190bb7db9edf9438fd827938", + ) + .unwrap(), + ), + ]; let seed = Seed::from_mnemonic(PHRASE).unwrap(); for (i, expected) in test_addresses { diff --git a/sia_sdk_derive/Cargo.toml b/sia_sdk_derive/Cargo.toml new file mode 100644 index 0000000..ae20da4 --- /dev/null +++ b/sia_sdk_derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sia_sdk_derive" +version = "0.0.1" +edition = "2021" +repository = "https://github.com/SiaFoundation/sia-sdk-rs" +license = "MIT" +description = "Macro implementations for sia-sdk" +authors = ["The Sia Foundation"] +categories = ["cryptography::cryptocurrencies"] +keywords = ["sia", "decentralized", "blockchain", "depin", "storage"] + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0" +quote = "1.0" +proc-macro2 = "1.0" \ No newline at end of file diff --git a/sia_sdk_derive/src/lib.rs b/sia_sdk_derive/src/lib.rs new file mode 100644 index 0000000..50b7088 --- /dev/null +++ b/sia_sdk_derive/src/lib.rs @@ -0,0 +1,184 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +#[proc_macro_derive(SiaEncode)] +pub fn derive_sia_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let encode_impl = match &input.data { + Data::Struct(data) => { + let fields = match &data.fields { + Fields::Named(fields) => { + let encodes = fields.named.iter().map(|f| { + let name = &f.ident; + quote! { self.#name.encode(w)?; } + }); + quote! { #(#encodes)* } + } + Fields::Unnamed(fields) => { + let encodes = fields.unnamed.iter().enumerate().map(|(i, _)| { + let index = syn::Index::from(i); + quote! { self.#index.encode(w)?; } + }); + quote! { #(#encodes)* } + } + Fields::Unit => quote! {}, + }; + quote! { + #fields + Ok(()) + } + } + Data::Enum(_) => panic!("enums not supported"), + Data::Union(_) => panic!("unions not supported"), + }; + + let expanded = quote! { + impl SiaEncodable for #name { + fn encode(&self, w: &mut W) -> crate::encoding::Result<()> { + #encode_impl + } + } + }; + + TokenStream::from(expanded) +} + +#[proc_macro_derive(SiaDecode)] +pub fn derive_sia_decode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let decode_impl = match &input.data { + Data::Struct(data) => { + let fields = match &data.fields { + Fields::Named(fields) => { + let decodes = fields.named.iter().map(|f| { + let name = &f.ident; + let ty = &f.ty; + quote! { #name: <#ty>::decode(r)?, } + }); + quote! { + Ok(Self { + #(#decodes)* + }) + } + } + Fields::Unnamed(fields) => { + let decodes = fields.unnamed.iter().map(|f| { + let ty = &f.ty; + quote! { <#ty>::decode(r)?, } + }); + quote! { + Ok(Self(#(#decodes)*)) + } + } + Fields::Unit => quote! { Ok(Self) }, + }; + fields + } + Data::Enum(_) => panic!("enums not supported"), + Data::Union(_) => panic!("unions not supported"), + }; + + let expanded = quote! { + impl SiaDecodable for #name { + fn decode(r: &mut R) -> crate::encoding::Result { + #decode_impl + } + } + }; + TokenStream::from(expanded) +} + +#[proc_macro_derive(V1SiaEncode)] +pub fn derive_v1_sia_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let encode_impl = match &input.data { + Data::Struct(data) => { + let fields = match &data.fields { + Fields::Named(fields) => { + let encodes = fields.named.iter().map(|f| { + let name = &f.ident; + quote! { self.#name.encode_v1(enc)?; } + }); + quote! { #(#encodes)* } + } + Fields::Unnamed(fields) => { + let encodes = fields.unnamed.iter().enumerate().map(|(i, _)| { + let index = syn::Index::from(i); + quote! { self.#index.encode_v1(enc)?; } + }); + quote! { #(#encodes)* } + } + Fields::Unit => quote! {}, + }; + quote! { + #fields + Ok(()) + } + } + Data::Enum(_) => panic!("enums not supported"), + Data::Union(_) => panic!("unions not supported"), + }; + + let expanded = quote! { + impl V1SiaEncodable for #name { + fn encode_v1(&self, enc: &mut W) -> crate::encoding::Result<()> { + #encode_impl + } + } + }; + TokenStream::from(expanded) +} + +#[proc_macro_derive(V1SiaDecode)] +pub fn derive_v1_sia_decode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let decode_impl = match &input.data { + Data::Struct(data) => { + let fields = match &data.fields { + Fields::Named(fields) => { + let decodes = fields.named.iter().map(|f| { + let name = &f.ident; + let ty = &f.ty; + quote! { #name: <#ty>::decode_v1(r)?, } + }); + quote! { + Ok(Self { + #(#decodes)* + }) + } + } + Fields::Unnamed(fields) => { + let decodes = fields.unnamed.iter().map(|f| { + let ty = &f.ty; + quote! { <#ty>::decode_v1(r)?, } + }); + quote! { + Ok(Self(#(#decodes)*)) + } + } + Fields::Unit => quote! { Ok(Self) }, + }; + fields + } + Data::Enum(_) => panic!("enums not supported"), + Data::Union(_) => panic!("unions not supported"), + }; + + let expanded = quote! { + impl V1SiaDecodable for #name { + fn decode_v1(r: &mut R) -> crate::encoding::Result { + #decode_impl + } + } + }; + TokenStream::from(expanded) +} diff --git a/src/encoding/deserializer.rs b/src/encoding/deserializer.rs deleted file mode 100644 index 949029e..0000000 --- a/src/encoding/deserializer.rs +++ /dev/null @@ -1,430 +0,0 @@ -use std::{fmt::Display, vec}; - -use serde::{de, Deserialize}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Custom(String), - - #[error("Failed to convert unsigned integer")] - ConversionFailed(#[from] std::num::TryFromIntError), - - #[error("Deserializing type '{0}' is not supported")] - UnsupportedType(&'static str), - - #[error("The Sia encoding is not a self-describing format and does therefore not support deserialize_any")] - DeserializeAnyUnavailable, - - #[error("The value {0} is invalid for a bool")] - InvalidBoolValue(u8), - - #[error("Failed to read from io::read")] - IO(#[from] std::io::Error), -} - -// Implement de::Error for Error -impl de::Error for Error { - fn custom(msg: T) -> Self - where - T: Display, - { - Error::Custom(msg.to_string()) - } -} - -#[allow(dead_code)] -pub fn from_reader<'a, T, R>(reader: &'a mut R) -> Result -where - T: Deserialize<'a>, - R: std::io::Read, -{ - T::deserialize(&mut Deserializer { reader }) -} - -impl<'a, R> Deserializer<'a, R> -where - R: std::io::Read, -{ - fn parse_bool(&mut self) -> Result { - let mut b: [u8; 1] = [0; 1]; - self.reader.read_exact(&mut b)?; - match b[0] { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(Error::InvalidBoolValue(b[0])), - } - } - - fn parse_u64(&mut self) -> Result { - let mut b: [u8; 8] = [0; 8]; - self.reader.read_exact(&mut b)?; - Ok(u64::from_le_bytes(b)) - } -} - -pub struct Deserializer<'de, R> -where - R: std::io::Read, -{ - reader: &'de mut R, -} - -impl<'de, 'a, R> de::Deserializer<'de> for &'a mut Deserializer<'de, R> -where - R: std::io::Read, -{ - type Error = Error; - - fn is_human_readable(&self) -> bool { - false - } - - // deserialize_any is not available for the Sia format since it's not - // self-describing - fn deserialize_any(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::DeserializeAnyUnavailable) - } - - fn deserialize_bool(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_bool(self.parse_bool()?) - } - - fn deserialize_i8(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("i8")) - } - - fn deserialize_i16(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("i16")) - } - - fn deserialize_i32(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("i32")) - } - - fn deserialize_i64(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("i64")) - } - - fn deserialize_u8(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - let mut v: [u8; 1] = [0; 1]; - self.reader.read_exact(&mut v)?; - visitor.visit_u8(v[0]) - } - - fn deserialize_u16(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_u16(self.parse_u64()? as u16) - } - - fn deserialize_u32(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_u32(self.parse_u64()? as u32) - } - - fn deserialize_u64(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_u64(self.parse_u64()?) - } - - fn deserialize_f32(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("f32")) - } - - fn deserialize_f64(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("f64")) - } - - fn deserialize_char(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("char")) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - self.deserialize_byte_buf(visitor) - } - - fn deserialize_string(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - self.deserialize_byte_buf(visitor) - } - - fn deserialize_bytes(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - self.deserialize_byte_buf(visitor) - } - - fn deserialize_byte_buf(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - let len = self.parse_u64()? as usize; - let mut buf = vec![0u8; len]; - self.reader.read_exact(&mut buf)?; - visitor.visit_byte_buf(buf) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - let b = self.parse_bool()?; - if b { - visitor.visit_some(self) - } else { - visitor.visit_none() - } - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, - _name: &'static str, - visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_newtype_struct( - self, - _name: &'static str, - visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - let len = self.parse_u64()? as usize; - visitor.visit_seq(PrefixedSeqAccess { - de: self, - remaining: len, - }) - } - - fn deserialize_tuple(self, len: usize, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_seq(PrefixedSeqAccess { - de: self, - remaining: len, - }) - } - - fn deserialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - _visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("tuple_struct")) - } - - fn deserialize_map(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("map")) - } - - fn deserialize_struct( - self, - _name: &'static str, - fields: &'static [&'static str], - visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - let len = fields.len(); - visitor.visit_seq(PrefixedSeqAccess { - remaining: len, - de: self, - }) - } - - fn deserialize_enum( - self, - _name: &'static str, - _variants: &'static [&'static str], - _visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("enum")) - } - - fn deserialize_identifier(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("identifier")) - } - - fn deserialize_ignored_any(self, _visitor: V) -> Result - where - V: de::Visitor<'de>, - { - Err(Error::UnsupportedType("ignored_any")) - } -} - -struct PrefixedSeqAccess<'a, 'de: 'a, R> -where - R: std::io::Read, -{ - de: &'a mut Deserializer<'de, R>, - remaining: usize, -} - -impl<'de, 'a, R> de::SeqAccess<'de> for PrefixedSeqAccess<'a, 'de, R> -where - R: std::io::Read, -{ - type Error = Error; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: de::DeserializeSeed<'de>, - { - if self.remaining == 0 { - return Ok(None); - } - self.remaining -= 1; - seed.deserialize(&mut *self.de).map(Some) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::Deserialize; - - #[test] - fn test_serializer() { - let data: [u8; 60] = [ - 1, // true - 0, // false - 1, // 1 - 2, 0, 0, 0, 0, 0, 0, 0, // 2 - 3, 0, 0, 0, 0, 0, 0, 0, // 3 - 4, 0, 0, 0, 0, 0, 0, 0, // 4 - 3, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111, // "foo" - 3, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, // var_bytes prefix + [1, 2, 3] - 1, 2, 3, // fixed_bytes [1, 2, 3] - 1, 1, // Some(true) - 1, 0, // Some(false) - 0, // None - // UnitStruct - 1, // NewTypeStrucdt(true) - 0, 1, // (false, true) - ]; - - #[derive(Debug, Deserialize, PartialEq)] - struct UnitStruct; - #[derive(Debug, Deserialize, PartialEq)] - struct NewTypeStruct(bool); - - #[derive(Debug, Deserialize, PartialEq)] - struct Test { - b_true: bool, - b_false: bool, - unsigned8: u8, - unsigned16: u16, - unsigned32: u32, - unsigned64: u64, - string: String, - var_bytes: Vec, // dynamic size slice - fixed_bytes: [u8; 3], // fixed size array - some_true: Option, - some_false: Option, - none: Option, - unit: UnitStruct, - new_type: NewTypeStruct, - tuple: (bool, bool), - } - - let expected = Test { - b_true: true, - b_false: false, - unsigned8: 1, - unsigned16: 2, - unsigned32: 3, - unsigned64: 4, - string: "foo".to_string(), - var_bytes: vec![1, 2, 3], - fixed_bytes: [1, 2, 3], - some_true: Some(true), - some_false: Some(false), - none: None, - unit: UnitStruct {}, - new_type: NewTypeStruct(true), - tuple: (false, true), - }; - - let t: Test = from_reader(&mut &data[..]).unwrap(); - assert_eq!(t, expected); - } -} diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs deleted file mode 100644 index 03e2192..0000000 --- a/src/encoding/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod deserializer; -mod serializer; - -// place the deserializer and serializer modules in the encoding module. -pub use deserializer::{from_reader, Deserializer, Error as DeserializeError}; -pub use serializer::{to_bytes, to_writer, Error as SerializeError}; diff --git a/src/encoding/serializer.rs b/src/encoding/serializer.rs deleted file mode 100644 index 85fe541..0000000 --- a/src/encoding/serializer.rs +++ /dev/null @@ -1,486 +0,0 @@ -use serde::{ - ser::{self, SerializeTuple}, - Serialize, -}; -use std::fmt::Display; -use std::io; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Custom(String), - - #[error("Serializing type '{0}' is not supported")] - UnsupportedType(&'static str), - - #[error("Can't serialize type when length is not available")] - LengthUnavailable, - - #[error("Failed to write to io::write: {0}")] - IO(#[from] io::Error), -} - -// Implement ser::Error for Error -impl ser::Error for Error { - fn custom(msg: T) -> Self - where - T: Display, - { - Error::Custom(msg.to_string()) - } -} - -pub fn to_bytes(value: &T) -> Result, Error> -where - T: Serialize, -{ - let mut buf = Vec::new(); - to_writer(&mut buf, value)?; - Ok(buf) -} - -pub fn to_writer(writer: &mut W, value: &T) -> Result<(), Error> -where - T: Serialize, - W: io::Write, -{ - let mut serializer = Serializer { writer }; - value.serialize(&mut serializer) -} - -pub struct Serializer<'a, W> -where - W: io::Write, -{ - writer: &'a mut W, -} - -impl ser::Serializer for &mut Serializer<'_, W> { - // Result is written to internal io::Write - type Ok = (); - - // Error is a standard error - type Error = Error; - - // Associated types for keeping track of additional state while serializing - // compound data structures like sequences and maps. In this case no - // additional state is required beyond what is already stored in the - // Serializer struct. - type SerializeSeq = Self; - type SerializeTuple = Self; - type SerializeTupleStruct = Self; - type SerializeTupleVariant = Self; - type SerializeMap = Self; - type SerializeStruct = Self; - type SerializeStructVariant = Self; - - fn is_human_readable(&self) -> bool { - false - } - - fn serialize_bool(self, v: bool) -> Result { - self.writer.write_all(if v { &[1] } else { &[0] })?; - Ok(()) - } - - fn serialize_i8(self, _v: i8) -> Result { - Err(Error::UnsupportedType("i8")) - } - - fn serialize_i16(self, _v: i16) -> Result { - Err(Error::UnsupportedType("i16")) - } - - fn serialize_i32(self, _v: i32) -> Result { - Err(Error::UnsupportedType("i32")) - } - - fn serialize_i64(self, _v: i64) -> Result { - Err(Error::UnsupportedType("i64")) - } - - fn serialize_u8(self, v: u8) -> Result { - self.writer.write_all(&v.to_le_bytes())?; - Ok(()) - } - - fn serialize_u16(self, v: u16) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u32(self, v: u32) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u64(self, v: u64) -> Result { - self.writer.write_all(&v.to_le_bytes())?; - Ok(()) - } - - fn serialize_f32(self, _v: f32) -> Result { - Err(Error::UnsupportedType("f32")) - } - - fn serialize_f64(self, _v: f64) -> Result { - Err(Error::UnsupportedType("f64")) - } - - fn serialize_char(self, _v: char) -> Result { - Err(Error::UnsupportedType("char")) - } - - fn serialize_str(self, v: &str) -> Result { - self.serialize_bytes(v.as_bytes()) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - let seq = self.serialize_seq(Some(v.len()))?; - seq.writer.write_all(v)?; - seq.end() - } - - // 'none' is serialized as a '0' byte - fn serialize_none(self) -> Result { - self.writer.write_all(&[0])?; - Ok(()) - } - - // 'some' is serialized by writing a '1' byte followed by the value - fn serialize_some(self, value: &T) -> Result - where - T: Serialize + ?Sized, - { - self.writer.write_all(&[1])?; - value.serialize(self) - } - - // units don't have a value so they are ignored - fn serialize_unit(self) -> Result { - Ok(()) - } - - // unit_structs don't have a value so they are ignored - fn serialize_unit_struct(self, _name: &'static str) -> Result { - Ok(()) - } - - // newtype_variants are not supported - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - ) -> Result { - Err(Error::UnsupportedType("unit_variant")) - } - - // As is done here, serializers are encouraged to treat newtype structs as - // insignificant wrappers around the data they contain - fn serialize_newtype_struct( - self, - _name: &'static str, - value: &T, - ) -> Result - where - T: Serialize + ?Sized, - { - value.serialize(self) - } - - // newtype_variants are not supported - fn serialize_newtype_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _value: &T, - ) -> Result - where - T: Serialize + ?Sized, - { - Err(Error::UnsupportedType("newtype_variant")) - } - - // sequences have an 8 byte, little-endian encoded prefix describing their - // length - fn serialize_seq(self, len: Option) -> Result { - match len { - Some(len) => { - self.writer.write_all(&(len as u64).to_le_bytes())?; - Ok(self) - } - None => Err(Error::LengthUnavailable), - } - } - - // serialize_tuple is called on fixed size arrays as well as tuples - fn serialize_tuple(self, _len: usize) -> Result { - Ok(self) - } - - // tuple structs are not supported - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Err(Error::UnsupportedType("tuple_struct")) - } - - // tuple variants are not supported - fn serialize_tuple_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Err(Error::UnsupportedType("tuple_variant")) - } - - // maps are not supported - fn serialize_map(self, _len: Option) -> Result { - Err(Error::UnsupportedType("map")) - } - - // serializing a struct doesn't require any 'setup' - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Ok(self) - } - - // struct variants are not supported - fn serialize_struct_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Err(Error::UnsupportedType("struct_variant")) - } -} - -// The following 7 impls deal with the serialization of compound types like -// sequences and maps. Serialization of such types is begun by a Serializer -// method and followed by zero or more calls to serialize individual elements of -// the compound type and one call to end the compound type. -// -// This impl is SerializeSeq so these methods are called after `serialize_seq` -// is called on the Serializer. -impl<'a, W: io::Write> ser::SerializeSeq for &'a mut Serializer<'_, W> { - // Must match the `Ok` type of the serializer. - type Ok = (); - // Must match the `Error` type of the serializer. - type Error = Error; - - // Serialize a single element of the sequence. - fn serialize_element(&mut self, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(&mut **self) - } - - // Close the sequence. - fn end(self) -> Result { - Ok(()) - } -} - -// Tuples are not supported -impl<'a, W: io::Write> ser::SerializeTuple for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - fn serialize_element(&mut self, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(&mut **self) - } - - fn end(self) -> Result { - Ok(()) - } -} - -// Tuple structs are serialized like their inner value -impl<'a, W: io::Write> ser::SerializeTupleStruct for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - fn serialize_field(&mut self, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(&mut **self) - } - - fn end(self) -> Result { - Ok(()) - } -} - -// Tuple variants aren't supported by the Sia encoding -impl<'a, W: io::Write> ser::SerializeTupleVariant for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - fn serialize_field(&mut self, _value: &T) -> Result - where - T: ?Sized + Serialize, - { - panic!("unsupported type - this should never be called") - } - - fn end(self) -> Result { - Ok(()) - } -} - -// Serializing maps is not supported -impl<'a, W: io::Write> ser::SerializeMap for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - // The Serde data model allows map keys to be any serializable type. JSON - // only allows string keys so the implementation below will produce invalid - // JSON if the key serializes as something other than a string. - // - // A real JSON serializer would need to validate that map keys are strings. - // This can be done by using a different Serializer to serialize the key - // (instead of `&mut **self`) and having that other serializer only - // implement `serialize_str` and return an error on any other data type. - fn serialize_key(&mut self, _key: &T) -> Result - where - T: ?Sized + Serialize, - { - panic!("unsupported type - this should never be called") - } - - // It doesn't make a difference whether the colon is printed at the end of - // `serialize_key` or at the beginning of `serialize_value`. In this case - // the code is a bit simpler having it here. - fn serialize_value(&mut self, _value: &T) -> Result - where - T: ?Sized + Serialize, - { - panic!("unsupported type - this should never be called") - } - - fn end(self) -> Result { - Ok(()) - } -} - -// Structs are serialized one field after another. -impl<'a, W: io::Write> ser::SerializeStruct for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - fn serialize_field(&mut self, _key: &'static str, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(&mut **self) - } - - fn end(self) -> Result { - Ok(()) - } -} - -// Struct variants are not supported -impl<'a, W: io::Write> ser::SerializeStructVariant for &'a mut Serializer<'_, W> { - type Ok = (); - type Error = Error; - - fn serialize_field( - &mut self, - _key: &'static str, - _value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - panic!("unsupported type - this should never be called") - } - - fn end(self) -> Result { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::Serialize; - - #[test] - fn test_deserializer() { - #[derive(Serialize)] - struct UnitStruct; - #[derive(Serialize)] - struct NewTypeStruct(bool); - - #[derive(Serialize)] - struct Test { - b_true: bool, - b_false: bool, - unsigned8: u8, - unsigned16: u16, - unsigned32: u32, - unsigned64: u64, - string: String, - var_bytes: Vec, // dynamic size slice - fixed_bytes: [u8; 3], // fixed size array - some_true: Option, - some_false: Option, - none: Option, - unit: UnitStruct, - new_type: NewTypeStruct, - tuple: (bool, bool), - } - - let test = Test { - b_true: true, - b_false: false, - unsigned8: 1, - unsigned16: 2, - unsigned32: 3, - unsigned64: 4, - string: "foo".to_string(), - var_bytes: vec![1, 2, 3], - fixed_bytes: [1, 2, 3], - some_true: Some(true), - some_false: Some(false), - none: None, - unit: UnitStruct {}, - new_type: NewTypeStruct(true), - tuple: (false, true), - }; - let expected = [ - 1, // true - 0, // false - 1, // 1 - 2, 0, 0, 0, 0, 0, 0, 0, // 2 - 3, 0, 0, 0, 0, 0, 0, 0, // 3 - 4, 0, 0, 0, 0, 0, 0, 0, // 4 - 3, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111, // "foo" - 3, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, // var_bytes prefix + [1, 2, 3] - 1, 2, 3, // fixed_bytes [1, 2, 3] - 1, 1, // Some(true) - 1, 0, // Some(false) - 0, // None - // UnitStruct - 1, // NewTypeStrucdt(true) - 0, 1, // (false, true) - ]; - assert_eq!(to_bytes(&test).unwrap(), expected); - } -} diff --git a/src/spendpolicy.rs b/src/spendpolicy.rs deleted file mode 100644 index 2262fea..0000000 --- a/src/spendpolicy.rs +++ /dev/null @@ -1,810 +0,0 @@ -use crate::encoding::to_writer; -use crate::signing::{PublicKey, Signature, SigningState}; -#[allow(deprecated)] -use crate::unlock_conditions::UnlockConditions; -use crate::{Address, Hash256}; -use blake2b_simd::Params; -use core::{fmt, slice::Iter}; -use serde::ser::SerializeTuple; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use std::time::{self, SystemTime}; -use thiserror::Error; - -#[derive(Debug, PartialEq, Error)] -pub enum ValidationError { - #[error("opaque policy")] - OpaquePolicy, - #[error("invalid policy")] - InvalidPolicy, - #[error("invalid signature")] - InvalidSignature, - #[error("invalid preimage")] - InvalidPreimage, - #[error("invalid height")] - InvalidHeight, - #[error("invalid timestamp")] - InvalidTimestamp, - #[error("missing signature")] - MissingSignature, - #[error("missing preimage")] - MissingPreimage, - #[error("threshold not met")] - ThresholdNotMet, -} - -/// A spend policy is a condition or set of conditions that must be met in -/// order to spend a UTXO. -#[derive(Debug, PartialEq, Clone)] -pub enum SpendPolicy { - /// A policy that is only valid after a block height - Above(u64), - /// A policy that is only valid after a timestamp - After(SystemTime), - /// A policy that requires a valid signature from an ed25519 key pair - PublicKey(PublicKey), - /// A policy that requires a valid SHA256 hash preimage - Hash([u8; 32]), - /// A threshold policy that requires n-of-m sub-policies to be met - Threshold(u8, Vec), - /// An opaque policy that is not directly spendable - Opaque(Address), - - /// A set of v1 unlock conditions for compatibility with v1 transactions - #[deprecated] - UnlockConditions(UnlockConditions), -} - -impl SpendPolicy { - fn type_prefix(&self) -> u8 { - match self { - SpendPolicy::Above(_) => 1, - SpendPolicy::After(_) => 2, - SpendPolicy::PublicKey(_) => 3, - SpendPolicy::Hash(_) => 4, - SpendPolicy::Threshold(_, _) => 5, - SpendPolicy::Opaque(_) => 6, - #[allow(deprecated)] - SpendPolicy::UnlockConditions(_) => 7, - } - } - - /// Create a policy that is only valid after a certain block height - pub fn above(height: u64) -> Self { - Self::Above(height) - } - - /// Create a policy that is only valid after a certain timestamp - pub fn after(timestamp: SystemTime) -> Self { - Self::After(timestamp) - } - - /// Create a policy that requires a valid signature from a public key - pub fn public_key(pk: PublicKey) -> Self { - Self::PublicKey(pk) - } - - /// Create a policy that requires a hash preimage - pub fn hash(hash: [u8; 32]) -> Self { - Self::Hash(hash) - } - - /// Create a threshold policy with n-of-m sub-policies - pub fn threshold(n: u8, policies: Vec) -> Self { - for policy in policies.iter() { - #[allow(deprecated)] - if let SpendPolicy::UnlockConditions(_) = policy { - panic!("UnlockConditions are not allowed in a threshold policy"); - } - } - Self::Threshold(n, policies) - } - - /// Create a v1 unlock conditions policy for compatibility with v1 - /// transactions. - #[deprecated] - pub fn unlock_conditions(uc: UnlockConditions) -> Self { - #[allow(deprecated)] - Self::UnlockConditions(uc) - } - - /// Returns the address of the policy. This is a hash of the policy that - /// can be used to receive funds. - pub fn address(&self) -> Address { - #[allow(deprecated)] - if let SpendPolicy::UnlockConditions(uc) = self { - return uc.address(); - } else if let SpendPolicy::Opaque(addr) = self { - return addr.clone(); - } - - let mut state = Params::new().hash_length(32).to_state(); - - state.update("sia/address|".as_bytes()); - - if let SpendPolicy::Threshold(n, of) = self { - let mut opaque = Vec::with_capacity(of.len()); - for policy in of { - opaque.push(SpendPolicy::Opaque(policy.address())) - } - to_writer(&mut state, &SpendPolicy::Threshold(*n, opaque)).unwrap(); - } else { - to_writer(&mut state, self).unwrap(); - } - Address::from(state.finalize().as_bytes()) - } - - /// Verify that the policy is satisfied by the given parameters. - pub fn verify( - &self, - signing_state: &SigningState, - hash: &Hash256, - signatures: &mut Iter<'_, Signature>, - preimages: &mut Iter<'_, Vec>, - ) -> Result<(), ValidationError> { - match self { - SpendPolicy::Above(height) => { - if *height > signing_state.index.height { - Err(ValidationError::InvalidHeight) - } else { - Ok(()) - } - } - SpendPolicy::After(time) => { - if *time > signing_state.median_timestamp { - Err(ValidationError::InvalidTimestamp) - } else { - Ok(()) - } - } - SpendPolicy::PublicKey(pk) => signatures - .next() - .ok_or(ValidationError::MissingSignature) - .and_then(|sig| { - pk.verify(hash.as_ref(), sig) - .then_some(()) - .ok_or(ValidationError::InvalidSignature) - }), - SpendPolicy::Hash(hash) => { - let preimage = preimages.next().ok_or(ValidationError::MissingPreimage)?; - - let mut hasher = Sha256::new(); - hasher.update(preimage); - - let res: [u8; 32] = hasher.finalize().into(); - if res == *hash { - Ok(()) - } else { - Err(ValidationError::InvalidPreimage) - } - } - SpendPolicy::Threshold(n, ref policies) => { - let mut remaining = *n; - for policy in policies { - #[allow(deprecated)] - if let SpendPolicy::UnlockConditions(_) = policy { - return Err(ValidationError::InvalidPolicy); - } - - if policy - .verify(signing_state, hash, signatures, preimages) - .is_err() - { - continue; - } - - remaining -= 1; - if remaining == 0 { - break; - } - } - if remaining == 0 { - Ok(()) - } else { - Err(ValidationError::ThresholdNotMet) - } - } - #[allow(deprecated)] - SpendPolicy::UnlockConditions(uc) => { - if uc.timelock > signing_state.index.height { - return Err(ValidationError::InvalidHeight); - } else if uc.signatures_required > 255 { - return Err(ValidationError::InvalidPolicy); - } - - let mut remaining = uc.signatures_required; - for pk in uc.public_keys.iter() { - let sig = signatures.next().ok_or(ValidationError::MissingSignature)?; - if pk.public_key().verify(hash.as_ref(), sig) { - remaining -= 1; - if remaining == 0 { - break; - } - } else { - return Err(ValidationError::InvalidSignature); - } - } - - if remaining == 0 { - return Ok(()); - } - Err(ValidationError::ThresholdNotMet) - } - SpendPolicy::Opaque(_) => Err(ValidationError::OpaquePolicy), - } - } - - /// Encode the policy to a writer. This is used to handle recursive - /// threshold policies. The version byte is only written for the top-level - /// policy. - fn serialize_policy(&self, s: &mut S) -> Result<(), S::Error> { - s.serialize_element(&self.type_prefix())?; // type prefix - match self { - SpendPolicy::Above(height) => s.serialize_element(height), - SpendPolicy::After(time) => { - s.serialize_element(&time.duration_since(time::UNIX_EPOCH).unwrap().as_secs()) - } - SpendPolicy::PublicKey(pk) => { - let mut arr: [u8; 32] = [0; 32]; - arr.copy_from_slice(pk.as_ref()); - s.serialize_element(&arr) - } - SpendPolicy::Hash(hash) => s.serialize_element(hash), - SpendPolicy::Threshold(n, policies) => { - let prefix = [*n, policies.len() as u8]; - s.serialize_element(&prefix)?; - for policy in policies { - policy.serialize_policy(s)?; - } - Ok(()) - } - SpendPolicy::Opaque(addr) => s.serialize_element(addr), - #[allow(deprecated)] - SpendPolicy::UnlockConditions(uc) => s.serialize_element(uc), - } - } -} - -impl Serialize for SpendPolicy { - fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - String::serialize(&self.to_string(), serializer) - } else { - // unknown length since policie are recursive and need custom - // serialize/deserialize implementations anyway. - let mut s = serializer.serialize_tuple(0)?; - s.serialize_element(&1u8)?; // version - self.serialize_policy(&mut s)?; - s.end() - } - } -} - -impl fmt::Display for SpendPolicy { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SpendPolicy::Above(height) => write!(f, "above({})", height), - SpendPolicy::After(time) => { - let duration = time - .duration_since(time::UNIX_EPOCH) - .map_err(|_| fmt::Error)?; - write!(f, "after({})", duration.as_secs()) - } - SpendPolicy::PublicKey(pk) => write!(f, "pk(0x{})", hex::encode(pk.as_ref())), - SpendPolicy::Hash(hash) => write!(f, "h(0x{})", hex::encode(hash)), - SpendPolicy::Threshold(n, policies) => { - write!(f, "thresh({},[", n)?; - for (i, policy) in policies.iter().enumerate() { - if i > 0 { - write!(f, ",")?; - } - write!(f, "{}", policy)?; - } - write!(f, "])") - } - SpendPolicy::Opaque(addr) => write!(f, "opaque(0x{})", hex::encode(addr)), - #[allow(deprecated)] - SpendPolicy::UnlockConditions(uc) => { - write!(f, "uc({},{},[", uc.timelock, uc.signatures_required)?; - for (i, pk) in uc.public_keys.iter().enumerate() { - if i > 0 { - write!(f, ",")?; - } - write!(f, "0x{}", hex::encode(pk.public_key().as_ref()))?; - } - write!(f, "])") - } - } - } -} - -/// A policy that has been satisfied by a set of preimages and signatures. -pub struct SatisfiedPolicy { - pub policy: SpendPolicy, - pub preimages: Vec>, - pub signatures: Vec, -} - -impl SatisfiedPolicy { - /// Create a new satisfied policy from a policy, preimages, and signatures. - pub fn new(policy: SpendPolicy, preimages: Vec>, signatures: Vec) -> Self { - Self { - policy, - preimages, - signatures, - } - } - - /// Verify that the policy is satisfied by the given parameters. - /// This is a convenience method that calls `verify` on the policy. - pub fn verify(&self, state: &SigningState, sig_hash: &Hash256) -> Result<(), ValidationError> { - self.policy.verify( - state, - sig_hash, - &mut self.signatures.iter(), - &mut self.preimages.iter(), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::signing::{NetworkHardforks, PrivateKey}; - use crate::ChainIndex; - use rand::prelude::*; - use std::time::Duration; - - #[test] - fn test_address() { - let test_cases = vec![ - ( - SpendPolicy::PublicKey(PublicKey::new([ - 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ])), - "addr:55a7793237722c6df8222fd512063cb74228085ef1805c5184713648c159b919ac792fbad0e1", - ), - ( - SpendPolicy::Above(100), - "addr:c2fba9b9607c800e80d9284ed0fb9a55737ba1bbd67311d0d9242dd6376bed0c6ee355e814fa", - ), - ( - SpendPolicy::After(time::UNIX_EPOCH + Duration::from_secs(1433600000)), - "addr:5bdb96e33ffdf72619ad38bee57ad4db9eb242aeb2ee32020ba16179af5d46d501bd2011806b", - ), - ( - SpendPolicy::Hash([ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]), - "addr:1cc0fc4cde659333cf7e61971cc5025c5a6b4759c9d1c1d438227c3eb57d841512d4cd4ce620", - ), - ( - SpendPolicy::Threshold( - 2, - vec![ - SpendPolicy::PublicKey(PublicKey::new([ - 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - ])), - SpendPolicy::Above(100), - SpendPolicy::Threshold( - 2, - vec![ - SpendPolicy::PublicKey(PublicKey::new([0; 32])), - SpendPolicy::After( - time::UNIX_EPOCH + Duration::from_secs(1433600000), - ), - ], - ), - ], - ), - "addr:30f516630280059c25ae92f3bf3c451be258ecd3249c43906e3d9dd9e86f2dc00ef5eeffc2c4", - ), - ]; - - for (policy, expected) in test_cases { - assert_eq!(policy.address().to_string(), expected); - } - } - - #[test] - fn test_verify() { - struct PolicyTest { - policy: SpendPolicy, - state: SigningState, - hash: Hash256, - signatures: Vec, - preimages: Vec>, - result: Result<(), ValidationError>, - } - let test_cases = vec![ - PolicyTest { - policy: SpendPolicy::Above(100), - state: SigningState { - index: ChainIndex { - height: 99, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(99), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Err(ValidationError::InvalidHeight), - }, - PolicyTest { - policy: SpendPolicy::Above(100), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(99), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Ok(()), - }, - PolicyTest { - policy: SpendPolicy::After(time::UNIX_EPOCH + Duration::from_secs(100)), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(99), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Err(ValidationError::InvalidTimestamp), - }, - PolicyTest { - policy: SpendPolicy::After(time::UNIX_EPOCH + Duration::from_secs(100)), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Ok(()), - }, - PolicyTest { - policy: SpendPolicy::PublicKey(PublicKey::new([0; 32])), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Err(ValidationError::MissingSignature), - }, - PolicyTest { - policy: SpendPolicy::PublicKey(PublicKey::new([0; 32])), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![Signature::new([0; 64])], - preimages: vec![], - result: Err(ValidationError::InvalidSignature), - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::PublicKey(pk.public_key()), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![pk.sign(sig_hash.as_ref())], - preimages: vec![], - result: Ok(()), - } - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::Threshold( - 2, - vec![ - SpendPolicy::PublicKey(pk.public_key()), - SpendPolicy::Above(100), - ], - ), - state: SigningState { - index: ChainIndex { - height: 99, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![pk.sign(sig_hash.as_ref())], - preimages: vec![], - result: Err(ValidationError::ThresholdNotMet), - } - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::Threshold( - 2, - vec![ - SpendPolicy::PublicKey(pk.public_key()), - SpendPolicy::Above(100), - ], - ), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![Signature::new([0; 64])], - preimages: vec![], - result: Err(ValidationError::ThresholdNotMet), - } - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::Threshold( - 2, - vec![ - SpendPolicy::PublicKey(pk.public_key()), - SpendPolicy::Above(100), - ], - ), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![pk.sign(sig_hash.as_ref())], - preimages: vec![], - result: Ok(()), - } - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::Threshold( - 1, - vec![ - SpendPolicy::PublicKey(pk.public_key()), - SpendPolicy::Opaque(Address::new([0; 32])), - ], - ), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![pk.sign(sig_hash.as_ref())], - preimages: vec![], - result: Ok(()), - } - }, - { - let pk = PrivateKey::from_seed(&random()); - let sig_hash = Hash256::from(random::<[u8; 32]>()); - - PolicyTest { - policy: SpendPolicy::Threshold( - 1, - vec![ - SpendPolicy::PublicKey(pk.public_key()), - SpendPolicy::Opaque(Address::new([0; 32])), - ], - ), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: sig_hash, - signatures: vec![], - preimages: vec![], - result: Err(ValidationError::ThresholdNotMet), - } - }, - { - let mut preimage = [0; 64]; - thread_rng().fill(&mut preimage); - - let mut hasher = Sha256::new(); - hasher.update(preimage); - let h: [u8; 32] = hasher.finalize().into(); - - PolicyTest { - policy: SpendPolicy::Hash(h), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![], - result: Err(ValidationError::MissingPreimage), - } - }, - { - let mut preimage = [0; 64]; - thread_rng().fill(&mut preimage); - - let mut hasher = Sha256::new(); - hasher.update(preimage); - let h: [u8; 32] = hasher.finalize().into(); - - PolicyTest { - policy: SpendPolicy::Hash(h), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![[0; 64].to_vec()], - result: Err(ValidationError::InvalidPreimage), - } - }, - { - let mut preimage = [0; 64]; - thread_rng().fill(&mut preimage); - - let mut hasher = Sha256::new(); - hasher.update(preimage); - let h: [u8; 32] = hasher.finalize().into(); - - PolicyTest { - policy: SpendPolicy::Hash(h), - state: SigningState { - index: ChainIndex { - height: 100, - id: [0; 32], - }, - median_timestamp: time::UNIX_EPOCH + Duration::from_secs(100), - hardforks: NetworkHardforks::default(), - }, - hash: Hash256::default(), - signatures: vec![], - preimages: vec![preimage.to_vec()], - result: Ok(()), - } - }, - ]; - - for test in test_cases { - let result = test.policy.verify( - &test.state, - &test.hash, - &mut test.signatures.iter(), - &mut test.preimages.iter(), - ); - assert_eq!(result, test.result, "{}", test.policy); - } - } - - #[test] - fn test_opaque_policy() { - let test_cases = vec![ - SpendPolicy::above(100), - SpendPolicy::after(time::UNIX_EPOCH + Duration::from_secs(100)), - SpendPolicy::public_key(PublicKey::new([0; 32])), - SpendPolicy::hash([0; 32]), - SpendPolicy::threshold( - 2, - vec![ - SpendPolicy::public_key(PublicKey::new([0; 32])), - SpendPolicy::above(100), - ], - ), - SpendPolicy::threshold( - 2, - vec![ - SpendPolicy::public_key(PublicKey::new([0; 32])), - SpendPolicy::above(100), - SpendPolicy::threshold( - 2, - vec![ - SpendPolicy::public_key(PublicKey::new([0; 32])), - SpendPolicy::after(time::UNIX_EPOCH + Duration::from_secs(100)), - ], - ), - SpendPolicy::PublicKey(PublicKey::new([1; 32])), - ], - ), - ]; - - for (i, policy) in test_cases.into_iter().enumerate() { - let policy = policy.clone(); - let address = policy.address(); - let expected_address = address.to_string(); - let opaque = SpendPolicy::Opaque(address); - assert_eq!( - opaque.address().to_string(), - expected_address, - "test case {}", - i - ); - - if let SpendPolicy::Threshold(n, of) = policy { - // test that the address of opaque threshold policies is the - // same as the address of normal threshold policies - for j in 0..of.len() { - let mut of = of.clone(); - of[j] = SpendPolicy::Opaque(of[j].address()); - let opaque_policy = SpendPolicy::threshold(n, of); - - assert_eq!( - opaque_policy.address().to_string(), - expected_address, - "test case {}-{}", - i, - j - ); - } - } - } - } -} diff --git a/src/transactions.rs b/src/transactions.rs deleted file mode 100644 index 74cafd9..0000000 --- a/src/transactions.rs +++ /dev/null @@ -1,1125 +0,0 @@ -use crate::encoding::{to_writer, SerializeError}; -use crate::signing::{PrivateKey, Signature, SigningState}; -use crate::specifier::{specifier, Specifier}; -use crate::unlock_conditions::UnlockConditions; -use crate::{Address, Currency, ImplHashID, Leaf}; -use crate::{Hash256, HexParseError}; -use blake2b_simd::{Params, State}; -use core::fmt; -use serde::{Deserialize, Serialize}; - -ImplHashID!(SiacoinOutputID, "scoid"); - -ImplHashID!(SiafundOutputID, "sfoid"); - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SiacoinInput { - #[serde(rename = "parentID")] - pub parent_id: SiacoinOutputID, - pub unlock_conditions: UnlockConditions, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SiacoinOutput { - pub value: Currency, - pub address: Address, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SiafundInput { - #[serde(rename = "parentID")] - pub parent_id: SiafundOutputID, - pub unlock_conditions: UnlockConditions, - pub claim_address: Address, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SiafundOutput { - pub value: Currency, - pub address: Address, - pub claim_start: Currency, -} - -ImplHashID!(FileContractID, "fcid"); - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FileContract { - #[serde(rename = "filesize")] - pub file_size: u64, - pub file_merkle_root: Hash256, - pub window_start: u64, - pub window_end: u64, - pub payout: Currency, - pub valid_proof_outputs: Vec, - pub missed_proof_outputs: Vec, - pub unlock_hash: Hash256, - pub revision_number: u64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FileContractRevision { - #[serde(rename = "parentID")] - pub parent_id: FileContractID, - pub unlock_conditions: UnlockConditions, - pub revision_number: u64, - #[serde(rename = "filesize")] - pub file_size: u64, - pub file_merkle_root: Hash256, - pub window_start: u64, - pub window_end: u64, - pub valid_proof_outputs: Vec, - pub missed_proof_outputs: Vec, - pub unlock_hash: Hash256, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StorageProof { - #[serde(rename = "parentID")] - pub parent_id: FileContractID, - pub leaf: Leaf, - pub proof: Vec, -} - -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CoveredFields { - pub whole_transaction: bool, - pub siacoin_inputs: Vec, - pub siacoin_outputs: Vec, - pub file_contracts: Vec, - pub file_contract_revisions: Vec, - pub storage_proofs: Vec, - pub siafund_inputs: Vec, - pub siafund_outputs: Vec, - pub miner_fees: Vec, - pub arbitrary_data: Vec, - pub signatures: Vec, -} - -impl CoveredFields { - pub fn whole_transaction() -> Self { - CoveredFields { - whole_transaction: true, - ..Default::default() - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionSignature { - #[serde(rename = "parentID")] - pub parent_id: Hash256, - pub public_key_index: u64, - pub timelock: u64, - pub covered_fields: CoveredFields, - pub signature: Signature, -} - -ImplHashID!(TransactionID, "txn"); - -#[derive(Default, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Transaction { - pub siacoin_inputs: Vec, - pub siacoin_outputs: Vec, - pub file_contracts: Vec, - pub file_contract_revisions: Vec, - pub storage_proofs: Vec, - pub siafund_inputs: Vec, - pub siafund_outputs: Vec, - pub miner_fees: Vec, - pub arbitrary_data: Vec>, - pub signatures: Vec, -} - -impl Transaction { - const SIACOIN_OUTPUT_ID_PREFIX: Specifier = specifier!("siacoin output"); - const SIAFUND_OUTPUT_ID_PREFIX: Specifier = specifier!("siafund output"); - - pub fn encode_no_sigs(&self) -> Vec { - let mut buf = Vec::new(); - - buf.extend_from_slice(&(self.siacoin_inputs.len() as u64).to_le_bytes()); - for input in &self.siacoin_inputs { - to_writer(&mut buf, input).unwrap(); - } - - buf.extend_from_slice(&(self.siacoin_outputs.len() as u64).to_le_bytes()); - for output in &self.siacoin_outputs { - to_writer(&mut buf, output).unwrap(); - } - - buf.extend_from_slice(&(self.file_contracts.len() as u64).to_le_bytes()); - for file_contract in &self.file_contracts { - to_writer(&mut buf, file_contract).unwrap(); - } - - buf.extend_from_slice(&(self.file_contract_revisions.len() as u64).to_le_bytes()); - for file_contract_revision in &self.file_contract_revisions { - to_writer(&mut buf, file_contract_revision).unwrap(); - } - - buf.extend_from_slice(&(self.storage_proofs.len() as u64).to_le_bytes()); - for storage_proof in &self.storage_proofs { - to_writer(&mut buf, storage_proof).unwrap(); - } - - buf.extend_from_slice(&(self.siafund_inputs.len() as u64).to_le_bytes()); - for input in &self.siafund_inputs { - to_writer(&mut buf, input).unwrap(); - } - - buf.extend_from_slice(&(self.siafund_outputs.len() as u64).to_le_bytes()); - for output in &self.siafund_outputs { - to_writer(&mut buf, output).unwrap(); - } - - buf.extend_from_slice(&(self.miner_fees.len() as u64).to_le_bytes()); - for fee in &self.miner_fees { - to_writer(&mut buf, fee).unwrap(); - } - - buf.extend_from_slice(&(self.arbitrary_data.len() as u64).to_le_bytes()); - for data in &self.arbitrary_data { - buf.extend_from_slice(&(data.len() as u64).to_le_bytes()); - buf.extend_from_slice(data); - } - buf - } - - pub fn hash_no_sigs(&self, state: &mut State) { - state.update(&(self.siacoin_inputs.len() as u64).to_le_bytes()); - for input in self.siacoin_inputs.iter() { - to_writer(state, input).unwrap(); - } - - state.update(&(self.siacoin_outputs.len() as u64).to_le_bytes()); - for output in self.siacoin_outputs.iter() { - to_writer(state, output).unwrap(); - } - - state.update(&(self.file_contracts.len() as u64).to_le_bytes()); - for file_contract in self.file_contracts.iter() { - to_writer(state, file_contract).unwrap(); - } - - state.update(&(self.file_contract_revisions.len() as u64).to_le_bytes()); - for file_contract_revision in self.file_contract_revisions.iter() { - to_writer(state, file_contract_revision).unwrap(); - } - - state.update(&(self.storage_proofs.len() as u64).to_le_bytes()); - for storage_proof in self.storage_proofs.iter() { - to_writer(state, storage_proof).unwrap(); - } - - state.update(&(self.siafund_inputs.len() as u64).to_le_bytes()); - for input in self.siafund_inputs.iter() { - to_writer(state, input).unwrap(); - } - - state.update(&(self.siafund_outputs.len() as u64).to_le_bytes()); - for output in self.siafund_outputs.iter() { - to_writer(state, output).unwrap(); - } - - state.update(&(self.miner_fees.len() as u64).to_le_bytes()); - for fee in self.miner_fees.iter() { - to_writer(state, fee).unwrap(); - } - - state.update(&(self.arbitrary_data.len() as u64).to_le_bytes()); - for data in self.arbitrary_data.iter() { - state.update(&(data.len() as u64).to_le_bytes()); - state.update(data); - } - } - - pub(crate) fn whole_sig_hash( - &self, - chain: &SigningState, - parent_id: &Hash256, - public_key_index: u64, - timelock: u64, - covered_sigs: &Vec, - ) -> Result { - let mut state = Params::new().hash_length(32).to_state(); - - state.update(&(self.siacoin_inputs.len() as u64).to_le_bytes()); - for input in self.siacoin_inputs.iter() { - state.update(chain.replay_prefix()); - to_writer(&mut state, input)?; - } - - state.update(&(self.siacoin_outputs.len() as u64).to_le_bytes()); - for output in self.siacoin_outputs.iter() { - to_writer(&mut state, output)?; - } - - state.update(&(self.file_contracts.len() as u64).to_le_bytes()); - for file_contract in self.file_contracts.iter() { - to_writer(&mut state, file_contract)?; - } - - state.update(&(self.file_contract_revisions.len() as u64).to_le_bytes()); - for file_contract_revision in self.file_contract_revisions.iter() { - to_writer(&mut state, file_contract_revision)?; - } - - state.update(&(self.storage_proofs.len() as u64).to_le_bytes()); - for storage_proof in self.storage_proofs.iter() { - to_writer(&mut state, storage_proof).unwrap(); - } - - state.update(&(self.siafund_inputs.len() as u64).to_le_bytes()); - for input in self.siafund_inputs.iter() { - state.update(chain.replay_prefix()); - to_writer(&mut state, input).unwrap(); - } - - state.update(&(self.siafund_outputs.len() as u64).to_le_bytes()); - for output in self.siafund_outputs.iter() { - to_writer(&mut state, output)?; - } - - state.update(&(self.miner_fees.len() as u64).to_le_bytes()); - for fee in self.miner_fees.iter() { - to_writer(&mut state, &fee)?; - } - - state.update(&(self.arbitrary_data.len() as u64).to_le_bytes()); - for data in self.arbitrary_data.iter() { - state.update(&(data.len() as u64).to_le_bytes()); - state.update(data); - } - - to_writer(&mut state, parent_id)?; - state.update(&public_key_index.to_le_bytes()); - state.update(&timelock.to_le_bytes()); - - for &i in covered_sigs { - if i >= self.signatures.len() { - return Err(SerializeError::Custom( - "signature index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.signatures[i])?; - } - - Ok(state.finalize().into()) - } - - pub(crate) fn partial_sig_hash( - &self, - chain: &SigningState, - covered_fields: &CoveredFields, - ) -> Result { - let mut state = Params::new().hash_length(32).to_state(); - - for &i in covered_fields.siacoin_inputs.iter() { - if i >= self.siacoin_inputs.len() { - return Err(SerializeError::Custom( - "siacoin_inputs index out of bounds".to_string(), - )); - } - state.update(chain.replay_prefix()); - to_writer(&mut state, &self.siacoin_inputs[i])?; - } - - for &i in covered_fields.siacoin_outputs.iter() { - if i >= self.siacoin_outputs.len() { - return Err(SerializeError::Custom( - "siacoin_outputs index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.siacoin_outputs[i])?; - } - - for &i in covered_fields.file_contracts.iter() { - if i >= self.file_contracts.len() { - return Err(SerializeError::Custom( - "file_contracts index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.file_contracts[i])?; - } - - for &i in covered_fields.file_contract_revisions.iter() { - if i >= self.file_contract_revisions.len() { - return Err(SerializeError::Custom( - "file_contract_revisions index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.file_contract_revisions[i])?; - } - - for &i in covered_fields.storage_proofs.iter() { - if i >= self.storage_proofs.len() { - return Err(SerializeError::Custom( - "storage_proofs index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.storage_proofs[i])?; - } - - for &i in covered_fields.siafund_inputs.iter() { - if i >= self.siafund_inputs.len() { - return Err(SerializeError::Custom( - "siafund_inputs index out of bounds".to_string(), - )); - } - state.update(chain.replay_prefix()); - to_writer(&mut state, &self.siafund_inputs[i])?; - } - - for &i in covered_fields.siafund_outputs.iter() { - if i >= self.siafund_outputs.len() { - return Err(SerializeError::Custom( - "siafund_outputs index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.siafund_outputs[i])?; - } - - for &i in covered_fields.miner_fees.iter() { - if i >= self.miner_fees.len() { - return Err(SerializeError::Custom( - "miner_fees index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.miner_fees[i])?; - } - - for &i in covered_fields.arbitrary_data.iter() { - if i >= self.arbitrary_data.len() { - return Err(SerializeError::Custom( - "arbitrary_data index out of bounds".to_string(), - )); - } - state.update(&(self.arbitrary_data[i].len() as u64).to_le_bytes()); - state.update(&self.arbitrary_data[i]); - } - - for &i in covered_fields.signatures.iter() { - if i >= self.signatures.len() { - return Err(SerializeError::Custom( - "signatures index out of bounds".to_string(), - )); - } - to_writer(&mut state, &self.signatures[i])?; - } - - Ok(state.finalize().into()) - } - - pub fn sign( - &self, - state: &SigningState, - covered_fields: &CoveredFields, - parent_id: Hash256, - public_key_index: u64, - timelock: u64, - private_key: &PrivateKey, - ) -> Result { - let sig_hash = if covered_fields.whole_transaction { - self.whole_sig_hash( - state, - &parent_id, - public_key_index, - timelock, - &covered_fields.signatures, - ) - } else { - self.partial_sig_hash(state, covered_fields) - }?; - - Ok(TransactionSignature { - parent_id, - public_key_index, - timelock, - covered_fields: covered_fields.clone(), - signature: private_key.sign(sig_hash.as_ref()), - }) - } - - pub fn id(&self) -> TransactionID { - let mut state = Params::new().hash_length(32).to_state(); - self.hash_no_sigs(&mut state); - let hash = state.finalize(); - let buf = hash.as_bytes(); - - TransactionID(buf.try_into().unwrap()) - } - - pub fn siacoin_output_id(&self, i: usize) -> SiacoinOutputID { - let mut state = Params::new().hash_length(32).to_state(); - - state.update(Self::SIACOIN_OUTPUT_ID_PREFIX.as_bytes()); - self.hash_no_sigs(&mut state); - - let h = state.update(&i.to_le_bytes()).finalize(); - SiacoinOutputID::from(h) - } - - pub fn siafund_output_id(&self, i: usize) -> SiafundOutputID { - let mut state = Params::new().hash_length(32).to_state(); - - state.update(Self::SIAFUND_OUTPUT_ID_PREFIX.as_bytes()); - self.hash_no_sigs(&mut state); - - let h = state.update(&i.to_le_bytes()).finalize(); - SiafundOutputID::from(h) - } -} - -#[cfg(test)] -mod tests { - use crate::encoding::{from_reader, to_bytes}; - use crate::signing::{NetworkHardforks, PublicKey}; - use crate::unlock_conditions::{Algorithm, UnlockKey}; - use crate::ChainIndex; - use std::time::SystemTime; - use std::vec; - - use super::*; - - #[test] - fn test_json_serialize_covered_fields() { - let mut cf = CoveredFields::default(); - cf.siacoin_inputs.push(1); - cf.siacoin_outputs.push(2); - cf.siacoin_outputs.push(3); - assert_eq!(serde_json::to_string(&cf).unwrap(), "{\"wholeTransaction\":false,\"siacoinInputs\":[1],\"siacoinOutputs\":[2,3],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]}") - } - - #[test] - fn test_serialize_covered_fields() { - let test_cases = vec![ - ( - CoveredFields::whole_transaction(), - vec![ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ], - ), - ( - CoveredFields::default(), - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ], - ), - ]; - - for (cf, expected) in test_cases { - let result = to_bytes(&cf).expect("failed to serialize covered fields"); - assert_eq!(result, expected); - } - } - - #[test] - fn test_serialize_siacoin_input() { - let siacoin_input = SiacoinInput { - parent_id: SiacoinOutputID::parse_string( - "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", - ) - .unwrap(), - unlock_conditions: UnlockConditions::new( - 123, - vec![UnlockKey::new( - Algorithm::ed25519(), - PublicKey::new([ - 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, - 0x47, 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, - 0x8c, 0x1a, 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, - ]), - )], - 1, - ), - }; - - // binary - let siacoin_input_serialized = to_bytes(&siacoin_input).unwrap(); - let siacoin_input_deserialized: SiacoinInput = - from_reader(&mut &siacoin_input_serialized[..]).unwrap(); - assert_eq!( - siacoin_input_serialized, - [ - 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, - 158, 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 123, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 32, 0, 0, 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, 253, 16, 121, 168, 198, 200, - 123, 71, 218, 29, 86, 126, 53, 185, 114, 52, 153, 60, 40, 140, 26, 208, 219, 29, - 28, 225, 182, 1, 0, 0, 0, 0, 0, 0, 0 - ] - ); - assert_eq!(siacoin_input_deserialized, siacoin_input); - - // json - let siacoin_input_serialized = serde_json::to_string(&siacoin_input).unwrap(); - let siacoin_input_deserialized: SiacoinInput = - serde_json::from_str(&siacoin_input_serialized).unwrap(); - assert_eq!(siacoin_input_serialized, "{\"parentID\":\"scoid:b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1}}"); - assert_eq!(siacoin_input_deserialized, siacoin_input); - } - - #[test] - fn test_serialize_siafund_input() { - let siafund_input = SiafundInput { - parent_id: SiafundOutputID::parse_string( - "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", - ) - .unwrap(), - unlock_conditions: UnlockConditions::new( - 123, - vec![UnlockKey::new( - Algorithm::ed25519(), - PublicKey::new([ - 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, - 0x47, 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, - 0x8c, 0x1a, 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, - ]), - )], - 1, - ), - claim_address: Address::new( - hex::decode("8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8c") - .unwrap() - .try_into() - .unwrap(), - ), - }; - - // binary - let siafund_input_serialized = to_bytes(&siafund_input).unwrap(); - let siafund_input_deserialized: SiafundInput = - from_reader(&mut &siafund_input_serialized[..]).unwrap(); - assert_eq!( - siafund_input_serialized, - [ - 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, - 158, 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 123, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 32, 0, 0, 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, 253, 16, 121, 168, 198, 200, - 123, 71, 218, 29, 86, 126, 53, 185, 114, 52, 153, 60, 40, 140, 26, 208, 219, 29, - 28, 225, 182, 1, 0, 0, 0, 0, 0, 0, 0, 143, 180, 156, 207, 23, 223, 220, 201, 82, - 109, 236, 110, 232, 165, 204, 162, 15, 248, 36, 115, 2, 5, 61, 55, 119, 65, 11, - 155, 4, 148, 186, 140 - ] - ); - assert_eq!(siafund_input_deserialized, siafund_input); - - // json - let siafund_input_serialized = serde_json::to_string(&siafund_input).unwrap(); - let siafund_input_deserialized: SiafundInput = - serde_json::from_str(&siafund_input_serialized).unwrap(); - assert_eq!(siafund_input_serialized, "{\"parentID\":\"sfoid:b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1},\"claimAddress\":\"addr:8fb49ccf17dfdcc9526dec6ee8a5cca20ff8247302053d3777410b9b0494ba8cdf32abee86f0\"}"); - assert_eq!(siafund_input_deserialized, siafund_input); - } - - #[test] - fn test_serialize_siacoin_output() { - let addr_str = - "addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69"; - let output = SiacoinOutput { - value: Currency::new(67856467336433871), - address: Address::parse_string(addr_str).unwrap(), - }; - - // binary - let output_serialized = to_bytes(&output).unwrap(); - let output_deserialized: SiacoinOutput = from_reader(&mut &output_serialized[..]).unwrap(); - assert_eq!( - output_serialized, - [ - 7, 0, 0, 0, 0, 0, 0, 0, 241, 19, 24, 247, 77, 16, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ] - ); - assert_eq!(output_deserialized, output); - - // json - let output_serialized = serde_json::to_string(&output).unwrap(); - let output_deserialized: SiacoinOutput = serde_json::from_str(&output_serialized).unwrap(); - assert_eq!( - output_serialized, - format!( - "{{\"value\":\"67856467336433871\",\"address\":\"{}\"}}", - addr_str - ) - ); - assert_eq!(output_deserialized, output); - } - - #[test] - fn test_serialize_transaction_signature() { - let signature = TransactionSignature { - parent_id: Hash256::parse_string( - "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", - ) - .unwrap(), - public_key_index: 1, - timelock: 2, - covered_fields: CoveredFields { - whole_transaction: true, - ..Default::default() - }, - signature: Signature::new([3u8; 64]), - }; - - // binary - let signature_serialized = to_bytes(&signature).unwrap(); - let signature_deserialized: TransactionSignature = - from_reader(&mut &signature_serialized[..]).unwrap(); - assert_eq!( - signature_serialized, - [ - 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, - 158, 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 1, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 - ] - ); - assert_eq!(signature_deserialized, signature); - - // json - let signature_serialized = serde_json::to_string(&signature).unwrap(); - let signature_deserialized: TransactionSignature = - serde_json::from_str(&signature_serialized).unwrap(); - assert_eq!( - signature_serialized, - "{\"parentID\":\"h:b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"publicKeyIndex\":1,\"timelock\":2,\"coveredFields\":{\"wholeTransaction\":true,\"siacoinInputs\":[],\"siacoinOutputs\":[],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]},\"signature\":\"AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw==\"}", - ); - assert_eq!(signature_deserialized, signature); - } - - #[test] - fn test_serialize_siafund_output() { - let addr_str = - "addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69"; - let output = SiafundOutput { - value: Currency::new(67856467336433871), - address: Address::parse_string(addr_str).unwrap(), - claim_start: Currency::new(123456789), - }; - - // binary - let output_serialized = to_bytes(&output).unwrap(); - let output_deserialized: SiafundOutput = from_reader(&mut &output_serialized[..]).unwrap(); - assert_eq!( - output_serialized, - [ - 7, 0, 0, 0, 0, 0, 0, 0, 241, 19, 24, 247, 77, 16, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, - 0, 0, 0, 7, 91, 205, 21 - ] - ); - assert_eq!(output_deserialized, output); - - // json - let output_serialized = serde_json::to_string(&output).unwrap(); - let output_deserialized: SiafundOutput = serde_json::from_str(&output_serialized).unwrap(); - assert_eq!( - output_serialized, - format!( - "{{\"value\":\"67856467336433871\",\"address\":\"{}\",\"claimStart\":\"123456789\"}}", - addr_str - ) - ); - assert_eq!(output_deserialized, output); - } - - #[test] - fn test_serialize_filecontract() { - let contract = FileContract { - file_size: 1, - file_merkle_root: Hash256::from([ - 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ]), - window_start: 2, - window_end: 3, - payout: Currency::new(456), - valid_proof_outputs: vec![SiacoinOutput { - value: Currency::new(789), - address: Address::new([ - 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]), - }], - missed_proof_outputs: vec![SiacoinOutput { - value: Currency::new(101112), - address: Address::new([ - 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]), - }], - unlock_hash: Hash256::from([ - 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ]), - revision_number: 4, - }; - - // binary - let contract_serialized = to_bytes(&contract).unwrap(); - let contract_deserialized: FileContract = - from_reader(&mut &contract_serialized[..]).unwrap(); - assert_eq!( - contract_serialized, - [ - 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, - 2, 0, 0, 0, 0, 0, 0, 0, 1, 200, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, - 21, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 138, 248, 3, - 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0 - ], - ); - assert_eq!(contract_deserialized, contract); - - // json - let contract_serialized = serde_json::to_string(&contract).unwrap(); - let contract_deserialized: FileContract = - serde_json::from_str(&contract_serialized).unwrap(); - assert_eq!(contract_serialized, "{\"filesize\":1,\"fileMerkleRoot\":\"h:0101010000000000000000000000000000000000000000000000000000000000\",\"windowStart\":2,\"windowEnd\":3,\"payout\":\"456\",\"validProofOutputs\":[{\"value\":\"789\",\"address\":\"addr:02020200000000000000000000000000000000000000000000000000000000008749787b31db\"}],\"missedProofOutputs\":[{\"value\":\"101112\",\"address\":\"addr:0303030000000000000000000000000000000000000000000000000000000000c596d559a239\"}],\"unlockHash\":\"h:0404040000000000000000000000000000000000000000000000000000000000\",\"revisionNumber\":4}"); - assert_eq!(contract_deserialized, contract); - } - - #[test] - fn test_serialize_filecontract_revision() { - let revision = FileContractRevision { - parent_id: FileContractID::from([ - 9, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ]), - file_size: 1, - file_merkle_root: Hash256::from([ - 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ]), - window_start: 2, - window_end: 3, - valid_proof_outputs: vec![SiacoinOutput { - value: Currency::new(789), - address: Address::new([ - 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]), - }], - missed_proof_outputs: vec![SiacoinOutput { - value: Currency::new(789), - address: Address::new([ - 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]), - }], - unlock_conditions: UnlockConditions::new( - 123, - vec![UnlockKey::new( - Algorithm::ed25519(), - PublicKey::new([ - 0x9a, 0xac, 0x1f, 0xfb, 0x1c, 0xfd, 0x10, 0x79, 0xa8, 0xc6, 0xc8, 0x7b, - 0x47, 0xda, 0x1d, 0x56, 0x7e, 0x35, 0xb9, 0x72, 0x34, 0x99, 0x3c, 0x28, - 0x8c, 0x1a, 0xd0, 0xdb, 0x1d, 0x1c, 0xe1, 0xb6, - ]), - )], - 1, - ), - unlock_hash: Hash256::from([ - 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ]), - revision_number: 4, - }; - - // binary - let revision_serialized = to_bytes(&revision).unwrap(); - let revision_deserialized: FileContractRevision = - from_reader(&mut &revision_serialized[..]).unwrap(); - assert_eq!( - revision_serialized, - [ - 9, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 100, 50, 53, 53, - 49, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 154, 172, 31, 251, 28, - 253, 16, 121, 168, 198, 200, 123, 71, 218, 29, 86, 126, 53, 185, 114, 52, 153, 60, - 40, 140, 26, 208, 219, 29, 28, 225, 182, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 21, 2, 2, 2, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 21, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ], - ); - assert_eq!(revision_deserialized, revision); - - // json - let revision_serialized = serde_json::to_string(&revision).unwrap(); - let revision_deserialized: FileContractRevision = - serde_json::from_str(&revision_serialized).unwrap(); - assert_eq!(revision_serialized, "{\"parentID\":\"fcid:0908070000000000000000000000000000000000000000000000000000000000\",\"unlockConditions\":{\"timelock\":123,\"publicKeys\":[\"ed25519:9aac1ffb1cfd1079a8c6c87b47da1d567e35b97234993c288c1ad0db1d1ce1b6\"],\"signaturesRequired\":1},\"revisionNumber\":4,\"filesize\":1,\"fileMerkleRoot\":\"h:0101010000000000000000000000000000000000000000000000000000000000\",\"windowStart\":2,\"windowEnd\":3,\"validProofOutputs\":[{\"value\":\"789\",\"address\":\"addr:02020200000000000000000000000000000000000000000000000000000000008749787b31db\"}],\"missedProofOutputs\":[{\"value\":\"789\",\"address\":\"addr:0303030000000000000000000000000000000000000000000000000000000000c596d559a239\"}],\"unlockHash\":\"h:0404040000000000000000000000000000000000000000000000000000000000\"}"); - assert_eq!(revision_deserialized, revision); - } - - #[test] - fn test_serialize_storage_proof() { - let storage_proof = StorageProof { - parent_id: FileContractID::parse_string( - "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24", - ) - .unwrap(), - leaf: Leaf::from([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, - ]), - proof: vec![ - Hash256::parse_string( - "0102030000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - Hash256::parse_string( - "0405060000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - ], - }; - - // binary - let storage_proof_serialized = to_bytes(&storage_proof).unwrap(); - let storage_proof_deserialized: StorageProof = - from_reader(&mut &storage_proof_serialized[..]).unwrap(); - assert_eq!( - storage_proof_serialized, - [ - 179, 99, 58, 19, 112, 167, 32, 2, 174, 42, 149, 109, 33, 232, 212, 129, 195, 166, - 158, 20, 102, 51, 71, 12, 246, 37, 236, 216, 63, 222, 170, 36, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 2, 0, 0, 0, 0, 0, 0, 0, - 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 - ] - ); - assert_eq!(storage_proof_deserialized, storage_proof); - - // json - let storage_proof_serialized = serde_json::to_string(&storage_proof).unwrap(); - let storage_proof_deserialized: StorageProof = - serde_json::from_str(&storage_proof_serialized).unwrap(); - assert_eq!(storage_proof_serialized, "{\"parentID\":\"fcid:b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24\",\"leaf\":\"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40\",\"proof\":[\"h:0102030000000000000000000000000000000000000000000000000000000000\",\"h:0405060000000000000000000000000000000000000000000000000000000000\"]}"); - assert_eq!(storage_proof_deserialized, storage_proof); - } - - #[test] - fn test_transaction_id() { - let txn = Transaction::default(); - let h = Params::new() - .hash_length(32) - .to_state() - .update(&txn.encode_no_sigs()) - .finalize(); - - assert_eq!( - txn.encode_no_sigs(), - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] - ); - let buf = h.as_bytes(); - assert_eq!( - hex::encode(buf), - "b3633a1370a72002ae2a956d21e8d481c3a69e146633470cf625ecd83fdeaa24" - ); - } - - #[test] - fn test_whole_sig_hash() { - let state = SigningState { - index: ChainIndex { - height: 0, - id: [0; 32], - }, - median_timestamp: SystemTime::now(), - hardforks: NetworkHardforks { - asic_height: 0, - foundation_height: 0, - v2_allow_height: 1000, - v2_require_height: 1000, - }, - }; - let pk = PrivateKey::from_seed(&[ - 136, 215, 58, 248, 45, 30, 78, 97, 128, 111, 82, 204, 43, 233, 223, 111, 110, 29, 73, - 157, 52, 25, 242, 96, 131, 16, 187, 22, 232, 107, 17, 205, - ]); - let test_cases = vec![ - ( - Transaction { - siacoin_inputs: vec![ - SiacoinInput{ - parent_id: SiacoinOutputID::from([32,11,215,36,166,174,135,0,92,215,179,18,74,229,52,154,221,194,213,216,219,47,225,205,251,84,248,2,69,252,37,117]), - unlock_conditions: UnlockConditions::standard_unlock_conditions(pk.public_key()), - } - ], - siacoin_outputs: vec![ - SiacoinOutput{ - value: Currency::new(67856467336433871), - address: Address::parse_string("addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69").unwrap(), - } - ], - file_contracts: Vec::new(), - file_contract_revisions: Vec::new(), - storage_proofs: Vec::new(), - siafund_inputs: Vec::new(), - siafund_outputs: Vec::new(), - miner_fees: Vec::new(), - arbitrary_data: Vec::new(), - signatures: Vec::new(), - }, - "h:a4b1855c546db7ec902237f730717faae96187db8ce9fe139504323a639f731e" - ) - ]; - - for (txn, expected) in test_cases { - let sig_hash = txn - .whole_sig_hash( - &state, - &Hash256::from(>::into( - txn.siacoin_inputs[0].parent_id, - )), - 0, - 0, - &vec![], - ) - .expect("expect tranasction to hash"); - - assert_eq!(sig_hash.to_string(), expected) - } - } - - #[test] - fn test_transaction_sign() { - let state = SigningState { - index: ChainIndex { - height: 0, - id: [0; 32], - }, - median_timestamp: SystemTime::now(), - hardforks: NetworkHardforks { - asic_height: 0, - foundation_height: 0, - v2_allow_height: 1000, - v2_require_height: 1000, - }, - }; - let pk = PrivateKey::from_seed(&[ - 136, 215, 58, 248, 45, 30, 78, 97, 128, 111, 82, 204, 43, 233, 223, 111, 110, 29, 73, - 157, 52, 25, 242, 96, 131, 16, 187, 22, 232, 107, 17, 205, - ]); - let test_cases = vec![ - ( - Transaction { - siacoin_inputs: vec![ - SiacoinInput{ - parent_id: SiacoinOutputID::from([32,11,215,36,166,174,135,0,92,215,179,18,74,229,52,154,221,194,213,216,219,47,225,205,251,84,248,2,69,252,37,117]), - unlock_conditions: UnlockConditions::standard_unlock_conditions(pk.public_key()), - } - ], - siacoin_outputs: vec![ - SiacoinOutput{ - value: Currency::new(67856467336433871), - address: Address::parse_string("addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69").unwrap(), - } - ], - file_contracts: Vec::new(), - file_contract_revisions: Vec::new(), - storage_proofs: Vec::new(), - siafund_inputs: Vec::new(), - siafund_outputs: Vec::new(), - miner_fees: Vec::new(), - arbitrary_data: Vec::new(), - signatures: Vec::new(), - }, - "sig:7a5db98318b5ecad2954d41ba2084c908823ebf4000b95543f352478066a0d04bf4829e3e6d086b42ff9d943f68981c479798fd42bf6f63dac254f4294a37609" - ) - ]; - - for (txn, expected) in test_cases { - let sig = txn - .sign( - &state, - &CoveredFields::whole_transaction(), - Hash256::from(>::into( - txn.siacoin_inputs[0].parent_id, - )), - 0, - 0, - &pk, - ) - .expect(""); - - assert_eq!(sig.signature.to_string(), expected) - } - } - - #[test] - fn test_serialize_transaction() { - let transaction = Transaction { - siacoin_inputs: Vec::new(), - siacoin_outputs: Vec::new(), - file_contracts: Vec::new(), - file_contract_revisions: Vec::new(), - storage_proofs: Vec::new(), - siafund_inputs: Vec::new(), - siafund_outputs: Vec::new(), - miner_fees: Vec::new(), - arbitrary_data: Vec::new(), - signatures: Vec::new(), - }; - - // binary - let transaction_serialized = to_bytes(&transaction).unwrap(); - let transaction_deserialized: Transaction = - from_reader(&mut &transaction_serialized[..]).unwrap(); - assert_eq!( - transaction_serialized, - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] - ); - assert_eq!(transaction_deserialized, transaction); - - // json - let transaction_serialized = serde_json::to_string(&transaction).unwrap(); - let transaction_deserialized: Transaction = - serde_json::from_str(&transaction_serialized).unwrap(); - assert_eq!(transaction_serialized, "{\"siacoinInputs\":[],\"siacoinOutputs\":[],\"fileContracts\":[],\"fileContractRevisions\":[],\"storageProofs\":[],\"siafundInputs\":[],\"siafundOutputs\":[],\"minerFees\":[],\"arbitraryData\":[],\"signatures\":[]}"); - assert_eq!(transaction_deserialized, transaction); - } -}