From b3737cc930232b986dcef70cd5dc464792684124 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 25 Nov 2022 11:51:53 +0100 Subject: [PATCH 1/6] Skip replication if there is a (cross)validation error --- lib/archethic/mining/distributed_workflow.ex | 229 ++++++------- lib/archethic/mining/validation_context.ex | 21 ++ .../mining/distributed_workflow_test.exs | 300 ++++++++++++++++++ 3 files changed, 425 insertions(+), 125 deletions(-) diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index 6f7a9b3d4..7c1e03857 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -502,11 +502,8 @@ defmodule Archethic.Mining.DistributedWorkflow do transaction_type: tx.type ) - next_events = [ - {:next_event, :internal, {:notify_error, :timeout}} - ] - - {:keep_state_and_data, next_events} + notify_error(:timeout, data) + :stop _ -> new_context = @@ -580,7 +577,12 @@ defmodule Archethic.Mining.DistributedWorkflow do {:add_cross_validation_stamp, cross_validation_stamp = %CrossValidationStamp{}}, :wait_cross_validation_stamps, data = %{ - context: context = %ValidationContext{transaction: tx} + context: + context = %ValidationContext{ + validation_stamp: validation_stamp, + cross_validation_stamps: cross_validation_stamps, + transaction: tx + } } ) do Logger.info("Add cross validation stamp", @@ -589,60 +591,42 @@ defmodule Archethic.Mining.DistributedWorkflow do ) new_context = ValidationContext.add_cross_validation_stamp(context, cross_validation_stamp) + new_data = %{data | context: new_context} if ValidationContext.enough_cross_validation_stamps?(new_context) do if ValidationContext.atomic_commitment?(new_context) do - {:next_state, :replication, %{data | context: new_context}} + {:next_state, :replication, new_data} else - next_events = [ - {:next_event, :internal, {:notify_error, :consensus_not_reached}} - ] + Logger.debug("Validation stamp: #{inspect(validation_stamp, limit: :infinity)}", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) - {:next_state, :consensus_not_reached, %{data | context: new_context}, next_events} + Logger.debug( + "Cross validation stamps: #{inspect(cross_validation_stamps, limit: :infinity)}", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + Logger.error("Consensus not reached", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + MaliciousDetection.start_link(context) + notify_error(:consensus_not_reached, new_data) + :stop end else - {:keep_state, %{data | context: new_context}} + {:keep_state, new_data} end end - def handle_event( - :enter, - :wait_cross_validation_stamps, - :consensus_not_reached, - _data = %{ - context: - context = %ValidationContext{ - transaction: tx, - cross_validation_stamps: cross_validation_stamps, - validation_stamp: validation_stamp - } - } - ) do - Logger.debug("Validation stamp: #{inspect(validation_stamp, limit: :infinity)}", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - Logger.debug("Cross validation stamps: #{inspect(cross_validation_stamps, limit: :infinity)}", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - Logger.error("Consensus not reached", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - MaliciousDetection.start_link(context) - - :keep_state_and_data - end - def handle_event( :enter, from_state, :replication, - _data = %{ + data = %{ context: context = %ValidationContext{ transaction: %Transaction{address: tx_address, type: type} @@ -650,13 +634,22 @@ defmodule Archethic.Mining.DistributedWorkflow do } ) when from_state in [:cross_validator, :wait_cross_validation_stamps] do - Logger.info("Start replication", - transaction_address: Base.encode16(tx_address), - transaction_type: type - ) + case ValidationContext.get_error_as_atom(context) do + nil -> + Logger.info("Start replication", + transaction_address: Base.encode16(tx_address), + transaction_type: type + ) - request_replication(context) - :keep_state_and_data + request_replication(context) + :keep_state_and_data + + err -> + Logger.info("Skipped replication because validation failed", err: err) + + notify_error(err, data) + :stop + end end def handle_event( @@ -766,19 +759,15 @@ defmodule Archethic.Mining.DistributedWorkflow do :info, {:replication_error, reason}, :replication, - _data = %{context: %ValidationContext{transaction: tx}} + data = %{context: %ValidationContext{transaction: tx}} ) do Logger.error("Replication error - #{inspect(reason)}", transaction_address: Base.encode16(tx.address), transaction_type: tx.type ) - next_events = [ - {:next_event, :internal, {:notify_error, reason}} - ] - - {:keep_state_and_data, next_events} - # :stop + notify_error(reason, data) + :stop end def handle_event( @@ -803,13 +792,8 @@ defmodule Archethic.Mining.DistributedWorkflow do transaction_type: tx.type ) - next_events = [ - {:next_event, :internal, {:notify_error, :timeout}} - ] - - {:keep_state_and_data, next_events} - - # :stop + notify_error(:timeout, data) + :stop _ -> nb_cross_validation_nodes = length(next_cross_validation_nodes) @@ -837,71 +821,14 @@ defmodule Archethic.Mining.DistributedWorkflow do {:timeout, :stop_timeout}, :any, _state, - _data = %{context: %ValidationContext{transaction: tx}} + data = %{context: %ValidationContext{transaction: tx}} ) do Logger.warning("Timeout reached during mining", transaction_type: tx.type, transaction_address: Base.encode16(tx.address) ) - next_events = [ - {:next_event, :internal, {:notify_error, :timeout}} - ] - - {:keep_state_and_data, next_events} - end - - def handle_event( - :internal, - {:notify_error, reason}, - _, - _data = %{ - context: - _context = %ValidationContext{ - welcome_node: welcome_node = %Node{}, - transaction: %Transaction{address: tx_address}, - pending_transaction_error_detail: pending_error_detail - } - } - ) do - {error_context, error_reason} = - case reason do - :invalid_pending_transaction -> - {:invalid_transaction, pending_error_detail} - - :invalid_inherit_constraints -> - {:invalid_transaction, "Inherit constraints not respected"} - - :insufficient_funds -> - {:invalid_transaction, "Insufficient funds"} - - :invalid_proof_of_work -> - {:invalid_transaction, "Invalid origin signature"} - - reason -> - {:network_issue, reason |> Atom.to_string() |> String.replace("_", " ")} - end - - Logger.warning("Invalid transaction #{inspect(error_reason)}", - transaction_address: Base.encode16(tx_address) - ) - - Logger.debug("Notify error back to the welcome node", - transaction_address: Base.encode16(tx_address) - ) - - # Notify error to the welcome node - message = %ValidationError{context: error_context, reason: error_reason, address: tx_address} - - Task.Supervisor.async_nolink(Archethic.TaskSupervisor, fn -> - P2P.send_message( - welcome_node, - message - ) - - :ok - end) - + notify_error(:timeout, data) :stop end @@ -1024,4 +951,56 @@ defmodule Archethic.Mining.DistributedWorkflow do P2P.broadcast_message(storage_nodes, message) end + + defp notify_error(reason, %{ + context: %ValidationContext{ + welcome_node: welcome_node = %Node{}, + transaction: %Transaction{address: tx_address}, + pending_transaction_error_detail: pending_error_detail + } + }) do + {error_context, error_reason} = + case reason do + :invalid_pending_transaction -> + {:invalid_transaction, pending_error_detail} + + :invalid_inherit_constraints -> + {:invalid_transaction, "Inherit constraints not respected"} + + :insufficient_funds -> + {:invalid_transaction, "Insufficient funds"} + + :invalid_proof_of_work -> + {:invalid_transaction, "Invalid origin signature"} + + :invalid_validation_stamp -> + {:network_issue, "Validation error"} + + :invalid_cross_validation_stamp -> + {:network_issue, "Cross validation error"} + + reason -> + {:network_issue, reason |> Atom.to_string() |> String.replace("_", " ")} + end + + Logger.warning("Invalid transaction #{inspect(error_reason)}", + transaction_address: Base.encode16(tx_address) + ) + + Logger.debug("Notify error back to the welcome node", + transaction_address: Base.encode16(tx_address) + ) + + # Notify error to the welcome node + message = %ValidationError{context: error_context, reason: error_reason, address: tx_address} + + Task.Supervisor.async_nolink(Archethic.TaskSupervisor, fn -> + P2P.send_message( + welcome_node, + message + ) + + :ok + end) + end end diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index 35bbac499..a174ed221 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -1261,4 +1261,25 @@ defmodule Archethic.Mining.ValidationContext do |> Enum.map(&Enum.at(storage_nodes, &1)) |> Enum.reject(&Utils.key_in_node_list?(chain_storage_nodes, &1.first_public_key)) end + + @spec get_error_as_atom(t()) :: atom() + def get_error_as_atom(ctx = %__MODULE__{}) do + case ctx.validation_stamp.error do + nil -> + inconsistencies = + ctx.cross_validation_stamps + |> Enum.flat_map(& &1.inconsistencies) + + case inconsistencies do + [] -> + nil + + _ -> + :invalid_cross_validation_stamp + end + + _ -> + :invalid_validation_stamp + end + end end diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index cda4712cc..82054bf5b 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -5,6 +5,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do alias Archethic.Crypto + @publickey1 Crypto.generate_deterministic_keypair("seed2") + @publickey2 Crypto.generate_deterministic_keypair("seed3") + alias Archethic.BeaconChain alias Archethic.BeaconChain.ReplicationAttestation alias Archethic.BeaconChain.SlotTimer, as: BeaconSlotTimer @@ -13,6 +16,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do alias Archethic.Election alias Archethic.Mining.DistributedWorkflow, as: Workflow + alias Archethic.Mining.Fee alias Archethic.Mining.ValidationContext alias Archethic.P2P @@ -20,17 +24,22 @@ defmodule Archethic.Mining.DistributedWorkflowTest do alias Archethic.P2P.Message.CrossValidate alias Archethic.P2P.Message.CrossValidationDone alias Archethic.P2P.Message.GetTransaction + alias Archethic.P2P.Message.GetTransactionSummary alias Archethic.P2P.Message.GetUnspentOutputs alias Archethic.P2P.Message.NotFound alias Archethic.P2P.Message.Ok alias Archethic.P2P.Message.Ping alias Archethic.P2P.Message.ReplicateTransactionChain alias Archethic.P2P.Message.UnspentOutputList + alias Archethic.P2P.Message.ValidationError alias Archethic.P2P.Node + alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.Transaction.CrossValidationStamp alias Archethic.TransactionChain.Transaction.ValidationStamp + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionSummary @@ -118,6 +127,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do _, %Ping{}, _ -> {:ok, %Ok{}} + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %GetUnspentOutputs{}, _ -> {:ok, %UnspentOutputList{unspent_outputs: []}} @@ -198,6 +210,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do _, %Ping{}, _ -> {:ok, %Ok{}} + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %GetUnspentOutputs{}, _ -> {:ok, %UnspentOutputList{unspent_outputs: []}} @@ -300,6 +315,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do _, %Ping{}, _ -> {:ok, %Ok{}} + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %GetUnspentOutputs{}, _ -> {:ok, %UnspentOutputList{unspent_outputs: []}} @@ -423,6 +441,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do _, %Ping{}, _ -> {:ok, %Ok{}} + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %GetUnspentOutputs{}, _ -> {:ok, %UnspentOutputList{unspent_outputs: []}} @@ -552,6 +573,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do _, %Ping{}, _ -> {:ok, %Ok{}} + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %GetUnspentOutputs{}, _ -> {:ok, %UnspentOutputList{unspent_outputs: []}} @@ -783,6 +807,12 @@ defmodule Archethic.Mining.DistributedWorkflowTest do MockClient |> stub(:send_message, fn + _, %ValidationError{}, _ -> + {:ok, %Ok{}} + + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + _, %Ping{}, _ -> {:ok, %Ok{}} @@ -968,7 +998,277 @@ defmodule Archethic.Mining.DistributedWorkflowTest do end assert_receive :replication_done + refute_receive :validation_error end end + + test "should not replicate if there is a validation error", %{tx: tx} do + validation_context = create_context(tx) + + validation_stamp = create_validation_stamp(validation_context) + validation_stamp = %ValidationStamp{validation_stamp | error: :invalid_pending_transaction} + + context = + validation_context + |> ValidationContext.add_validation_stamp(validation_stamp) + + me = self() + + MockClient + |> stub(:send_message, fn + _, %Ping{}, _ -> + {:ok, %Ok{}} + + _, %GetUnspentOutputs{}, _ -> + {:ok, %UnspentOutputList{unspent_outputs: []}} + + _, %ValidationError{}, _ -> + send(me, :validation_error) + {:ok, %Ok{}} + + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + + _, %GetTransaction{}, _ -> + {:ok, %Transaction{}} + end) + + {:ok, coordinator_pid} = + Workflow.start_link( + transaction: context.transaction, + welcome_node: context.welcome_node, + validation_nodes: [context.coordinator_node | context.cross_validation_nodes], + node_public_key: context.coordinator_node.last_public_key + ) + + :sys.replace_state(coordinator_pid, fn {:coordinator, %{context: _}} -> + {:wait_cross_validation_stamps, %{context: context}} + end) + + Workflow.add_cross_validation_stamp( + coordinator_pid, + %CrossValidationStamp{ + signature: + Crypto.sign( + [ValidationStamp.serialize(context.validation_stamp), <<1>>], + elem(@publickey1, 1) + ), + node_public_key: elem(@publickey1, 0), + inconsistencies: [:signature] + } + ) + + Workflow.add_cross_validation_stamp( + coordinator_pid, + %CrossValidationStamp{ + signature: + Crypto.sign( + [ValidationStamp.serialize(context.validation_stamp), <<1>>], + elem(@publickey2, 1) + ), + node_public_key: elem(@publickey2, 0), + inconsistencies: [:signature] + } + ) + + assert_receive :validation_error + refute_receive :ack_replication + refute_receive :replication_done + end + + test "should not replicate if there is a cross validation error", %{tx: tx} do + validation_context = create_context(tx) + + context = + validation_context + |> ValidationContext.add_validation_stamp(create_validation_stamp(validation_context)) + + me = self() + + MockClient + |> stub(:send_message, fn + _, %Ping{}, _ -> + {:ok, %Ok{}} + + _, %GetUnspentOutputs{}, _ -> + {:ok, %UnspentOutputList{unspent_outputs: []}} + + _, %ValidationError{}, _ -> + send(me, :validation_error) + {:ok, %Ok{}} + + _, %GetTransactionSummary{}, _ -> + {:ok, %NotFound{}} + + _, %GetTransaction{}, _ -> + {:ok, %Transaction{}} + end) + + {:ok, coordinator_pid} = + Workflow.start_link( + transaction: context.transaction, + welcome_node: context.welcome_node, + validation_nodes: [context.coordinator_node | context.cross_validation_nodes], + node_public_key: context.coordinator_node.last_public_key + ) + + :sys.replace_state(coordinator_pid, fn {:coordinator, %{context: _}} -> + {:wait_cross_validation_stamps, %{context: context}} + end) + + Workflow.add_cross_validation_stamp( + coordinator_pid, + %CrossValidationStamp{ + signature: + Crypto.sign( + [ValidationStamp.serialize(context.validation_stamp), <<1>>], + elem(@publickey1, 1) + ), + node_public_key: elem(@publickey1, 0), + inconsistencies: [:signature] + } + ) + + Workflow.add_cross_validation_stamp( + coordinator_pid, + %CrossValidationStamp{ + signature: + Crypto.sign( + [ValidationStamp.serialize(context.validation_stamp), <<1>>], + elem(@publickey2, 1) + ), + node_public_key: elem(@publickey2, 0), + inconsistencies: [:signature] + } + ) + + assert_receive :validation_error + refute_receive :ack_replication + refute_receive :replication_done + end + end + + defp create_context( + tx, + validation_time \\ DateTime.utc_now() |> DateTime.truncate(:millisecond) + ) do + {pub1, _} = Crypto.generate_deterministic_keypair("seed") + + welcome_node = %Node{ + last_public_key: pub1, + first_public_key: pub1, + geo_patch: "AAA", + network_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3000, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + } + + coordinator_node = %Node{ + first_public_key: Crypto.first_node_public_key(), + last_public_key: Crypto.last_node_public_key(), + geo_patch: "AAA", + network_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3000, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + } + + cross_validation_nodes = [ + %Node{ + first_public_key: elem(@publickey1, 0), + last_public_key: elem(@publickey1, 0), + geo_patch: "AAA", + network_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3000, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + }, + %Node{ + first_public_key: elem(@publickey2, 0), + last_public_key: elem(@publickey2, 0), + geo_patch: "AAA", + network_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3000, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + } + ] + + previous_storage_nodes = [ + %Node{ + last_public_key: "key2", + first_public_key: "key2", + geo_patch: "AAA", + network_patch: "AAA", + available?: true, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + }, + %Node{ + last_public_key: "key3", + first_public_key: "key3", + geo_patch: "DEA", + network_patch: "DEA", + available?: true, + reward_address: :crypto.strong_rand_bytes(32), + authorized?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-2) + } + ] + + P2P.add_and_connect_node(welcome_node) + P2P.add_and_connect_node(coordinator_node) + Enum.each(cross_validation_nodes, &P2P.add_and_connect_node(&1)) + Enum.each(previous_storage_nodes, &P2P.add_and_connect_node(&1)) + + %ValidationContext{ + transaction: tx, + previous_storage_nodes: previous_storage_nodes, + unspent_outputs: [ + %UnspentOutput{ + from: "@Alice2", + amount: 204_000_000, + type: :UCO, + timestamp: validation_time + } + ], + welcome_node: welcome_node, + coordinator_node: coordinator_node, + cross_validation_nodes: cross_validation_nodes, + cross_validation_nodes_confirmation: <<1::1, 1::1>>, + valid_pending_transaction?: true, + validation_time: validation_time + } + end + + defp create_validation_stamp(%ValidationContext{ + transaction: tx, + unspent_outputs: unspent_outputs, + validation_time: timestamp + }) do + %ValidationStamp{ + timestamp: timestamp, + proof_of_work: Crypto.origin_node_public_key(), + proof_of_integrity: TransactionChain.proof_of_integrity([tx]), + proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), + ledger_operations: + %LedgerOperations{ + fee: Fee.calculate(tx, 0.07, timestamp), + transaction_movements: Transaction.get_movements(tx) + } + |> LedgerOperations.from_transaction(tx, timestamp) + |> LedgerOperations.consume_inputs(tx.address, unspent_outputs, timestamp), + protocol_version: ArchethicCase.current_protocol_version() + } end end From 6696407cc98ebb7c300ad332ecb3c203449a7e66 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 25 Nov 2022 14:14:12 +0100 Subject: [PATCH 2/6] put back the consensus_not_reached state --- lib/archethic/mining/distributed_workflow.ex | 66 +++++++++++--------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index 7c1e03857..f8713c300 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -577,12 +577,7 @@ defmodule Archethic.Mining.DistributedWorkflow do {:add_cross_validation_stamp, cross_validation_stamp = %CrossValidationStamp{}}, :wait_cross_validation_stamps, data = %{ - context: - context = %ValidationContext{ - validation_stamp: validation_stamp, - cross_validation_stamps: cross_validation_stamps, - transaction: tx - } + context: context = %ValidationContext{transaction: tx} } ) do Logger.info("Add cross validation stamp", @@ -591,37 +586,52 @@ defmodule Archethic.Mining.DistributedWorkflow do ) new_context = ValidationContext.add_cross_validation_stamp(context, cross_validation_stamp) - new_data = %{data | context: new_context} if ValidationContext.enough_cross_validation_stamps?(new_context) do if ValidationContext.atomic_commitment?(new_context) do - {:next_state, :replication, new_data} + {:next_state, :replication, %{data | context: new_context}} else - Logger.debug("Validation stamp: #{inspect(validation_stamp, limit: :infinity)}", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - Logger.debug( - "Cross validation stamps: #{inspect(cross_validation_stamps, limit: :infinity)}", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - Logger.error("Consensus not reached", - transaction_address: Base.encode16(tx.address), - transaction_type: tx.type - ) - - MaliciousDetection.start_link(context) - notify_error(:consensus_not_reached, new_data) - :stop + {:next_state, :consensus_not_reached, %{data | context: new_context}} end else - {:keep_state, new_data} + {:keep_state, %{data | context: new_context}} end end + def handle_event( + :enter, + :wait_cross_validation_stamps, + :consensus_not_reached, + data = %{ + context: + context = %ValidationContext{ + transaction: tx, + cross_validation_stamps: cross_validation_stamps, + validation_stamp: validation_stamp + } + } + ) do + Logger.debug("Validation stamp: #{inspect(validation_stamp, limit: :infinity)}", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + Logger.debug("Cross validation stamps: #{inspect(cross_validation_stamps, limit: :infinity)}", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + Logger.error("Consensus not reached", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + MaliciousDetection.start_link(context) + + notify_error(:consensus_not_reached, data) + :keep_state_and_data + end + def handle_event( :enter, from_state, From f40dfd69efb8c14c8d1c1595330133e8f9501cfb Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 25 Nov 2022 15:57:52 +0100 Subject: [PATCH 3/6] stop the FSM in case of consensus_not_reached --- lib/archethic/mining/distributed_workflow.ex | 8 +------- lib/archethic/mining/validation_context.ex | 8 ++++---- test/archethic/mining/distributed_workflow_test.exs | 2 ++ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index f8713c300..012b944ca 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -629,7 +629,7 @@ defmodule Archethic.Mining.DistributedWorkflow do MaliciousDetection.start_link(context) notify_error(:consensus_not_reached, data) - :keep_state_and_data + :stop end def handle_event( @@ -983,12 +983,6 @@ defmodule Archethic.Mining.DistributedWorkflow do :invalid_proof_of_work -> {:invalid_transaction, "Invalid origin signature"} - :invalid_validation_stamp -> - {:network_issue, "Validation error"} - - :invalid_cross_validation_stamp -> - {:network_issue, "Cross validation error"} - reason -> {:network_issue, reason |> Atom.to_string() |> String.replace("_", " ")} end diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index a174ed221..d2ed46f5d 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -1274,12 +1274,12 @@ defmodule Archethic.Mining.ValidationContext do [] -> nil - _ -> - :invalid_cross_validation_stamp + [first_error | _] -> + first_error end - _ -> - :invalid_validation_stamp + err -> + err end end end diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 82054bf5b..df17440ec 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -1074,6 +1074,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do assert_receive :validation_error refute_receive :ack_replication refute_receive :replication_done + refute Process.alive?(coordinator_pid) end test "should not replicate if there is a cross validation error", %{tx: tx} do @@ -1145,6 +1146,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do assert_receive :validation_error refute_receive :ack_replication refute_receive :replication_done + refute Process.alive?(coordinator_pid) end end From b00372635939f7eecb30df57f9b655911eed3864 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 28 Nov 2022 14:13:01 +0100 Subject: [PATCH 4/6] Rename get_error_as_atom to get_first_error and remove the cross validation inconsistency in the test on validation stamp --- lib/archethic/mining/distributed_workflow.ex | 2 +- lib/archethic/mining/validation_context.ex | 7 +++++-- test/archethic/mining/distributed_workflow_test.exs | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index 012b944ca..1e0864b6a 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -644,7 +644,7 @@ defmodule Archethic.Mining.DistributedWorkflow do } ) when from_state in [:cross_validator, :wait_cross_validation_stamps] do - case ValidationContext.get_error_as_atom(context) do + case ValidationContext.get_first_error(context) do nil -> Logger.info("Start replication", transaction_address: Base.encode16(tx_address), diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index d2ed46f5d..014237daa 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -1262,8 +1262,11 @@ defmodule Archethic.Mining.ValidationContext do |> Enum.reject(&Utils.key_in_node_list?(chain_storage_nodes, &1.first_public_key)) end - @spec get_error_as_atom(t()) :: atom() - def get_error_as_atom(ctx = %__MODULE__{}) do + @doc """ + Get the first available error or nil + """ + @spec get_first_error(t()) :: atom() + def get_first_error(ctx = %__MODULE__{}) do case ctx.validation_stamp.error do nil -> inconsistencies = diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index df17440ec..7803bb003 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -1050,11 +1050,11 @@ defmodule Archethic.Mining.DistributedWorkflowTest do %CrossValidationStamp{ signature: Crypto.sign( - [ValidationStamp.serialize(context.validation_stamp), <<1>>], + [ValidationStamp.serialize(context.validation_stamp), <<>>], elem(@publickey1, 1) ), node_public_key: elem(@publickey1, 0), - inconsistencies: [:signature] + inconsistencies: [] } ) @@ -1063,11 +1063,11 @@ defmodule Archethic.Mining.DistributedWorkflowTest do %CrossValidationStamp{ signature: Crypto.sign( - [ValidationStamp.serialize(context.validation_stamp), <<1>>], + [ValidationStamp.serialize(context.validation_stamp), <<>>], elem(@publickey2, 1) ), node_public_key: elem(@publickey2, 0), - inconsistencies: [:signature] + inconsistencies: [] } ) From ac1b7401c7bc8a10b34e1be828857fe23cb6590b Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Wed, 30 Nov 2022 09:28:03 +0100 Subject: [PATCH 5/6] refactor get_first_error --- lib/archethic/mining/validation_context.ex | 29 ++++++++-------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index 014237daa..5222fb785 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -1266,23 +1266,16 @@ defmodule Archethic.Mining.ValidationContext do Get the first available error or nil """ @spec get_first_error(t()) :: atom() - def get_first_error(ctx = %__MODULE__{}) do - case ctx.validation_stamp.error do - nil -> - inconsistencies = - ctx.cross_validation_stamps - |> Enum.flat_map(& &1.inconsistencies) - - case inconsistencies do - [] -> - nil - - [first_error | _] -> - first_error - end - - err -> - err - end + def get_first_error(%__MODULE__{ + validation_stamp: %ValidationStamp{error: nil}, + cross_validation_stamps: cross_validation_stamps + }) do + cross_validation_stamps + |> Enum.reduce_while(nil, fn + %CrossValidationStamp{inconsistencies: []}, nil -> {:cont, nil} + %CrossValidationStamp{inconsistencies: [first_error | _rest]}, nil -> {:halt, first_error} + end) end + + def get_first_error(%__MODULE__{validation_stamp: %ValidationStamp{error: error}}), do: error end From 8c80d688c54a6d85ec967001988269b08befed56 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 2 Dec 2022 09:08:18 +0100 Subject: [PATCH 6/6] Distributed Workflow now checks for funds validation_stamp will have an error if it's the case --- lib/archethic/mining/validation_context.ex | 151 +++++++++++------- .../transaction/validation_stamp.ex | 4 +- .../mining/validation_context_test.exs | 8 + 3 files changed, 106 insertions(+), 57 deletions(-) diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index 5222fb785..d8aafeeec 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -717,6 +717,46 @@ defmodule Archethic.Mining.ValidationContext do resolved_addresses: resolved_addresses } ) do + resolved_recipients = + Enum.reduce(resolved_addresses, [], fn {to, resolved}, acc -> + if to in recipients do + [resolved | acc] + else + acc + end + end) + + ledger_operations = get_ledger_operations(context) + + validation_stamp = + %ValidationStamp{ + protocol_version: Mining.protocol_version(), + timestamp: validation_time, + proof_of_work: do_proof_of_work(tx), + proof_of_integrity: TransactionChain.proof_of_integrity([tx, prev_tx]), + proof_of_election: Election.validation_nodes_election_seed_sorting(tx, validation_time), + ledger_operations: ledger_operations, + recipients: resolved_recipients, + error: + get_validation_error( + prev_tx, + tx, + ledger_operations, + unspent_outputs, + valid_pending_transaction? + ) + } + |> ValidationStamp.sign() + + %{context | validation_stamp: validation_stamp} + end + + defp get_ledger_operations(%__MODULE__{ + transaction: tx, + unspent_outputs: unspent_outputs, + validation_time: validation_time, + resolved_addresses: resolved_addresses + }) do usd_price = validation_time |> OracleChain.get_uco_price() @@ -750,51 +790,49 @@ defmodule Archethic.Mining.ValidationContext do acc end) - resolved_recipients = - Enum.reduce(resolved_addresses, [], fn {to, resolved}, acc -> - if to in recipients do - [resolved | acc] - else - acc - end - end) + %LedgerOperations{ + fee: fee, + transaction_movements: resolved_movements + } + |> LedgerOperations.from_transaction(tx, validation_time) + |> LedgerOperations.consume_inputs( + tx.address, + unspent_outputs, + validation_time |> DateTime.truncate(:millisecond) + ) + end - error = - cond do - chain_error?(prev_tx, tx) -> - :invalid_inherit_constraints + @spec get_validation_error( + nil | Transaction.t(), + Transaction.t(), + LedgerOperations.t(), + list(UnspentOutput.t()), + boolean() + ) :: nil | ValidationStamp.error() + defp get_validation_error( + prev_tx, + tx, + ledger_operations, + unspent_outputs, + valid_pending_transaction? + ) do + cond do + chain_error?(prev_tx, tx) -> + :invalid_inherit_constraints - valid_pending_transaction? -> - nil + has_insufficient_funds?(ledger_operations, unspent_outputs) -> + :insufficient_funds - true -> - :invalid_pending_transaction - end + not valid_pending_transaction? -> + :invalid_pending_transaction - validation_stamp = - %ValidationStamp{ - protocol_version: Mining.protocol_version(), - timestamp: validation_time, - proof_of_work: do_proof_of_work(tx), - proof_of_integrity: TransactionChain.proof_of_integrity([tx, prev_tx]), - proof_of_election: Election.validation_nodes_election_seed_sorting(tx, validation_time), - ledger_operations: - %LedgerOperations{ - fee: fee, - transaction_movements: resolved_movements - } - |> LedgerOperations.from_transaction(tx, validation_time) - |> LedgerOperations.consume_inputs( - tx.address, - unspent_outputs, - validation_time |> DateTime.truncate(:millisecond) - ), - recipients: resolved_recipients, - error: error - } - |> ValidationStamp.sign() + true -> + nil + end + end - %{context | validation_stamp: validation_stamp} + defp has_insufficient_funds?(ledger_ops, inputs) do + not LedgerOperations.sufficient_funds?(ledger_ops, inputs) end defp chain_error?(nil, _tx = %Transaction{}), do: false @@ -1055,24 +1093,25 @@ defmodule Archethic.Mining.ValidationContext do ) == fee end - defp valid_stamp_error?(stamp = %ValidationStamp{error: error}, %__MODULE__{ - transaction: tx, - previous_transaction: prev_tx, - valid_pending_transaction?: valid_pending_transaction? - }) do - validated_tx = %{tx | validation_stamp: stamp} + defp valid_stamp_error?( + stamp = %ValidationStamp{error: error}, + context = %__MODULE__{ + transaction: tx, + previous_transaction: prev_tx, + valid_pending_transaction?: valid_pending_transaction?, + unspent_outputs: unspent_outputs + } + ) do + validated_context = %{context | transaction: %{tx | validation_stamp: stamp}} expected_error = - cond do - chain_error?(prev_tx, validated_tx) -> - :invalid_inherit_constraints - - valid_pending_transaction? -> - nil - - true -> - :invalid_pending_transaction - end + get_validation_error( + prev_tx, + tx, + get_ledger_operations(validated_context), + unspent_outputs, + valid_pending_transaction? + ) error == expected_error end diff --git a/lib/archethic/transaction_chain/transaction/validation_stamp.ex b/lib/archethic/transaction_chain/transaction/validation_stamp.ex index e43cf1622..1bc9fc4b4 100755 --- a/lib/archethic/transaction_chain/transaction/validation_stamp.ex +++ b/lib/archethic/transaction_chain/transaction/validation_stamp.ex @@ -21,7 +21,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do error: nil ] - @type error :: :invalid_pending_transaction | :invalid_inherit_constraints + @type error :: :invalid_pending_transaction | :invalid_inherit_constraints | :insufficient_funds @typedoc """ Validation performed by a coordinator: @@ -426,8 +426,10 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do defp serialize_error(nil), do: 0 defp serialize_error(:invalid_pending_transaction), do: 1 defp serialize_error(:invalid_inherit_constraints), do: 2 + defp serialize_error(:insufficient_funds), do: 3 defp deserialize_error(0), do: nil defp deserialize_error(1), do: :invalid_pending_transaction defp deserialize_error(2), do: :invalid_inherit_constraints + defp deserialize_error(3), do: :insufficient_funds end diff --git a/test/archethic/mining/validation_context_test.exs b/test/archethic/mining/validation_context_test.exs index 5c9b71b19..ac72e4989 100644 --- a/test/archethic/mining/validation_context_test.exs +++ b/test/archethic/mining/validation_context_test.exs @@ -41,6 +41,14 @@ defmodule Archethic.Mining.ValidationContextTest do |> ValidationContext.cross_validate() end + test "should get inconsistency when the user has not enough funds" do + validation_context = + %ValidationContext{create_context() | unspent_outputs: []} + |> ValidationContext.create_validation_stamp() + + assert validation_context.validation_stamp.error == :insufficient_funds + end + test "should get inconsistency when the validation stamp signature is invalid" do validation_context = create_context()