From cf02c191a75e7bf222e1812d2df76eb01c6ec59f Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 29 Dec 2022 19:29:03 +0100 Subject: [PATCH 01/29] Export safes to binary. --- src/io.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++--- src/safe_db/db.rs | 4 +++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/io.rs b/src/io.rs index 8969d21..b29057b 100644 --- a/src/io.rs +++ b/src/io.rs @@ -46,7 +46,7 @@ pub fn read_edges_csv(path: &String) -> Result { pub fn write_edges_binary(edges: &EdgeDB, path: &String) -> Result<(), io::Error> { let mut file = File::create(path)?; - let address_index = write_address_index(&mut file, edges)?; + let address_index = write_address_index(&mut file, addresses_from_edges(edges))?; write_edges(&mut file, edges, &address_index) } @@ -123,6 +123,46 @@ pub fn import_from_safes_binary(path: &str) -> Result { Ok(DB::new(safes, token_owner)) } +pub fn export_safes_to_binary(db: &DB, path: &str) -> Result<(), io::Error> { + let mut file = File::create(path)?; + + let address_index = write_address_index(&mut file, addresses_from_safes(db.safes()))?; + + // organizations + let organizations = db.safes().iter().filter(|s| s.1.organization); + write_u32(&mut file, organizations.clone().count() as u32)?; + for (user, _) in organizations { + write_address(&mut file, user, &address_index)?; + } + + // trust edges + let trust_edges = db.safes().iter().flat_map(|(user, safe)| { + safe.limit_percentage + .iter() + .map(|(other, percentage)| (*user, other, percentage)) + }); + write_u32(&mut file, trust_edges.clone().count() as u32)?; + for (user, send_to, percentage) in trust_edges { + write_address(&mut file, &user, &address_index)?; + write_address(&mut file, send_to, &address_index)?; + write_u8(&mut file, *percentage)?; + } + + // balances + let balances = db.safes().iter().flat_map(|(user, safe)| { + safe.balances + .iter() + .map(|(token_owner, amount)| (*user, token_owner, amount)) + }); + write_u32(&mut file, balances.clone().count() as u32)?; + for (user, token_owner, amount) in balances { + write_address(&mut file, &user, &address_index)?; + write_address(&mut file, token_owner, &address_index)?; + write_u256(&mut file, amount)?; + } + Ok(()) +} + fn read_address_index(file: &mut File) -> Result, io::Error> { let address_count = read_u32(file)?; let mut addresses = HashMap::new(); @@ -134,10 +174,7 @@ fn read_address_index(file: &mut File) -> Result, io::Erro Ok(addresses) } -fn write_address_index( - file: &mut File, - edges: &EdgeDB, -) -> Result, io::Error> { +fn addresses_from_edges(edges: &EdgeDB) -> BTreeSet
{ let mut addresses = BTreeSet::new(); for Edge { from, to, token, .. @@ -147,6 +184,37 @@ fn write_address_index( addresses.insert(*to); addresses.insert(*token); } + addresses +} + +fn addresses_from_safes(safes: &BTreeMap) -> BTreeSet
{ + let mut addresses = BTreeSet::new(); + for ( + user, + Safe { + token_address, + balances, + limit_percentage, + organization: _, + }, + ) in safes + { + addresses.insert(*user); + addresses.insert(*token_address); + for other in balances.keys() { + addresses.insert(*other); + } + for other in limit_percentage.keys() { + addresses.insert(*other); + } + } + addresses +} + +fn write_address_index( + file: &mut File, + addresses: BTreeSet
, +) -> Result, io::Error> { write_u32(file, addresses.len() as u32)?; let mut index = HashMap::new(); for (i, addr) in addresses.into_iter().enumerate() { diff --git a/src/safe_db/db.rs b/src/safe_db/db.rs index 1abc7da..e331b96 100644 --- a/src/safe_db/db.rs +++ b/src/safe_db/db.rs @@ -21,6 +21,10 @@ impl DB { db } + pub fn safes(&self) -> &BTreeMap { + &self.safes + } + pub fn edges(&self) -> &EdgeDB { &self.edges } From f4973641fcf6f575f7b943ec7030b72b3d4a58f1 Mon Sep 17 00:00:00 2001 From: jaensen Date: Wed, 18 Jan 2023 02:24:56 +0100 Subject: [PATCH 02/29] add a dockerfile and github workflow to build it --- .github/workflows/build-and-push.yml | 88 ++++++++++++++++++++++++++++ .github/workflows/dev.yml | 17 ++++++ Dockerfile | 15 +++++ 3 files changed, 120 insertions(+) create mode 100644 .github/workflows/build-and-push.yml create mode 100644 .github/workflows/dev.yml create mode 100644 Dockerfile diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..b5390bb --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,88 @@ +name: Build and push image from ref + +on: + workflow_call: + inputs: + ref: + description: "A ref from this repository, CirclesUBI/pathfinder2" + required: true + type: string + image: + description: "The desired name of the image to build" + default: 'pathfinder2' + required: false + type: string + workflow_dispatch: + inputs: + ref: + description: "A ref from this repository, CirclesUBI/pathfinder2" + required: true + type: string + image: + description: "The desired name of the image to build" + default: 'pathfinder2' + required: false + type: string + +jobs: + + build-and-push-image: + + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + + - + name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - + name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ vars.GC_REGISTRY }}/${{ vars.GC_PROJECT_ID }}/${{ inputs.image }} + labels: | + org.opencontainers.image.title=${{ inputs.image }} + org.opencontainers.image.vendor=CirclesUBI + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + {{ tag }} + {{ base_ref }} + {{ branch }} + type=sha,prefix={{branch}}- + {{ sha }} + + - + name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v1 + with: + workload_identity_provider: "${{ vars.GC_WLI_PROVIDER }}" + service_account: "${{ vars.GC_WLI_SA }}" + token_format: 'access_token' + + - + name: Login to Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ vars.GC_REGISTRY }} + username: 'oauth2accesstoken' + password: '${{ steps.auth.outputs.access_token }}' + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..9d62173 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,17 @@ +name: Build and push the dev image + +on: + push: + branches: [ feature/dockerfile ] + +jobs: + call-build-and-push: + name: Trigger container build and push + permissions: + contents: read + id-token: write + uses: ./.github/workflows/build-and-push.yml + with: + ref: "${{ github.ref }}" + image: "pathfinder2" + secrets: inherit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ab8283 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS build + +WORKDIR /build +COPY . . + +RUN cargo install --path . +RUN cargo build --release + +FROM rust AS app + +WORKDIR /app +COPY --from=build /build/target/release . +RUN chmod +x ./server + +ENTRYPOINT ["./server"] From 822974137040c7e71ba2c6a1339405737d510127 Mon Sep 17 00:00:00 2001 From: Jon Richter Date: Thu, 19 Jan 2023 00:41:16 +0100 Subject: [PATCH 03/29] ci : add Docker Hub and GitHub Packages registries --- .github/workflows/build-and-push.yml | 37 ++++++++++++++++++---------- .github/workflows/dev.yml | 4 +-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index b5390bb..1143006 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,12 +1,8 @@ -name: Build and push image from ref +name: Build and push image on: workflow_call: inputs: - ref: - description: "A ref from this repository, CirclesUBI/pathfinder2" - required: true - type: string image: description: "The desired name of the image to build" default: 'pathfinder2' @@ -14,10 +10,6 @@ on: type: string workflow_dispatch: inputs: - ref: - description: "A ref from this repository, CirclesUBI/pathfinder2" - required: true - type: string image: description: "The desired name of the image to build" default: 'pathfinder2' @@ -32,14 +24,13 @@ jobs: permissions: contents: read id-token: write + packages: write steps: - name: Checkout repository uses: actions/checkout@v3 - with: - ref: ${{ inputs.ref }} - name: Set up Docker Buildx @@ -52,6 +43,8 @@ jobs: with: images: | ${{ vars.GC_REGISTRY }}/${{ vars.GC_PROJECT_ID }}/${{ inputs.image }} + docker.io/${{ vars.DH_ORGANIZATION }}/${{ inputs.image }} + ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} labels: | org.opencontainers.image.title=${{ inputs.image }} org.opencontainers.image.vendor=CirclesUBI @@ -74,15 +67,33 @@ jobs: token_format: 'access_token' - - name: Login to Container Registry + name: Login to Google Cloud Container Registry uses: docker/login-action@v2 with: registry: ${{ vars.GC_REGISTRY }} username: 'oauth2accesstoken' password: '${{ steps.auth.outputs.access_token }}' - - name: Build and push Docker image + - + name: Login to Docker Hub Registry + uses: docker/login-action@v2 + with: + registry: docker.io + username: ${{ vars.DH_USERNAME }} + password: ${{ secrets.DH_TOKEN }} + + - + name: Login to GitHub Packages Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - + name: Build and push Container image uses: docker/build-push-action@v3 with: push: true tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 9d62173..d961675 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -10,8 +10,6 @@ jobs: permissions: contents: read id-token: write + packages: write uses: ./.github/workflows/build-and-push.yml - with: - ref: "${{ github.ref }}" - image: "pathfinder2" secrets: inherit From eeee335c8f1b55dc4d6f5be40360352bc3e35e78 Mon Sep 17 00:00:00 2001 From: Jon Richter Date: Thu, 19 Jan 2023 00:49:06 +0100 Subject: [PATCH 04/29] upd(ci): downgrade buildkit Resolves errors upstream, see: - https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381 - https://github.com/moby/buildkit/issues/3347#issuecomment-1381855209 --- .github/workflows/build-and-push.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 1143006..fff9d39 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -35,6 +35,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + with: + driver-opts: | + image=moby/buildkit:v0.10.6 - name: Docker meta From cd00e1999255291ae02c071ae5bcc398f3e96104 Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Fri, 17 Feb 2023 01:39:59 +0100 Subject: [PATCH 05/29] return decimal values instead of hex values for json-rpc transfer results --- src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.rs b/src/server.rs index 5b894c2..a6408f4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -162,13 +162,13 @@ fn compute_transfer( &(jsonrpc_result( request.id.clone(), json::object! { - flow: flow.to_string(), + flow: flow.to_decimal(), final: max_distance.is_none(), transfers: transfers.into_iter().map(|e| json::object! { from: e.from.to_checksummed_hex(), to: e.to.to_checksummed_hex(), token_owner: e.token.to_checksummed_hex(), - value: e.capacity.to_string() + value: e.capacity.to_decimal(), }).collect::>(), }, ) + "\r\n"), From b120a45d86740445514e4de585901da592729b7e Mon Sep 17 00:00:00 2001 From: JacqueGM Date: Tue, 21 Feb 2023 13:08:37 +0100 Subject: [PATCH 06/29] update json keys --- src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.rs b/src/server.rs index a6408f4..ca01750 100644 --- a/src/server.rs +++ b/src/server.rs @@ -162,9 +162,9 @@ fn compute_transfer( &(jsonrpc_result( request.id.clone(), json::object! { - flow: flow.to_decimal(), + maxFlowValue: flow.to_decimal(), final: max_distance.is_none(), - transfers: transfers.into_iter().map(|e| json::object! { + transferSteps: transfers.into_iter().map(|e| json::object! { from: e.from.to_checksummed_hex(), to: e.to.to_checksummed_hex(), token_owner: e.token.to_checksummed_hex(), From cadee86f6ab9b65dad80a73c81c5b60d7d929cb0 Mon Sep 17 00:00:00 2001 From: jaensen Date: Wed, 18 Jan 2023 02:24:56 +0100 Subject: [PATCH 07/29] add a dockerfile and github workflow to build it --- .github/workflows/build-and-push.yml | 88 ++++++++++++++++++++++++++++ .github/workflows/dev.yml | 17 ++++++ Dockerfile | 15 +++++ 3 files changed, 120 insertions(+) create mode 100644 .github/workflows/build-and-push.yml create mode 100644 .github/workflows/dev.yml create mode 100644 Dockerfile diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..b5390bb --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,88 @@ +name: Build and push image from ref + +on: + workflow_call: + inputs: + ref: + description: "A ref from this repository, CirclesUBI/pathfinder2" + required: true + type: string + image: + description: "The desired name of the image to build" + default: 'pathfinder2' + required: false + type: string + workflow_dispatch: + inputs: + ref: + description: "A ref from this repository, CirclesUBI/pathfinder2" + required: true + type: string + image: + description: "The desired name of the image to build" + default: 'pathfinder2' + required: false + type: string + +jobs: + + build-and-push-image: + + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + + - + name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - + name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ vars.GC_REGISTRY }}/${{ vars.GC_PROJECT_ID }}/${{ inputs.image }} + labels: | + org.opencontainers.image.title=${{ inputs.image }} + org.opencontainers.image.vendor=CirclesUBI + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + {{ tag }} + {{ base_ref }} + {{ branch }} + type=sha,prefix={{branch}}- + {{ sha }} + + - + name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v1 + with: + workload_identity_provider: "${{ vars.GC_WLI_PROVIDER }}" + service_account: "${{ vars.GC_WLI_SA }}" + token_format: 'access_token' + + - + name: Login to Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ vars.GC_REGISTRY }} + username: 'oauth2accesstoken' + password: '${{ steps.auth.outputs.access_token }}' + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..9d62173 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,17 @@ +name: Build and push the dev image + +on: + push: + branches: [ feature/dockerfile ] + +jobs: + call-build-and-push: + name: Trigger container build and push + permissions: + contents: read + id-token: write + uses: ./.github/workflows/build-and-push.yml + with: + ref: "${{ github.ref }}" + image: "pathfinder2" + secrets: inherit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ab8283 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS build + +WORKDIR /build +COPY . . + +RUN cargo install --path . +RUN cargo build --release + +FROM rust AS app + +WORKDIR /app +COPY --from=build /build/target/release . +RUN chmod +x ./server + +ENTRYPOINT ["./server"] From d90005285e9dd84f713d1ea78abfbc2af75b6e83 Mon Sep 17 00:00:00 2001 From: Jon Richter Date: Thu, 19 Jan 2023 00:41:16 +0100 Subject: [PATCH 08/29] ci : add Docker Hub and GitHub Packages registries --- .github/workflows/build-and-push.yml | 37 ++++++++++++++++++---------- .github/workflows/dev.yml | 4 +-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index b5390bb..1143006 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,12 +1,8 @@ -name: Build and push image from ref +name: Build and push image on: workflow_call: inputs: - ref: - description: "A ref from this repository, CirclesUBI/pathfinder2" - required: true - type: string image: description: "The desired name of the image to build" default: 'pathfinder2' @@ -14,10 +10,6 @@ on: type: string workflow_dispatch: inputs: - ref: - description: "A ref from this repository, CirclesUBI/pathfinder2" - required: true - type: string image: description: "The desired name of the image to build" default: 'pathfinder2' @@ -32,14 +24,13 @@ jobs: permissions: contents: read id-token: write + packages: write steps: - name: Checkout repository uses: actions/checkout@v3 - with: - ref: ${{ inputs.ref }} - name: Set up Docker Buildx @@ -52,6 +43,8 @@ jobs: with: images: | ${{ vars.GC_REGISTRY }}/${{ vars.GC_PROJECT_ID }}/${{ inputs.image }} + docker.io/${{ vars.DH_ORGANIZATION }}/${{ inputs.image }} + ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} labels: | org.opencontainers.image.title=${{ inputs.image }} org.opencontainers.image.vendor=CirclesUBI @@ -74,15 +67,33 @@ jobs: token_format: 'access_token' - - name: Login to Container Registry + name: Login to Google Cloud Container Registry uses: docker/login-action@v2 with: registry: ${{ vars.GC_REGISTRY }} username: 'oauth2accesstoken' password: '${{ steps.auth.outputs.access_token }}' - - name: Build and push Docker image + - + name: Login to Docker Hub Registry + uses: docker/login-action@v2 + with: + registry: docker.io + username: ${{ vars.DH_USERNAME }} + password: ${{ secrets.DH_TOKEN }} + + - + name: Login to GitHub Packages Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - + name: Build and push Container image uses: docker/build-push-action@v3 with: push: true tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 9d62173..d961675 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -10,8 +10,6 @@ jobs: permissions: contents: read id-token: write + packages: write uses: ./.github/workflows/build-and-push.yml - with: - ref: "${{ github.ref }}" - image: "pathfinder2" secrets: inherit From 21f74acc17fd8b791df9f3c09d6de4ba81299562 Mon Sep 17 00:00:00 2001 From: Jon Richter Date: Thu, 19 Jan 2023 00:49:06 +0100 Subject: [PATCH 09/29] upd(ci): downgrade buildkit Resolves errors upstream, see: - https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381 - https://github.com/moby/buildkit/issues/3347#issuecomment-1381855209 --- .github/workflows/build-and-push.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 1143006..fff9d39 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -35,6 +35,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + with: + driver-opts: | + image=moby/buildkit:v0.10.6 - name: Docker meta From 1f156f2916e9d79248b2e3b3dd8dd62b5215c3c5 Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Fri, 24 Mar 2023 01:54:07 +0100 Subject: [PATCH 10/29] build on dev --- .github/workflows/dev.yml | 2 +- src/bin/server.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index d961675..f4787e4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -2,7 +2,7 @@ name: Build and push the dev image on: push: - branches: [ feature/dockerfile ] + branches: [ dev ] jobs: call-build-and-push: diff --git a/src/bin/server.rs b/src/bin/server.rs index f3d5524..f18b9b3 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -6,5 +6,18 @@ fn main() { let listen_at = env::args() .nth(1) .unwrap_or_else(|| "127.0.0.1:8080".to_string()); - server::start_server(&listen_at, 10, 4); + + let queue_size = env::args() + .nth(2) + .unwrap_or_else(|| "10".to_string()) + .parse::() + .unwrap();; + + let thread_count = env::args() + .nth(3) + .unwrap_or_else(|| "4".to_string()) + .parse::() + .unwrap();; + + server::start_server(&listen_at, queue_size, thread_count); } From 063df4a3ab114b0374cd5ae33b56eb90cfae30c5 Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Fri, 24 Mar 2023 02:05:38 +0100 Subject: [PATCH 11/29] build on dev --- .github/workflows/build-and-push.yml | 108 +++------------------------ .github/workflows/dev.yml | 15 ---- 2 files changed, 10 insertions(+), 113 deletions(-) delete mode 100644 .github/workflows/dev.yml diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index fff9d39..a66a4af 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,102 +1,14 @@ -name: Build and push image +name: Images on: - workflow_call: - inputs: - image: - description: "The desired name of the image to build" - default: 'pathfinder2' - required: false - type: string - workflow_dispatch: - inputs: - image: - description: "The desired name of the image to build" - default: 'pathfinder2' - required: false - type: string + push: + branches: + - dev jobs: - - build-and-push-image: - - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - packages: write - - steps: - - - - name: Checkout repository - uses: actions/checkout@v3 - - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - driver-opts: | - image=moby/buildkit:v0.10.6 - - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - ${{ vars.GC_REGISTRY }}/${{ vars.GC_PROJECT_ID }}/${{ inputs.image }} - docker.io/${{ vars.DH_ORGANIZATION }}/${{ inputs.image }} - ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} - labels: | - org.opencontainers.image.title=${{ inputs.image }} - org.opencontainers.image.vendor=CirclesUBI - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=semver,pattern={{version}} - {{ tag }} - {{ base_ref }} - {{ branch }} - type=sha,prefix={{branch}}- - {{ sha }} - - - - name: Authenticate to Google Cloud - id: auth - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: "${{ vars.GC_WLI_PROVIDER }}" - service_account: "${{ vars.GC_WLI_SA }}" - token_format: 'access_token' - - - - name: Login to Google Cloud Container Registry - uses: docker/login-action@v2 - with: - registry: ${{ vars.GC_REGISTRY }} - username: 'oauth2accesstoken' - password: '${{ steps.auth.outputs.access_token }}' - - - - name: Login to Docker Hub Registry - uses: docker/login-action@v2 - with: - registry: docker.io - username: ${{ vars.DH_USERNAME }} - password: ${{ secrets.DH_TOKEN }} - - - - name: Login to GitHub Packages Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - - name: Build and push Container image - uses: docker/build-push-action@v3 - with: - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + call-build-and-push: + name: Call + uses: CirclesUBI/.github/.github/workflows/build-and-push.yml@main + with: + image-name: "pathfinder2 + secrets: inherit diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index d961675..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Build and push the dev image - -on: - push: - branches: [ feature/dockerfile ] - -jobs: - call-build-and-push: - name: Trigger container build and push - permissions: - contents: read - id-token: write - packages: write - uses: ./.github/workflows/build-and-push.yml - secrets: inherit From 0e067c116845705c04e06b4a565c815bfd155a9c Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Fri, 24 Mar 2023 02:12:01 +0100 Subject: [PATCH 12/29] build on dev and make thread count and queue size configurable --- .github/workflows/build-and-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index a66a4af..67e6535 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -10,5 +10,5 @@ jobs: name: Call uses: CirclesUBI/.github/.github/workflows/build-and-push.yml@main with: - image-name: "pathfinder2 + image-name: pathfinder2 secrets: inherit From 38c4a8593a1a5dde79d1d097cc7673fbd03026c0 Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:04:24 +0200 Subject: [PATCH 13/29] chore: lint --- src/bin/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/server.rs b/src/bin/server.rs index f18b9b3..089f0ac 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -11,13 +11,13 @@ fn main() { .nth(2) .unwrap_or_else(|| "10".to_string()) .parse::() - .unwrap();; + .unwrap(); let thread_count = env::args() .nth(3) .unwrap_or_else(|| "4".to_string()) .parse::() - .unwrap();; + .unwrap(); server::start_server(&listen_at, queue_size, thread_count); } From f55ef54d9913689f66e07a5b34152c75258b3ac4 Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:05:14 +0200 Subject: [PATCH 14/29] chore: fmt --- src/bin/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/server.rs b/src/bin/server.rs index 089f0ac..6b0b2ec 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -7,13 +7,13 @@ fn main() { .nth(1) .unwrap_or_else(|| "127.0.0.1:8080".to_string()); - let queue_size = env::args() + let queue_size = env::args() .nth(2) .unwrap_or_else(|| "10".to_string()) .parse::() .unwrap(); - let thread_count = env::args() + let thread_count = env::args() .nth(3) .unwrap_or_else(|| "4".to_string()) .parse::() From a0db77996db8516afa5a79a952e7e94b25624d09 Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:10:58 +0200 Subject: [PATCH 15/29] feat(workflow/test): Format and Lint before Build and Test --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e64dce5..6863f0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,13 +27,13 @@ jobs: run: echo ~/.foundry/bin/ >> $GITHUB_PATH - name: Install Dependencies run: curl -L https://foundry.paradigm.xyz | bash && foundryup + - name: Format + run: cargo fmt --check --verbose + - name: Lint + run: cargo clippy --all --all-features -- -D warnings - name: Build run: cargo build --verbose - name: Download safes run: wget -q -c https://rpc.circlesubi.id/pathfinder-db/capacity_graph.db - name: Run tests run: cargo test --verbose - - name: Lint - run: cargo clippy --all --all-features -- -D warnings - - name: Format - run: cargo fmt --check --verbose From 73cbdae59687fe18c19cce29fcb22e8e41a82643 Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:11:27 +0200 Subject: [PATCH 16/29] chore(gitignore): add capacity_graph.db --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 088ba6b..35ae214 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +# Capacity graph runtime state +capacity_graph.db From 4052333a08548133205d6f8ed85e026d3bca468d Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:55:00 +0200 Subject: [PATCH 17/29] chore(README): formatting --- README.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 26708a3..da6064b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -## Pathfinder2 +# Pathfinder2 Pathfinder is a collection of tools related to computing transitive transfers in the [CirclesUBI](https://joincircles.net) trust graph. -### Building +## Building -This is a rust project, so assuming `cargo` is installed, `cargo build` -creates two binaries: The server (default) and the cli. +This is a rust project, so assuming `cargo` is installed, `cargo build` creates three binaries: +The `server` (default), the `cli` and the `convert` tool. -Both need a file that contains the trust graph edges to work. +All need a file that contains the trust graph edges to work. A reasonably up to date edge database file can be obtained from -https://chriseth.github.io/pathfinder2/edges.dat +- https://chriseth.github.io/pathfinder2/edges.dat -#### Using the Server +### Using the Server `cargo run --release :` will start a JSON-RPC server listening on the given port. @@ -29,18 +29,15 @@ Number of worker threads: 4 Size of request queue: 10 -#### Using the CLI - -The CLI will load an edge database file and compute the transitive transfers -from one source to one destination. You can limit the number of hops to explore -and the maximum amount of circles to transfer. +### Using the CLI +The CLI will load an edge database file and compute the transitive transfers from one source to one destination. You can limit the number of hops to explore and the maximum amount of circles to transfer. The options are: `cargo run --release --bin cli [ []] [--dot ]` -For example +For example: `cargo run --release --bin cli 0x9BA1Bcd88E99d6E1E03252A70A63FEa83Bf1208c 0x42cEDde51198D1773590311E2A340DC06B24cB37 edges.dat 3 1000000000000000000` @@ -48,12 +45,12 @@ Computes a transfer of at most `1000000000000000000`, exploring 3 hops. If you specify `--dot `, a graphviz/dot representation of the transfer graph is written to the given file. -#### Conversion Tool +### Conversion Tool -The conversion tool can convert between different ways of representing the edge and trust relations in the circles system. +The conversion tool can convert between different ways of representing the edge and trust relations in the circles system. All data formats are described in https://hackmd.io/Gg04t7gjQKeDW2Q6Jchp0Q -It can read an edge database both in CSV and binary formatand a "safe database" in json and binary format. +It can read an edge database both in CSV and binary formatand a "safe database" in json and binary format. The output is always an edge database in either binary or CSV format. Example: From 21ce9b9ce7c4eaed438070fe83bcf5b5a71f5469 Mon Sep 17 00:00:00 2001 From: jon r Date: Tue, 28 Mar 2023 19:59:26 +0200 Subject: [PATCH 18/29] chore(README): add two spaces where a line break seems implied --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da6064b..eb39b8d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Pathfinder2 -Pathfinder is a collection of tools related to -computing transitive transfers in the +Pathfinder is a collection of tools related to +computing transitive transfers in the [CirclesUBI](https://joincircles.net) trust graph. ## Building From 949969b47e9eef07a42a3342cd31d8f906ac34ba Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Thu, 30 Mar 2023 04:11:39 +0200 Subject: [PATCH 19/29] Added new InputValidationError and validate the U256 input string for 'compute_transfer' with a BigUint. --- src/server.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/server.rs b/src/server.rs index ca01750..304f8bc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,13 +4,16 @@ use crate::types::edge::EdgeDB; use crate::types::{Address, Edge, U256}; use json::JsonValue; use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; use std::io::Read; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::ops::Deref; +use std::str::FromStr; use std::sync::mpsc::TrySendError; use std::sync::{mpsc, Arc, Mutex, RwLock}; use std::thread; +use num_bigint::BigUint; struct JsonRpcRequest { id: JsonValue, @@ -131,12 +134,48 @@ fn load_safes_binary(edges: &RwLock>, file: &str) -> Result) -> std::fmt::Result { + write!(f, "Error: {}", self.0) + } +} +impl Display for InputValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Error: {}", self.0) + } +} + +impl Error for InputValidationError {} + fn compute_transfer( request: JsonRpcRequest, edges: &EdgeDB, mut socket: TcpStream, ) -> Result<(), Box> { socket.write_all(chunked_header().as_bytes())?; + + let parsed_value_param = match request.params["value"].as_str() { + Some(value_str) => match BigUint::from_str(value_str) { + Ok(parsed_value) => parsed_value, + Err(e) => { + return Err(Box::new(InputValidationError(format!( + "Invalid value: {}. Couldn't parse value: {}", + value_str, e + )))); + } + }, + None => U256::MAX.into(), + }; + + if parsed_value_param > U256::MAX.into() { + return Err(Box::new(InputValidationError(format!( + "Value {} is too large. Maximum value is {}.", + parsed_value_param, U256::MAX + )))); + } + let max_distances = if request.params["iterative"].as_bool().unwrap_or_default() { vec![Some(1), Some(2), None] } else { @@ -148,11 +187,7 @@ fn compute_transfer( &Address::from(request.params["from"].to_string().as_str()), &Address::from(request.params["to"].to_string().as_str()), edges, - if request.params.has_key("value") { - U256::from(request.params["value"].to_string().as_str()) - } else { - U256::MAX - }, + U256::from_bigint_truncating(parsed_value_param.clone()), max_distance, max_transfers, ); From ec0df1959ec2e7c3ec185a88009dd0b37e63f62b Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Thu, 30 Mar 2023 04:30:34 +0200 Subject: [PATCH 20/29] moved the InputValidationError to the top of server.rs --- src/server.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/server.rs b/src/server.rs index 304f8bc..1cc68ec 100644 --- a/src/server.rs +++ b/src/server.rs @@ -21,6 +21,20 @@ struct JsonRpcRequest { params: JsonValue, } +struct InputValidationError(String); +impl Error for InputValidationError {} + +impl Debug for InputValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Error: {}", self.0) + } +} +impl Display for InputValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Error: {}", self.0) + } +} + pub fn start_server(listen_at: &str, queue_size: usize, threads: u64) { let edges: Arc>> = Arc::new(RwLock::new(Arc::new(EdgeDB::default()))); @@ -134,21 +148,6 @@ fn load_safes_binary(edges: &RwLock>, file: &str) -> Result) -> std::fmt::Result { - write!(f, "Error: {}", self.0) - } -} -impl Display for InputValidationError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "Error: {}", self.0) - } -} - -impl Error for InputValidationError {} - fn compute_transfer( request: JsonRpcRequest, edges: &EdgeDB, From 6b62ff0aa7e1411024390999f4fdae554b0e3a2f Mon Sep 17 00:00:00 2001 From: ele Date: Tue, 4 Apr 2023 09:55:28 +0200 Subject: [PATCH 21/29] Fix subgraph name --- download_safes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/download_safes.py b/download_safes.py index 0c52e1e..630e289 100755 --- a/download_safes.py +++ b/download_safes.py @@ -15,7 +15,7 @@ }""".replace('\n', ' ') #API='https://graph.circles.garden/subgraphs/name/CirclesUBI/circles-subgraph' -API='https://api.thegraph.com/subgraphs/name/circlesubi/circles' +API='https://api.thegraph.com/subgraphs/name/circlesubi/circles-ubi' lastID = 0 From aadcc84f7b0cb72e05b51f778afcfd8e2340b94b Mon Sep 17 00:00:00 2001 From: jon r Date: Wed, 19 Apr 2023 16:23:43 +0200 Subject: [PATCH 22/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb39b8d..b3ba534 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The `server` (default), the `cli` and the `convert` tool. All need a file that contains the trust graph edges to work. A reasonably up to date edge database file can be obtained from -- https://chriseth.github.io/pathfinder2/edges.dat +- https://circlesubi.github.io/pathfinder2/edges.dat ### Using the Server From fdf1e544a5806defbaa1a721ee2727b5683dc6c2 Mon Sep 17 00:00:00 2001 From: jaensen <4954577+jaensen@users.noreply.github.com> Date: Tue, 23 May 2023 17:25:07 +0200 Subject: [PATCH 23/29] added a binary dump of the balances and trusts to the repo --- README.md | 33 +++++++++++++++++++++++++++++++++ graph_at_20230523_15_00.db | Bin 0 -> 13700138 bytes 2 files changed, 33 insertions(+) create mode 100644 graph_at_20230523_15_00.db diff --git a/README.md b/README.md index eb39b8d..cd326c8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,39 @@ Number of worker threads: 4 Size of request queue: 10 +#### Run with test data +1) Download the balances and trust binary dump from [binary dump from 2023-05-23](graph_at_20230523_15_00.db) +2) Start the server with `cargo run --release :` +3) Import the data with the curl command below +4) Query the server with the curl command below + +The data can be imported into a running pathfinder2 server with the following command: +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "id": "timestamp_value", + "method": "load_safes_binary", + "params": { + "file": "/path/to/graph_at_20230523_15_00.db" + } +}' \ + "http://:" +``` +afterward the server can be queried with the following command: +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "from": "0x000...", + "to": "0x000...", + "value": 999999999999, + "iterative": false, + "prune": true +}' \ + "http://:" +``` + ### Using the CLI The CLI will load an edge database file and compute the transitive transfers from one source to one destination. You can limit the number of hops to explore and the maximum amount of circles to transfer. diff --git a/graph_at_20230523_15_00.db b/graph_at_20230523_15_00.db new file mode 100644 index 0000000000000000000000000000000000000000..adadedcefe4222da69d7325cb31f634411592b96 GIT binary patch literal 13700138 zcmV(lK=i)=0o>x+8V;5p?x!FKQfNr{?58x* z7J0-_m5J`*74Uq$Vg{;o^gYbLH{yW}tOionEX~%x6w+GTc)4y*_en1Hm^)d`w>2*I zG@q6dPiB?%x9DcBvaY^1(&^*nKEbsszlWz}u9l{sW6|+lCFQ6$*ZpyF-yc`*fz`Zs zWN0n0qS9u#5b6WM!SRq0b*9?Z73b^Q_6B&!8AY-3d@k;;7yGmDeTbQj8ptqt%dN=$Qu0V~U1 zBR($zgguKm>)#%59Sh82n_b-!9`F%s4iv5rf~4J$+fEYIhj78)*YiWW$kD;>(@agh zH3OipC8m@YrT}y@$ysr|W`wIv!Jg6%Tb`%=I`Eb%{hUzF=5aDh8SrJvIyTa9xv!5{ zCghaB)(?EyydG&g>VdmeY(6ewc@!%A_Q~SLUI(t(Lf*|BBVl8V>fSTr(@*Kx&nuv* z7R5&fF72bqe2(HY>70PRKAr_}P^bb)%$$lN {J046@$x{WiKRfw;ZeTx|N%B4E zOo{T9Q+PN&n$H&Dq-h{mvq|g$A=}N%vlo}!NrD0Ps_Mq4!8C!!sh{uONZSchfnOZ| zu4IN%tpv^<`#9$wLhuhB?KE3c0_3lWtByhKC-vPUN1FZ&&#;QI^@i4wW>}LUT*5UGz?43SzI+p9(usz`Z z8+$ZEqnd}KzEo;}erO!9&cX5ptJkwmG)93laxAl)SEa@JEQ*jzHr|ijx5^yuuSw*t zTR>p}5^`ZrG3giIM@3C**8faA`m{H3_h6djC@c)L>zhKsu9QTw(l4Jq(; ztsR0Ijj}YBB_c*6gm5L8L|+@7>^A5P3#G^AU;xu$uHz)}e0t9?B1ZbkwF`+1xom2- zM*N-`Y}Tqa>_y%{H>kk`RSOOljfTI?RViOLA+qfIUgEftmVE&N=^@0%J=pChhLnBj z6IA#Jh}C)+N=)nDYap31j!X@pcX&$dG+^EUj&^! zxU!)CA&#Kl^#)76I`E1!*9hm5RhYLp>0E@R*}Gk#Nk)cIjH>SwIpe-jA?OMp1m}%O zG7}|k$N5~bAErx+MBq98@O9(PT3WZ{B>CliBTX?21l{E~s?15>gnns~v& z`pXQ82b`{wjU_M;?kLiSg)HOq93+@M8zWZuDcx{oc?+0zonA-e15zSnN=#1?nx(JQ z2Gv)i)RP`(Jp73>zZhM}79uXI-1HpR_p06@p6?iMBXhd-3i>MeDnE^W>NZf6{`{mL zh4PERKJ>Rvri3e7hk``aZmiD2+C@97$zlHiWP=0eSzUiUN@`mFg{art&G6hIzEH#2^$ir) zz7m2zrN(O=j3E^xN{GQEyyphQ01!Zl(*5(B{nh1)p#$VO2$*{StqIEE(dDD`<-f8T z?&)Ns3#-;eC^qOKt=y4yQ~ImN|Fj%sU#Y0L!}k5trP)=41`;?Y`@EY;@TBV^R1(Glag&%B&g zy|c>=lK6Gb2flHei`aErfY}pKoRW%8mJ+yN)+sP#PIp2&@Knq74N6xC)kV`^VCsXK@;oQ zG$Dr1?4?l-YAcs$jh}fB3qd?B3K(BarAXTm6922?g_D669E}svc6sp{ zz<`VM88qENN0AWPo0Zzykc*xe!M8TQRJu=l2?l`TLP6H5;CPbPwPvymbJW+kxs?OfEe}ek z>oEtw2?TuF_$ORt>r<5%<*bL_o=up1dHSo2Ifp7kO^g#>;hd3-v`R-k6d;@#9YbS` zjc)_mdFJWxUTj}$w=5}Bsk3b`Vpp%g3u0-IqZ^O%{@{3ZW3_-aWQjcIlSfV!hZ?=J zJWOK>LUjYJr&J;XYO&R;Xw#e(vfnpA{3##!&kQw8HICW@yWO+hr;UZ4rs8M89Ontk zBCTwz1mS5t4zOvmwh37{jWQE)2G)OJXi(yu_`2xi{P^&)??b(vEvNu1#+O&<>zRhX z9plKM0N*#jRy@U+lP-yi6U@8;;r?lf3sUpT=a+cC+$TM~K)h>C&Kcd7DtNw4rALJR z@E=OI_tLst5A9iUE2PU=8vc!K)Wc(E&xw+cP;!G+nK1Nlis8h@lGl7|YYt>F*n zQYRt}=pjLPJ*#YBt2VFSt@rSg4f5hPbkkOyyOIPGf34d>3@kM~x-p^>V$)X%sXA@9 zU7%v(?rkyarJ=n8P|fb+5B^RcsL@E~L}8!|$e2j6S)%*fL~H$q@!T)f<6Q7@0)Y2D z&&F7-J>b)D_)pT=b2TQt287$N4j_p5o7&OfG-P4UjSh*>&v(AV8%WPKg&}Ne_1Ah�++aQ5|b2&9GLrDZsk+)&W{{SkAL z(Gi}r&mvWOVPHLxFWmtPM+ipG zY?%I=EIL#wiu=Ex8Tni@d@A|p>ccP{ztn8Y;cH|~pW4##h+8ReX`8h}*i# z#u$EP%?K>&3@xv|8K(b98=WbkFxd%A z^C)F^@Rh5bt*P`_M1t=klFI3P+q??__xt#djfIXU_ulfl4#8FZgdJ~Rx0V$v@^42o zH;J8RdxGENmaS@=ThJkp*A_kUCLI;}s>#jnda7+ij)}yLn5s1U;&Kpiy(^;|wXv%| zVH7FyujG0&oVfZ5E2Q_tbuj zRX7YSb#I(VM-vTblXk*ZsXO~g`|esy=~Atp2B!;w1UWUxdEw8Y<{i4Z6XYv^t7H&i zSQdT@vj5hy;K{1t7_@g&cbu5R; zcMkhewROmD34E#o{~oIOA-+|