sia_core 0.4.1

Low-level SDK for interacting with the Sia decentralized storage network
Documentation
use std::fmt::Display;

use crate::blake2::Blake2b256;
use crate::signing::PrivateKey;
use bip39::{Error as MnemonicError, Language, Mnemonic};
use sha2::Digest;
use thiserror::Error;
use zeroize::ZeroizeOnDrop;

#[derive(ZeroizeOnDrop)]
pub struct Seed {
    seed: [u8; 16],
    entropy: [u8; 32],
}

#[derive(Debug, PartialEq, Error)]
pub enum SeedError {
    #[error("failed to parse recovery phrase")]
    MnemonicError(#[from] MnemonicError),
    #[error("invalid length of entropy, must be 16 bytes")]
    InvalidLength,
}

impl Seed {
    pub fn new(s: &str) -> Result<Self, SeedError> {
        let m = Mnemonic::parse_in(Language::English, s)?;
        let buf = m.to_entropy();
        if buf.len() != 16 {
            return Err(SeedError::InvalidLength);
        }
        let mut seed = [0u8; 16];
        seed.copy_from_slice(&buf);
        Ok(Self::from_seed(seed))
    }

    pub fn from_seed(seed: [u8; 16]) -> Self {
        let mut h = Blake2b256::new();
        h.update(seed);

        let entropy: [u8; 32] = h.finalize().into();
        Seed { seed, entropy }
    }

    pub fn entropy(&self) -> &[u8] {
        &self.entropy
    }

    /// Derive a private key from the seed
    pub fn private_key(&self, index: u64) -> PrivateKey {
        let mut h = Blake2b256::new();
        h.update(self.entropy());
        h.update(index.to_le_bytes());
        let hash: [u8; 32] = h.finalize().into();

        PrivateKey::from_seed(&hash)
    }
}

impl Display for Seed {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let m = Mnemonic::from_entropy(&self.seed).expect("invalid seed");
        write!(f, "{}", m)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_seed_from_entropy() {
        let test_cases = vec![
            (
                [
                    23, 154, 249, 239, 129, 81, 216, 147, 144, 163, 207, 136, 238, 88, 11, 253,
                ],
                "bleak style know actor budget endorse dream ketchup material index actual wide",
            ),
            (
                [
                    125, 190, 141, 81, 70, 235, 204, 217, 162, 19, 65, 96, 237, 125, 157, 255,
                ],
                "laundry virtual february miss rubber holiday marriage habit genius hip guess yard",
            ),
            (
                [
                    56, 47, 30, 87, 122, 143, 19, 221, 189, 249, 105, 45, 161, 38, 172, 91,
                ],
                "deal jump noise vital van uphold wave coffee color ankle prison repeat",
            ),
            (
                [
                    68, 205, 7, 92, 32, 16, 228, 222, 144, 102, 94, 12, 179, 15, 67, 251,
                ],
                "dynamic habit strike dizzy atom hungry dose slim arrow observe special wash",
            ),
            (
                [
                    113, 45, 77, 233, 42, 222, 26, 5, 158, 171, 102, 114, 10, 7, 178, 19,
                ],
                "illness heavy kid fiber ticket actress kingdom holiday improve expand uncle chest",
            ),
            (
                [
                    189, 194, 141, 142, 240, 157, 147, 143, 61, 104, 167, 223, 33, 191, 95, 226,
                ],
                "saddle behave glove thrive summer shy volcano belt tennis assume subject series",
            ),
            (
                [
                    156, 21, 48, 230, 15, 181, 230, 46, 105, 106, 91, 163, 205, 77, 45, 150,
                ],
                "orchard praise define buyer fury blame pizza enter phrase heavy enter collect",
            ),
            (
                [
                    14, 38, 59, 189, 192, 248, 53, 60, 139, 36, 93, 58, 42, 156, 174, 238,
                ],
                "athlete crack urge limit local oxygen clutch merry demand female close talent",
            ),
            (
                [
                    141, 252, 20, 155, 232, 12, 56, 225, 252, 11, 92, 219, 9, 189, 23, 179,
                ],
                "mistake thing cheap source seminar ill usual high swallow evil echo grid",
            ),
            (
                [
                    25, 192, 89, 200, 149, 97, 136, 115, 38, 103, 19, 229, 88, 165, 62, 169,
                ],
                "border actress impulse client blush define office tiny torch share exile famous",
            ),
            (
                [
                    255, 43, 106, 70, 38, 84, 73, 72, 184, 0, 154, 228, 158, 156, 171, 32,
                ],
                "you forget muscle erosion duty picture theme battle tonight visual client double",
            ),
            (
                [
                    222, 237, 244, 22, 242, 80, 27, 122, 27, 91, 110, 101, 44, 200, 107, 151,
                ],
                "ten hurry aisle tool accuse rug hope horse gown green brain comfort",
            ),
            (
                [
                    96, 74, 173, 157, 208, 13, 130, 1, 168, 248, 254, 178, 92, 220, 59, 233,
                ],
                "gate fever guess parade subway absorb physical cabin rather tragic auction spread",
            ),
            (
                [
                    176, 115, 232, 168, 178, 206, 187, 177, 117, 105, 82, 1, 211, 62, 184, 132,
                ],
                "race palm clay grain two suffer stick clean achieve okay purchase anger",
            ),
            (
                [
                    162, 226, 161, 55, 247, 115, 251, 40, 6, 205, 151, 77, 203, 35, 63, 198,
                ],
                "pepper bench evil upon distance neglect brass real evidence flip soup mind",
            ),
            (
                [
                    199, 68, 177, 121, 94, 197, 135, 255, 140, 56, 181, 119, 99, 179, 124, 65,
                ],
                "shrug cereal furnace rural flash zone couch birth jazz budget tenant lock",
            ),
            (
                [
                    149, 139, 33, 29, 170, 228, 57, 90, 209, 219, 67, 202, 162, 198, 242, 138,
                ],
                "night flip electric fiction drum pulp electric half skirt bike royal benefit",
            ),
            (
                [
                    32, 138, 79, 90, 166, 241, 239, 197, 108, 63, 107, 211, 140, 3, 80, 129,
                ],
                "calm fame stove evil bus tired rail uniform squeeze gas stage acoustic",
            ),
            (
                [
                    231, 198, 66, 33, 240, 199, 105, 69, 236, 87, 87, 250, 128, 220, 227, 145,
                ],
                "treat craft mask thunder isolate pepper rally turtle whisper alone decline card",
            ),
            (
                [
                    130, 4, 98, 53, 124, 85, 66, 215, 112, 229, 188, 157, 95, 195, 49, 201,
                ],
                "link cart minute weather feature hill seminar resource outer wrap small narrow",
            ),
            (
                [
                    228, 84, 238, 177, 92, 231, 129, 253, 139, 4, 83, 68, 252, 160, 139, 22,
                ],
                "tone polar proof right job yard clown media eager topic carpet cluster",
            ),
            (
                [
                    227, 39, 94, 239, 75, 67, 63, 122, 188, 27, 58, 162, 126, 135, 55, 250,
                ],
                "tobacco depend rookie notable crop run vacant guard pen vintage social visual",
            ),
            (
                [
                    228, 96, 128, 106, 248, 223, 176, 232, 179, 247, 6, 219, 81, 173, 27, 141,
                ],
                "tongue advice boy vast wild inmate sound this swap miracle eight bottom",
            ),
            (
                [
                    204, 38, 228, 251, 95, 106, 131, 62, 103, 254, 162, 86, 97, 48, 254, 222,
                ],
                "slow damp disagree salute popular palace paper stairs filter another distance sadness",
            ),
        ];

        for (entropy, expected) in test_cases {
            let seed = Seed::from_seed(entropy);
            assert_eq!(seed.to_string(), expected.to_string());
        }
    }

    #[test]
    fn test_seed_private_key() {
        const PHRASE: &str =
            "wealth salon venue abstract blossom hollow south over accuse bunker guide saddle";
        let test_addresses = vec![
			(0, hex::decode("e313a1aa2dbe411b5335ced5592e87cb002f47a874e27e9cb90eab285c675e366d29b52b7b312fb5e4f657afd0105d3d6dcc5c326131a033597501d25612789f").unwrap()),
			(1, hex::decode("0a909bf1d36c876cb776b81e19c8b4a1351c644e329db3be07f6dfce59b75f4d3fa53cfea6763b07cc4202a0ba36574d99fa6ca3f807dbff2f2266c4d0a0a76d").unwrap()),
			(2, hex::decode("866b40a6ee117ab8e65ee0772ca4e463e98edbf0793beae08a784745e7f10554294324450371bb263bc02c4536a04afa355ca490ef6481fd682dfd44bdb0f464").unwrap()),
			(3, hex::decode("f713e2a9cc2415d7069d136c73dd3a67c5f2a63cc04f1106b980d6d6cd816f6bf710d69b256ae23f4b28d1f02f714fed04ea2c9268598835713eec36697bf179").unwrap()),
			(4, hex::decode("433b5bf2c3ec44895af7299148ba38deaa7324c5146821fcef407708abc211bcb12b2a480977ffdc4c3801752b0e2bee06219311b7bdce80189be961f47d7ac9").unwrap()),
			(5, hex::decode("48a4765ece4d7e6b12f4f8b20caaca4b2249654ada2b9d0d31d855517244b1ed8850f06b52e7ce6b5ea061ac6b69f3febb3fc96e58c590c975300fb20f317dcc").unwrap()),
			(6, hex::decode("2de36d94f299ab39511e9eb3fe0cf5cc989b25e2943ca9c3a87ac592831791d76b0ee63d4be3b5296fe3961150b6bc3dd5f0acc56235fb8a62143a7eb73bdaa7").unwrap()),
			(7, hex::decode("ecbd64189b9429583ad62173035cf3680238e5d90727220f55d466e88dc631b70299cbd2b777df0e62099f3f5f913692d022a3faabd461a2933754ec3aa35c21").unwrap()),
			(8, hex::decode("5e836458fccb204dfe0e300c66ca2c47ad7efe9f835cda99d1a3cf22cf642634d53903b6ba22cf84adcae25f3d27d90323017ff793115b559df26fc0a4450cf4").unwrap()),
			(9, hex::decode("9abd3e40d6d3b10d36966cec65861d7f08c6aa7f2d2845b0e9f10e15cc9e9f28eb63b79a7719c1a8323fe2d3da06d121ccbda1342d9f0913860f5e0817af1390").unwrap()),
			(4294967295, hex::decode("71945cbc310c189f01fe8727a0060c007528aa0fd4812e4c5c7aa8b0e518906fc7dcf5e7623152e310ed440b8abf02e02fbead45553f13b3e8a7bea78a16d1b8").unwrap()),
			(4294967296, hex::decode("41602321666c7ba93b05729208897ddf89940afdf02c38ebbd88f0a4906839232cd126afb5ccaef91ab77fcc27d076d94c5d152729bde3794bcd03226679889c").unwrap()),
			(18446744073709551615, hex::decode("c03b2570cb69e300cd4ccbe0c4d8ee7b8ccfad7383f10aa2df52a4a9d05ab843d39fbd56458e94d711061748a051d434d2200e1af71df56070d2df0883453b2c").unwrap()),
		];

        let seed = Seed::new(PHRASE).unwrap();
        for (i, expected) in test_addresses {
            let pk = seed.private_key(i);
            assert_eq!(pk.as_ref(), expected, "index {i}");
        }
    }

    #[test]
    fn test_seed_public_key() {
        const PHRASE: &str =
            "wealth salon venue abstract blossom hollow south over accuse bunker guide saddle";
        let test_addresses = vec![
			(0, hex::decode("e313a1aa2dbe411b5335ced5592e87cb002f47a874e27e9cb90eab285c675e366d29b52b7b312fb5e4f657afd0105d3d6dcc5c326131a033597501d25612789f").unwrap()),
			(1, hex::decode("0a909bf1d36c876cb776b81e19c8b4a1351c644e329db3be07f6dfce59b75f4d3fa53cfea6763b07cc4202a0ba36574d99fa6ca3f807dbff2f2266c4d0a0a76d").unwrap()),
			(2, hex::decode("866b40a6ee117ab8e65ee0772ca4e463e98edbf0793beae08a784745e7f10554294324450371bb263bc02c4536a04afa355ca490ef6481fd682dfd44bdb0f464").unwrap()),
			(3, hex::decode("f713e2a9cc2415d7069d136c73dd3a67c5f2a63cc04f1106b980d6d6cd816f6bf710d69b256ae23f4b28d1f02f714fed04ea2c9268598835713eec36697bf179").unwrap()),
			(4, hex::decode("433b5bf2c3ec44895af7299148ba38deaa7324c5146821fcef407708abc211bcb12b2a480977ffdc4c3801752b0e2bee06219311b7bdce80189be961f47d7ac9").unwrap()),
			(5, hex::decode("48a4765ece4d7e6b12f4f8b20caaca4b2249654ada2b9d0d31d855517244b1ed8850f06b52e7ce6b5ea061ac6b69f3febb3fc96e58c590c975300fb20f317dcc").unwrap()),
			(6, hex::decode("2de36d94f299ab39511e9eb3fe0cf5cc989b25e2943ca9c3a87ac592831791d76b0ee63d4be3b5296fe3961150b6bc3dd5f0acc56235fb8a62143a7eb73bdaa7").unwrap()),
			(7, hex::decode("ecbd64189b9429583ad62173035cf3680238e5d90727220f55d466e88dc631b70299cbd2b777df0e62099f3f5f913692d022a3faabd461a2933754ec3aa35c21").unwrap()),
			(8, hex::decode("5e836458fccb204dfe0e300c66ca2c47ad7efe9f835cda99d1a3cf22cf642634d53903b6ba22cf84adcae25f3d27d90323017ff793115b559df26fc0a4450cf4").unwrap()),
			(9, hex::decode("9abd3e40d6d3b10d36966cec65861d7f08c6aa7f2d2845b0e9f10e15cc9e9f28eb63b79a7719c1a8323fe2d3da06d121ccbda1342d9f0913860f5e0817af1390").unwrap()),
			(4294967295, hex::decode("71945cbc310c189f01fe8727a0060c007528aa0fd4812e4c5c7aa8b0e518906fc7dcf5e7623152e310ed440b8abf02e02fbead45553f13b3e8a7bea78a16d1b8").unwrap()),
			(4294967296, hex::decode("41602321666c7ba93b05729208897ddf89940afdf02c38ebbd88f0a4906839232cd126afb5ccaef91ab77fcc27d076d94c5d152729bde3794bcd03226679889c").unwrap()),
			(18446744073709551615, hex::decode("c03b2570cb69e300cd4ccbe0c4d8ee7b8ccfad7383f10aa2df52a4a9d05ab843d39fbd56458e94d711061748a051d434d2200e1af71df56070d2df0883453b2c").unwrap()),
		];

        let seed = Seed::new(PHRASE).unwrap();
        for (i, expected) in test_addresses {
            let pk = seed.private_key(i).public_key();
            assert_eq!(pk.as_ref(), &expected[32..], "index {i}");
        }
    }
}