Skip to content

Commit 07fef0b

Browse files
authored
Merge pull request #193 from input-output-hk/golddydev/epoch-nonce
feat: implement epoch nonce calculation in epochs state
2 parents ce70f67 + e5c44d5 commit 07fef0b

File tree

22 files changed

+722
-61
lines changed

22 files changed

+722
-61
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/calculations.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ pub fn slot_to_epoch_with_shelley_params(
2020
}
2121
}
2222

23+
pub fn epoch_to_first_slot_with_shelley_params(
24+
epoch: u64,
25+
shelley_epoch: u64,
26+
shelley_epoch_len: u64,
27+
) -> u64 {
28+
if epoch >= shelley_epoch {
29+
shelley_epoch * BYRON_SLOTS_PER_EPOCH + (epoch - shelley_epoch) * shelley_epoch_len
30+
} else {
31+
epoch * BYRON_SLOTS_PER_EPOCH
32+
}
33+
}
34+
2335
pub fn slot_to_timestamp_with_params(slot: u64, byron_timestamp: u64, shelley_epoch: u64) -> u64 {
2436
let shelley_start_slot = shelley_epoch * BYRON_SLOTS_PER_EPOCH;
2537
if slot < shelley_start_slot {
@@ -42,6 +54,10 @@ mod tests {
4254
slot_to_epoch_with_shelley_params(slot, SHELLEY_START_EPOCH, SHELLEY_SLOTS_PER_EPOCH)
4355
}
4456

57+
fn epoch_to_first_slot(epoch: u64) -> u64 {
58+
epoch_to_first_slot_with_shelley_params(epoch, SHELLEY_START_EPOCH, SHELLEY_SLOTS_PER_EPOCH)
59+
}
60+
4561
fn slot_to_timestamp(slot: u64) -> u64 {
4662
slot_to_timestamp_with_params(slot, BYRON_START_TIMESTAMP, SHELLEY_START_EPOCH)
4763
}
@@ -83,4 +99,12 @@ mod tests {
8399
assert_eq!(slot_to_epoch(98_272_003), (425, 35_203));
84100
assert_eq!(slot_to_timestamp(98_272_003), 1689838294);
85101
}
102+
103+
#[test]
104+
fn epoch_to_first_slot_test() {
105+
assert_eq!(slot_to_epoch(epoch_to_first_slot(208)), (208, 0));
106+
assert_eq!(slot_to_epoch(epoch_to_first_slot(0)), (0, 0));
107+
assert_eq!(slot_to_epoch(epoch_to_first_slot(209)), (209, 0));
108+
assert_eq!(slot_to_epoch(epoch_to_first_slot(150)), (150, 0));
109+
}
86110
}

common/src/genesis_values.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
use crate::calculations::{slot_to_epoch_with_shelley_params, slot_to_timestamp_with_params};
1+
use crate::calculations::{
2+
epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params,
3+
slot_to_timestamp_with_params,
4+
};
5+
const MAINNET_SHELLEY_GENESIS_HASH: &str =
6+
"1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81";
27

38
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49
pub struct GenesisValues {
510
pub byron_timestamp: u64,
611
pub shelley_epoch: u64,
712
pub shelley_epoch_len: u64,
13+
pub shelley_genesis_hash: [u8; 32],
814
}
915

1016
impl GenesisValues {
@@ -13,6 +19,10 @@ impl GenesisValues {
1319
byron_timestamp: 1506203091,
1420
shelley_epoch: 208,
1521
shelley_epoch_len: 432000,
22+
shelley_genesis_hash: hex::decode(MAINNET_SHELLEY_GENESIS_HASH)
23+
.unwrap()
24+
.try_into()
25+
.unwrap(),
1626
}
1727
}
1828

@@ -22,4 +32,8 @@ impl GenesisValues {
2232
pub fn slot_to_timestamp(&self, slot: u64) -> u64 {
2333
slot_to_timestamp_with_params(slot, self.byron_timestamp, self.shelley_epoch)
2434
}
35+
36+
pub fn epoch_to_first_slot(&self, epoch: u64) -> u64 {
37+
epoch_to_first_slot_with_shelley_params(epoch, self.shelley_epoch, self.shelley_epoch_len)
38+
}
2539
}

common/src/messages.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
use crate::genesis_values::GenesisValues;
77
use crate::ledger_state::SPOState;
8-
use crate::protocol_params::ProtocolParams;
8+
use crate::protocol_params::{NonceHash, ProtocolParams};
99
use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse};
1010
use crate::queries::{
1111
accounts::{AccountsStateQuery, AccountsStateQueryResponse},
@@ -143,6 +143,9 @@ pub struct EpochActivityMessage {
143143
/// List of all VRF vkey hashes used on blocks (SPO indicator) and
144144
/// number of blocks produced
145145
pub vrf_vkey_hashes: Vec<(KeyHash, usize)>,
146+
147+
/// Nonce
148+
pub nonce: Option<NonceHash>,
146149
}
147150

148151
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]

common/src/protocol_params.rs

Lines changed: 190 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use crate::{
2+
genesis_values::GenesisValues,
23
rational_number::{ChameleonFraction, RationalNumber},
3-
BlockVersionData, Committee, Constitution, CostModel, DRepVotingThresholds, ExUnitPrices,
4-
ExUnits, PoolVotingThresholds, ProtocolConsts,
4+
BlockVersionData, Committee, Constitution, CostModel, DRepVotingThresholds, Era, ExUnitPrices,
5+
ExUnits, NetworkId, PoolVotingThresholds, ProtocolConsts,
56
};
7+
use anyhow::Result;
8+
use blake2::{digest::consts::U32, Blake2b, Digest};
69
use chrono::{DateTime, Utc};
710
use serde_with::serde_as;
811

@@ -95,12 +98,6 @@ pub struct ShelleyProtocolParams {
9598
pub pool_pledge_influence: RationalNumber,
9699
}
97100

98-
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99-
pub enum NetworkId {
100-
Testnet,
101-
Mainnet,
102-
}
103-
104101
#[serde_as]
105102
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
106103
#[serde(rename_all = "camelCase")]
@@ -127,6 +124,72 @@ pub struct ShelleyParams {
127124
pub update_quorum: u32,
128125
}
129126

127+
#[serde_as]
128+
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
129+
#[serde(rename_all = "camelCase")]
130+
pub struct PraosParams {
131+
pub security_param: u32,
132+
#[serde_as(as = "ChameleonFraction")]
133+
pub active_slots_coeff: RationalNumber,
134+
pub epoch_length: u32,
135+
pub max_kes_evolutions: u32,
136+
pub max_lovelace_supply: u64,
137+
pub network_id: NetworkId,
138+
pub slot_length: u32,
139+
pub slots_per_kes_period: u32,
140+
141+
/// Relative slot from which data of the previous epoch can be considered stable.
142+
/// This value is used for all TPraos eras AND Babbage Era from Praos
143+
pub stability_window: u64,
144+
145+
/// Number of slots at the end of each epoch which do NOT contribute randomness to the candidate
146+
/// nonce of the following epoch.
147+
/// This value is used for all Praos eras except Babbage
148+
pub randomness_stabilization_window: u64,
149+
}
150+
151+
impl PraosParams {
152+
pub fn mainnet() -> Self {
153+
PraosParams {
154+
security_param: 2160,
155+
active_slots_coeff: RationalNumber::new(1, 20),
156+
epoch_length: 432000,
157+
max_kes_evolutions: 62,
158+
max_lovelace_supply: 45_000_000_000_000_000,
159+
network_id: NetworkId::Mainnet,
160+
slot_length: 1,
161+
slots_per_kes_period: 129600,
162+
stability_window: 129600,
163+
randomness_stabilization_window: 172800,
164+
}
165+
}
166+
}
167+
168+
impl From<&ShelleyParams> for PraosParams {
169+
fn from(params: &ShelleyParams) -> Self {
170+
let active_slots_coeff = params.active_slots_coeff;
171+
let security_param = params.security_param;
172+
let stability_window =
173+
(security_param as u64) * active_slots_coeff.denom() / active_slots_coeff.numer() * 3;
174+
let randomness_stabilization_window =
175+
(security_param as u64) * active_slots_coeff.denom() / active_slots_coeff.numer() * 4;
176+
177+
Self {
178+
security_param: security_param,
179+
active_slots_coeff: active_slots_coeff,
180+
epoch_length: params.epoch_length,
181+
max_kes_evolutions: params.max_kes_evolutions,
182+
max_lovelace_supply: params.max_lovelace_supply,
183+
network_id: params.network_id.clone(),
184+
slot_length: params.slot_length,
185+
slots_per_kes_period: params.slots_per_kes_period,
186+
187+
stability_window: stability_window,
188+
randomness_stabilization_window: randomness_stabilization_window,
189+
}
190+
}
191+
}
192+
130193
//
131194
// Babbage protocol parameters
132195
//
@@ -164,16 +227,132 @@ pub struct ProtocolVersion {
164227
pub major: u64,
165228
}
166229

167-
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
230+
#[derive(
231+
Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
232+
)]
168233
#[serde(rename_all = "PascalCase")]
169234
pub enum NonceVariant {
235+
#[default]
170236
NeutralNonce,
171237
Nonce,
172238
}
173239

174-
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
240+
pub type NonceHash = [u8; 32];
241+
242+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
175243
#[serde(rename_all = "camelCase")]
176244
pub struct Nonce {
177245
pub tag: NonceVariant,
178-
pub hash: Option<Vec<u8>>,
246+
pub hash: Option<NonceHash>,
247+
}
248+
249+
impl Default for Nonce {
250+
fn default() -> Self {
251+
Self {
252+
tag: NonceVariant::NeutralNonce,
253+
hash: None,
254+
}
255+
}
256+
}
257+
258+
impl From<NonceHash> for Nonce {
259+
fn from(hash: NonceHash) -> Self {
260+
Self {
261+
tag: NonceVariant::Nonce,
262+
hash: Some(hash),
263+
}
264+
}
265+
}
266+
267+
#[derive(
268+
Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize,
269+
)]
270+
pub struct Nonces {
271+
pub epoch: u64,
272+
pub active: Nonce,
273+
pub evolving: Nonce,
274+
pub candidate: Nonce,
275+
// Nonce constructed from the hash of the Last Applied Block
276+
pub lab: Nonce,
277+
// Nonce corresponding to the LAB nonce of the last block of the previous epoch
278+
pub prev_lab: Nonce,
279+
}
280+
281+
impl Nonces {
282+
pub fn shelley_genesis_nonces(genesis: &GenesisValues) -> Nonces {
283+
Nonces {
284+
epoch: genesis.shelley_epoch,
285+
active: genesis.shelley_genesis_hash.into(),
286+
evolving: genesis.shelley_genesis_hash.into(),
287+
candidate: genesis.shelley_genesis_hash.into(),
288+
lab: Nonce::default(),
289+
prev_lab: Nonce::default(),
290+
}
291+
}
292+
293+
pub fn from_candidate(candidate: &Nonce, prev_lab: &Nonce) -> Result<Nonce> {
294+
let Some(candidate_hash) = candidate.hash.as_ref() else {
295+
return Err(anyhow::anyhow!("Candidate hash is not set"));
296+
};
297+
298+
// if prev_lab is Neutral then just return candidate
299+
// this is for second shelley epoch boundary (from 208 to 209 in mainnet)
300+
match prev_lab.tag {
301+
NonceVariant::NeutralNonce => {
302+
return Ok(candidate.clone());
303+
}
304+
NonceVariant::Nonce => {
305+
let Some(prev_lab_hash) = prev_lab.hash.as_ref() else {
306+
return Err(anyhow::anyhow!("Prev lab hash is not set"));
307+
};
308+
let mut hasher = Blake2b::<U32>::new();
309+
hasher.update(&[&candidate_hash.clone()[..], &prev_lab_hash.clone()[..]].concat());
310+
let hash: NonceHash = hasher.finalize().into();
311+
Ok(Nonce::from(hash))
312+
}
313+
}
314+
}
315+
316+
/// Evolve the current nonce by combining it with the current rolling nonce and the
317+
/// range-extended tagged leader VRF output.
318+
///
319+
/// Specifically, we combine it with `η` (a.k.a eta), which is a blake2b-256 hash of the
320+
/// tagged leader VRF output after a range extension. The range extension is, yet another
321+
/// blake2b-256 hash.
322+
pub fn evolve(current: &Nonce, nonce_vrf_output: &Vec<u8>) -> Result<Nonce> {
323+
// first hash nonce_vrf_output
324+
let mut hasher = Blake2b::<U32>::new();
325+
hasher.update(nonce_vrf_output.as_slice());
326+
let nonce_vrf_output_hash: [u8; 32] = hasher.finalize().into();
327+
328+
match current.hash.as_ref() {
329+
Some(nonce) => {
330+
let mut hasher = Blake2b::<U32>::new();
331+
hasher.update(&[&nonce.clone()[..], &nonce_vrf_output_hash[..]].concat());
332+
let hash: NonceHash = hasher.finalize().into();
333+
Ok(Nonce::from(hash))
334+
}
335+
_ => Err(anyhow::anyhow!("Current nonce is not set")),
336+
}
337+
}
338+
339+
pub fn randomness_stability_window(
340+
era: Era,
341+
slot: u64,
342+
genesis: &GenesisValues,
343+
params: &PraosParams,
344+
) -> bool {
345+
let (epoch, _) = genesis.slot_to_epoch(slot);
346+
let next_epoch_first_slot = genesis.epoch_to_first_slot(epoch + 1);
347+
348+
// For Praos in Babbage (just as in all TPraos eras) we use the
349+
// smaller (3k/f vs 4k/f slots) stability window here for
350+
// backwards-compatibility.
351+
let window = match era {
352+
Era::Conway => params.randomness_stabilization_window,
353+
_ => params.stability_window,
354+
};
355+
356+
slot + window < next_epoch_first_slot
357+
}
179358
}

common/src/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ use std::collections::{HashMap, HashSet};
1818
use std::fmt::{Display, Formatter};
1919
use std::ops::Neg;
2020

21+
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22+
pub enum NetworkId {
23+
Testnet,
24+
#[default]
25+
Mainnet,
26+
}
27+
2128
/// Protocol era
2229
#[derive(
2330
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,

modules/accounts_state/src/monetary.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ fn calculate_monetary_expansion(
107107
mod tests {
108108
use super::*;
109109
use acropolis_common::protocol_params::{
110-
NetworkId, Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams,
110+
Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams,
111111
};
112112
use acropolis_common::rational_number::rational_number_from_f32;
113+
use acropolis_common::NetworkId;
113114
use chrono::{DateTime, Utc};
114115

115116
// Known values at start of Shelley - from Java reference and DBSync
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
828f1a0044850f1a00448e005820f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a45758209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda58206d930cc9d1baade5cd1c70fbc025d3377ce946760c48e511d1abdf8acff6ff1c82584036ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab7365850405aa370ff009544a2be4aa5bc52c4456333f4f9b6571d66c5590d2e5629d08b3a3609f4bd0502ed5d0be1abdb7f2ab76aaeae47fe111b0335a4e4def64693162794b8d3c1ca71500f16b1e244724c03825840da5ccc9f8fd62f6c290b5bb2ed9a4258fc9481dd8a0ac80f8936702ad7709a87814d14dca02ce22d7a3e150d171e57914cc058081941e3c6737987524076b6935850402fcbd1d50e4b42fa81dce2eada18df1803af49273d05cbe4a1870983d86759a61005d5d942a53fb68389fe119b5dae823fa3cd6668dc257556bc52086fd879b082dace60edcf0cd592f63103bcd10e0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff75820ae981f4a58d135f98d0a0c5aab9fc04944c8409f65187f9778b57905e43769570000584008d56fe9c28eeeb99bda8920a9887f468f1d74bb03c37ef3caffffbee68d88eb33341498ac145a2422c77546db61c2da782587cdf934d69b113ce52ab8bd8b0b02005901c03de52bc307718194c07824cdc0d2fa016e60b19bc1c7162978fdac55cf43d2ac7f3aea635ffc98d53570a98ac19c703dfda935aac478d84f0281f7c8bc7a180b6aa7e738e9381ebb86baf842af4ee229a3e3908cfec16153f1dd5e21dba0316e7625e4c2a8db495d80ca3c93862fcf4ad0a8a79a20f7d3d9733f9dccbb8ac6d623bbec2bb6dbf170a3c33d32a15d2cc02b3ce78c8d61af768fa82067254b2fe3bd7a1e5cdacf71b50998eec98a48b6970eacdc74d5ae40224f95850fbd88df96e192767c641a70ebf2bb9dc7d32a1e36d1113c09f5591622b35aed5c06bf7b894b882c56bd8e12b7da5b16a341e59d3239a3d14f9d0a5ac006dfdc258e215eacb563954ea234797754b9276f9746d1d6589e10f0dfda8130d422db636d02dec7c8b45ff97de40ac541421e3f2d1aed4246cc5bb54600fa567275046a979f3b4d067c0869732129234df68f1150b09285ecf71385034c79ace9e74186bd811770fcd43a449aa9466165731f92ae73ba4ef9c665a1f8851420a8b1568b711fabca735f474a6a800751f73fbbb3ed2ace0c8bdb2c14eed4032b085dd0ac8f6b0d984a441d337ffe6903aa794728a56360e106c84fc1b943af0e383fd1f1939a2416
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
828f1a004485101a00448e285820aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de582061261a95b7613ee6bf2067dad77b70349729b0c50d57bc1cf30de0db4a1e73a858209c1ba7decef73b79a3acc4c054168b5ab2ebd7dec397b5b57335bcf43f016a4c825840e0bf34a6b73481302f22987cde4c12807cbc2c3fea3f7fcb77261385a50e8ccdda3226db3efff73e9fb15eecf841bbc85ce37550de0435ebcdcb205e0ed0846758502c56d7d9ab6f683a2f61be63b150d8d6ff14da4327318a1a5677589974fae350a2b206304456e5f534a503fd8803fe44ca41c9670f3c2a808bbe33a571a6173cb5a9ce28f8e35c74934cc117e76dfd0f82584046454e5ba46ce2c9bb14383e48cad4b458e32cc1fcf843200f79ab93376ad6e894f121cb1e229da1ec64bbb5f40492c341363be51b80bbe034f50ba1fc3009c65850c04aca35045f37fd34601a1b69b66e84f7c20c9fe05808c9385d604523aaa369e0d4872ce7dd451a41b93ee5e7a0998628a46e564f7e2dcb67a216a506f50ca881057bf6655cb8f40097d606a56b0a020358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff75820452ff31b02a1c2d21c981c56fade107a0c5f8ecf7c89c992d39797344adb558e000058405217d9aeb22d0f47e380062930c8ec706b20a6231b2717a0fe75129e1221d6b6ae0b6bbaf34e5c56da82304403459839c78c1288aecd01b4b109e4b1de97690202005901c0d834fc0b6324cc1438c2e4ec5fc3813f14a1f7ed1c20bc7a860a2efea8e8a814abfa3a4303fd2b2316815e37cfe92a76995a2fd62c790cb8e80dce78435e020b49c983ad9815f5f2e6c73f132c36da7d956f85eee0b6e226edc5d1b5afc5ebb303b20ca0b580b3cce9a703e85df197302a8e562aa98c3e75c6081364b355f20dd6d1f62ed5ef999c13c6bc22cd75eda90b63c3871153e5a2928a172cc5f97e1665e174bf8eff6595e0da243758abc595d9cde362c717ef6bbfce9d7beabc72559496163c8f53b2b0813f621024d44cbc2bc2420de9a2b09f7228c90210f111f89a326ab398be0935fea0dc01f01f6db9fb91f5c5e3c45b973b1ae7e2b499307c60737792aaef27f8e5584de96302d2fb2ef4659f7eb95db9558b89065c024f5d7273403ae72e6ce1364c898b33eef710340a0f1d2deaef2e779a16e94524e881d157e937d9644271a94d3213826d605cd6953a0358eb50cd22ea3482f34cf4d0a38be526c535f32455a7bf213b132a12c997ab7ff585ed85d0588d0efe159b44a260e816bd9b55014c233d12d08cb4f3d5f81ae22eff23eb8757b8aeb6858524e29f2713124846e73daa88b861703049ade314da1cc7564667abd22e41ace2b0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
828f1a0044d9431a004b25805820a0805ae8e52318f0e499be7f85d3f1d5c7dddeacdca0dab9e9d9a8ae6c49a22c58209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda58206d930cc9d1baade5cd1c70fbc025d3377ce946760c48e511d1abdf8acff6ff1c82584015c9ed3a0ec467930e03cc70590e06a2ba36e10f1d2e9a689db34adbabcc9e74313d29d4a4d29b33b12ffdf1b21cc65510ef0bd4aa49e2741cb3ecd2046d053d5850d345bc5b186448fc17305c6a9885215cb806d70018824f9299a68d912760a6f8ceac9bd44cf6bdb5807f0a13f36d8137d4077f8ce4e5b1de10ddb7a920e77a3d80b627381f1ba2e31883a1225965a90182584095fae02f6724f6401edaf4f2e847ae1e6792d1842fbad2b828dd2d54811f49dc014b3dd435059e667c40f86625809338b2ae048fa87c0c85de6c1f40c70eec3258502456e5e98914b9a8f8d0367ac4c06ede978ca7bef8b602d79b3309dcd0f9e7b3ff1de476f10ac393861a93330190f69c002e9f40f9d9aa2f0215dced3423789c3ffdd95c8b25ee7fcd36229dcdb3530a190333582076c672745154c1e2d2337dc97cee36e7433c2a20f4efbee40f8088336db309195820ae981f4a58d135f98d0a0c5aab9fc04944c8409f65187f9778b57905e43769570000584008d56fe9c28eeeb99bda8920a9887f468f1d74bb03c37ef3caffffbee68d88eb33341498ac145a2422c77546db61c2da782587cdf934d69b113ce52ab8bd8b0b02005901c0ba65f78ece0466d1c9b75412417668c352495927d341d1bd7eeb615d15c81b3ead9b4c9810e50051638c7b5fe6c6a861e3cf2ae4dc143c89168c83e09087ca0aad923d22b4d3b0d11dd6422218e3f7d848312a1118d1e0dddfa1fabac3d7ae880b13ecfa2029432599801b6e539d8899fd7e4cd69eec947bb454a5118d046634479c4391527959205279cc6cfc1233a76c58924faa1a8cd986ca234ac0587bbd6b70c7ef79d5daed4c04c70db5e5743d020f186d7e636b26b2fba45c2e04999be192767c641a70ebf2bb9dc7d32a1e36d1113c09f5591622b35aed5c06bf7b894b882c56bd8e12b7da5b16a341e59d3239a3d14f9d0a5ac006dfdc258e215eacb563954ea234797754b9276f9746d1d6589e10f0dfda8130d422db636d02dec7c8b45ff97de40ac541421e3f2d1aed4246cc5bb54600fa567275046a979f3b4d067c0869732129234df68f1150b09285ecf71385034c79ace9e74186bd811770fcd43a449aa9466165731f92ae73ba4ef9c665a1f8851420a8b1568b711fabca735f474a6a800751f73fbbb3ed2ace0c8bdb2c14eed4032b085dd0ac8f6b0d984a441d337ffe6903aa794728a56360e106c84fc1b943af0e383fd1f1939a2416

0 commit comments

Comments
 (0)