Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat: add in memory policy #404

Closed
wants to merge 2 commits into from

Conversation

mattsse
Copy link
Collaborator

@mattsse mattsse commented Aug 23, 2021

Motivation

Follow up #400 to add an in-memory policy implementation

Solution

very primitive rule set as enum which variants represent individual rules.
There is still some work to be done, any feedback is welcome.

ReceiverValueCap(HashMap<Address, U256>),
InvalidSelector(HashSet<Selector>),
InvalidReceiverSelector(HashMap<Address, Selector>),
// Other(Box<dyn Fn(&TypedTransaction) -> Result<(), PolicyError> + Send + Sync>)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you figure out a way to get this working, it's worth turning ensure to async, and making the Other variant be an async function. That way, we could even contact 3rd party service providers (or policy engines), and even read any expensive rules (e.g. ones which may need a lot of policy info from disk) concurrently. h/t @prestwich for this suggestion

Err(())
}
}

/// Middleware used to enforce certain policies for transactions.
#[derive(Clone, Debug)]
pub struct PolicyMiddleware<M, P> {
Copy link
Collaborator

@prestwich prestwich Aug 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need a policy combinator, otherwise all PolicyMiddlewares in the middleware stack will be evaluated sequentially

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type TypelessError = Box<dyn Error + Send + Sync>;
type TypelessPolicy = Box<dyn Policy<Error = TypelessError>>;

#[derive(Debug)]
struct PolicyCombinator {
    inner: Vec<TypelessPolicy>,
}

#[async_trait]
impl Policy for PolicyCombinator {
    type Error = TypelessError;

    async fn ensure_can_send(&self, tx: TypedTransaction) -> Result<TypedTransaction, Self::Error> {
        let futs = self.inner.iter().map(|i| i.ensure_can_send(tx.clone()));

        // assumes that no policy modifies the tx request
        Ok(join_all(futs)
            .await
            .into_iter()
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .nth(0)
            .unwrap())
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question:

is there a way to structure the Policy trait that does not require type-erasing the error in order to build a combinator?

Copy link
Collaborator

@prestwich prestwich Aug 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it easier to type erase things in the meantime


/// A buildable policy combinator
#[derive(Debug)]
pub struct PolicyCombinator {
    inner: Vec<TypelessPolicy>,
}

#[async_trait]
impl Policy for PolicyCombinator {
    type Error = TypelessError;

    async fn ensure_can_send(&self, tx: TypedTransaction) -> Result<TypedTransaction, Self::Error> {
        let futs = self.inner.iter().map(|i| i.ensure_can_send(tx.clone()));

        // assumes that no policy modifies the tx request
        Ok(join_all(futs)
            .await
            .into_iter()
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .nth(0)
            .unwrap_or(tx))
    }
}

impl PolicyCombinator {
    /// Add a policy to this combinator
    pub fn with<P, E>(mut self, policy: P) -> Self
    where
        P: Policy<Error = E> + 'static,
        E: std::error::Error + Send + Sync + 'static,
    {
        let policy = Erased { inner: policy };

        self.inner.push(Box::new(policy));
        self
    }

    /// Converts into a PolicyMiddleware
    pub fn wrap<M>(self, middleware: M) -> PolicyMiddleware<M, PolicyCombinator>
    where
        M: Middleware,
    {
        PolicyMiddleware::new(middleware, self)
    }
}

#[derive(Debug)]
struct Erased<P, E>
where
    P: Policy<Error = E>,
    E: std::error::Error + Send + Sync + 'static,
{
    inner: P,
}

#[async_trait]
impl<P, E> Policy for Erased<P, E>
where
    P: Policy<Error = E>,
    E: std::error::Error + Send + Sync + 'static,
{
    type Error = TypelessError;

    async fn ensure_can_send(&self, tx: TypedTransaction) -> Result<TypedTransaction, Self::Error> {
        Ok(self.inner.ensure_can_send(tx).await?)
    }
}

Copy link
Collaborator Author

@mattsse mattsse Aug 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool,
just to make sure I understand that right, this is aimed to make it easier to combine different Policy implementations, such as MemoryPolicy which is treated as a combination of several Rules which represent cases you might want to check against. Since they don't require async, I'd stick to the current implementation?

The PolicyCombinator looks like a great way to then combine custom Policy impls, for example combine multiple database lookups?

looking at this

        let futs = self.inner.iter().map(|i| i.ensure_can_send(tx.clone()));

        // assumes that no policy modifies the tx request
        Ok(join_all(futs)
            .await
            .into_iter()
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .nth(0)
            .unwrap_or(tx))

I'm thinking the ensure_can_send needs some work, maybe we make this take an Arc<Tx> to ensure it wont get modified and to remove the need for cloning the tx for each Policy

The policy middleware will then be something like:

tx = Arc::new(tx);
select first failing policy(tx.clone())?;
inner(tx.unwrap_or_clone)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a COW?

Copy link
Collaborator

@prestwich prestwich Aug 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also heres a version that returns all errors instead of only 1

        let (oks, errs): (_, Vec<_>) = join_all(futs).await.into_iter().partition(|x| x.is_ok());

        if errs.is_empty() {
            Ok(oks.into_iter().nth(0).unwrap_or(Ok(tx)).unwrap())
        } else {
            Err(errs.into_iter().map(|r| r.unwrap_err()).collect())
        }

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also collect::<Result<_, _>, _>? and this will return on the first error occurred.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh this is in case you specifically want to return ALL errors in a Result<_, Vec<Error>>

@gakonst
Copy link
Owner

gakonst commented Sep 29, 2021

It'd be nice if instead of filtering on Selector, we could filter on Function and then regex match the arguments inside a function e.g. I should be able to specify "allow all transfer(address,uint256) calldata where fn(Address) -> bool && fn(U256) -> bool returns true, and the user would custom-define the fn, e.g. in Address it can be inclusion in a hashset and for U256 it'd be "over 100 less than 1000". I guess that's what the Other policy could do, in which case makes sense to add some more opinionated helper funcs for it.

@gakonst
Copy link
Owner

gakonst commented Nov 24, 2021

I think given we haven't thought about this for some time, and in the interest of keeping the repo PRs clean, we can close this and revisit if we want it again @mattsse?

@gakonst gakonst closed this Nov 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants