From c02037e96db34b980b66483b41a50fe0bb4e8aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= <34384633+tdelabro@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:05:43 +0200 Subject: [PATCH] feat: verify inputs (#24) --- lib/cashubrew/NUTs/NUT-03/impl.ex | 3 +- lib/cashubrew/NUTs/NUT-04/impl.ex | 6 +- lib/cashubrew/core/mint.ex | 74 +--------- lib/cashubrew/core/mint_verification.ex | 172 ++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 lib/cashubrew/core/mint_verification.ex diff --git a/lib/cashubrew/NUTs/NUT-03/impl.ex b/lib/cashubrew/NUTs/NUT-03/impl.ex index a5a92f6..86b5b5e 100644 --- a/lib/cashubrew/NUTs/NUT-03/impl.ex +++ b/lib/cashubrew/NUTs/NUT-03/impl.ex @@ -17,10 +17,11 @@ defmodule Cashubrew.Nuts.Nut03.Impl do raise "SwapHasDuplicateOutputs" end + {keyset_id, total_amount} = Mint.Verification.Outputs.verify!(repo, blinded_messages) total_amount_proofs = Enum.reduce(proofs, 0, fn p, acc -> acc + p.amount end) signatures = - case Mint.create_blinded_signatures(blinded_messages) do + case Mint.create_blinded_signatures(repo, blinded_messages) do {:ok, signatures} -> signatures {:error, reason} -> raise reason end diff --git a/lib/cashubrew/NUTs/NUT-04/impl.ex b/lib/cashubrew/NUTs/NUT-04/impl.ex index b7d28a3..bf12020 100644 --- a/lib/cashubrew/NUTs/NUT-04/impl.ex +++ b/lib/cashubrew/NUTs/NUT-04/impl.ex @@ -58,14 +58,12 @@ defmodule Cashubrew.Nuts.Nut04.Impl do mint_quote = MintQuoteMutex.check_and_acquire!(repo, quote_id) try do - {keyset_id, total_amount} = Mint.Verification.Outputs.verify!(repo, blinded_messages) + {keyset, total_amount} = Mint.Verification.Outputs.verify!(repo, blinded_messages) if total_amount != mint_quote.amount do raise "TotalOutputAmountDoesNotEqualMintQuoteAmount" end - keyset = repo.get!(Schema.Keyset, keyset_id) - if !keyset.active do raise "KeysetIsNotActive" end @@ -80,7 +78,7 @@ defmodule Cashubrew.Nuts.Nut04.Impl do raise "QuoteExpired" end - promises = Mint.generate_promises(repo, keyset_id, blinded_messages) + promises = Mint.generate_promises(repo, keyset.id, blinded_messages) repo.insert_all(Schema.Promises, promises) after MintQuoteMutex.release!(repo, quote_id) diff --git a/lib/cashubrew/core/mint.ex b/lib/cashubrew/core/mint.ex index 491f730..2629a2f 100644 --- a/lib/cashubrew/core/mint.ex +++ b/lib/cashubrew/core/mint.ex @@ -180,12 +180,7 @@ defmodule Cashubrew.Mint do end end - def create_blinded_signatures(blinded_messages) do - repo = Application.get_env(:cashubrew, :repo) - create_blinded_signatures(repo, blinded_messages) - end - - defp create_blinded_signatures(repo, blinded_messages) do + def create_blinded_signatures(repo, blinded_messages) do signatures = Enum.map(blinded_messages, fn bm -> # Get key from database @@ -273,71 +268,4 @@ defmodule Cashubrew.Mint do ] end) end - - defmodule Verification do - @moduledoc """ - Logic to verify user-provided data - """ - defmodule Outputs do - @moduledoc """ - Logic to verify protocol "outputs" (blinded messages) - """ - alias Cashubrew.Nuts.Nut02 - - # Will perform all the check required upon some user provided 'output' - def verify!(repo, outputs) do - inner_verify!(repo, outputs, nil, [], 0) - end - - # Empty list - defp inner_verify!(_repo, [], nil, _seen_B, _total_amount) do - raise "EmptyListOfBlindMessages" - end - - # First elem - defp inner_verify!(repo, [head | tail], nil, seen_B, total_amount) do - verify_blind_message!(repo, head) - inner_verify!(repo, tail, head.id, [head."B_" | seen_B], total_amount + head.amount) - end - - # End of list - defp inner_verify!(repo, [], id, _seen_B, total_amount) do - keyset = repo.get!(Schema.Keyset, id) - - if !keyset.active do - raise "InactiveKeyset" - end - - {id, total_amount} - end - - # Middle elems - defp inner_verify!(repo, [head | tail], id, seen_B, total_amount) do - if head.id != id do - raise "BlindMessagesBelongsToDifferentKeysetIds" - end - - if Enum.member?(seen_B, head."B_") do - "DuplicateBlindMessageInList" - end - - verify_blind_message!(repo, head) - inner_verify!(repo, tail, id, [head."B_" | seen_B], total_amount + head.amount) - end - - defp verify_blind_message!(repo, blind_message) do - if blind_message.amount < 0 do - raise "BlindMessageAmountShoulBePositive" - end - - if blind_message.amount > 2 ** Nut02.Keyset.max_order() do - raise "BlindMessageAmountNotExceed2^MaxOrder" - end - - if repo.exists?(Schema.Promises, blind_message."B_") do - raise "BlindedSignatureAlreadyEmittedForThisMessage" - end - end - end - end end diff --git a/lib/cashubrew/core/mint_verification.ex b/lib/cashubrew/core/mint_verification.ex new file mode 100644 index 0000000..9ca28df --- /dev/null +++ b/lib/cashubrew/core/mint_verification.ex @@ -0,0 +1,172 @@ +defmodule Cashubrew.Mint.Verification.Amount do + @moduledoc """ + Logic to verify user-provided amount + """ + alias Cashubrew.Nuts.Nut02 + + def verify!(amount) do + if amount < 0 do + raise "BlindMessageAmountShouldBePositive" + end + + if amount > 2 ** Nut02.Keyset.max_order() do + raise "BlindMessageAmountNotExceed2^MaxOrder" + end + end +end + +defmodule Cashubrew.Mint.Verification.Inputs do + @moduledoc """ + Logic to verify mint/melt user-provided payload + """ + alias Cashubrew.Mint + alias Cashubrew.Nuts.Nut00.BDHKE + + defp max_secret_length do + 512 + end + + def verify!(repo, inputs) do + inner_verify!(repo, inputs, nil, MapSet.new(), 0, 0) + end + + # Empty list + defp inner_verify!(_repo, [], nil, _seen_secrets, _total_fee, _total_amount) do + raise "EmptyListOfProof" + end + + # End of list + defp inner_verify!(_repo, [], unit, _seen_secrets, total_fee, total_amount) do + {total_fee, total_amount, unit} + end + + # Elems + defp inner_verify!(repo, [head | tail], unit, seen_secrets, total_fee, total_amount) do + Cashubrew.Mint.Verification.Amount.verify!(head) + + if head.secret == nil or head.secret == "" do + raise "NoSecretInProof" + end + + if head.secret > max_secret_length() do + raise "SecretTooLong" + end + + if Enum.member?(seen_secrets, head.secret) do + "DuplicateProofInList" + end + + seen_secrets.put(head.secret) + + keyset = Mint.get_keyset(repo, head.id) + + if keyset == nil do + raise "UnkownKeyset" + end + + # No check for first elem + if unit != nil and unit != keyset.unit do + raise "DifferentUnits" + end + + key = Mint.get_key_for_amount(repo, head.id, head.amount) + + if !BDHKE.verify(key, head."C", head.secret) do + raise "InvalidProof" + end + + # TODO when NUT-11 and NUT-14 are implemented: check spending condition + + inner_verify!( + repo, + tail, + keyset.unit, + seen_secrets, + total_fee + keyset.input_fee_ppk, + total_amount + head.amount + ) + end +end + +defmodule Cashubrew.Mint.Verification.Outputs do + @moduledoc """ + Logic to verify protocol "outputs" (blinded messages) + """ + + # Will perform all the check required upon some user provided 'output' + def verify!(repo, outputs) do + inner_verify!(repo, outputs, nil, MapSet.new(), 0) + end + + # Empty list + defp inner_verify!(_repo, [], nil, _seen_Bs, _total_amount) do + raise "Empty" + end + + # End of list + defp inner_verify!(repo, [], id, _seen_Bs, total_amount) do + keyset = repo.get!(Schema.Keyset, id) + + if !keyset.active do + raise "InactiveKeyset" + end + + {keyset, total_amount} + end + + # Elems + defp inner_verify!(repo, [head | tail], id, seen_Bs, total_amount) do + # No check for first elem + if id != nil do + if head.id != id do + raise "DifferentKeysetIds" + end + + if Enum.member?(seen_Bs, head."B_") do + "Duplicate" + end + end + + seen_Bs.put(head."B_") + + verify_blind_message!(repo, head) + inner_verify!(repo, tail, head.id, seen_Bs, total_amount + head.amount) + end + + defp verify_blind_message!(repo, blind_message) do + Cashubrew.Mint.Verification.Amount.verify!(blind_message.amount) + + if repo.exists?(Schema.Promises, blind_message."B_") do + raise "AlreadyEmitted" + end + end +end + +defmodule Cashubrew.Mint.Verification.InputsAndOutputs do + @moduledoc """ + Logic to verify protocol "inputs" and "outputs" together + """ + + def verify!(repo, inputs, outputs) do + {total_fee, input_total_amount, unit} = + Cashubrew.Mint.Verification.Inputs.verify!(repo, inputs) + + # Empty outputs means that we are melting + if outputs != [] do + {output_keyset, output_total_amount} = + Cashubrew.Mint.Verification.Outputs.verify!(repo, outputs) + + if output_keyset.unit != unit do + raise "DifferentInputAndOutputUnit" + end + + if output_total_amount + total_fee != input_total_amount do + "InvalidAmountAndFee" + end + + # TODO when NUT-11 and NUT-14 are implemented: check output spending condition + end + end +end + +# TODO: write unit tests