Wire Sysio Wire Sysion 1.0.0
Loading...
Searching...
No Matches
test_modular_arithmetic.cpp File Reference
#include <boost/test/included/unit_test.hpp>
#include <fc/exception/exception.hpp>
#include <fc/crypto/hex.hpp>
#include <fc/crypto/modular_arithmetic.hpp>
#include <fc/utility.hpp>
#include <chrono>
#include <random>
#include <limits>
#include "test_utils.hpp"
Include dependency graph for test_modular_arithmetic.cpp:

Go to the source code of this file.

Namespaces

namespace  std
 

Macros

#define BOOST_TEST_MODULE   modular_arithmetic
 

Functions

std::ostream & std::operator<< (std::ostream &st, const std::variant< fc::modular_arithmetic_error, bytes > &err)
 
 BOOST_AUTO_TEST_CASE (modexp)
 
 FC_LOG_AND_RETHROW ()
 
 BOOST_AUTO_TEST_CASE (modexp_benchmarking)
 

Macro Definition Documentation

◆ BOOST_TEST_MODULE

#define BOOST_TEST_MODULE   modular_arithmetic

Definition at line 1 of file test_modular_arithmetic.cpp.

Function Documentation

◆ BOOST_AUTO_TEST_CASE() [1/2]

BOOST_AUTO_TEST_CASE ( modexp )

Definition at line 30 of file test_modular_arithmetic.cpp.

30 {
31
32
33 using modexp_test = std::tuple<std::vector<string>, std::variant<fc::modular_arithmetic_error, bytes>>;
34
35 const std::vector<modexp_test> tests {
36 //test1
37 {
38 {
39 "03",
40 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
41 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
42 },
43 to_bytes("0000000000000000000000000000000000000000000000000000000000000001"),
44 },
45
46 //test2
47 {
48 {
49 "",
50 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
51 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
52 },
53 to_bytes("0000000000000000000000000000000000000000000000000000000000000000")
54 },
55
56 //test3
57 {
58 {
59 "01",
60 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
61 "",
62 },
63 modular_arithmetic_error::modulus_len_zero
64 },
65
66 //test4
67 {
68 {
69 "01",
70 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
71 "0000",
72 },
73 to_bytes("0000")
74 },
75
76 //test5
77 {
78 {
79 "00",
80 "00",
81 "0F",
82 },
83 to_bytes("01"),
84 },
85
86 //test6
87 {
88 {
89 "00",
90 "01",
91 "0F",
92 },
93 to_bytes("00"),
94 },
95
96 //test7
97 {
98 {
99 "01",
100 "00",
101 "0F",
102 },
103 to_bytes("01"),
104 },
105
106 };
107
108 for(const auto& test : tests) {
109 const auto& parts = std::get<0>(test);
110 const auto& expected_result = std::get<1>(test);
111
112 auto base = to_bytes(parts[0]);
113 auto exponent = to_bytes(parts[1]);
114 auto modulus = to_bytes(parts[2]);
115
116 auto res = fc::modexp(base, exponent, modulus);
117 BOOST_CHECK_EQUAL(res, expected_result);
118 }
119
std::variant< modular_arithmetic_error, bytes > modexp(const bytes &_base, const bytes &_exponent, const bytes &_modulus)
const unsigned char expected_result[]
Definition ssh.c:73
FC_LOG_AND_RETHROW()
bytes to_bytes(const std::string &source)
Definition test_utils.hpp:6
Here is the call graph for this function:

◆ BOOST_AUTO_TEST_CASE() [2/2]

BOOST_AUTO_TEST_CASE ( modexp_benchmarking )

Definition at line 122 of file test_modular_arithmetic.cpp.

122 {
123
124 std::mt19937 r(0x11223344);
125
126 auto generate_random_bytes = [](std::mt19937& rand_eng, unsigned int num_bytes) {
127 std::vector<char> result(num_bytes);
128
129 uint_fast32_t v = 0;
130 for(int byte_pos = 0, end = result.size(); byte_pos < end; ++byte_pos) {
131 if ((byte_pos & 0x03) == 0) { // if divisible by 4
132 v = rand_eng();
133 }
134 result[byte_pos] = v & 0xFF;
135 v >>= 8;
136 }
137
138 return result;
139 };
140
141 static constexpr unsigned int num_trials = 10; // 10000
142
143 static_assert(num_trials > 0);
144
145 static constexpr unsigned int bit_calc_limit = 101; // 120
146
147 static constexpr unsigned int start_num_bytes = 1;
148 static constexpr unsigned int end_num_bytes = 1 << ((bit_calc_limit + 7)/8);
149
150 static_assert(start_num_bytes <= end_num_bytes);
151
152 struct statistics {
153 unsigned int modulus_bit_size; // bit size of modulus and base
154 unsigned int exponent_bit_size; // bit size of exponent
155 int64_t min_time_ns;
156 int64_t max_time_ns;
157 int64_t avg_time_ns;
158 };
159
160 std::vector<statistics> stats;
161
162 auto ceil_log2 = [](uint32_t n) -> uint32_t {
163 if (n <= 1) {
164 return 0;
165 }
166 return 32 - __builtin_clz(n - 1);
167 };
168
169 BOOST_CHECK(ceil_log2(0) == 0);
170 BOOST_CHECK(ceil_log2(1) == 0);
171 BOOST_CHECK(ceil_log2(2) == 1);
172 BOOST_CHECK(ceil_log2(3) == 2);
173 BOOST_CHECK(ceil_log2(4) == 2);
174 BOOST_CHECK(ceil_log2(5) == 3);
175 BOOST_CHECK(ceil_log2(15) == 4);
176 BOOST_CHECK(ceil_log2(16) == 4);
177 BOOST_CHECK(ceil_log2(17) == 5);
178
179 for (unsigned int n = start_num_bytes; n <= end_num_bytes; n *= 2) {
180 unsigned int bit_calc = 8 * ceil_log2(n);
181 for (unsigned int exponent_num_bytes = 1;
182 exponent_num_bytes <= 2*n && bit_calc <= bit_calc_limit;
183 exponent_num_bytes *= 2, bit_calc += 5)
184 {
185 int64_t min_duration_ns = std::numeric_limits<int64_t>::max();
186 int64_t max_duration_ns = 0;
187 int64_t total_duration_ns = 0;
188
189 for (unsigned int trial = 0; trial < num_trials; ++trial) {
190 auto base = generate_random_bytes(r, n);
191 auto exponent = generate_random_bytes(r, exponent_num_bytes);
192 auto modulus = generate_random_bytes(r, n);
193
194 auto start_time = std::chrono::steady_clock::now();
195
196 auto res = fc::modexp(base, exponent, modulus);
197
198 auto end_time = std::chrono::steady_clock::now();
199
200 int64_t duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time).count();
201
202 //ilog("(${base})^(${exp}) % ${mod} = ${result} [took ${duration} ns]",
203 // ("base", base)("exp", exponent)("mod", modulus)("result", std::get<bytes>(res))("duration", duration_ns)
204 // );
205
206 min_duration_ns = std::min(min_duration_ns, duration_ns);
207 max_duration_ns = std::max(max_duration_ns, duration_ns);
208 total_duration_ns += duration_ns;
209 }
210
211 stats.push_back(statistics{
212 .modulus_bit_size = n * 8,
213 .exponent_bit_size = exponent_num_bytes * 8,
214 .min_time_ns = min_duration_ns,
215 .max_time_ns = max_duration_ns,
216 .avg_time_ns = (total_duration_ns / num_trials),
217 });
218
219 const auto& stat = stats.back();
220
221 ilog("Completed random runs of mod_exp with ${bit_width}-bit width base and modulus values and "
222 "${exp_bit_width}-bit width exponent values. "
223 "Min time: ${min} ns; Average time: ${avg} ns; Max time: ${max} ns.",
224 ("bit_width", stat.modulus_bit_size)("exp_bit_width", stat.exponent_bit_size)
225 ("min", stat.min_time_ns)("avg", stat.avg_time_ns)("max", stat.max_time_ns)
226 );
227
228 }
229 }
230
231 std::string stats_output = "Table (in csv format) summarizing statistics from runs:\n";
232 stats_output += "Modulus/Base Bit Size,Exponent Bit Size,Average Time (ns)\n";
233 for (const auto& stat : stats) {
234 stats_output += std::to_string(stat.modulus_bit_size);
235 stats_output += ',';
236 stats_output += std::to_string(stat.exponent_bit_size);
237 stats_output += ',';
238 stats_output += std::to_string(stat.avg_time_ns);
239 stats_output += '\n';
240 }
241
242 ilog(stats_output);
243
244 // Running the above benchmark (using commented values for num_trials and bit_calc_limit) with a release build on
245 // an AMD 3.4 GHz CPU provides average durations for executing mod_exp for varying bit sizes for the values
246 // (but with base and modulus bit sizes kept equal to one another).
247
248 // Holding the base/modulus bit size constant and increasing the exponent bit size shows a linear relationship with increasing bit
249 // size on the average time to execute the modular exponentiation. The slope of the best fit line to the empirical data appears
250 // to scale super-linearly with base/modulus size. A quadratic (degree 2) fit works okay, but it appears that a better fit is to
251 // model the slope of the linear relationship between average time and exponent bit size as a the base/modulus bit size taken to
252 // the 1.6 power and then scaled by some constant.
253
254 // Holding the exponent bit size constant and increasing the base/modulus bit size shows a super-linear relationship with
255 // increasing bit size on the average time to execute the modular exponentiation. A quadratic relationship works pretty well
256 // but perhaps a fractional exponent between 1 and 2 (e.g. 1.6) would work well as well.
257
258 // What is particularly revealing is plotting the average time with respect to some combination of the bit sizes of base/modulus and
259 // exponent. If the independent variable is the product of the exponent bit size and the base/modulus bit size, the correlation is
260 // not great. Even if the independent variable is the product of the exponent bit size and the base/modulus bit size taken to some power,
261 // the correlation is still not great.
262 // It seems that trying to capture all the data using a model like that breaks down when the exponent bit size is greater than the
263 // base/modulus bit size.
264 // If we filter out all the data points where the exponent bit size is greater than the base/modulus bit size, and then choose as
265 // then independent variable the product of the exponent bit size and the base/modulus bit size taken to some power, then we get
266 // a pretty good linear correlation when a power of 1.6 is chosen.
267
268 // TODO: See if theoretical analysis of the modular exponentiation algorithm also justifies these scaling properties.
269
270 // Example results for average time:
271 // | Modulus/Base Bit Size | Exponent Bit Size | Average Time (ns) |
272 // | --------------------- | ----------------- | ----------------- |
273 // | 2048 | 32 | 33826 |
274 // | 2048 | 256 | 250067 |
275 // | 2048 | 2048 | 1891095 |
276 // | 4096 | 32 | 129181 |
277 // | 4096 | 256 | 954024 |
278 // | 4096 | 2048 | 7205115 |
279 // | 8192 | 32 | 347938 |
280 // | 8192 | 256 | 2503652 |
281 // | 8192 | 2048 | 19199775 |
282
283 // The empirical results show that the average time stays well below 5 ms if the exponent bit size does not exceed the
284 // modulus/base bit size and the product of the exponent bit size and the
285 // (modulus/base bit size)^1.6 does not exceed 550,000,000.
286 // Another way of satisfying that constraint is to require that the 5*ceil(log2(exponent bit size)) + 8*ceil(log2(modulus bit size))
287 // be less than or equal to 5*floor(log2(500000000)) = 145.
288 // Or equivalently, assuming the bit sizes are multiples of 8:
289 // 5*ceil(log2(exponent bit size/8)) + 8*ceil(log2(modulus bit size/8)) <= 106.
290
291 // Take, as an example, a 8192-bit modulus/base and a 128-bit exponent (which on average took 1.29 ms).
292 // 5*ceil(log2(128)) + 8*ceil(log2(8192)) = 5*7 + 8*13 = 139 which is less than the limit of 145.
293 //
294 // Or, as an other example, a 2048-bit modulus/base and a 2048-bit exponent (which on average took 1.89 ms).
295 // 5*ceil(log2(2048)) + 8*ceil(log2(2048)) = 5*11 + 8*11 = 143 which is less than the limit of 145.
296 //
297 // On the other hand, consider a 4096-bit modulus/base and a 1024-bit exponent (which on average took 3.69 ms).
298 // 5*ceil(log2(1024)) + 8*ceil(log2(4096)) = 5*10 + 8*12 = 146 which is greater than the limit of 145.
299
const mie::Vuint & r
Definition bn.cpp:28
#define ilog(FORMAT,...)
Definition logger.hpp:118
long long start_time
Definition profiling.cpp:53
signed __int64 int64_t
Definition stdint.h:135
unsigned int uint32_t
Definition stdint.h:126
uint32_t uint_fast32_t
Definition stdint.h:156
Here is the call graph for this function:

◆ FC_LOG_AND_RETHROW()

FC_LOG_AND_RETHROW ( )