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
//! Various accumulator variants of the DAPOL+ protocol.
//!
//! This is the top-most module in the hierarchy of the [dapol] crate. An
//! accumulator defines how the binary tree is built. There are different types
//! of accumulators, which can all be found under this module. Each accumulator
//! has different configuration requirements, which are detailed in each of the
//! sub-modules. The currently supported accumulator types are:
//! - [Non-Deterministic Mapping Sparse Merkle Tree]
//!
//! Accumulators can be constructed via the configuration parsers:
//! - [AccumulatorConfig] is used to deserialize config from a file (the
//! specific type of accumulator is determined from the config file). After
//! parsing the config the accumulator can be constructed.
//! - [NdmSmtConfigBuilder] is used to construct the
//! config for the NDM-SMT accumulator type using a builder pattern. The config
//! can then be parsed to construct an NDM-SMT.
//!
//! [Non-Deterministic Mapping Sparse Merkle Tree]: crate::accumulators::NdmSmt

use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::{
    read_write_utils::{self, ReadWriteError},
    utils::LogOnErr,
    AggregationFactor, EntityId, InclusionProof,
};

mod config;
pub use config::{AccumulatorConfig, AccumulatorConfigError, AccumulatorParserError};

mod ndm_smt;
pub use ndm_smt::{
    NdmSmt, NdmSmtConfig, NdmSmtConfigBuilder, NdmSmtError, NdmSmtConfigParserError, NdmSmtSecrets,
    NdmSmtSecretsParser, RandomXCoordGenerator
};

const SERIALIZED_ACCUMULATOR_EXTENSION: &str = "dapoltree";
const SERIALIZED_ACCUMULATOR_FILE_PREFIX: &str = "accumulator_";

/// Various supported accumulator types.
///
/// Accumulators can be constructed via the configuration parsers:
/// - [AccumulatorConfig] is used to deserialize config from a file (the
/// specific type of accumulator is determined from the config file). After
/// parsing the config the accumulator can be constructed.
/// - [NdmSmtConfigBuilder] is used to construct the
/// config for the NDM-SMT accumulator type using a builder pattern. The config
/// can then be parsed to construct an NDM-SMT.
#[derive(Serialize, Deserialize)]
pub enum Accumulator {
    NdmSmt(ndm_smt::NdmSmt),
    // TODO other accumulators..
}

impl Accumulator {
    /// Try deserialize an accumulator from the given file path.
    ///
    /// The file is assumed to be in [bincode] format.
    ///
    /// An error is logged and returned if
    /// 1. The file cannot be opened.
    /// 2. The [bincode] deserializer fails.
    pub fn deserialize(path: PathBuf) -> Result<Accumulator, AccumulatorError> {
        debug!(
            "Deserializing accumulator from file {:?}",
            path.clone().into_os_string()
        );

        match path.extension() {
            Some(ext) => {
                if ext != SERIALIZED_ACCUMULATOR_EXTENSION {
                    Err(ReadWriteError::UnsupportedFileExtension {
                        expected: SERIALIZED_ACCUMULATOR_EXTENSION.to_owned(),
                        actual: ext.to_os_string(),
                    })?;
                }
            }
            None => Err(ReadWriteError::NotAFile(path.clone().into_os_string()))?,
        }

        let accumulator: Accumulator =
            read_write_utils::deserialize_from_bin_file(path.clone()).log_on_err()?;

        let root_hash = match &accumulator {
            Accumulator::NdmSmt(ndm_smt) => ndm_smt.root_hash(),
        };

        info!(
            "Successfully deserialized accumulator from file {:?} with root hash {:?}",
            path.clone().into_os_string(),
            root_hash
        );

        Ok(accumulator)
    }

    /// Parse `path` as one that points to a serialized dapol tree file.
    ///
    /// `path` can be either of the following:
    /// 1. Existing directory: in this case a default file name is appended to
    /// `path`. 2. Non-existing directory: in this case all dirs in the path
    /// are created, and a default file name is appended.
    /// 3. File in existing dir: in this case the extension is checked to be
    /// [SERIALIZED_ACCUMULATOR_EXTENSION], then `path` is returned.
    /// 4. File in non-existing dir: dirs in the path are created and the file
    /// extension is checked.
    ///
    /// The file prefix is [SERIALIZED_ACCUMULATOR_FILE_PREFIX].
    ///
    /// Example:
    /// ```
    /// use dapol::Accumulator;
    /// use std::path::PathBuf;
    ///
    /// let dir = PathBuf::from("./");
    /// let path = Accumulator::parse_accumulator_serialization_path(dir).unwrap();
    /// ```
    pub fn parse_accumulator_serialization_path(path: PathBuf) -> Result<PathBuf, ReadWriteError> {
        read_write_utils::parse_serialization_path(
            path,
            SERIALIZED_ACCUMULATOR_EXTENSION,
            SERIALIZED_ACCUMULATOR_FILE_PREFIX,
        )
    }

    /// Serialize to a file.
    ///
    /// Serialization is done using [bincode]
    ///
    /// An error is returned if
    /// 1. [bincode] fails to serialize the file.
    /// 2. There is an issue opening or writing the file.
    pub fn serialize(&self, path: PathBuf) -> Result<(), AccumulatorError> {
        info!(
            "Serializing accumulator to file {:?}",
            path.clone().into_os_string()
        );

        read_write_utils::serialize_to_bin_file(self, path).log_on_err()?;
        Ok(())
    }

    /// Generate an inclusion proof for the given `entity_id`.
    ///
    /// `aggregation_factor` is used to determine how many of the range proofs
    /// are aggregated. Those that do not form part of the aggregated proof
    /// are just proved individually. The aggregation is a feature of the
    /// Bulletproofs protocol that improves efficiency.
    ///
    /// `upper_bound_bit_length` is used to determine the upper bound for the
    /// range proof, which is set to `2^upper_bound_bit_length` i.e. the
    /// range proof shows `0 <= liability <= 2^upper_bound_bit_length` for
    /// some liability. The type is set to `u8` because we are not expected
    /// to require bounds higher than $2^256$. Note that if the value is set
    /// to anything other than 8, 16, 32 or 64 the Bulletproofs code will return
    /// an Err.
    pub fn generate_inclusion_proof_with(
        &self,
        entity_id: &EntityId,
        aggregation_factor: AggregationFactor,
        upper_bound_bit_length: u8,
    ) -> Result<InclusionProof, NdmSmtError> {
        match self {
            Accumulator::NdmSmt(ndm_smt) => ndm_smt.generate_inclusion_proof_with(
                entity_id,
                aggregation_factor,
                upper_bound_bit_length,
            ),
        }
    }

    /// Generate an inclusion proof for the given `entity_id`.
    pub fn generate_inclusion_proof(
        &self,
        entity_id: &EntityId,
    ) -> Result<InclusionProof, NdmSmtError> {
        match self {
            Accumulator::NdmSmt(ndm_smt) => ndm_smt.generate_inclusion_proof(entity_id),
        }
    }
}

/// Errors encountered when handling an [Accumulator].
#[derive(thiserror::Error, Debug)]
pub enum AccumulatorError {
    #[error("Error serializing/deserializing file")]
    SerdeError(#[from] ReadWriteError),
}

// NOTE no unit tests here because this code is tested in the integration tests.