rippled
Loading...
Searching...
No Matches
compression_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright 2020 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <test/jtx/Account.h>
21#include <test/jtx/Env.h>
22#include <test/jtx/WSClient.h>
23#include <test/jtx/amount.h>
24#include <test/jtx/pay.h>
25#include <xrpld/app/ledger/Ledger.h>
26#include <xrpld/app/ledger/LedgerMaster.h>
27#include <xrpld/overlay/Compression.h>
28#include <xrpld/overlay/Message.h>
29#include <xrpld/overlay/detail/Handshake.h>
30#include <xrpld/overlay/detail/ProtocolMessage.h>
31#include <xrpld/overlay/detail/ZeroCopyStream.h>
32#include <xrpld/shamap/SHAMapNodeID.h>
33#include <xrpl/basics/random.h>
34#include <xrpl/beast/unit_test.h>
35#include <xrpl/beast/utility/Journal.h>
36#include <xrpl/protocol/HashPrefix.h>
37#include <xrpl/protocol/PublicKey.h>
38#include <xrpl/protocol/SecretKey.h>
39#include <xrpl/protocol/Sign.h>
40#include <xrpl/protocol/digest.h>
41#include <xrpl/protocol/jss.h>
42#include <xrpl/protocol/messages.h>
43
44#include <boost/asio/ip/address_v4.hpp>
45#include <boost/beast/core/multi_buffer.hpp>
46#include <boost/endian/conversion.hpp>
47
48#include <algorithm>
49
50namespace ripple {
51
52namespace test {
53
54using namespace ripple::test;
55using namespace ripple::test::jtx;
56
57static uint256
59{
60 return ripple::sha512Half(
62 std::uint32_t(info.seq),
64 info.parentHash,
65 info.txHash,
66 info.accountHash,
71}
72
74{
77
78public:
80 {
81 }
82
83 template <typename T>
84 void
87 protocol::MessageType mt,
88 uint16_t nbuffers,
89 std::string msg)
90 {
91 testcase("Compress/Decompress: " + msg);
92
93 Message m(*proto, mt);
94
95 auto& buffer = m.getBuffer(Compressed::On);
96
97 boost::beast::multi_buffer buffers;
98
99 // simulate multi-buffer
100 auto sz = buffer.size() / nbuffers;
101 for (int i = 0; i < nbuffers; i++)
102 {
103 auto start = buffer.begin() + sz * i;
104 auto end = i < nbuffers - 1 ? (buffer.begin() + sz * (i + 1))
105 : buffer.end();
106 std::vector<std::uint8_t> slice(start, end);
107 buffers.commit(boost::asio::buffer_copy(
108 buffers.prepare(slice.size()), boost::asio::buffer(slice)));
109 }
110
111 boost::system::error_code ec;
113 ec, buffers.data(), buffer.size());
114
115 BEAST_EXPECT(header);
116
117 if (!header || header->algorithm == Algorithm::None)
118 return;
119
120 std::vector<std::uint8_t> decompressed;
121 decompressed.resize(header->uncompressed_size);
122
123 BEAST_EXPECT(
124 header->payload_wire_size == buffer.size() - header->header_size);
125
126 ZeroCopyInputStream stream(buffers.data());
127 stream.Skip(header->header_size);
128
129 auto decompressedSize = ripple::compression::decompress(
130 stream,
131 header->payload_wire_size,
132 decompressed.data(),
133 header->uncompressed_size);
134 BEAST_EXPECT(decompressedSize == header->uncompressed_size);
135 auto const proto1 = std::make_shared<T>();
136
137 BEAST_EXPECT(
138 proto1->ParseFromArray(decompressed.data(), decompressedSize));
139 auto uncompressed = m.getBuffer(Compressed::Off);
140 BEAST_EXPECT(std::equal(
141 uncompressed.begin() + ripple::compression::headerBytes,
142 uncompressed.end(),
143 decompressed.begin()));
144 }
145
148 {
149 auto manifests = std::make_shared<protocol::TMManifests>();
150 manifests->mutable_list()->Reserve(n);
151 for (int i = 0; i < n; i++)
152 {
153 auto master = randomKeyPair(KeyType::ed25519);
154 auto signing = randomKeyPair(KeyType::ed25519);
156 st[sfSequence] = i;
157 st[sfPublicKey] = std::get<0>(master);
158 st[sfSigningPubKey] = std::get<0>(signing);
159 st[sfDomain] = makeSlice(
160 std::string("example") + std::to_string(i) +
161 std::string(".com"));
162 sign(
163 st,
166 std::get<1>(master),
167 sfMasterSignature);
168 sign(
169 st,
172 std::get<1>(signing));
173 Serializer s;
174 st.add(s);
175 auto* manifest = manifests->add_list();
176 manifest->set_stobject(s.data(), s.size());
177 }
178 return manifests;
179 }
180
183 {
184 auto endpoints = std::make_shared<protocol::TMEndpoints>();
185 endpoints->mutable_endpoints_v2()->Reserve(n);
186 for (int i = 0; i < n; i++)
187 {
188 auto ep = endpoints->add_endpoints_v2();
189 ep->set_endpoint(std::string("10.0.1.") + std::to_string(i));
190 ep->set_hops(i);
191 }
192 endpoints->set_version(2);
193
194 return endpoints;
195 }
196
199 {
200 Env env(*this, envconfig());
201 int fund = 10000;
202 auto const alice = Account("alice");
203 auto const bob = Account("bob");
204 env.fund(XRP(fund), "alice", "bob");
205 env.trust(bob["USD"](fund), alice);
206 env.close();
207
208 auto toBinary = [this](std::string const& text) {
209 auto blob = strUnHex(text);
210 BEAST_EXPECT(blob);
211 return std::string{
212 reinterpret_cast<char const*>(blob->data()), blob->size()};
213 };
214
215 std::string usdTxBlob = "";
216 auto wsc = makeWSClient(env.app().config());
217 {
218 Json::Value jrequestUsd;
219 jrequestUsd[jss::secret] = toBase58(generateSeed("bob"));
220 jrequestUsd[jss::tx_json] =
221 pay("bob", "alice", bob["USD"](fund / 2));
222 Json::Value jreply_usd = wsc->invoke("sign", jrequestUsd);
223
224 usdTxBlob =
225 toBinary(jreply_usd[jss::result][jss::tx_blob].asString());
226 }
227
228 auto transaction = std::make_shared<protocol::TMTransaction>();
229 transaction->set_rawtransaction(usdTxBlob);
230 transaction->set_status(protocol::tsNEW);
231 transaction->set_receivetimestamp(rand_int<std::uint64_t>());
232 transaction->set_deferred(true);
233
234 return transaction;
235 }
236
239 {
240 auto getLedger = std::make_shared<protocol::TMGetLedger>();
241 getLedger->set_itype(protocol::liTS_CANDIDATE);
242 getLedger->set_ltype(protocol::TMLedgerType::ltACCEPTED);
243 uint256 const hash(ripple::sha512Half(123456789));
244 getLedger->set_ledgerhash(hash.begin(), hash.size());
245 getLedger->set_ledgerseq(123456789);
246 ripple::SHAMapNodeID sha(64, hash);
247 getLedger->add_nodeids(sha.getRawString());
248 getLedger->set_requestcookie(123456789);
249 getLedger->set_querytype(protocol::qtINDIRECT);
250 getLedger->set_querydepth(3);
251 return getLedger;
252 }
253
255 buildLedgerData(uint32_t n, Logs& logs)
256 {
257 auto ledgerData = std::make_shared<protocol::TMLedgerData>();
258 uint256 const hash(ripple::sha512Half(12356789));
259 ledgerData->set_ledgerhash(hash.data(), hash.size());
260 ledgerData->set_ledgerseq(123456789);
261 ledgerData->set_type(protocol::TMLedgerInfoType::liAS_NODE);
262 ledgerData->set_requestcookie(123456789);
263 ledgerData->set_error(protocol::TMReplyError::reNO_LEDGER);
264 ledgerData->mutable_nodes()->Reserve(n);
265 uint256 parentHash(0);
266
267 NetClock::duration const resolution{10};
268 NetClock::time_point ct{resolution};
269
270 for (int i = 0; i < n; i++)
271 {
272 LedgerInfo info;
273 info.seq = i;
274 info.parentCloseTime = ct;
275 info.hash = ripple::sha512Half(i);
276 info.txHash = ripple::sha512Half(i + 1);
277 info.accountHash = ripple::sha512Half(i + 2);
278 info.parentHash = parentHash;
279 info.drops = XRPAmount(10);
280 info.closeTimeResolution = resolution;
281 info.closeTime = ct;
282 ct += resolution;
283 parentHash = ledgerHash(info);
284 Serializer nData;
285 ripple::addRaw(info, nData);
286 ledgerData->add_nodes()->set_nodedata(
287 nData.getDataPtr(), nData.getLength());
288 }
289
290 return ledgerData;
291 }
292
295 {
296 auto getObject = std::make_shared<protocol::TMGetObjectByHash>();
297
298 getObject->set_type(protocol::TMGetObjectByHash_ObjectType::
299 TMGetObjectByHash_ObjectType_otTRANSACTION);
300 getObject->set_query(true);
301 getObject->set_seq(123456789);
302 uint256 hash(ripple::sha512Half(123456789));
303 getObject->set_ledgerhash(hash.data(), hash.size());
304 getObject->set_fat(true);
305 for (int i = 0; i < 100; i++)
306 {
308 auto object = getObject->add_objects();
309 object->set_hash(hash.data(), hash.size());
310 ripple::SHAMapNodeID sha(64, hash);
311 object->set_nodeid(sha.getRawString());
312 object->set_index("");
313 object->set_data("");
314 object->set_ledgerseq(i);
315 }
316 return getObject;
317 }
318
321 {
322 auto list = std::make_shared<protocol::TMValidatorList>();
323
324 auto master = randomKeyPair(KeyType::ed25519);
325 auto signing = randomKeyPair(KeyType::ed25519);
327 st[sfSequence] = 0;
328 st[sfPublicKey] = std::get<0>(master);
329 st[sfSigningPubKey] = std::get<0>(signing);
330 st[sfDomain] = makeSlice(std::string("example.com"));
331 sign(
332 st,
335 std::get<1>(master),
336 sfMasterSignature);
337 sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
338 Serializer s;
339 st.add(s);
340 list->set_manifest(s.data(), s.size());
341 list->set_version(3);
342 STObject signature(sfSignature);
344 st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
345 Serializer s1;
346 st.add(s1);
347 list->set_signature(s1.data(), s1.size());
348 list->set_blob(strHex(s.slice()));
349 return list;
350 }
351
354 {
355 auto list = std::make_shared<protocol::TMValidatorListCollection>();
356
357 auto master = randomKeyPair(KeyType::ed25519);
358 auto signing = randomKeyPair(KeyType::ed25519);
360 st[sfSequence] = 0;
361 st[sfPublicKey] = std::get<0>(master);
362 st[sfSigningPubKey] = std::get<0>(signing);
363 st[sfDomain] = makeSlice(std::string("example.com"));
364 sign(
365 st,
368 std::get<1>(master),
369 sfMasterSignature);
370 sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
371 Serializer s;
372 st.add(s);
373 list->set_manifest(s.data(), s.size());
374 list->set_version(4);
375 STObject signature(sfSignature);
377 st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
378 Serializer s1;
379 st.add(s1);
380 auto& blob = *list->add_blobs();
381 blob.set_signature(s1.data(), s1.size());
382 blob.set_blob(strHex(s.slice()));
383 return list;
384 }
385
386 void
388 {
390 auto logs = std::make_unique<Logs>(thresh);
391
392 protocol::TMManifests manifests;
393 protocol::TMEndpoints endpoints;
394 protocol::TMTransaction transaction;
395 protocol::TMGetLedger get_ledger;
396 protocol::TMLedgerData ledger_data;
397 protocol::TMGetObjectByHash get_object;
398 protocol::TMValidatorList validator_list;
399 protocol::TMValidatorListCollection validator_list_collection;
400
401 // 4.5KB
402 doTest(buildManifests(20), protocol::mtMANIFESTS, 4, "TMManifests20");
403 // 22KB
404 doTest(buildManifests(100), protocol::mtMANIFESTS, 4, "TMManifests100");
405 // 131B
406 doTest(buildEndpoints(10), protocol::mtENDPOINTS, 4, "TMEndpoints10");
407 // 1.3KB
408 doTest(buildEndpoints(100), protocol::mtENDPOINTS, 4, "TMEndpoints100");
409 // 242B
410 doTest(
411 buildTransaction(*logs),
412 protocol::mtTRANSACTION,
413 1,
414 "TMTransaction");
415 // 87B
416 doTest(buildGetLedger(), protocol::mtGET_LEDGER, 1, "TMGetLedger");
417 // 61KB
418 doTest(
419 buildLedgerData(500, *logs),
420 protocol::mtLEDGER_DATA,
421 10,
422 "TMLedgerData500");
423 // 122 KB
424 doTest(
425 buildLedgerData(1000, *logs),
426 protocol::mtLEDGER_DATA,
427 20,
428 "TMLedgerData1000");
429 // 1.2MB
430 doTest(
431 buildLedgerData(10000, *logs),
432 protocol::mtLEDGER_DATA,
433 50,
434 "TMLedgerData10000");
435 // 12MB
436 doTest(
437 buildLedgerData(100000, *logs),
438 protocol::mtLEDGER_DATA,
439 100,
440 "TMLedgerData100000");
441 // 61MB
442 doTest(
443 buildLedgerData(500000, *logs),
444 protocol::mtLEDGER_DATA,
445 100,
446 "TMLedgerData500000");
447 // 7.7KB
448 doTest(
450 protocol::mtGET_OBJECTS,
451 4,
452 "TMGetObjectByHash");
453 // 895B
454 doTest(
456 protocol::mtVALIDATORLIST,
457 4,
458 "TMValidatorList");
459 doTest(
461 protocol::mtVALIDATORLISTCOLLECTION,
462 4,
463 "TMValidatorListCollection");
464 }
465
466 void
468 {
469 testcase("Handshake");
470 auto getEnv = [&](bool enable) {
471 Config c;
473 str << "[reduce_relay]\n"
474 << "vp_enable=1\n"
475 << "vp_squelch=1\n"
476 << "[compression]\n"
477 << enable << "\n";
478 c.loadFromString(str.str());
479 auto env = std::make_shared<jtx::Env>(*this);
480 env->app().config().COMPRESSION = c.COMPRESSION;
481 env->app().config().VP_REDUCE_RELAY_ENABLE =
483 env->app().config().VP_REDUCE_RELAY_SQUELCH =
485 return env;
486 };
487 auto handshake = [&](int outboundEnable, int inboundEnable) {
488 beast::IP::Address addr =
489 boost::asio::ip::address::from_string("172.1.1.100");
490
491 auto env = getEnv(outboundEnable);
492 auto request = ripple::makeRequest(
493 true,
494 env->app().config().COMPRESSION,
495 false,
496 env->app().config().TX_REDUCE_RELAY_ENABLE,
497 env->app().config().VP_REDUCE_RELAY_ENABLE);
498 http_request_type http_request;
499 http_request.version(request.version());
500 http_request.base() = request.base();
501 // feature enabled on the peer's connection only if both sides are
502 // enabled
503 auto const peerEnabled = inboundEnable && outboundEnable;
504 // inbound is enabled if the request's header has the feature
505 // enabled and the peer's configuration is enabled
506 auto const inboundEnabled = peerFeatureEnabled(
507 http_request, FEATURE_COMPR, "lz4", inboundEnable);
508 BEAST_EXPECT(!(peerEnabled ^ inboundEnabled));
509
510 env.reset();
511 env = getEnv(inboundEnable);
512 auto http_resp = ripple::makeResponse(
513 true,
514 http_request,
515 addr,
516 addr,
517 uint256{1},
518 1,
519 {1, 0},
520 env->app());
521 // outbound is enabled if the response's header has the feature
522 // enabled and the peer's configuration is enabled
523 auto const outboundEnabled = peerFeatureEnabled(
524 http_resp, FEATURE_COMPR, "lz4", outboundEnable);
525 BEAST_EXPECT(!(peerEnabled ^ outboundEnabled));
526 };
527 handshake(1, 1);
528 handshake(1, 0);
529 handshake(0, 1);
530 handshake(0, 0);
531 }
532
533 void
534 run() override
535 {
536 testProtocol();
538 }
539};
540
541BEAST_DEFINE_TESTSUITE_MANUAL(compression, ripple_data, ripple);
542
543} // namespace test
544} // namespace ripple
T begin(T... args)
Represents a JSON value.
Definition: json_value.h:148
A testsuite class.
Definition: suite.h:55
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:155
virtual Config & config()=0
bool VP_REDUCE_RELAY_ENABLE
Definition: Config.h:248
void loadFromString(std::string const &fileContents)
Load the config from the contents of the string.
Definition: Config.cpp:477
bool COMPRESSION
Definition: Config.h:220
bool VP_REDUCE_RELAY_SQUELCH
Definition: Config.h:257
Manages partitions for logging.
Definition: Log.h:51
std::vector< uint8_t > const & getBuffer(Compressed tryCompressed)
Retrieve the packed message data.
Definition: Message.cpp:210
Identifies a node inside a SHAMap.
Definition: SHAMapNodeID.h:34
std::string getRawString() const
void add(Serializer &s) const override
Definition: STObject.cpp:141
std::size_t size() const noexcept
Definition: Serializer.h:73
void const * data() const noexcept
Definition: Serializer.h:79
Slice slice() const noexcept
Definition: Serializer.h:67
int getLength() const
Definition: Serializer.h:234
const void * getDataPtr() const
Definition: Serializer.h:224
constexpr value_type drops() const
Returns the number of drops.
Definition: XRPAmount.h:177
Implements ZeroCopyInputStream around a buffer sequence.
iterator begin()
Definition: base_uint.h:136
pointer data()
Definition: base_uint.h:125
static constexpr std::size_t size()
Definition: base_uint.h:526
std::shared_ptr< protocol::TMGetObjectByHash > buildGetObjectByHash()
void run() override
Runs the suite.
std::shared_ptr< protocol::TMManifests > buildManifests(int n)
std::shared_ptr< protocol::TMLedgerData > buildLedgerData(uint32_t n, Logs &logs)
std::shared_ptr< protocol::TMValidatorListCollection > buildValidatorListCollection()
void doTest(std::shared_ptr< T > proto, protocol::MessageType mt, uint16_t nbuffers, std::string msg)
std::shared_ptr< protocol::TMValidatorList > buildValidatorList()
std::shared_ptr< protocol::TMGetLedger > buildGetLedger()
std::shared_ptr< protocol::TMTransaction > buildTransaction(Logs &logs)
std::shared_ptr< protocol::TMEndpoints > buildEndpoints(int n)
Immutable cryptographic account descriptor.
Definition: Account.h:39
A transaction testing environment.
Definition: Env.h:118
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:115
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition: Env.cpp:262
Application & app()
Definition: Env.h:256
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition: Env.cpp:231
T data(T... args)
T equal(T... args)
boost::asio::ip::address Address
Definition: IPAddress.h:43
std::size_t constexpr headerBytes
Definition: Compression.h:30
std::size_t decompress(InputStream &in, std::size_t inSize, std::uint8_t *decompressed, std::size_t decompressedSize, Algorithm algorithm=Algorithm::LZ4)
Decompress input stream.
Definition: Compression.h:49
std::optional< MessageHeader > parseMessageHeader(boost::system::error_code &ec, BufferSequence const &bufs, std::size_t size)
Parse a message header.
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition: pay.cpp:29
void fund(jtx::Env &env, jtx::Account const &gw, std::vector< jtx::Account > const &accounts, std::vector< STAmount > const &amts, Fund how)
Definition: AMMTest.cpp:34
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition: envconfig.h:54
void sign(Json::Value &jv, Account const &account)
Sign automatically.
Definition: utility.cpp:45
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:104
static uint256 ledgerHash(LedgerInfo const &info)
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpc_version, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition: WSClient.cpp:301
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition: AccountID.cpp:114
std::optional< Blob > strUnHex(std::size_t strSize, Iterator begin, Iterator end)
static constexpr char FEATURE_COMPR[]
Definition: Handshake.h:140
SField const sfGeneric
http_response_type makeResponse(bool crawlPublic, http_request_type const &req, beast::IP::Address public_ip, beast::IP::Address remote_ip, uint256 const &sharedValue, std::optional< std::uint32_t > networkID, ProtocolVersion protocol, Application &app)
Make http response.
Definition: Handshake.cpp:391
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
Definition: SecretKey.cpp:255
std::string strHex(FwdIt begin, FwdIt end)
Definition: strHex.h:30
std::enable_if_t< std::is_same< T, char >::value||std::is_same< T, unsigned char >::value, Slice > makeSlice(std::array< T, N > const &a)
Definition: Slice.h:244
boost::beast::http::request< boost::beast::http::dynamic_body > http_request_type
Definition: Handoff.h:33
bool peerFeatureEnabled(headers const &request, std::string const &feature, std::string value, bool config)
Check if a feature should be enabled for a peer.
Definition: Handshake.h:197
auto makeRequest(bool crawlPublic, bool comprEnabled, bool ledgerReplayEnabled, bool txReduceRelayEnabled, bool vpReduceRelayEnabled) -> request_type
Make outbound http request.
Definition: Handshake.cpp:364
std::pair< PublicKey, SecretKey > randomKeyPair(KeyType type)
Create a key pair using secure random numbers.
Definition: SecretKey.cpp:385
@ manifest
Manifest.
@ ledgerMaster
ledger master data for signing
void addRaw(LedgerHeader const &, Serializer &, bool includeHash=false)
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition: Seed.cpp:76
sha512_half_hasher::result_type sha512Half(Args const &... args)
Returns the SHA512-Half of a series of objects.
Definition: digest.h:225
T resize(T... args)
T size(T... args)
T str(T... args)
Information about the notional ledger backing the view.
Definition: LedgerHeader.h:34
NetClock::time_point closeTime
Definition: LedgerHeader.h:72
NetClock::duration closeTimeResolution
Definition: LedgerHeader.h:66
NetClock::time_point parentCloseTime
Definition: LedgerHeader.h:42
T time_since_epoch(T... args)
T to_string(T... args)