Skip to content

Commit

Permalink
update scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
SachinMeier committed Sep 16, 2024
1 parent be1f137 commit 1a65cb8
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 140 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: test
test:
MIX_ENV=test mix test

.PHONY: format
format:
MIX_ENV=test mix format
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# ProofOfReserves
# Proof Of Reserves

River's Proof of Reserves implementation in Elixir. This implementation is based on BitMEX's Proof of Reseres Python implementation, found [here](https://github.com/BitMEX/proof-of-reserves-liabilities).

This library is used by River to generate its Proof of Liabilities tree and to allow users to download and verify the Proof of Liabilities.

See River's Proof of Reserves [here](https://river.com/reserves).

## Verifying River's Proof of Reserves

This library comes with a `verify_liabilities.exs` script that will verify River's Proof of Reserves and the balances of any accounts you provide. To run the script, run the following command:
This library comes with a `verify_liabilities.exs` script that will verify River's Proof of Reserves and the balances of any accounts you provide.

### 2. Fetch the Proof of Reserves Data
### 1. Fetch the Proof of Reserves Data

Go to River's [Proof of Reserves](https://river.com/reserves) page. Log in with the email address you used to sign up for River.

Click "Verify Liabilities" and select the "Verify on your computer" option. This will allow you to download the Proof of Liabilities CSV file. Note the path to this file, as you will need to provide it to the command in the next step.

Click "Continue" and you will be prompted to run the setup script also seen in the next step.

### 1. Setup the Project
### 2. Setup the Project
In your terminal, run this command to clone the repository and install the dependencies.

```bash
Expand All @@ -27,11 +29,11 @@ cd proof-of-reserves

This script will install Erlang/Elixir and the project dependencies. It will then compile the library.

Back on the River [Proof of Reserves](https://river.com/reserves) page, click "Continue" and you will see the verification command, also shown in the step below.
Back in River's Proof of Reserves flow, click "Continue" and you will see the verification command, also shown in the step below.

### 2. Run the Verification Script

You will need to replace a few variables in the command below to run the script successfully. These values can all be found in the River "Verify Liabilities" flow. If you followed the steps above, you will now see the command in the final screen. The command on the River page will already have your email and account string(s) filled in. You will need to fill in the `<CSV_PATH>` with the path to the CSV file you downloaded in Step 1.
You will need to replace a few variables in the command below to run the script successfully. These values can all be found in the River Proof of Reserves flow. If you followed the steps above, you will now see the command in the final screen. The command on the River page will already have your email and account string(s) filled in. You will need to fill in the `<CSV_PATH>` with the path to the CSV file you downloaded in Step 1.

- `<EMAIL>` with the email address you used to sign up for River.
- `<ACCOUNT_STRING>` in the format `<ACCOUNT_ID>:<ACCOUNT_KEY>`.
Expand All @@ -45,7 +47,7 @@ If the verification is successful, it will output your balance and a summary of

If you click continue on the River page, you can check that these balances are correct.

## Installation As a Library
## Installation as a Library

This section is for developers who want to use this library in their own projects.

Expand Down
1 change: 1 addition & 0 deletions data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.csv
72 changes: 36 additions & 36 deletions lib/proof_of_reserves.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,48 +75,48 @@ defmodule ProofOfReserves do
# ACCOUNT BALANCE CALCULATIONS

@doc """
find_account_leaves finds all leaves that belong to a particular
find_balances_for_accounts finds all leaves that belong to a particular
account using the attestation_key
"""
def find_account_leaves(leaves, block_height, account_id, account_subkey) do
attestation_key = Util.calculate_attestation_key(account_subkey, block_height, account_id)
find_account_leaves(leaves, attestation_key)
end
@spec find_balances_for_accounts(
list(MerkleSumTree.Node.t()),
non_neg_integer(),
list(%{
account_id: non_neg_integer(),
account_subkey: binary()
})
) ::
list(%{
account_id: non_neg_integer(),
balance: non_neg_integer(),
attestation_key: binary()
})
def find_balances_for_accounts(leaves, block_height, accounts) do
account_balances =
Enum.map(accounts, fn %{account_id: account_id, account_subkey: subkey} ->
%{
account_id: account_id,
balance: 0,
attestation_key: Util.calculate_attestation_key(subkey, block_height, account_id)
}
end)

def find_account_leaves(leaves, attestation_key) do
leaves
# enumerate the leaves with their index since index is used in identifying th leaf hash
|> Enum.with_index()
|> Enum.filter(fn {%{value: value, hash: hash}, idx} ->
Util.leaf_hash(value, attestation_key, idx) == hash
# reduce over the leaves and sum account balances for each account
|> Enum.reduce(account_balances, fn {%{value: value, hash: hash}, idx}, account_balances ->
# only one match will occur per this map
Enum.map(account_balances, fn %{balance: balance, attestation_key: attestation_key} =
account_balance ->
if Util.leaf_hash(value, attestation_key, idx) == hash do
# if the leaf hash matches, we add the value to the balance
Map.put(account_balance, :balance, balance + value)
else
account_balance
end
end)
end)
|> Enum.map(fn {node, _} -> node end)
end

@doc """
get_account_balance calculates the balance of an account by summing the
values of all leaves that belong to the account in a given tree.
"""
@spec get_account_balance(
list(list(MerkleSumTree.Node.t())),
non_neg_integer(),
non_neg_integer(),
String.t()
) :: non_neg_integer()
def get_account_balance(tree, block_height, account_id, account_subkey) do
attestation_key = Util.calculate_attestation_key(account_subkey, block_height, account_id)
get_account_balance(tree, attestation_key)
end

@doc """
get_account_balance calculates the balance of an account by summing the
values of all leaves that belong to the account in a given tree.
"""
@spec get_account_balance(list(list(MerkleSumTree.Node.t())), String.t()) :: non_neg_integer()
def get_account_balance(tree, attestation_key) do
tree
|> MerkleSumTree.get_leaves()
|> find_account_leaves(attestation_key)
|> Enum.reduce(0, fn node, acc -> acc + node.value end)
end

@doc """
Expand Down
6 changes: 5 additions & 1 deletion lib/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ defmodule ProofOfReserves.Util do
end

@doc """
calculate_attestation_key generates the sub nonce for the liability in a specific attestation.
calculate_attestation_key generates the attestation key for the account in a specific attestation.
attestation_key = sha256(account_subkey || block_height || account_id)
block_height and account_id must be 8-byte ints.
"""
Expand All @@ -165,4 +165,8 @@ defmodule ProofOfReserves.Util do
msg = int_to_little(value, 8) <> int_to_little(leaf_index, 8)
sha256hmac(attestation_key, msg)
end

def sats_to_btc(sats) do
sats / 100_000_000
end
end
81 changes: 63 additions & 18 deletions scripts/setup.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/bash
set -euo pipefail

# This script is used to setup the project, install erlang, elixir, and mix dependencies.
Expand All @@ -10,14 +9,16 @@ function install_asdf_deps() {
# MacOS
Darwin*)
echo "Setting up project for MacOS"
brew install coreutils curl git
brew install coreutils curl git openssl
;;
# Linux
Linux*)
echo "Setting up project for Linux"

# Currently supporting APT & DNF only.
for candidate in apt-get dnf
required_packages="curl git automake autoconf libncurses-dev unzip gcc build-essential autoconf m4 libncurses-dev libwxgtk3.2-dev libwxgtk-webview3.2-dev libgl1-mesa-dev libglu1-mesa-dev libpng-dev libssh-dev unixodbc-dev xsltproc fop libxml2-utils openjdk-17-jdk"

# Currently supporting APT, APK, and DNF only.
for candidate in apt-get dnf
do
if [ -x "$(command -v ${candidate})" ]
then
Expand All @@ -31,7 +32,8 @@ function install_asdf_deps() {
exit +1
fi

sudo "${package_manager}" install -y curl git automake autoconf libncurses-dev
# Not all systems will have sudo
"${package_manager}" install -y $required_packages
;;
*)
>&2 echo "Unsupported OS: $(uname -s)"
Expand All @@ -41,12 +43,13 @@ function install_asdf_deps() {
}

function install_asdf() {
echo "Installing asdf (version manager for erlang and elixir)..."
ASDF_DIR="$HOME/.asdf"

# Avoid reinstalling if ASDF directory exists.
# Avoid reinstalling if asdf directory exists.
if [ -d "$ASDF_DIR" ]
then
echo "Existing ASDF directory found, skipping installation..."
echo "Existing asdf directory found, skipping installation..."
return
fi

Expand All @@ -55,18 +58,28 @@ function install_asdf() {
ASDF_ENV="$ASDF_DIR/asdf.sh"

# Add asdf to your shell and source RC file to include ASDF path additions.
if [ -z "${SHELL+X}" ]; then
SHELL=$(echo $0)
fi

case $SHELL in
*/bash)
echo -e "\nsource $ASDF_ENV" >> ~/.bashrc
echo "\nsource $ASDF_ENV" >> ~/.bashrc
source "$ASDF_ENV"

echo "Sourcing asdf $ASDF_ENV"
;;
*/zsh)
echo -e "\nsource $ASDF_ENV" >> ~/.zshrc
echo "\nsource $ASDF_ENV" >> ~/.zshrc
source "$ASDF_ENV"

echo "Sourcing asdf $ASDF_ENV"
;;
*/fish)
echo -e "\nsource $HOME/.asdf/asdf.fish" >> ~/.config/fish/config.fish
echo "\nsource $HOME/.asdf/asdf.fish" >> ~/.config/fish/config.fish
# TODO: Reload to adjust path?

echo "Sourcing asdf $ASDF_ENV"
;;
*)
>&2 echo "Unsupported shell: $SHELL"
Expand All @@ -78,31 +91,63 @@ function install_asdf() {
}

function install_erlang_elixir() {
echo "Installing erlang and elixir..."
# Install erlang & elixir using asdf.
# The plugins will be added to asdf and the versions will be installed according to the .tool-versions file.
asdf plugin add erlang
asdf plugin add elixir
asdf install
ASDF=$(which asdf)
if [ -f "$HOME/.asdf/asdf.sh" ]; then
echo "Sourcing asdf"
source "$HOME/.asdf/asdf.sh"
fi

if [ -z "${ASDF+X}" ]; then
echo "asdf not found, please install asdf first"
exit +4
fi

echo "Adding asdf plugins: $ASDF"
$ASDF plugin add erlang || true
$ASDF plugin add elixir || true
$ASDF install
}

function install_mix_deps() {
# Fetch and compile the Elixir project dependencies.
mix deps.get && mix deps.compile
echo "Installing and compiling elixir dependencies..."
# Fetch and compile the Elixir project dependencies.
# Exclude dev and test dependencies to speed up the process.
MIX_ENV=prod mix deps.get && mix deps.compile
}

function compile_project() {
echo "Compiling elixir tool..."
# Compile the Elixir project.
mix compile
MIX_ENV=prod mix compile
}

# This function is the main entry point for the setup script.
# It installs the dependencies, sets up the project, and compiles the project.
function setup_project() {
echo "Installing dependencies (elixir, erlang, etc.)..."
# Install dependencies for asdf and Elixir.
# Openssl is required for elixir's crypto library.
install_asdf_deps
install_asdf
install_erlang_elixir
if ! command -v elixir &> /dev/null; then
echo "Elixir is not installed, searching for asdf to install elixir..."
if ! command -v asdf &> /dev/null; then
echo "asdf not found, installing dependencies for asdf..."
install_asdf
else
echo "asdf found, installing elixir..."
fi
install_erlang_elixir
else
echo "Elixir found, skipping installation..."
fi

install_mix_deps
compile_project

echo "Setup complete! You are ready to run the verification script."
}

setup_project
27 changes: 15 additions & 12 deletions test/proof_of_reserves_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,16 @@ defmodule ProofOfReservesTest do
acct_key =
Util.hex_to_bin!("abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234")

balance = 2

liabilities = [
Liabilities.fake_liability(1),
# create a liability with a different account_id so
# we can test that the function only returns the leaves
ProofOfReserves.Liability.new(
acct_id,
acct_key,
2
balance
),
Liabilities.fake_liability(3),
Liabilities.fake_liability(4),
Expand All @@ -233,13 +235,14 @@ defmodule ProofOfReservesTest do
)

leaves = ProofOfReserves.MerkleSumTree.get_leaves(tree)
my_leaves = ProofOfReserves.find_account_leaves(leaves, block_height, acct_id, acct_key)
assert length(my_leaves) == 2

attestation_key = Util.calculate_attestation_key(acct_key, block_height, acct_id)
[%{account_id: res_acct_id, balance: res_balance}] =
ProofOfReserves.find_balances_for_accounts(leaves, block_height, [
%{account_id: acct_id, account_subkey: acct_key}
])

my_balance = ProofOfReserves.get_account_balance(tree, attestation_key)
assert my_balance == 2
assert res_acct_id == acct_id
assert res_balance == balance
end

test "test larger balance", %{
Expand Down Expand Up @@ -278,14 +281,14 @@ defmodule ProofOfReservesTest do
)

leaves = ProofOfReserves.MerkleSumTree.get_leaves(tree)
my_leaves = ProofOfReserves.find_account_leaves(leaves, block_height, acct_id, acct_key)
# we can't predict the exact number of leaves, but we know it's at least 3
assert length(my_leaves) >= 3

attestation_key = Util.calculate_attestation_key(acct_key, block_height, acct_id)
[%{account_id: res_acct_id, balance: res_balance}] =
ProofOfReserves.find_balances_for_accounts(leaves, block_height, [
%{account_id: acct_id, account_subkey: acct_key}
])

my_balance = ProofOfReserves.get_account_balance(tree, attestation_key)
assert my_balance == balance
assert res_acct_id == acct_id
assert res_balance == balance
end
end
end
Loading

0 comments on commit 1a65cb8

Please sign in to comment.