Skip to content

Commit

Permalink
mdbook contributions for new crates (#845)
Browse files Browse the repository at this point in the history
docs(arbiter-engine / arbiter-macros): mdbook contributions for new crates and their usage.
  • Loading branch information
Autoparallel committed Feb 5, 2024
1 parent 28f7ab3 commit ad701cb
Show file tree
Hide file tree
Showing 14 changed files with 429 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions arbiter-engine/src/examples/minter/token_minter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::{str::FromStr, time::Duration};

use agents::{token_admin::TokenAdmin, token_requester::TokenRequester};
use arbiter_core::data_collection::EventLogger;
use arbiter_macros::Behaviors;
use ethers::types::Address;
use tokio::time::timeout;

use super::*;
use crate::world::World;

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn token_minter_simulation() {
let mut world = World::new("test_world");
let client = RevmMiddleware::new(&world.environment, None).unwrap();

// Create the token admin agent
let token_admin = Agent::builder(TOKEN_ADMIN_ID);
let mut token_admin_behavior = TokenAdmin::new(Some(4));
token_admin_behavior.add_token(TokenData {
name: TOKEN_NAME.to_owned(),
symbol: TOKEN_SYMBOL.to_owned(),
decimals: TOKEN_DECIMALS,
address: None,
});
// Create the token requester agent
let token_requester = Agent::builder(REQUESTER_ID);
let mut token_requester_behavior = TokenRequester::new(Some(4));
world.add_agent(token_requester.with_behavior(token_requester_behavior));

world.add_agent(token_admin.with_behavior(token_admin_behavior));

let arb = ArbiterToken::new(
Address::from_str("0x240a76d4c8a7dafc6286db5fa6b589e8b21fc00f").unwrap(),
client.clone(),
);
let transfer_event = arb.transfer_filter();

let transfer_stream = EventLogger::builder()
.add_stream(arb.transfer_filter())
.stream()
.unwrap();
let mut stream = Box::pin(transfer_stream);
world.run().await;
let mut idx = 0;

loop {
match timeout(Duration::from_secs(1), stream.next()).await {
Ok(Some(event)) => {
println!("Event received in outside world: {:?}", event);
idx += 1;
if idx == 4 {
break;
}
}
_ => {
panic!("Timeout reached. Test failed.");
}
}
}
}

#[derive(Serialize, Deserialize, Debug, Behaviors)]
enum Behaviors {
TokenAdmin(TokenAdmin),
TokenRequester(TokenRequester),
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn config_test() {
let mut world = World::new("world");
world.build_with_config::<Behaviors>("src/examples/minter/config.toml");

world.run().await;
}
6 changes: 6 additions & 0 deletions arbiter-engine/src/examples/timed_message/config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[[ping]]
TimedMessage = { delay = 1, send_data = "ping", receive_data = "pong", startup_message = "ping" }

[[ping]]
TimedMessage = { delay = 1, send_data = "zam", receive_data = "zim", startup_message = "zam" }

[[pong]]
TimedMessage = { delay = 1, send_data = "pong", receive_data = "ping" }

[[pong]]
TimedMessage = { delay = 1, send_data = "zim", receive_data = "zam" }
4 changes: 4 additions & 0 deletions arbiter-engine/src/examples/timed_message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ fn default_max_count() -> Option<u64> {
Some(3)
}

fn default_max_count() -> Option<u64> {
Some(3)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TimedMessage {
delay: u64,
Expand Down
1 change: 0 additions & 1 deletion arbiter-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[package]
name = "arbiter-macros"
version = "0.1.0"

[lib]
proc-macro = true
Expand Down
7 changes: 6 additions & 1 deletion documentation/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
- [Arbiter Core](./usage/arbiter_core/index.md)
- [Environment](./usage/arbiter_core/environment.md)
- [Middleware](./usage/arbiter_core/middleware.md)
- [Arbiter Engine](./usage/arbiter_engine.md)
- [Arbiter Engine](./usage/arbiter_engine/index.md)
- [Behaviors](./usage/arbiter_engine/behaviors.md)
- [Agents and Engines](./usage/arbiter_engine/agents_and_engines.md)
- [Worlds and Universes](./usage/arbiter_engine/worlds_and_universes.md)
- [Configuration](./usage/arbiter_engine/configuration.md)
- [Arbiter Macros](./usage/arbiter_macros.md)
- [Techniques](./usage/techniques/index.md)
- [Stateful Testing](./usage/techniques/stateful_testing.md)
- [Anomaly Detection](./usage/techniques/anomaly_detection.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The stage is now set for you to begin writing your simulation code.
This will consist of the following:
- Creating a [`Environment`](../usage/arbiter_core/environment.md) for your simulation.
- Creating agents.
(TODO: More documentation here for [`arbiter-engine`](../usage/arbiter_engine.md))
(TODO: More documentation here for [`arbiter-engine`](../usage/arbiter_engine/index.md))
- Creating a [`RevmMiddleware`](../usage/arbiter_core/middleware.md) for each agent in your simulation.
- Deploy contracts using the binding's `MyContract::deploy()` method which will need a client `Arc<RevmMiddleware>` and constructor arguments passed as a tuple.
Or, if you want to use a forked state, use the binding's `MyContract::new()` method and pass it the relevant client and address.
Expand Down
5 changes: 0 additions & 5 deletions documentation/src/usage/arbiter_engine.md

This file was deleted.

58 changes: 58 additions & 0 deletions documentation/src/usage/arbiter_engine/agents_and_engines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Agents and Engines
`Behavior`s are the heartbeat of your `Agent`s and they are wrapped by `Engine`s.
The main idea here is that you can have an `Agent` that has as many `Behavior`s as you like, and each of those behaviors may process different types of events.
This gives flexibility in how you want to design your `Agent`s and what emergent properties you want to observe.

## Design Principles
It is up to you whether or not you prefer to have `Agent`s have multiple `Behavior`s or not or if you want them to have a single `Behavior` that processes all events.
For the former case, you will build `Behavior<E>` for different types `E` and place these inside of an `Agent`.
For the latter, you will create an `enum` that wraps all the different types of events that you want to process and then implement `Behavior` on that `enum`.
The latter will also require a `stream::select` type of operation to merge all the different event streams into one, though this is not difficult to do.

## `struct Agent`
The `Agent` struct is the primary struct that you will be working with.
It contains an ID, a client (`Arc<RevmMiddleware>`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`.
It looks like this:
```rust
pub struct Agent {
pub id: String,
pub messager: Messager,
pub client: Arc<RevmMiddleware>,
pub(crate) behavior_engines: Vec<Box<dyn StateMachine>>,
}
```

Your work will only be to define `Behavior`s and then add them to an `Agent` with the `Agent::with_behavior` method.

The `Agent` is inactive until it is paired with a `World` and then it is ready to be run.
This is handled by creating a world (see: [Worlds and Universes](./worlds_and_universes.md)) and then adding the `Agent` to the `World` with the `World::add_agent` method.
Some of the intermediary representations are below:

#### `struct AgentBuilder`
The `AgentBuilder` struct is a builder pattern for creating `Agent`s.
This is essentially invisible for the end-user, but it is used internally so that `Agent`s can be built in a more ergonomic way.

#### `struct Engine<B,E>`
Briefly, the `Engine<B,E>` struct provides the machinery to run a `Behavior<E>` and it is not necessary for you to handle this directly.
The purpose of this design is to encapsulate the `Behavior<E>` and the event stream `Stream<Item = E>` that the `Behavior<E>` will use for processing.
This encapsulation also allows the `Agent` to hold onto `Behavior<E>` for various different types of `E` all at once.

## Example
Let's create an `Agent` that has two `Behavior`s using the `Replier` behavior from before.
```rust
use arbiter_engine::agent::Agent;
use crate::Replier;

fn setup() {
let ping_replier = Replier::new("ping", "pong", 5, None);
let pong_replier = Replier::new("pong", "ping", 5, Some("ping"));
let agent = Agent::new("my_agent")
.with_behavior(ping_replier)
.with_behavior(pong_replier);
}
```
In this example, we have created an `Agent` with two `Replier` behaviors.
The `ping_replier` will reply to a message with "pong" and the `pong_replier` will reply to a message with "ping".
Given that the `pong_replier` has a `startup_message` of "ping", it will send a message to everyone (including the "my_agent" itself who holds the `ping_replier` behavior) when it starts up.
This will start a chain of messages that will continue in a "ping" "pong" fashion until the `max_count` is reached.
```
95 changes: 95 additions & 0 deletions documentation/src/usage/arbiter_engine/behaviors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Behaviors
The design of `arbiter-engine` is centered around the concept of `Agent`s and `Behavior`s.
At the core, we place `Behavior`s as the event-driven machinery that defines the entire simulation.
What we want is that your simulation is defined completely with how your `Agent`s behaviors are defined.
All you should be looking for is how to define your `Agent`s behaviors and what emergent properties you want to observe.

## `trait Behavior<E>`
To define a `Behavior`, you need to implement the `Behavior` trait on a struct of your own design.
The `Behavior` trait is defined as follows:
```rust
pub trait Behavior<E> {
fn startup(&mut self, client: Arc<RevmMiddleware>, messager: Messager) -> EventStream<E>;
fn process(&mut self, event: E) -> Option<MachineHalt>;
}
```
To outline the design principles here:
- `startup` is a method that initializes the `Behavior` and returns an `EventStream` that the `Behavior` will use for processing.
- This method yields a client and messager from the `Agent` that owns the `Behavior`.
In this method you should take the client and messager and store them in your struct if you will need them in the processing of events.
Note, you may not need both or even either!
- `process` is a method that processes an event of type `E` and returns an `Option<MachineHalt>`.
- If `process` returns `Some(MachineHalt)`, then the `Behavior` will stop processing events completely.

**Summary:** A `Behavior<E>` is tantamount to engage the processing some events of type `E`.

**Advice:** `Behavior`s should be limited in scope and should be a simplistic action driven from a single event.
Otherwise you risk having a `Behavior` that is too complex and difficult to understand and maintain.

### Example
To see this in use, let's take a look at an example of a `Behavior` called `Replier` that replies to a message with a message of its own, and stops once it has replied a certain number of times.
```rust
use std::sync::Arc;
use arbiter_core::middleware::RevmMiddleware;
use arbiter_engine::{
machine::{Behavior, MachineHalt},
messager::{Messager, To},
EventStream};

pub struct Replier {
receive_data: String,
send_data: String,
max_count: u64,
startup_message: Option<String>,
count: u64,
messager: Option<Messager>,
}

impl Replier {
pub fn new(
receive_data: String,
send_data: String,
max_count: u64,
startup_message: Option<String>,
) -> Self {
Self {
receive_data,
send_data,
startup_message,
max_count,
count: 0,
messager: None,
}
}
}

impl Behavior<Message> for Replier {
async fn startup(
&mut self,
client: Arc<RevmMiddleware>,
messager: Messager,
) -> EventStream<Message> {
if let Some(startup_message) = &self.startup_message {
messager.send(To::All, startup_message).await;
}
self.messager = Some(messager.clone());
return messager.stream();
}

async fn process(&mut self, event: Message) -> Option<MachineHalt> {
if event.data == self.receive_data {
self.messager.unwrap().messager.send(To::All, send_data).await;
self.count += 1;
}
if self.count == self.max_count {
return Some(MachineHalt);
}
return None
}
}
```
In this example, we have a `Behavior` that upon `startup` will see if there is a `startup_message` assigned and if so, send it to all `Agent`s that are listening to their `Messager`.
Then, it will store the `Messager` for sending messages later on and start a stream of incoming messages so that we have `E = Message` in this case.
Once these are completed, the `Behavior` automatically transitions into the `process`ing stage where events are popped from the `EventStream<E>` and fed to the `process` method.

As messages come in, if the `receive_data` matches the incoming message, then the `Behavior` will send the `send_data` to all `Agent`s listening to their `Messager` a message with data `send_data`.
66 changes: 66 additions & 0 deletions documentation/src/usage/arbiter_engine/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Configuration
To make it so you rarely have to recompile your project, you can use a configuration file to set the parameters of your simulation once your `Behavior`s have been defined.
Let's take a look at how to do this.

## Behavior Enum
It is good practice to take your `Behavior`s and wrap them in an `enum` so that you can use them in a configuration file.
For instance, let's say you have two struct `Maker` and `Taker` that implement `Behavior<E>` for their own `E`.
Then you can make your `enum` like this:
```rust
use arbiter_macros::Behaviors;

#[derive(Behaviors)]
pub enum Behaviors {
Maker(Maker),
Taker(Taker),
}
```
Notice that we used the `Behaviors` derive macro from the `arbiter_macros` crate.
This macro will generate an implementation of a `CreateStateMachine` trait for the `Behaviors` enum and ultimately save you from having to write a lot of boilerplate code.
The macro solely requires that the `Behavior`s you have implement the `Behavior` trait and that the necessary imports are in scope.

## Configuration File
Now that you have your `enum` of `Behavior`s, you can configure your `World` and the `Agent`s inside of it from configuration file.
Since the `World` and your simulation is completely defined by the `Agent` `Behavior`s you make, all you need to do is specify your `Agent`s in the configuration file.
For example, let's say we have the `Replier` behavior from before, so we have:
```rust
#[derive(Behaviors)]
pub enum Behaviors {
Replier(Replier),
}

pub struct Replier {
receive_data: String,
send_data: String,
max_count: u64,
startup_message: Option<String>,
count: u64,
messager: Option<Messager>,
}
```
Then, we can specify the "ping" and "pong" `Behavior`s like this:
```toml
[[my_agent]]
Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" }

[[my_agent]]
Replier = { send_data = "pong", receive_data = "ping", max_count = 5 }
```
If you instead wanted to specify two `Agent`s "Alice" and "Bob" each with one of the `Replier` `Behavior`s, you could do it like this:
```toml
[[alice]]
Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" }

[[bob]]
Replier = { send_data = "pong", receive_data = "ping", max_count = 5 }
```

## Loading the Configuration
Once you have your configuration file located at `./path/to/config.toml`, you can load it and run your simulation like this:
```rust
fn main() {
let world = World::from_config("./path/to/config.toml")?;
world.run().await;
}
```
At the moment, we do not configure `Universe`s from a configuration file, but this is a feature that is planned for the future.
27 changes: 27 additions & 0 deletions documentation/src/usage/arbiter_engine/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Arbiter Engine
`arbiter-engine` provides the machinery to build agent based / event driven simulations and should be the primary entrypoint for using Arbiter.
The goal of this crate is to abstract away the work required to set up agents, their behaviors, and the worlds they live in.
At the moment, all interaction of agents is done through the `arbiter-core` crate and is meant to be for local simulations and it is not yet generalized for the case of live network automation.

## Heirarchy

Check failure on line 6 in documentation/src/usage/arbiter_engine/index.md

View workflow job for this annotation

GitHub Actions / codespell

Heirarchy ==> Hierarchy
The primary components of `arbiter-engine` are, from the bottom up:
- `Behavior<E>`: This is an event-driven behavior that takes in some item of type `E` and can act on that.
The `Behavior<E>` has two methods: `startup` and `process`.
- `startup` is meant to initialize the `Behavior<E>` and any context around it.
An example could be an agent that deploys token contracts on startup.
- `process` is meant to be a stage that runs on every event that comes in.
An example could be an agent that deployed token contracts on startup, and now wants to process queries about the tokens deployed in the simulation (e.g., what their addresses are).
- `Engine<B,E>` and `StateMachine`: The `Engine` is a struct that implements the `StateMachine` trait as an entrypoint to run `Behavior`s.
- `Engine<B,E>` is a struct owns a `B: Behavior<E>` and the event stream `Stream<Item = E>` that the `Behavior<E>` will use for processing.
- `StateMachine` is a trait that reduces the interface to `Engine<B,E>` to a single method: `execute`.
This trait allows `Agent`s to have multiple behaviors that may not use the same event type.
- `Agent` a struct that contains an ID, a client (`Arc<RevmMiddleware>`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`.
- `Messager` is a struct that owns a `Sender` and `Receiver` for sending and receiving messages.
This is a way for `Agent`s to communicate with each other.
It can also be streamed and used for processing messages in a `Behavior<Message>`.
- `Agent` also owns a `Vec<Box<dyn StateMachine>>` which is a list of `StateMachine`s that the `Agent` will run.
This is a way for `Agent`s to have multiple `Behavior`s that may not use the same event type.
- `World` is a struct that has an ID, an Arbiter `Environment`, a mapping of `Agent`s, and a `Messager`.
- The `World` is tasked with letting `Agent`s join in, and when they do so, to connect them to the `Environment` with a client and `Messager` with the `Agent`'s ID.
- `Universe` is a struct that wraps a mapping of `World`s.
- The `Universe` is tasked with letting `World`s join in and running those `World`s in parallel.
Loading

0 comments on commit ad701cb

Please sign in to comment.