diff --git a/hoon/anoma.hoon b/hoon/anoma.hoon index 41240d25c..31186522f 100644 --- a/hoon/anoma.hoon +++ b/hoon/anoma.hoon @@ -2,11 +2,11 @@ !. =~ %909 ~% %k.909 ~ ~ -:: layer 0: version stub (+3) +:: layer 0: version stub (+7) |% ++ anoma + -- -:: layer 1: basic arithmetic (+1) +:: layer 1: basic arithmetic (+3) ~% %one + ~ |% ++ dec :: +342 @@ -76,4 +76,38 @@ ?< =(0 b) (sub a (mul b (div a b))) -- +:: layer 2: fancy arithmetic (+1) +|% +++ modulo :: name this 'mod' and rename 'mod' to 'rem'? + |_ modulus=@ + ++ reduce + |= a=@ + ^- @ + (mod a modulus) + ++ congruent + |= [a=@ b=@] + .= (reduce a) (reduce b) + ++ add + |= [a=@ b=@] + ^- @ + (reduce (^add a b)) + ++ sub + |= [a=@ b=@] + ^- @ + (reduce (^sub (^add modulus a) (reduce b))) + ++ mul + |= [a=@ b=@] + ^- @ + (reduce (^mul a b)) + ++ neg + |= a=@ + ^- @ + (^sub modulus (reduce a)) + ++ inv :: only works in prime fields + !! + ++ div :: only works in prime fields + |= [a=@ b=@] + (mul a (inv b)) + -- +-- == diff --git a/lib/anoma/delta.ex b/lib/anoma/delta.ex new file mode 100644 index 000000000..bef0a765c --- /dev/null +++ b/lib/anoma/delta.ex @@ -0,0 +1,15 @@ +defmodule Anoma.Delta do + @moduledoc false + + @type t() :: %{binary() => non_neg_integer()} + + def add(d1, d2) do + Map.merge(d1, d2, fn _k, v1, v2 -> v1 + v2 end) + |> Map.reject(fn {_k, v} -> v == 0 end) + end + + def sub(d1, d2) do + Map.merge(d1, d2, fn _k, v1, v2 -> v1 - v2 end) + |> Map.reject(fn {_k, v} -> v == 0 end) + end +end diff --git a/lib/anoma/proof.ex b/lib/anoma/proof.ex new file mode 100644 index 000000000..871daf6eb --- /dev/null +++ b/lib/anoma/proof.ex @@ -0,0 +1,10 @@ +defmodule Anoma.Proof do + @moduledoc false + + use TypedStruct + + # a transparent resource logic proof is just the resource + typedstruct enforce: true do + field(:resource, Anoma.Resource.t()) + end +end diff --git a/lib/anoma/proof_record.ex b/lib/anoma/proof_record.ex new file mode 100644 index 000000000..a4cd7d643 --- /dev/null +++ b/lib/anoma/proof_record.ex @@ -0,0 +1,20 @@ +defmodule Anoma.ProofRecord do + @moduledoc false + + alias __MODULE__ + use TypedStruct + + alias Anoma.Proof + + typedstruct enforce: true do + field(:proof, Anoma.Proof.t(), default: nil) + end + + def prove(resource) do + %ProofRecord{ + proof: %Proof{ + resource: resource + } + } + end +end diff --git a/lib/anoma/resource.ex b/lib/anoma/resource.ex index 8925d97f8..e2d784386 100644 --- a/lib/anoma/resource.ex +++ b/lib/anoma/resource.ex @@ -1,61 +1,165 @@ defmodule Anoma.Resource do @moduledoc """ - Ι represent a resource + Ι represent a resource. + + Do not create with `%Anoma.Resource{}` directly, instead use + `%{Anoma.Resource.new | ...}` for random nonce and seed. """ alias __MODULE__ use TypedStruct - typedstruct do - # TODO Should we make this a sexp or a logic? - field(:logic, Anoma.Logic.t(), default: 0) - field(:quantity, integer(), enforce: true) - field(:value, binary(), default: <<>>) - # also known as dynamic data - field(:suffix, binary(), default: <<>>) - # also known as static data - field(:prefix, binary(), default: <<>>) - field(:data, any(), default: <<>>) + alias Anoma.Sign + + typedstruct enforce: true do + # resource logic + field(:logic, Noun.t(), default: [[1 | 0], 0 | 0]) + # fungibility label + field(:label, binary(), default: <<>>) + # quantity + field(:quantity, non_neg_integer(), default: 0) + # arbitrary data + field(:data, binary(), default: <<>>) + # ephemerality flag + field(:eph, bool(), default: false) + # resource nonce + field(:nonce, <<_::256>>, default: <<0::256>>) + # nullifier public key + field(:npk, Sign.ed25519_public(), default: <<0::256>>) + # random seed + field(:rseed, <<_::256>>, default: <<0::256>>) + end + + @doc "New blank resource. Randomized nonce and seed." + def new do + nonce = :crypto.strong_rand_bytes(32) + rseed = :crypto.strong_rand_bytes(32) + %Resource{nonce: nonce, rseed: rseed} + end + + @doc """ + Helper to pass in the npk for initializing a valid but meaningless + resource. + """ + def new_with_npk(npk) do + %{new() | npk: npk} + end + + @doc "A commitment to the given resource." + def commitment(resource = %Resource{}) do + "committo" <> :erlang.term_to_binary(resource) + end + + @doc """ + The nullifier of the given resource. + (It's up to the caller to use the right secret.) + """ + def nullifier(resource = %Resource{}, secret) do + ("annullo" <> :erlang.term_to_binary(resource)) + |> Sign.sign(secret) end - @spec denomination(t()) :: binary() - def denomination(denom) do - Anoma.Serializer.serialize([denom.logic, denom.prefix]) + @doc """ + The kind of the given resource (labelled logic). + """ + def kind(resource = %Resource{}) do + :erlang.term_to_binary(resource.logic) <> :erlang.term_to_binary(resource.label) end @doc """ - Create an empty resource with a given quantity + The delta of the given resource (kind and quantity). """ - @spec new(integer()) :: t() - def new(num) do - %Resource{quantity: num} + def delta(resource = %Resource{}) do + %{kind(resource) => resource.quantity} + end + + def transparent_committed_resource(commitment) do + with "committo" <> committed_resource_bytes <- commitment do + {:ok, :erlang.binary_to_term(committed_resource_bytes)} + else + _ -> :error + end end @doc """ + Whether a commitment commits to a given resource. + """ + def commits_to(commitment, resource) do + with {:ok, committed_resource} <- transparent_committed_resource(commitment) do + committed_resource == resource + else + _ -> false + end + end - I help create a completely empty resource, with a given term as the - suffix. + def commits_to_any(commitment, resources) do + Enum.any?(resources, fn r -> commitment |> commits_to(r) end) + end - This is mainly helpful in testing, as we can create unique empty - resources. + @doc """ + Whether a nullifier nullifies a given resource. + """ + def nullifies(nullifier, resource) do + with {:ok, verified_nullifier} <- Sign.verify(nullifier, resource.npk), + "annullo" <> nullified_resource_bytes <- verified_nullifier do + :erlang.binary_to_term(nullified_resource_bytes) == resource + else + _ -> false + end + end - ## Parameters + def nullifies_any(nullifier, resources) do + Enum.any?(resources, fn r -> nullifier |> nullifies(r) end) + end - - `suffix` - any term that will be turned into a binary for testing + def run_resource_logic(transaction, resource) do + logic = resource.logic + result = Nock.nock(logic, [9, 2, 0 | 1]) + IO.inspect(result, label: "nock result") - ## Output + case result do + {:ok, 0} -> + true - - The empty resource + _ -> + false + end + end + @doc """ + The resource as a noun. """ - @spec make_empty(term()) :: t() - def make_empty(suffix) do - %Resource{quantity: 0, suffix: :erlang.term_to_binary(suffix)} + def to_noun(resource = %Resource{}) do + [ + resource.logic, + Noun.atom_binary_to_integer(resource.label), + resource.quantity, + Noun.atom_binary_to_integer(resource.data), + if resource.eph do + 0 + else + 1 + end, + Noun.atom_binary_to_integer(resource.nonce), + Noun.atom_binary_to_integer(resource.npk) + | Noun.atom_binary_to_integer(resource.rseed) + ] end -end -defimpl Anoma.Intent, for: Anoma.Resource do - def is_intent(_data) do - true + def from_noun([logic, label, quantity, data, eph, nonce, npk | rseed]) do + %Resource{ + logic: logic, + label: Noun.atom_integer_to_binary(label), + quantity: quantity, + data: Noun.atom_integer_to_binary(data), + eph: + case eph do + 0 -> true + 1 -> false + end, + nonce: Noun.atom_integer_to_binary(nonce), + npk: Noun.atom_integer_to_binary(npk), + rseed: Noun.atom_integer_to_binary(rseed) + } end end diff --git a/lib/anoma/sign.ex b/lib/anoma/sign.ex new file mode 100644 index 000000000..bf2a522ee --- /dev/null +++ b/lib/anoma/sign.ex @@ -0,0 +1,21 @@ +defmodule Anoma.Sign do + @moduledoc false + + @type ed25519_public() :: <<_::256>> + @type ed25519_secret() :: <<_::512>> + + @spec new_keypair() :: %{public: ed25519_public(), secret: ed25519_secret()} + def new_keypair do + :enacl.crypto_sign_ed25519_keypair() + end + + @spec sign(binary(), ed25519_secret()) :: binary() + def sign(message, secret) do + :enacl.sign(message, secret) + end + + @spec verify(binary, ed25519_public()) :: binary() + def verify(signed_message, public) do + :enacl.sign_open(signed_message, public) + end +end diff --git a/lib/anoma/transaction.ex b/lib/anoma/transaction.ex index 3cf9f5c99..cbf9621cb 100644 --- a/lib/anoma/transaction.ex +++ b/lib/anoma/transaction.ex @@ -1,41 +1,94 @@ defmodule Anoma.Transaction do @moduledoc """ - I represent an Anoma Transaction - - I can be viewed as a wrapper over `Anoma.Intent` where I contain the - intents used in a transaction - + I represent a resource machine transaction """ + alias __MODULE__ use TypedStruct - typedstruct do - field(:intents, list(Anoma.Intent.t()), default: []) - field(:transaction, Anoma.PartialTx.t(), require: true) + import Anoma.Resource + alias Anoma.Delta + + # doesn't have all the fields yet. + typedstruct enforce: true do + field(:roots, list(binary()), default: []) + field(:commitments, list(binary()), default: []) + field(:nullifiers, list(binary()), default: []) + field(:proofs, list(Anoma.ProofRecord.t()), default: []) + field(:delta, Anoma.Delta.t(), default: %{}) + field(:extra, list(binary()), default: []) + field(:preference, term(), default: nil) end - @doc """ + def verify(transaction) do + # the transparent proofs are just all the involved resources + resources = + for proof_record <- transaction.proofs do + proof_record.proof.resource + end - Creates a new transaction. the `intents_used` are optional, as one - can create a fully formed transaction without any intents! + # todo: check that this is an exact partition + {committed, nullified} = + partition_resources(resources, transaction.commitments, transaction.nullifiers) - ### Parameters + committed_delta_sum = + for %{resource: r} <- committed, reduce: %{} do + sum -> + IO.inspect(sum, label: "running committed delta sum") + Delta.add(sum, delta(r)) + end - - `transaction` - the transaction - - `intents_used` - the intents used in forming the transaction + IO.inspect(committed_delta_sum, label: "committed delta sum") - ### Output + nullified_delta_sum = + for %{resource: r} <- nullified, reduce: %{} do + sum -> + IO.inspect(sum, label: "running nullified delta sum") + Delta.add(sum, delta(r)) + end - - The Transaction itself - """ - @spec new(Anoma.PartialTx.t(), list(Anoma.Intent.t())) :: t() - def new(transaction, intents_used \\ []) do - %Transaction{intents: intents_used, transaction: transaction} + IO.inspect(nullified_delta_sum, label: "nullified delta sum") + + tx_delta_sum = Delta.sub(committed_delta_sum, nullified_delta_sum) + IO.inspect(tx_delta_sum, label: "summed deltas") + + delta_valid = tx_delta_sum == transaction.delta + IO.inspect(delta_valid, label: "delta valid") + + # now run the resource logics, passing the transactions + logic_valid = + for resource <- resources, reduce: true do + acc -> + result = run_resource_logic(transaction, resource) + IO.inspect(result, label: "ran resource logic") + acc && result + end + + IO.inspect(logic_valid, label: "all logics valid") + + delta_valid && logic_valid end - @spec intents(t()) :: list(Anoma.Intent.t()) - def intents(t), do: t.intents + # todo: not efficient + def partition_resources(resources, commitments, nullifiers) do + {committed_set, nullified_set} = + for r <- resources, + c <- commitments, + n <- nullifiers, + reduce: {MapSet.new(), MapSet.new()} do + {committed, nullified} -> + cond do + c |> commits_to(r) -> + {MapSet.put(committed, %{commitment: c, resource: r}), nullified} - @spec transaction(t()) :: Anoma.PartialTx.t() - def transaction(t), do: t.transaction + n |> nullifies(r) -> + {committed, MapSet.put(nullified, %{nullifier: n, resource: r})} + + true -> + {committed, nullified} + end + end + + {MapSet.to_list(committed_set), MapSet.to_list(nullified_set)} + end end diff --git a/mix.exs b/mix.exs index a262075a1..d7a000726 100644 --- a/mix.exs +++ b/mix.exs @@ -26,6 +26,7 @@ defmodule Anoma.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + {:enacl, "~> 1.2"}, {:mnesia_rocksdb, git: "https://github.com/mariari/mnesia_rocksdb"}, {:typed_struct, "~> 0.3.0"}, {:xxhash, "~> 0.3"}, diff --git a/mix.lock b/mix.lock index 1d4fda97a..17cb84ddb 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "enacl": {:hex, :enacl, "1.2.1", "7776480b9b3d42a51d66dbbcbf17fa3d79285b3d2adcb4d5b5bd0b70f0ef1949", [:rebar3], [], "hexpm", "67bbbeddd2564dc899a3dcbc3765cd6ad71629134f1e500a50ec071f0f75e552"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"}, "mnesia_rocksdb": {:git, "https://github.com/mariari/mnesia_rocksdb", "b07f7438af140947efed633c0391f0db839d95fb", []}, diff --git a/test/communicator_test.exs b/test/communicator_test.exs index 700a293a2..f0b5ca4bb 100644 --- a/test/communicator_test.exs +++ b/test/communicator_test.exs @@ -1,43 +1,3 @@ defmodule AnomaTest.Communicator do use ExUnit.Case, async: true - - import Anoma.Node.Executor.Communicator - - alias Anoma.Node.Executor.Communicator - - alias Anoma.Subscriber.Basic - - alias Anoma.PartialTx - alias Anoma.Node.Executor, as: Node - doctest(Anoma.Node.Executor.Communicator) - - test "Proper Execution" do - {:ok, supervisor} = Node.start_link(:p_exec) - - empty_tx = PartialTx.empty() - - successful_tx = - empty_tx - |> PartialTx.add_input(%Anoma.Resource{quantity: 2, logic: 0}) - - failing_tx = - empty_tx - |> PartialTx.add_input(%Anoma.Resource{quantity: 2, logic: 1}) - - assert Communicator.new_transactions(:p_exec_com, [empty_tx]) - - assert Communicator.new_transactions(:p_exec_com, [ - empty_tx, - successful_tx - ]) - - assert Communicator.new_transactions(:p_exec_com, [failing_tx]) == false - - assert Communicator.new_transactions(:p_exec_com, [ - failing_tx, - successful_tx - ]) == false - - Node.shutdown(supervisor) - end end diff --git a/test/partialtx_test.exs b/test/partialtx_test.exs index 3b78a8290..fac405e5e 100644 --- a/test/partialtx_test.exs +++ b/test/partialtx_test.exs @@ -1,72 +1,3 @@ defmodule AnomaTest.PartialTx do use ExUnit.Case, async: true - - alias Anoma.Node.Communicator - - alias Anoma.Subscriber.Basic - - alias Anoma.PartialTx - - doctest(Anoma.PartialTx) - - test "indeed we are balanced! Or not!?!?!" do - resource_1 = %Anoma.Resource{quantity: 1} - resource_2 = %Anoma.Resource{quantity: 1, prefix: <<131, 109, 255>>} - - tx = PartialTx.empty() - - assert PartialTx.balanced(tx) - - tx = - tx |> PartialTx.add_input(resource_1) |> PartialTx.add_input(resource_2) - - assert PartialTx.balanced(tx) == false - - tx = - tx - |> PartialTx.add_output(resource_1) - |> PartialTx.add_output(resource_2) - - assert PartialTx.balanced(tx) - end - - test "double resource add" do - resource_1 = %Anoma.Resource{quantity: 1} - - tx = PartialTx.empty() - - assert PartialTx.balanced(tx) - - tx = - tx - |> PartialTx.add_input(resource_1) - |> PartialTx.add_output(resource_1) - - assert PartialTx.balanced(tx) - - tx = tx |> PartialTx.add_input(resource_1) - - assert PartialTx.balanced(tx) == false - - tx = tx |> PartialTx.add_output(resource_1) - - assert PartialTx.balanced(tx) - end - - test "checking validity" do - r_true = %Anoma.Resource{quantity: 1, logic: 0} - r_false = %Anoma.Resource{quantity: 1, logic: 1} - empty = PartialTx.empty() - - assert PartialTx.is_valid(empty) - assert PartialTx.is_valid(empty |> PartialTx.add_input(r_true)) - assert PartialTx.is_valid(empty |> PartialTx.add_input(r_false)) == false - - multi = - empty - |> PartialTx.add_input(r_true) - |> PartialTx.add_input(r_false) - - assert PartialTx.is_valid(multi) == false - end end diff --git a/test/resource_test.exs b/test/resource_test.exs new file mode 100644 index 000000000..0be753673 --- /dev/null +++ b/test/resource_test.exs @@ -0,0 +1,61 @@ +defmodule AnomaTest.Resource do + use ExUnit.Case, async: true + doctest Anoma.Resource + + import Anoma.Resource + alias Anoma.Sign + + test "commitments and nullifiers" do + keypair_a = Sign.new_keypair() + keypair_b = Sign.new_keypair() + + a_r1 = new_with_npk(keypair_a.public) + a_r2 = new_with_npk(keypair_a.public) + b_r0 = new_with_npk(keypair_b.public) + + # just in case + assert a_r1 != a_r2 + + c_a_r1 = commitment(a_r1) + c_a_r2 = commitment(a_r2) + c_b_r0 = commitment(b_r0) + + n_a_r1 = nullifier(a_r1, keypair_a.secret) + n_a_r2 = nullifier(a_r2, keypair_a.secret) + n_b_r0 = nullifier(b_r0, keypair_b.secret) + + assert c_a_r1 |> commits_to(a_r1) + refute c_a_r1 |> commits_to(a_r2) + refute c_a_r1 |> commits_to(b_r0) + + refute c_a_r2 |> commits_to(a_r1) + assert c_a_r2 |> commits_to(a_r2) + refute c_a_r2 |> commits_to(b_r0) + + refute c_b_r0 |> commits_to(a_r1) + refute c_b_r0 |> commits_to(a_r2) + assert c_b_r0 |> commits_to(b_r0) + + assert n_a_r1 |> nullifies(a_r1) + refute n_a_r1 |> nullifies(a_r2) + refute n_a_r1 |> nullifies(b_r0) + + refute n_a_r2 |> nullifies(a_r1) + assert n_a_r2 |> nullifies(a_r2) + refute n_a_r2 |> nullifies(b_r0) + + refute n_b_r0 |> nullifies(a_r1) + refute n_b_r0 |> nullifies(a_r2) + assert n_b_r0 |> nullifies(b_r0) + end + + test "nullify with wrong key" do + keypair_a = Sign.new_keypair() + keypair_b = Sign.new_keypair() + + a_resource = new_with_npk(keypair_a.public) + wrong_nullifier = nullifier(a_resource, keypair_b.secret) + + refute wrong_nullifier |> nullifies(a_resource) + end +end diff --git a/test/transaction_test.exs b/test/transaction_test.exs new file mode 100644 index 000000000..4c9c9b060 --- /dev/null +++ b/test/transaction_test.exs @@ -0,0 +1,111 @@ +defmodule AnomaTest.Transaction do + use ExUnit.Case, async: true + doctest Anoma.Transaction + + import alias Anoma.Transaction + import Anoma.Resource + import Anoma.ProofRecord + + test "consumable resource" do + keypair = Anoma.Sign.new_keypair() + + # the default logic is always-true + resource_to_consume = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 10 + } + + resource_to_produce = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 10 + } + + # generate the derived values + n_to_consume = nullifier(resource_to_consume, keypair.secret) + p_to_consume = prove(resource_to_consume) + + c_to_produce = commitment(resource_to_produce) + p_to_produce = prove(resource_to_produce) + + # build the transaction + tx = %Transaction{ + proofs: [p_to_produce, p_to_consume], + commitments: [c_to_produce], + nullifiers: [n_to_consume] + } + + assert verify(tx) + end + + test "make change" do + keypair = Anoma.Sign.new_keypair() + + resource_to_consume = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 10 + } + + n_to_consume = nullifier(resource_to_consume, keypair.secret) + p_to_consume = prove(resource_to_consume) + + resource_to_produce_1 = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 5 + } + + c_to_produce_1 = commitment(resource_to_produce_1) + p_to_produce_1 = prove(resource_to_produce_1) + + resource_to_produce_2 = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 5 + } + + c_to_produce_2 = commitment(resource_to_produce_2) + p_to_produce_2 = prove(resource_to_produce_2) + + tx = %Transaction{ + proofs: [p_to_consume, p_to_produce_1, p_to_produce_2], + commitments: [c_to_produce_1, c_to_produce_2], + nullifiers: [n_to_consume] + } + + assert verify(tx) + end + + test "logic rejects" do + keypair = Anoma.Sign.new_keypair() + + resource_to_consume = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 10 + } + + n_to_consume = nullifier(resource_to_consume, keypair.secret) + p_to_consume = prove(resource_to_consume) + + resource_to_produce = %{ + new_with_npk(keypair.public) + | label: "cool resource", + quantity: 10, + logic: Noun.Format.parse_always("[[1 1] 0 0]") + } + + c_to_produce = commitment(resource_to_produce) + p_to_produce = prove(resource_to_produce) + + tx = %Transaction{ + proofs: [p_to_consume, p_to_produce], + commitments: [c_to_produce], + nullifiers: [n_to_consume] + } + + refute verify(tx) + end +end