yedad_ladder 0.1.0

Core library for Yedad ladder hash algorithm - digital signature based on double SHA-256 for decentralized networks
Documentation
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
#![warn(missing_docs)]
//! # yedad_ladder
//!
//! Optimized core implementation of the Yedad ladder hash algorithm.
//!
//! This library provides structures and methods for generating a
//! hierarchical hash ladder from a private key, producing verifiable
//! proofs, and securely advancing through ladder steps. It is suitable
//! for deterministic key derivation, proof generation, and lightweight
//! cryptographic operations.

use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use thiserror::Error;

// ===== Constants =====

/// Hash size in bytes (32 bytes for SHA-256)
pub const HASH_SIZE: usize = 32;

/// Number of rounds used for Master Seed generation
pub const MASTER_SEED_ROUNDS: usize = 2048;

/// Number of rounds used for root generation
pub const ROOT_GENERATION_ROUNDS: usize = 2048;

/// Default ladder height (number of steps)
pub const DEFAULT_LADDER_HEIGHT: usize = 100_000;

/// Domain separation tag for master seed hashing
const DOMAIN_MASTER: &[u8] = b"YEDAD_MASTER";

/// Domain separation tag for root hash computation
const DOMAIN_ROOT: &[u8] = b"YEDAD_ROOT";

/// Domain separation tag for ladder step hashing
const DOMAIN_STEP: &[u8] = b"YEDAD_STEP";

/// Type alias for a 32-byte SHA-256 hash
pub type Hash = [u8; HASH_SIZE];

// ===== Errors =====

/// Errors that can occur in Yedad ladder operations.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum YedadError {
    /// Input provided is invalid or has incorrect size.
    #[error("invalid input size")]
    InvalidInputSize,

    /// A provided proof does not match the expected hash.
    #[error("invalid proof")]
    InvalidProof,

    /// Attempted to advance past the end of the ladder.
    #[error("ladder exhausted")]
    LadderExhausted,

    /// Ladder index is invalid (must be greater than zero).
    #[error("invalid ladder index (must be > 0)")]
    InvalidLadderIndex,

    /// Step index is out of the allowed range.
    #[error("step out of range")]
    StepOutOfRange,

    /// Target hash not found in the ladder.
    #[error("hash not found in ladder")]
    HashNotFound,
}

// ===== Internal helper functions =====

/// Compute SHA-256 hash of the input.
#[inline]
fn sha256(data: &[u8]) -> Hash {
    let mut hasher = Sha256::new();
    hasher.update(data);
    let result = hasher.finalize();

    let mut out = [0u8; HASH_SIZE];
    out.copy_from_slice(&result);
    out
}

/// Constant-time equality comparison for hashes using `subtle`.
#[inline]
fn ct_eq(a: &Hash, b: &Hash) -> bool {
    a.ct_eq(b).into()
}

/// Double SHA-256 with domain separation.
#[inline]
fn double_hash_domain(domain: &[u8], data: &[u8]) -> Hash {
    let mut hasher = Sha256::new();
    hasher.update(domain);
    hasher.update(data);
    let first = hasher.finalize();
    sha256(&first)
}

/// Public double hash in the step domain.
#[inline]
pub fn double_hash(data: &[u8]) -> Hash {
    double_hash_domain(DOMAIN_STEP, data)
}

/// Iteratively compute double hashes for a given number of rounds.
fn iterative_double_hash(initial: &[u8], rounds: usize, domain: &[u8]) -> Hash {
    let mut current = double_hash_domain(domain, initial);
    for _ in 1..rounds {
        current = double_hash_domain(domain, &current);
    }
    current
}

/// Find the index of a target hash in a ladder starting from root.
fn find_position(root: Hash, target: &Hash, height: usize) -> Option<usize> {
    let mut current = root;
    for idx in 0..=height {
        if ct_eq(&current, target) {
            return Some(idx);
        }
        current = double_hash(&current);
    }
    None
}

// ===== Core Yedad =====

/// Core Yedad ladder structure holding the master seed.
#[derive(Debug, Clone)]
pub struct Yedad {
    master_seed: Hash,
}

impl Yedad {
    /// Create a new Yedad instance from a private key.
    ///
    /// # Arguments
    /// * `private_key` - Secret input used to derive the master seed.
    pub fn from_private_key<T: AsRef<[u8]>>(private_key: T) -> Self {
        let master_seed =
            iterative_double_hash(private_key.as_ref(), MASTER_SEED_ROUNDS, DOMAIN_MASTER);
        Self { master_seed }
    }

    /// Compute the root hash for a given ladder ID.
    ///
    /// # Errors
    /// Returns `InvalidLadderIndex` if ladder_id is 0.
    pub fn root(&self, ladder_id: u32) -> Result<Hash, YedadError> {
        if ladder_id == 0 {
            return Err(YedadError::InvalidLadderIndex);
        }

        let mut buf = [0u8; HASH_SIZE + 4];
        buf[..HASH_SIZE].copy_from_slice(&self.master_seed);
        buf[HASH_SIZE..].copy_from_slice(&ladder_id.to_be_bytes());

        Ok(iterative_double_hash(
            &buf,
            ROOT_GENERATION_ROUNDS,
            DOMAIN_ROOT,
        ))
    }

    /// Compute the hash at a specific step index of a ladder.
    pub fn step(&self, ladder_id: u32, step_index: usize) -> Result<Hash, YedadError> {
        let mut current = self.root(ladder_id)?;
        for _ in 0..step_index {
            current = double_hash(&current);
        }
        Ok(current)
    }

    /// Compute the last hash (top of the ladder) for a given ladder.
    pub fn last_step(&self, ladder_id: u32, ladder_height: usize) -> Result<Hash, YedadError> {
        self.step(ladder_id, ladder_height)
    }

    /// Get the proof (hash) for a given position in the ladder.
    pub fn get_proof(
        &self,
        ladder_id: u32,
        position: usize,
        ladder_height: usize,
    ) -> Result<Hash, YedadError> {
        if position >= ladder_height {
            return Err(YedadError::LadderExhausted);
        }
        let step_index = ladder_height - 1 - position;
        self.step(ladder_id, step_index)
    }

    /// Find the previous proof for a given current hash.
    pub fn find_proof_by_current_hash(
        &self,
        ladder_id: u32,
        current_hash: &Hash,
        ladder_height: usize,
    ) -> Result<Hash, YedadError> {
        let root = self.root(ladder_id)?;
        if let Some(idx) = find_position(root, current_hash, ladder_height) {
            if idx == 0 {
                return Err(YedadError::StepOutOfRange);
            }
            let mut prev = root;
            let mut cur = double_hash(&prev);
            for _ in 1..idx {
                prev = cur;
                cur = double_hash(&cur);
            }
            Ok(prev)
        } else {
            Err(YedadError::HashNotFound)
        }
    }

    /// Get reference to the master seed.
    pub fn master_seed(&self) -> &Hash {
        &self.master_seed
    }
}

// ===== Slot =====

/// Represents a single ladder slot, maintaining current state.
#[derive(Debug, Clone, Copy)]
pub struct Slot {
    current_hash: Hash,
    ladder_id: u32,
    position: usize,
}

impl Slot {
    /// Create a new slot from the top of a ladder.
    pub fn new(yedad: &Yedad, ladder_id: u32, ladder_height: usize) -> Result<Self, YedadError> {
        let current_hash = yedad.last_step(ladder_id, ladder_height)?;
        Ok(Self {
            current_hash,
            ladder_id,
            position: 0,
        })
    }

    /// Restore a slot from a saved state.
    pub fn from_state(current_hash: Hash, ladder_id: u32, position: usize) -> Self {
        Self {
            current_hash,
            ladder_id,
            position,
        }
    }

    /// Verify a proof and advance the slot.
    pub fn verify_and_advance(
        &mut self,
        proof: &Hash,
        ladder_height: usize,
    ) -> Result<(), YedadError> {
        if self.position >= ladder_height {
            return Err(YedadError::LadderExhausted);
        }

        let check = double_hash(proof);
        if !ct_eq(&check, &self.current_hash) {
            return Err(YedadError::InvalidProof);
        }

        self.current_hash = *proof;
        self.position += 1;
        Ok(())
    }

    /// Check if the ladder slot is exhausted.
    pub fn is_exhausted(&self, ladder_height: usize) -> bool {
        self.position >= ladder_height
    }

    /// Remaining steps in the ladder.
    pub fn remaining(&self, ladder_height: usize) -> usize {
        ladder_height.saturating_sub(self.position)
    }

    /// Get the full state of the slot.
    pub fn state(&self) -> (Hash, u32, usize) {
        (self.current_hash, self.ladder_id, self.position)
    }

    /// Get current hash.
    pub fn current_hash(&self) -> &Hash {
        &self.current_hash
    }

    /// Ladder ID.
    pub fn ladder_id(&self) -> u32 {
        self.ladder_id
    }

    /// Current position in the ladder.
    pub fn position(&self) -> usize {
        self.position
    }
}

// ===== Account =====

/// Represents an account with ladder state derived from a private key.
#[derive(Debug, Clone)]
pub struct Account {
    yedad: Yedad,
    ladder_id: u32,
    position: usize,
    current_hash: Hash,
}

impl Account {
    /// Create a new account from a private key.
    pub fn new_private(
        private_key: &[u8],
        ladder_height: usize,
    ) -> Result<(Self, Hash), YedadError> {
        let yedad = Yedad::from_private_key(private_key);
        let ladder_id = 1;
        let current_hash = yedad.last_step(ladder_id, ladder_height)?;

        let account = Self {
            yedad,
            ladder_id,
            position: 0,
            current_hash,
        };

        Ok((account, current_hash))
    }

    /// Load an account from a previous state.
    pub fn load(
        private_key: &[u8],
        ladder_id: u32,
        current_hash: &Hash,
        ladder_height: usize,
    ) -> Result<Self, YedadError> {
        let yedad = Yedad::from_private_key(private_key);
        let root = yedad.root(ladder_id)?;

        if let Some(idx) = find_position(root, current_hash, ladder_height) {
            let position = ladder_height - idx;
            Ok(Self {
                yedad,
                ladder_id,
                position,
                current_hash: *current_hash,
            })
        } else {
            Err(YedadError::HashNotFound)
        }
    }

    /// Get the next proof without advancing.
    pub fn next_proof(&self, ladder_height: usize) -> Result<Hash, YedadError> {
        if self.position >= ladder_height {
            return Err(YedadError::LadderExhausted);
        }
        let proof_index = ladder_height - 1 - self.position;
        self.yedad.step(self.ladder_id, proof_index)
    }

    /// Advance the account by verifying a proof.
    pub fn advance(&mut self, proof: &Hash, ladder_height: usize) -> Result<(), YedadError> {
        if self.position >= ladder_height {
            return Err(YedadError::LadderExhausted);
        }

        let check = double_hash(proof);
        if !ct_eq(&check, &self.current_hash) {
            return Err(YedadError::InvalidProof);
        }

        self.current_hash = *proof;
        self.position += 1;
        Ok(())
    }

    /// Get current ladder state (ID and hash).
    pub fn state(&self) -> (u32, Hash) {
        (self.ladder_id, self.current_hash)
    }

    /// Remaining steps in the ladder.
    pub fn remaining(&self, ladder_height: usize) -> usize {
        ladder_height.saturating_sub(self.position)
    }

    /// Access underlying Yedad instance.
    pub fn yedad(&self) -> &Yedad {
        &self.yedad
    }

    /// Ladder ID.
    pub fn ladder_id(&self) -> u32 {
        self.ladder_id
    }

    /// Current position in the ladder.
    pub fn position(&self) -> usize {
        self.position
    }

    /// Current hash.
    pub fn current_hash(&self) -> &Hash {
        &self.current_hash
    }
}