Skip to content

first pass of rolling time windows using a decaying moving average #1

@bonedaddy

Description

@bonedaddy

todo: add rolling time period

// two possible ways of structuring this, the current method which reduces
// the need for multiple transactions triggering rate limit evaluation within the same block
// from having to pay the gas costs associated with calculating the decayed value of the
// previous time period, at the expense of increased storage costs of ~16 bytes per rate limit
// and slightly increased gas consumption the first time a rate limit is evaluated in a new period
pub struct RateLimitValue {
    pub previous_period_value: u64,
    pub current_period_value: u64,
    // height that interval parameters were last updated, and if this differs
    // from the height reported in cosmwasm_std::Env, a decay update operation is applied
    pub interval_check_height: u64,
    // timestamp at which the current period began
    pub period_start: Timestamp,
    // timestamp at which the current period ends
    pub period_end: Timestamp,
    // @note: needs to be updated whenever the interval_check_height differs from the block's current height
    pub decayed_value: cosmwasm_std::Decimal
}

impl RateLimitValue {
    // averages the input against the decayed value of the previous period
    // @note: for brevity the process of computing the input value is left out, for example it may be
    // the net flow of a channel (such as in the existing osmosis rate limits)
    pub fn averaged_value(&mut self, env: cosmwasm_std::Env, input: u64) -> Option<cosmwasm_std::Decimal> {
        self.current_period_value = input;
        // when a rule is first initialized there is no previous period value, so there is nothing to average
        if self.previous_period_value == 0 {
            return cosmwasm_std::Decimal::from_atomics(self.current_period_value, 0).ok();
        }
        let current_value = cosmwasm_std::Decimal::from_atomics(self.current_period_value, 0).ok()?;
        let decayed_value = self.check_decay_rate(env)?;
        Some((current_value + decayed_value) / cosmwasm_std::Decimal::from_atomics(2_u64, 0).ok()?)

    }
    // returns the amount of time that has passed in the given time period, based on the current timestamp recorded in the block
    // this transaction is executing in
    pub fn period_percent_passed(&self, block_time_second: u64) -> cosmwasm_std::Decimal {
        // todo: measure the gas costs of calling `self.period_start.seconds()` twice vs storing the result of the function call in memory as is done now
        let period_start_seconds = self.period_start.seconds();
        return cosmwasm_std::Decimal::percent(((block_time_second - period_start_seconds) * 100) / (self.period_end.seconds() - period_start_seconds));
    }
    // checks if a decay operation should be applied to the value from the previous time period
    // returning the existing decayed value if there is no difference in block height or timestamp
    pub fn check_decay_rate(&mut self, env: cosmwasm_std::Env) -> Option<cosmwasm_std::Decimal> {
        if self.interval_check_height == env.block.height {
            return Some(self.decayed_value);
        }
        // should realistically only happen the first period after the rate limit is initialized
        if self.previous_period_value == 0 {
            return cosmwasm_std::Decimal::from_atomics(self.current_period_value, 0).ok();
        }

        if self.period_start == env.block.time {
            // no time passed, return zero, this has the edge case of two blocks potentially having
            // the same timestamp under certain conditions (fast block, loose constraints around timestamp requirements, etc...)
            return Some(self.decayed_value);
        }
        let percent_passed = self.period_percent_passed(env.block.time.seconds());
        self.decayed_value = cosmwasm_std::Decimal::from_atomics(self.previous_period_value, 0).ok()? * percent_passed;
        return Some(self.decayed_value);
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use cosmwasm_std::testing::{mock_env};

    #[test]
    fn test_rate_limit_value() {
        let mut env = mock_env();
        env.block.height = 12_345;
        env.block.time = Timestamp::from_seconds(1690757434);
        let mut rv = RateLimitValue {
            previous_period_value: 10_000,
            current_period_value: 5_000,
            period_start: Timestamp::from_seconds(1690757434),
            period_end: Timestamp::from_seconds(1690786248),
            interval_check_height: 12_344,
            decayed_value: cosmwasm_std::Decimal::raw(0),
        };
        let rate = rv.check_decay_rate(env.clone()).unwrap();
        assert!(rate == cosmwasm_std::Decimal::zero());
        env.block.height = 12_346;
        let rate = rv.check_decay_rate(env.clone()).unwrap();
        assert!(rate == cosmwasm_std::Decimal::zero());
        env.block.time = Timestamp::from_seconds(1690763805);
        let rate: u128 = rv.check_decay_rate(env.clone()).unwrap().atomics().into();
        assert!(rate == 2200000000000000000000);
        let val: u128 = rv.averaged_value(env.clone(), 8_000).unwrap().atomics().into();
        assert!(val == 5100000000000000000000);


    }
}```

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions