From 2813bb7990af4e71c3118e0f1ccd7be85271edbd Mon Sep 17 00:00:00 2001 From: bchamagne <74045243+bchamagne@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:45:15 +0100 Subject: [PATCH 1/5] Add the genesis address in the validation_stamp (#1588) * add stamp.genesis_address * migration: add genesis to io * code_change * add genesis to graphql * add genesis to explorer * add genesis column to transactions list * set the genesis before decoding columns * code_change fix * remove opts from deserialize * add missing aliases * update graphql desc * Add genesis address only if requested * Hide genesis address in mobile view --------- Co-authored-by: Neylix --- lib/archethic/bootstrap/network_init.ex | 1 + .../db/embedded_impl/chain_reader.ex | 41 ++++--- .../db/embedded_impl/chain_writer.ex | 2 +- lib/archethic/db/embedded_impl/encoding.ex | 81 ++++++++------ lib/archethic/mining.ex | 3 +- lib/archethic/mining/distributed_workflow.ex | 23 +++- lib/archethic/mining/validation_context.ex | 10 +- .../transaction/cross_validation_stamp.ex | 12 ++- .../transaction/validation_stamp.ex | 101 ++++++++++-------- .../api/graphql/schema/transaction_type.ex | 2 + .../explorer/components/transactions_list.ex | 27 ++++- .../chains/node_shared_secrets_chain_live.ex | 22 +++- .../explorer/live/chains/oracle_chain_live.ex | 11 +- .../explorer/live/chains/origin_chain_live.ex | 10 +- .../explorer/live/chains/reward_chain_live.ex | 15 +-- .../live/transaction_details_live.html.heex | 21 +++- priv/migration_tasks/prod/1.6.0@genesis.exs | 33 ++++++ test/archethic/db/embedded_impl_test.exs | 18 ++-- .../mining/distributed_workflow_test.exs | 3 + .../mining/validation_context_test.exs | 17 +++ test/archethic/p2p/messages_test.exs | 3 + .../cross_validation_stamp_test.exs | 7 +- .../transaction/validation_stamp_test.exs | 54 +++++++++- .../transaction_chain/transaction_test.exs | 1 + test/support/transaction_factory.ex | 7 ++ 25 files changed, 394 insertions(+), 131 deletions(-) create mode 100644 priv/migration_tasks/prod/1.6.0@genesis.exs diff --git a/lib/archethic/bootstrap/network_init.ex b/lib/archethic/bootstrap/network_init.ex index f760c322ad..e88e586b49 100644 --- a/lib/archethic/bootstrap/network_init.ex +++ b/lib/archethic/bootstrap/network_init.ex @@ -200,6 +200,7 @@ defmodule Archethic.Bootstrap.NetworkInit do validation_stamp = %ValidationStamp{ + genesis_address: Transaction.previous_address(tx), protocol_version: 1, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index 566c39ab51..69a0d54444 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -50,6 +50,7 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do fd |> read_transaction(column_names, size, 0) |> Enum.into(%{}) + |> set_genesis_address(column_names, genesis_address) |> decode_transaction_columns(version) File.close(fd) @@ -163,12 +164,13 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do case File.open(filepath, [:binary, :read]) do {:ok, fd} -> Stream.resource( - fn -> process_get_chain(fd, fields, [], db_path) end, + fn -> process_get_chain(fd, genesis_address, fields, [], db_path) end, fn {transactions, true, paging_address} -> next_transactions = process_get_chain( fd, + genesis_address, fields, [paging_address: paging_address], db_path @@ -273,14 +275,17 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do _ -> opts end - process_get_chain(fd, fields, opts, db_path) + process_get_chain(fd, genesis_address, fields, opts, db_path) :desc -> # if the paging_address=genesis_address, # we return empty case Keyword.get(opts, :paging_address) do - ^genesis_address -> {[], false, nil} - _ -> process_get_chain_desc(fd, genesis_address, fields, opts, db_path) + ^genesis_address -> + {[], false, nil} + + _ -> + process_get_chain_desc(fd, genesis_address, fields, opts, db_path) end end @@ -288,18 +293,18 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do {transactions, more?, paging_address} end - defp process_get_chain(fd, fields, opts, db_path) do + defp process_get_chain(fd, genesis_address, fields, opts, db_path) do # Set the file cursor position to the paging state case Keyword.get(opts, :paging_address) do nil -> :file.position(fd, 0) - do_process_get_chain(fd, fields) + do_process_get_chain(fd, genesis_address, fields) paging_address -> case ChainIndex.get_tx_entry(paging_address, db_path) do {:ok, %{offset: offset, size: size}} -> :file.position(fd, offset + size) - do_process_get_chain(fd, fields) + do_process_get_chain(fd, genesis_address, fields) {:error, :not_exists} -> {[], false, nil} @@ -307,7 +312,7 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do end end - defp do_process_get_chain(fd, fields) do + defp do_process_get_chain(fd, genesis_address, fields) do # Always return transaction address fields = if Enum.empty?(fields), do: fields, else: Enum.uniq([:address | fields]) @@ -328,7 +333,7 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do end # Read the transactions until the nb of transactions to fullfil the page (ie. 10 transactions) - {transactions, more?, paging_address} = get_paginated_chain(fd, column_names) + {transactions, more?, paging_address} = get_paginated_chain(fd, genesis_address, column_names) {transactions, more?, paging_address} end @@ -376,14 +381,14 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do # call the ASC function and ignore the more? and paging_address {transactions, _more?, _paging_address} = - process_get_chain(fd, fields, [paging_address: paging_address], db_path) + process_get_chain(fd, genesis_address, fields, [paging_address: paging_address], db_path) transactions = Enum.take(transactions, nb_to_take) {Enum.reverse(transactions), more?, new_paging_address} end - defp get_paginated_chain(fd, fields, acc \\ []) do + defp get_paginated_chain(fd, genesis_address, fields, acc \\ []) do case :file.read(fd, 8) do {:ok, <>} -> if length(acc) == @page_size do @@ -393,9 +398,10 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do tx = fd |> read_transaction(fields, size, 0) + |> set_genesis_address(fields, genesis_address) |> decode_transaction_columns(version) - get_paginated_chain(fd, fields, [tx | acc]) + get_paginated_chain(fd, genesis_address, fields, [tx | acc]) end :eof -> @@ -553,4 +559,15 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do |> Utils.atomize_keys() |> Transaction.cast() end + + defp set_genesis_address(column_values, [], genesis_address), + do: Map.put(column_values, "validation_stamp.genesis_address", genesis_address) + + defp set_genesis_address(column_values, fields, genesis_address) do + genesis_field_name = "validation_stamp.genesis_address" + + if Enum.member?(fields, genesis_field_name), + do: Map.put(column_values, genesis_field_name, genesis_address), + else: column_values + end end diff --git a/lib/archethic/db/embedded_impl/chain_writer.ex b/lib/archethic/db/embedded_impl/chain_writer.ex index c99671e2ad..4c674ec289 100644 --- a/lib/archethic/db/embedded_impl/chain_writer.ex +++ b/lib/archethic/db/embedded_impl/chain_writer.ex @@ -40,7 +40,7 @@ defmodule Archethic.DB.EmbeddedImpl.ChainWriter do filename = io_path(db_path, address) - data = Encoding.encode(tx) + data = Encoding.encode(tx, storage_type: :io) File.write!( filename, diff --git a/lib/archethic/db/embedded_impl/encoding.ex b/lib/archethic/db/embedded_impl/encoding.ex index 4cde7efddd..847b51a474 100644 --- a/lib/archethic/db/embedded_impl/encoding.ex +++ b/lib/archethic/db/embedded_impl/encoding.ex @@ -25,39 +25,47 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do @doc """ Encode a transaction + + Opts: + storage_type: :io | :chain (default: :chain) + """ - @spec encode(Transaction.t()) :: binary() - def encode(%Transaction{ - version: tx_version, - address: address, - type: type, - data: %TransactionData{ - content: content, - code: code, - ownerships: ownerships, - ledger: %Ledger{uco: uco_ledger, token: token_ledger}, - recipients: recipients - }, - previous_public_key: previous_public_key, - previous_signature: previous_signature, - origin_signature: origin_signature, - validation_stamp: %ValidationStamp{ - timestamp: timestamp, - proof_of_work: proof_of_work, - proof_of_integrity: proof_of_integrity, - proof_of_election: proof_of_election, - ledger_operations: %LedgerOperations{ - fee: fee, - transaction_movements: transaction_movements, - unspent_outputs: unspent_outputs, - consumed_inputs: consumed_inputs + @spec encode(transaction :: Transaction.t(), opts :: Keyword.t()) :: binary() + def encode( + %Transaction{ + version: tx_version, + address: address, + type: type, + data: %TransactionData{ + content: content, + code: code, + ownerships: ownerships, + ledger: %Ledger{uco: uco_ledger, token: token_ledger}, + recipients: recipients }, - recipients: resolved_recipients, - signature: validation_stamp_sig, - protocol_version: protocol_version + previous_public_key: previous_public_key, + previous_signature: previous_signature, + origin_signature: origin_signature, + validation_stamp: %ValidationStamp{ + genesis_address: genesis_address, + timestamp: timestamp, + proof_of_work: proof_of_work, + proof_of_integrity: proof_of_integrity, + proof_of_election: proof_of_election, + ledger_operations: %LedgerOperations{ + fee: fee, + transaction_movements: transaction_movements, + unspent_outputs: unspent_outputs, + consumed_inputs: consumed_inputs + }, + recipients: resolved_recipients, + signature: validation_stamp_sig, + protocol_version: protocol_version + }, + cross_validation_stamps: cross_validation_stamps }, - cross_validation_stamps: cross_validation_stamps - }) do + opts \\ [] + ) do ownerships_encoding = ownerships |> Enum.map(&Ownership.serialize(&1, tx_version)) @@ -143,6 +151,7 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do {"cross_validation_stamps", <>} ] + |> maybe_add_genesis_address(genesis_address, Keyword.get(opts, :storage_type, :chain)) |> Enum.map(fn {column, value} -> wrapped_value = Utils.wrap_binary(value) @@ -210,6 +219,10 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do put_in(acc, [Access.key(:validation_stamp, %{}), :proof_of_election], poe) end + def decode(_version, "validation_stamp.genesis_address", genesis_address, acc) do + put_in(acc, [Access.key(:validation_stamp, %{}), :genesis_address], genesis_address) + end + def decode(_version, "validation_stamp.ledger_operations.fee", <>, acc) do put_in( acc, @@ -361,4 +374,12 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do {stamp, rest} = CrossValidationStamp.deserialize(rest) deserialize_cross_validation_stamps(rest, nb, [stamp | acc]) end + + defp maybe_add_genesis_address(encodings, _genesis_address, :chain) do + encodings + end + + defp maybe_add_genesis_address(encodings, genesis_address, :io) do + [{"validation_stamp.genesis_address", genesis_address} | encodings] + end end diff --git a/lib/archethic/mining.ex b/lib/archethic/mining.ex index 4918cba9fa..47ae964a12 100644 --- a/lib/archethic/mining.ex +++ b/lib/archethic/mining.ex @@ -34,7 +34,8 @@ defmodule Archethic.Mining do # version 5->6 the POI changed and is now done with tx.data.recipients.args serialized with :extended mode # version 6->7 add Add consumed inputs in tx.validation_stamp.ledger_operations # version 7->8 movement resolved address are now the genesis address of the destination - @protocol_version 8 + # version 8->9 genesis in the validation stamp + @protocol_version 9 @lock_threshold 0.75 diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index 0dce7fd31d..763838ded6 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -56,7 +56,7 @@ defmodule Archethic.Mining.DistributedWorkflow do require Logger use GenStateMachine, callback_mode: [:handle_event_function, :state_enter], restart: :temporary - @vsn 2 + @vsn 3 @mining_timeout Application.compile_env!(:archethic, [__MODULE__, :global_timeout]) @coordinator_timeout_supplement Application.compile_env!(:archethic, [ @@ -1022,6 +1022,27 @@ defmodule Archethic.Mining.DistributedWorkflow do {:keep_state_and_data, :postpone} end + def code_change(2, state, data, _extra) do + {:ok, state, + case Map.get(data, :context) do + ctx = %Archethic.Mining.ValidationContext{ + genesis_address: genesis_address, + validation_stamp: stamp + } + when not is_nil(genesis_address) and not is_nil(stamp) -> + %{ + data + | context: %Archethic.Mining.ValidationContext{ + ctx + | validation_stamp: Map.put(stamp, :genesis_address, genesis_address) + } + } + + _ -> + data + end} + end + def code_change(_old_vsn, state, data, _extra), do: {:ok, state, data} defp notify_transaction_context( diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index 8b1a127dd9..5238ff5873 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -703,6 +703,7 @@ defmodule Archethic.Mining.ValidationContext do @spec create_validation_stamp(t()) :: t() def create_validation_stamp( context = %__MODULE__{ + genesis_address: genesis_address, transaction: tx = %Transaction{data: %TransactionData{recipients: recipients}}, previous_transaction: prev_tx, validation_time: validation_time, @@ -728,6 +729,7 @@ defmodule Archethic.Mining.ValidationContext do get_ledger_operations(context, fee, validation_time, encoded_state) validation_stamp = %ValidationStamp{ + genesis_address: genesis_address, protocol_version: Mining.protocol_version(), timestamp: validation_time, proof_of_work: do_proof_of_work(tx), @@ -1146,7 +1148,8 @@ defmodule Archethic.Mining.ValidationContext do consumed_inputs: fn -> valid_consumed_inputs?(stamp, ledger_operations) end, unspent_outputs: fn -> valid_stamp_unspent_outputs?(stamp, ledger_operations) end, error: fn -> valid_stamp_error?(stamp, context) end, - protocol_version: fn -> valid_protocol_version?(stamp) end + protocol_version: fn -> valid_protocol_version?(stamp) end, + genesis_address: fn -> valid_genesis_address?(stamp, context) end ] subsets_verifications @@ -1261,6 +1264,11 @@ defmodule Archethic.Mining.ValidationContext do defp valid_protocol_version?(%ValidationStamp{protocol_version: version}), do: Mining.protocol_version() == version + defp valid_genesis_address?(%ValidationStamp{genesis_address: genesis_address}, %__MODULE__{ + genesis_address: ctx_genesis_address + }), + do: genesis_address == ctx_genesis_address + @doc """ Get the chain storage node position """ diff --git a/lib/archethic/transaction_chain/transaction/cross_validation_stamp.ex b/lib/archethic/transaction_chain/transaction/cross_validation_stamp.ex index 370a854a0a..eeaa43f7a1 100644 --- a/lib/archethic/transaction_chain/transaction/cross_validation_stamp.ex +++ b/lib/archethic/transaction_chain/transaction/cross_validation_stamp.ex @@ -23,6 +23,7 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStamp do | :protocol_version | :consumed_inputs | :aggregated_utxos + | :genesis_address @typedoc """ A cross validation stamp is composed from: @@ -42,9 +43,10 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStamp do @spec sign(t(), ValidationStamp.t()) :: t() def sign( cross_stamp = %__MODULE__{inconsistencies: inconsistencies}, - validation_stamp = %ValidationStamp{} + validation_stamp = %ValidationStamp{protocol_version: protocol_version} ) do - raw_stamp = ValidationStamp.serialize(validation_stamp) + raw_stamp = + ValidationStamp.serialize(validation_stamp, serialize_genesis?: protocol_version >= 9) signature = [raw_stamp, marshal_inconsistencies(inconsistencies)] @@ -66,9 +68,9 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStamp do inconsistencies: inconsistencies, node_public_key: node_public_key }, - stamp = %ValidationStamp{} + stamp = %ValidationStamp{protocol_version: protocol_version} ) do - raw_stamp = ValidationStamp.serialize(stamp) + raw_stamp = ValidationStamp.serialize(stamp, serialize_genesis?: protocol_version >= 9) data = [raw_stamp, marshal_inconsistencies(inconsistencies)] Crypto.verify?(signature, data, node_public_key) @@ -132,6 +134,7 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStamp do defp serialize_inconsistency(:consumed_inputs), do: 10 defp serialize_inconsistency(:aggregated_utxos), do: 11 defp serialize_inconsistency(:recipients), do: 12 + defp serialize_inconsistency(:genesis_address), do: 13 @doc """ Deserialize an encoded cross validation stamp @@ -201,6 +204,7 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStamp do defp do_reduce_inconsistencies(<<10::8, rest::bitstring>>), do: {:consumed_inputs, rest} defp do_reduce_inconsistencies(<<11::8, rest::bitstring>>), do: {:aggregated_utxos, rest} defp do_reduce_inconsistencies(<<12::8, rest::bitstring>>), do: {:recipients, rest} + defp do_reduce_inconsistencies(<<13::8, rest::bitstring>>), do: {:genesis_address, rest} @spec cast(map()) :: t() def cast(stamp = %{}) do diff --git a/lib/archethic/transaction_chain/transaction/validation_stamp.ex b/lib/archethic/transaction_chain/transaction/validation_stamp.ex index 17cfb4262e..460fb66582 100755 --- a/lib/archethic/transaction_chain/transaction/validation_stamp.ex +++ b/lib/archethic/transaction_chain/transaction/validation_stamp.ex @@ -12,6 +12,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do defstruct [ :protocol_version, + :genesis_address, :timestamp, :signature, :proof_of_work, @@ -43,6 +44,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do - Signature: generated from the coordinator private key to avoid non-repudiation of the stamp - Error: Error returned by the pending transaction validation or after mining context - Protocol version: Version of the protocol + - Genesis address: Genesis of the chain. Added in protocol_version=9 """ @type t :: %__MODULE__{ timestamp: DateTime.t(), @@ -51,17 +53,18 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do proof_of_integrity: Crypto.versioned_hash(), proof_of_election: binary(), ledger_operations: LedgerOperations.t(), - recipients: list(Crypto.versioned_hash()), + recipients: list(Crypto.prepended_hash()), + genesis_address: Crypto.prepended_hash(), error: error() | nil, protocol_version: non_neg_integer() } @spec sign(__MODULE__.t()) :: __MODULE__.t() - def sign(stamp = %__MODULE__{}) do + def sign(stamp = %__MODULE__{protocol_version: protocol_version}) do raw_stamp = stamp |> extract_for_signature() - |> serialize() + |> serialize(serialize_genesis?: protocol_version >= 9) sig = Crypto.sign_with_last_node_key(raw_stamp) @@ -80,7 +83,8 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do ledger_operations: ops, recipients: recipients, error: error, - protocol_version: version + protocol_version: version, + genesis_address: genesis_address }) do %__MODULE__{ timestamp: timestamp, @@ -90,25 +94,35 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do ledger_operations: ops, recipients: recipients, error: error, - protocol_version: version + protocol_version: version, + genesis_address: genesis_address } end @doc """ Serialize a validation stamp info binary format + + Opts: + serialize_genesis?: true | false """ @spec serialize(t()) :: bitstring() - def serialize(%__MODULE__{ - timestamp: timestamp, - proof_of_work: pow, - proof_of_integrity: poi, - proof_of_election: poe, - ledger_operations: ledger_operations, - recipients: recipients, - error: error, - signature: nil, - protocol_version: version - }) do + def serialize(stamp, opts \\ []) + + def serialize( + %__MODULE__{ + timestamp: timestamp, + proof_of_work: pow, + proof_of_integrity: poi, + proof_of_election: poe, + ledger_operations: ledger_operations, + recipients: recipients, + error: error, + signature: signature, + protocol_version: version, + genesis_address: genesis_address + }, + opts + ) do pow = if pow == "" do # Empty public key if the no public key matching the origin signature @@ -123,39 +137,26 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do poe::binary, LedgerOperations.serialize(ledger_operations, version)::bitstring, encoded_recipients_len::binary, :erlang.list_to_binary(recipients)::binary, serialize_error(error)::8>> + |> maybe_add_signature(signature) + |> maybe_add_genesis(genesis_address, Keyword.get(opts, :serialize_genesis?, true)) end - def serialize(%__MODULE__{ - timestamp: timestamp, - proof_of_work: pow, - proof_of_integrity: poi, - proof_of_election: poe, - ledger_operations: ledger_operations, - recipients: recipients, - error: error, - signature: signature, - protocol_version: version - }) do - pow = - if pow == "" do - # Empty public key if the no public key matching the origin signature - <<0::8, 0::8, 0::256>> - else - pow - end + defp maybe_add_signature(bin, nil), do: bin - encoded_recipients_len = length(recipients) |> VarInt.from_value() + defp maybe_add_signature(bin, signature), + do: <> - <> - end + defp maybe_add_genesis(bin, _genesis_address, false), do: bin + + defp maybe_add_genesis(bin, genesis_address, true), + do: <> @doc """ Deserialize an encoded validation stamp + + Never used after a serialize(serialize_genesis?: false) """ - @spec deserialize(bitstring()) :: {t(), bitstring()} + @spec deserialize(bin :: bitstring()) :: {t(), bitstring()} def deserialize(<>) do <> = rest pow_key_size = Crypto.key_size(pow_curve_id) @@ -177,8 +178,11 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do <> = rest + {genesis_address, rest} = Utils.deserialize_address(rest) + { %__MODULE__{ + genesis_address: genesis_address, timestamp: DateTime.from_unix!(timestamp, :millisecond), proof_of_work: pow, proof_of_integrity: <>, @@ -205,6 +209,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do recipients: Map.get(stamp, :recipients, []), signature: Map.get(stamp, :signature), error: Map.get(stamp, :error), + genesis_address: Map.get(stamp, :genesis_address), protocol_version: Map.get(stamp, :protocol_version) } end @@ -220,7 +225,8 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do ledger_operations: ledger_operations, recipients: recipients, signature: signature, - error: error + error: error, + genesis_address: genesis_address }) do %{ timestamp: timestamp, @@ -230,7 +236,8 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do ledger_operations: LedgerOperations.to_map(ledger_operations), recipients: recipients, signature: signature, - error: error + error: error, + genesis_address: genesis_address } end @@ -243,14 +250,14 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do def valid_signature?(%__MODULE__{signature: nil}, _public_key), do: false def valid_signature?( - stamp = %__MODULE__{signature: signature}, + stamp = %__MODULE__{signature: signature, protocol_version: protocol_version}, public_key ) when is_binary(signature) do raw_stamp = stamp - |> extract_for_signature - |> serialize + |> extract_for_signature() + |> serialize(serialize_genesis?: protocol_version >= 9) Crypto.verify?(signature, raw_stamp, public_key) end @@ -263,6 +270,8 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do def generate_dummy(opts \\ []) do %__MODULE__{ timestamp: Keyword.get(opts, :timestamp, DateTime.utc_now()), + genesis_address: + Keyword.get(opts, :genesis_address, <<0::16, :crypto.strong_rand_bytes(32)::binary>>), protocol_version: 1, proof_of_work: :crypto.strong_rand_bytes(32), proof_of_election: :crypto.strong_rand_bytes(32), diff --git a/lib/archethic_web/api/graphql/schema/transaction_type.ex b/lib/archethic_web/api/graphql/schema/transaction_type.ex index 3082b0912f..9448ec5c78 100644 --- a/lib/archethic_web/api/graphql/schema/transaction_type.ex +++ b/lib/archethic_web/api/graphql/schema/transaction_type.ex @@ -138,6 +138,7 @@ defmodule ArchethicWeb.API.GraphQL.Schema.TransactionType do - Ledger operations: All the operations performed by the transaction - Signature: Coordinator signature of the stamp - Protocol version: Version of the transaction validation protocol + - Genesis address: Genesis address of the chain """ object :validation_stamp do field(:timestamp, :timestamp) @@ -146,6 +147,7 @@ defmodule ArchethicWeb.API.GraphQL.Schema.TransactionType do field(:ledger_operations, :ledger_operations) field(:signature, :hex) field(:protocol_version, :integer) + field(:genesis_address, :hex) end @desc """ diff --git a/lib/archethic_web/explorer/components/transactions_list.ex b/lib/archethic_web/explorer/components/transactions_list.ex index 99f7bffc68..99cab4eaf2 100644 --- a/lib/archethic_web/explorer/components/transactions_list.ex +++ b/lib/archethic_web/explorer/components/transactions_list.ex @@ -52,6 +52,7 @@ defmodule ArchethicWeb.Explorer.Components.TransactionsList do
  • Address
    +
    Genesis
    Type
    Date (UTC)
    Fee
    @@ -69,6 +70,16 @@ defmodule ArchethicWeb.Explorer.Components.TransactionsList do ) ) %> +
    + <%= link(short_address(get_genesis(tx)), + to: + Routes.live_path( + @socket, + ArchethicWeb.Explorer.TransactionChainLive, + address: Base.encode16(get_genesis(tx)) + ) + ) %> +
    <%= format_transaction_type(tx.type) %>
    <%= format_date(get_timestamp(tx), display_utc: false) %> @@ -87,18 +98,30 @@ defmodule ArchethicWeb.Explorer.Components.TransactionsList do """ end - # reward/nss/home... + # beacon defp get_timestamp(%TransactionSummary{timestamp: timestamp}), do: timestamp # chain page defp get_timestamp(%Transaction{validation_stamp: %ValidationStamp{timestamp: timestamp}}), do: timestamp - # oracle/origin page + # nss/reward/oracle/origin page defp get_timestamp(map) do Map.get(map, :timestamp) end + defp get_genesis(%Transaction{ + validation_stamp: %ValidationStamp{genesis_address: genesis_address} + }), + do: genesis_address + + defp get_genesis(%TransactionSummary{genesis_address: genesis_address}), + do: genesis_address + + defp get_genesis(map) do + Map.get(map, :genesis_address) + end + # reward/nss/home... defp get_fee(%TransactionSummary{fee: fee}), do: fee diff --git a/lib/archethic_web/explorer/live/chains/node_shared_secrets_chain_live.ex b/lib/archethic_web/explorer/live/chains/node_shared_secrets_chain_live.ex index d899e5e2bb..873fb031f8 100644 --- a/lib/archethic_web/explorer/live/chains/node_shared_secrets_chain_live.ex +++ b/lib/archethic_web/explorer/live/chains/node_shared_secrets_chain_live.ex @@ -3,6 +3,7 @@ defmodule ArchethicWeb.Explorer.NodeSharedSecretsChainLive do use ArchethicWeb.Explorer, :live_view + alias Archethic.Crypto alias Archethic.OracleChain alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction @@ -110,7 +111,15 @@ defmodule ArchethicWeb.Explorer.NodeSharedSecretsChainLive do |> update( :transactions, fn tx_list -> - [display_data(address, nb_auth_nodes, timestamp) | tx_list] + [ + display_data( + SharedSecrets.genesis_address(@txn_type), + address, + nb_auth_nodes, + timestamp + ) + | tx_list + ] |> Enum.take(@display_limit) end ) @@ -135,8 +144,8 @@ defmodule ArchethicWeb.Explorer.NodeSharedSecretsChainLive do if nb_drops < 0, do: {0, @display_limit + nb_drops}, else: {nb_drops, @display_limit} case SharedSecrets.genesis_address(@txn_type) do - address when is_binary(address) -> - address + genesis_address when is_binary(genesis_address) -> + genesis_address |> TransactionChain.list_chain_addresses() |> Stream.drop(nb_drops) |> Stream.take(display_limit) @@ -144,6 +153,7 @@ defmodule ArchethicWeb.Explorer.NodeSharedSecretsChainLive do nb_authorized_nodes = nb_of_authorized_keys(addr) display_data( + genesis_address, addr, nb_authorized_nodes, timestamp @@ -168,13 +178,15 @@ defmodule ArchethicWeb.Explorer.NodeSharedSecretsChainLive do end @spec display_data( - address :: binary(), + genesis_address :: Crypto.prepended_hash(), + address :: Crypto.prepended_hash(), nb_authorized_nodes :: non_neg_integer(), timestamp :: DateTime.t() ) :: map() - defp display_data(address, nb_authorized_nodes, timestamp) do + defp display_data(genesis_address, address, nb_authorized_nodes, timestamp) do %{ + genesis_address: genesis_address, address: address, type: @txn_type, timestamp: timestamp, diff --git a/lib/archethic_web/explorer/live/chains/oracle_chain_live.ex b/lib/archethic_web/explorer/live/chains/oracle_chain_live.ex index 86100a12da..5e02f569e2 100644 --- a/lib/archethic_web/explorer/live/chains/oracle_chain_live.ex +++ b/lib/archethic_web/explorer/live/chains/oracle_chain_live.ex @@ -153,17 +153,24 @@ defmodule ArchethicWeb.Explorer.OracleChainLive do |> TransactionChain.get([ :address, :type, - validation_stamp: [:timestamp, ledger_operations: [:fee]] + validation_stamp: [:genesis_address, :timestamp, ledger_operations: [:fee]] ]) |> Enum.map(fn %Transaction{ address: address, type: type, validation_stamp: %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, ledger_operations: %LedgerOperations{fee: fee} } } -> - %{address: address, type: type, timestamp: timestamp, fee: fee} + %{ + address: address, + type: type, + timestamp: timestamp, + fee: fee, + genesis_address: genesis_address + } end) |> Enum.reverse() end diff --git a/lib/archethic_web/explorer/live/chains/origin_chain_live.ex b/lib/archethic_web/explorer/live/chains/origin_chain_live.ex index 238fb18219..209092e21c 100644 --- a/lib/archethic_web/explorer/live/chains/origin_chain_live.ex +++ b/lib/archethic_web/explorer/live/chains/origin_chain_live.ex @@ -128,11 +128,14 @@ defmodule ArchethicWeb.Explorer.OriginChainLive do with {:ok, %Transaction{ data: %TransactionData{content: content}, - validation_stamp: %ValidationStamp{timestamp: timestamp} + validation_stamp: %ValidationStamp{ + timestamp: timestamp, + genesis_address: genesis_address + } }} <- TransactionChain.get_transaction(address, data: [:content], - validation_stamp: [:timestamp] + validation_stamp: [:timestamp, :genesis_address] ), {pb_key, _} <- Utils.deserialize_public_key(content), family_id <- SharedSecrets.origin_family_from_public_key(pb_key) do @@ -140,7 +143,8 @@ defmodule ArchethicWeb.Explorer.OriginChainLive do address: address, type: @txn_type, timestamp: timestamp, - family_of_origin: family_id + family_of_origin: family_id, + genesis_address: genesis_address } else _ -> [] diff --git a/lib/archethic_web/explorer/live/chains/reward_chain_live.ex b/lib/archethic_web/explorer/live/chains/reward_chain_live.ex index 9fb6aa2074..8b8d7abb98 100644 --- a/lib/archethic_web/explorer/live/chains/reward_chain_live.ex +++ b/lib/archethic_web/explorer/live/chains/reward_chain_live.ex @@ -3,6 +3,7 @@ defmodule ArchethicWeb.Explorer.RewardChainLive do use ArchethicWeb.Explorer, :live_view + alias Archethic.Crypto alias Archethic.OracleChain alias Archethic.TransactionChain alias Archethic.PubSub @@ -95,7 +96,7 @@ defmodule ArchethicWeb.Explorer.RewardChainLive do 1 -> socket |> update(:transactions, fn tx_list -> - [display_data(address, type, timestamp) | tx_list] + [display_data(Reward.genesis_address(), address, type, timestamp) | tx_list] |> Enum.take(@display_limit) end) |> assign(:tx_count, tx_count + 1) @@ -115,13 +116,14 @@ defmodule ArchethicWeb.Explorer.RewardChainLive do if nb_drops < 0, do: {0, @display_limit + nb_drops}, else: {nb_drops, @display_limit} case Reward.genesis_address() do - address when is_binary(address) -> - address + genesis_address when is_binary(genesis_address) -> + genesis_address |> TransactionChain.list_chain_addresses() |> Stream.drop(nb_drops) |> Stream.take(display_limit) |> Stream.map(fn {addr, timestamp} -> display_data( + genesis_address, addr, (TransactionChain.get_transaction(addr, [:type]) |> elem(1)).type, timestamp @@ -135,12 +137,13 @@ defmodule ArchethicWeb.Explorer.RewardChainLive do end @spec display_data( - address :: binary(), + genesis_address :: Crypto.prepended_hash(), + address :: Crypto.prepended_hash(), type :: :node_rewards | :mint_rewards, timestamp :: DateTime.t() ) :: map() - defp display_data(address, type, timestamp) do - %{address: address, type: type, timestamp: timestamp} + defp display_data(genesis_address, address, type, timestamp) do + %{genesis_address: genesis_address, address: address, type: type, timestamp: timestamp} end end diff --git a/lib/archethic_web/explorer/live/transaction_details_live.html.heex b/lib/archethic_web/explorer/live/transaction_details_live.html.heex index ae326d6702..80afdf4928 100644 --- a/lib/archethic_web/explorer/live/transaction_details_live.html.heex +++ b/lib/archethic_web/explorer/live/transaction_details_live.html.heex @@ -19,9 +19,9 @@ <% end %>
    <% end %> - <%= if @address != burning_address() do %> + <%= if @address != burning_address() && not is_nil(@transaction) do %>
    - <%= link class: "simple-button", to: Routes.live_path(@socket, ArchethicWeb.Explorer.TransactionChainLive, address: Base.encode16(@address)) do %> + <%= link class: "simple-button", to: Routes.live_path(@socket, ArchethicWeb.Explorer.TransactionChainLive, address: Base.encode16(@transaction.validation_stamp.genesis_address)) do %> Explore chain <% end %>
    @@ -64,6 +64,21 @@ <% true -> %>
    + <%!-------------------------------- GENESIS --------------------------------%> +
    +
    Genesis
    +
    + <%= link(short_address(@transaction.validation_stamp.genesis_address), + to: + Routes.live_path( + @socket, + ArchethicWeb.Explorer.TransactionChainLive, + address: Base.encode16(@transaction.validation_stamp.genesis_address) + ) + ) %> +
    +
    + <%!-------------------------------- TYPE --------------------------------%>
    Type
    @@ -185,7 +200,7 @@ x-show="show" class="language-json" phx-hook="CodeViewer" - > + > <%= print_state(state_utxo) %> <% end %> diff --git a/priv/migration_tasks/prod/1.6.0@genesis.exs b/priv/migration_tasks/prod/1.6.0@genesis.exs new file mode 100644 index 0000000000..2c4e9e0a5b --- /dev/null +++ b/priv/migration_tasks/prod/1.6.0@genesis.exs @@ -0,0 +1,33 @@ +defmodule Migration_1_6_0 do + @moduledoc false + + alias Archethic.DB + alias Archethic.DB.EmbeddedImpl.ChainWriter + alias Archethic.Election + alias Archethic.P2P + alias Archethic.TransactionChain + + require Logger + + def run() do + nodes = P2P.authorized_and_available_nodes() + db_path = :persistent_term.get(:archethic_db_path) + + DB.list_io_transactions([]) + |> Stream.each(fn transaction -> + if is_nil(transaction.validation_stamp.genesis_address) do + # query for genesis + storage_nodes = Election.storage_nodes(transaction.address, nodes) + {:ok, genesis_address} = TransactionChain.fetch_genesis_address(transaction.address, storage_nodes) + + # update in memory + transaction = put_in(transaction, [Access.key!(:validation_stamp), Access.key!(:genesis_address)], genesis_address) + + # update on disk + File.rm!(ChainWriter.io_path(db_path, transaction.address)) + ChainWriter.write_io_transaction(transaction, db_path) + end + end) + |> Stream.run() + end +end diff --git a/test/archethic/db/embedded_impl_test.exs b/test/archethic/db/embedded_impl_test.exs index e46cf2eb73..b40c371a12 100644 --- a/test/archethic/db/embedded_impl_test.exs +++ b/test/archethic/db/embedded_impl_test.exs @@ -86,7 +86,7 @@ defmodule Archethic.DB.EmbeddedTest do contents = File.read!(filename) - assert contents == Encoding.encode(tx1) + assert contents == Encoding.encode(tx1, storage_type: :io) end test "should delete transaction in io storage after writing it in chain storage", %{ @@ -295,7 +295,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return a page and its paging state" do transactions = - Enum.map(1..20, fn i -> + Enum.map(0..19, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -325,7 +325,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return an empty list when the Paging Address is not found" do transactions = - Enum.map(1..15, fn i -> + Enum.map(0..14, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -352,7 +352,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return entire chain if paging_address is the genesis (asc)" do transactions = - Enum.map(1..5, fn i -> + Enum.map(0..4, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -379,7 +379,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return empty if paging_address is the genesis (desc)" do transactions = - Enum.map(1..5, fn i -> + Enum.map(0..4, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -414,7 +414,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return all transactions if there are less than one page (10)" do transactions = - Enum.map(1..9, fn i -> + Enum.map(0..8, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -435,7 +435,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return transactions paginated if there are more than one page (10)" do transactions = - Enum.map(1..28, fn i -> + Enum.map(0..27, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -474,7 +474,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should be able to load the last page if there are 10 transactions (for a page_size=10)" do transactions = - Enum.map(1..30, fn i -> + Enum.map(0..29, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, @@ -519,7 +519,7 @@ defmodule Archethic.DB.EmbeddedTest do test "should return the number of transaction in a chain" do transactions = - Enum.map(1..20, fn i -> + Enum.map(0..19, fn i -> tx = TransactionFactory.create_valid_transaction([], index: i, diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 4fde39277e..3e2d6e76b1 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -1322,6 +1322,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do Enum.each(previous_storage_nodes, &P2P.add_and_connect_node(&1)) %ValidationContext{ + genesis_address: Transaction.previous_address(tx), transaction: tx, previous_storage_nodes: previous_storage_nodes, unspent_outputs: [ @@ -1344,6 +1345,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do end defp create_validation_stamp(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -1366,6 +1368,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), diff --git a/test/archethic/mining/validation_context_test.exs b/test/archethic/mining/validation_context_test.exs index ec5f2714ad..63d80de14a 100644 --- a/test/archethic/mining/validation_context_test.exs +++ b/test/archethic/mining/validation_context_test.exs @@ -606,6 +606,7 @@ defmodule Archethic.Mining.ValidationContextTest do tx |> Transaction.get_movements() |> Enum.map(&{&1.to, &1.to}) |> Map.new() %ValidationContext{ + genesis_address: Transaction.previous_address(tx), transaction: tx, previous_storage_nodes: previous_storage_nodes, unspent_outputs: unspent_outputs, @@ -619,6 +620,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_signature(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -640,6 +642,7 @@ defmodule Archethic.Mining.ValidationContextTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -651,6 +654,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_proof_of_work(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -672,6 +676,7 @@ defmodule Archethic.Mining.ValidationContextTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -683,6 +688,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -704,6 +710,7 @@ defmodule Archethic.Mining.ValidationContextTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -716,6 +723,7 @@ defmodule Archethic.Mining.ValidationContextTest do defp create_validation_stamp_with_invalid_transaction_fee( %ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -737,6 +745,7 @@ defmodule Archethic.Mining.ValidationContextTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -748,6 +757,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_transaction_movements(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, validation_time: timestamp, unspent_outputs: unspent_outputs @@ -771,6 +781,7 @@ defmodule Archethic.Mining.ValidationContextTest do } %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -782,11 +793,13 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_unspent_outputs(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp }) do %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -810,6 +823,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_errors(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, unspent_outputs: unspent_outputs, validation_time: timestamp @@ -830,6 +844,7 @@ defmodule Archethic.Mining.ValidationContextTest do |> LedgerValidation.to_ledger_operations() %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -842,6 +857,7 @@ defmodule Archethic.Mining.ValidationContextTest do end defp create_validation_stamp_with_invalid_consumed_inputs(%ValidationContext{ + genesis_address: genesis_address, transaction: tx, validation_time: timestamp, unspent_outputs: unspent_outputs @@ -874,6 +890,7 @@ defmodule Archethic.Mining.ValidationContextTest do ) %ValidationStamp{ + genesis_address: genesis_address, timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), diff --git a/test/archethic/p2p/messages_test.exs b/test/archethic/p2p/messages_test.exs index e333b3b5a6..cc8cdcb2eb 100644 --- a/test/archethic/p2p/messages_test.exs +++ b/test/archethic/p2p/messages_test.exs @@ -240,6 +240,7 @@ defmodule Archethic.P2P.MessageTest do <<0, 0, 227, 129, 244, 35, 48, 113, 14, 75, 1, 127, 107, 32, 29, 93, 232, 119, 254, 1, 65, 32, 47, 129, 164, 142, 240, 43, 22, 81, 188, 212, 56, 238>>, validation_stamp: %ValidationStamp{ + genesis_address: random_address(), timestamp: ~U[2020-06-26 06:37:04.000Z], proof_of_work: <<0, 0, 206, 159, 122, 114, 106, 65, 116, 18, 224, 214, 2, 26, 213, 36, 82, 175, 176, @@ -296,6 +297,7 @@ defmodule Archethic.P2P.MessageTest do <<0, 0, 227, 129, 244, 35, 48, 113, 14, 75, 1, 127, 107, 32, 29, 93, 232, 119, 254, 1, 65, 32, 47, 129, 164, 142, 240, 43, 22, 81, 188, 212, 56, 238>>, validation_stamp: %ValidationStamp{ + genesis_address: random_address(), timestamp: ~U[2020-06-26 06:37:04.000Z], proof_of_work: <<0, 0, 206, 159, 122, 114, 106, 65, 116, 18, 224, 214, 2, 26, 213, 36, 82, 175, 176, @@ -484,6 +486,7 @@ defmodule Archethic.P2P.MessageTest do 175, 135, 180, 179, 28, 57, 84, 35, 156, 173, 212, 235, 155, 226, 41, 148, 171, 132, 196, 120, 51, 136, 4, 78, 123, 70, 44, 76, 162>>, validation_stamp: %ValidationStamp{ + genesis_address: random_address(), timestamp: ~U[2020-06-26 06:37:04.000Z], proof_of_work: <<0, 0, 206, 159, 122, 114, 106, 65, 116, 18, 224, 214, 2, 26, 213, 36, 82, 175, diff --git a/test/archethic/transaction_chain/transaction/cross_validation_stamp_test.exs b/test/archethic/transaction_chain/transaction/cross_validation_stamp_test.exs index 0df354b1e7..61b552b2f9 100644 --- a/test/archethic/transaction_chain/transaction/cross_validation_stamp_test.exs +++ b/test/archethic/transaction_chain/transaction/cross_validation_stamp_test.exs @@ -1,6 +1,7 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStampTest do use ArchethicCase use ExUnitProperties + import ArchethicCase alias Archethic.Crypto alias Archethic.TransactionChain.Transaction.CrossValidationStamp @@ -15,18 +16,20 @@ defmodule Archethic.TransactionChain.Transaction.CrossValidationStampTest do pow <- StreamData.binary(length: 32), poi <- StreamData.binary(length: 33), poe <- StreamData.binary(length: 64), - signature <- StreamData.binary(length: 64) + signature <- StreamData.binary(length: 64), + protocol_version <- StreamData.integer(1..Archethic.Mining.protocol_version()) ) do pub = Crypto.last_node_public_key() validation_stamp = %ValidationStamp{ + genesis_address: random_address(), timestamp: DateTime.utc_now(), proof_of_work: <<0::8, 0::8, pow::binary>>, proof_of_integrity: poi, proof_of_election: poe, ledger_operations: %LedgerOperations{}, signature: signature, - protocol_version: ArchethicCase.current_protocol_version() + protocol_version: protocol_version } cross_stamp = diff --git a/test/archethic/transaction_chain/transaction/validation_stamp_test.exs b/test/archethic/transaction_chain/transaction/validation_stamp_test.exs index ece0788ade..64798bbfaf 100644 --- a/test/archethic/transaction_chain/transaction/validation_stamp_test.exs +++ b/test/archethic/transaction_chain/transaction/validation_stamp_test.exs @@ -1,7 +1,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStampTest do use ArchethicCase - import ArchethicCase, only: [current_protocol_version: 0] + import ArchethicCase use ExUnitProperties alias Archethic.Crypto @@ -22,17 +22,21 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStampTest do proof_of_work <- StreamData.binary(length: 33), proof_of_integrity <- StreamData.binary(length: 33), proof_of_election <- StreamData.binary(length: 32), - ledger_operations <- gen_ledger_operations() + genesis_address <- + StreamData.binary(length: 32) |> StreamData.map(&<<0::16, &1::binary>>), + ledger_operations <- gen_ledger_operations(), + protocol_version <- StreamData.integer(1..Archethic.Mining.protocol_version()) ) do pub = Crypto.last_node_public_key() assert %ValidationStamp{ + genesis_address: genesis_address, timestamp: DateTime.utc_now(), proof_of_work: proof_of_work, proof_of_integrity: proof_of_integrity, proof_of_election: proof_of_election, ledger_operations: ledger_operations, - protocol_version: current_protocol_version() + protocol_version: protocol_version } |> ValidationStamp.sign() |> ValidationStamp.valid_signature?(pub) @@ -91,6 +95,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStampTest do describe "symmetric serialization" do test "should support latest version" do stamp = %ValidationStamp{ + genesis_address: random_address(), timestamp: ~U[2021-05-07 13:11:19.000Z], proof_of_work: <<0, 0, 34, 248, 200, 166, 69, 102, 246, 46, 84, 7, 6, 84, 66, 27, 8, 78, 103, 37, 155, @@ -134,4 +139,47 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStampTest do |> elem(0) end end + + test "should support serialize_genesis? flag" do + stamp = %ValidationStamp{ + genesis_address: random_address(), + timestamp: ~U[2021-05-07 13:11:19.000Z], + proof_of_work: + <<0, 0, 34, 248, 200, 166, 69, 102, 246, 46, 84, 7, 6, 84, 66, 27, 8, 78, 103, 37, 155, + 114, 208, 205, 40, 44, 6, 159, 178, 5, 186, 168, 237, 206>>, + proof_of_integrity: + <<0, 49, 174, 251, 208, 41, 135, 147, 199, 114, 232, 140, 254, 103, 186, 138, 175, 28, + 156, 201, 30, 100, 75, 172, 95, 135, 167, 180, 242, 16, 74, 87, 170>>, + proof_of_election: + <<195, 51, 61, 55, 140, 12, 138, 246, 249, 106, 198, 175, 145, 9, 255, 133, 67, 240, 175, + 53, 236, 65, 151, 191, 128, 11, 58, 103, 82, 6, 218, 31, 220, 114, 65, 3, 151, 209, 9, + 84, 209, 105, 191, 180, 156, 157, 95, 25, 202, 2, 169, 112, 109, 54, 99, 40, 47, 96, 93, + 33, 82, 40, 100, 13>>, + ledger_operations: %LedgerOperations{ + fee: 10_000_000, + transaction_movements: [], + unspent_outputs: [], + consumed_inputs: [ + %UnspentOutput{ + from: + <<0, 0, 173, 169, 83, 136, 99, 24, 144, 188, 36, 180, 147, 166, 126, 118, 48, 185, + 248, 65, 34, 85, 12, 87, 197, 69, 121, 0, 21, 5, 152, 20, 7, 197>>, + amount: 100_000_000, + type: :UCO, + timestamp: ~U[2021-05-05 13:11:19.000Z] + } + |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) + ] + }, + signature: + <<67, 12, 4, 246, 155, 34, 32, 108, 195, 54, 139, 8, 77, 152, 5, 55, 233, 217, 126, 181, + 204, 195, 215, 239, 124, 186, 99, 187, 251, 243, 201, 6, 122, 65, 238, 221, 14, 89, 120, + 225, 39, 33, 95, 95, 225, 113, 143, 200, 47, 96, 239, 66, 182, 168, 35, 129, 240, 35, + 183, 47, 69, 154, 37, 172>>, + protocol_version: current_protocol_version() + } + + refute ValidationStamp.serialize(stamp) == + ValidationStamp.serialize(stamp, serialize_genesis?: false) + end end diff --git a/test/archethic/transaction_chain/transaction_test.exs b/test/archethic/transaction_chain/transaction_test.exs index 05097210f9..d715058283 100644 --- a/test/archethic/transaction_chain/transaction_test.exs +++ b/test/archethic/transaction_chain/transaction_test.exs @@ -534,6 +534,7 @@ defmodule Archethic.TransactionChain.TransactionTest do 13, 122, 125, 219, 122, 131, 73, 6>>, type: :oracle, validation_stamp: %Archethic.TransactionChain.Transaction.ValidationStamp{ + genesis_address: random_address(), ledger_operations: %Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations{ fee: 0, diff --git a/test/support/transaction_factory.ex b/test/support/transaction_factory.ex index 92da4b8893..f267ed5275 100644 --- a/test/support/transaction_factory.ex +++ b/test/support/transaction_factory.ex @@ -114,6 +114,7 @@ defmodule Archethic.TransactionFactory do validation_stamp = %ValidationStamp{ + genesis_address: seed |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_election: Election.validation_nodes_election_seed_sorting(tx, timestamp), @@ -166,6 +167,7 @@ defmodule Archethic.TransactionFactory do validation_stamp = %ValidationStamp{ + genesis_address: "seed" |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -210,6 +212,7 @@ defmodule Archethic.TransactionFactory do |> LedgerValidation.to_ledger_operations() validation_stamp = %ValidationStamp{ + genesis_address: "seed" |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: <<0, 0, :crypto.strong_rand_bytes(32)::binary>>, proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -258,6 +261,7 @@ defmodule Archethic.TransactionFactory do |> LedgerValidation.to_ledger_operations() validation_stamp = %ValidationStamp{ + genesis_address: seed |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -300,6 +304,7 @@ defmodule Archethic.TransactionFactory do validation_stamp = %ValidationStamp{ + genesis_address: "seed" |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_election: Election.validation_nodes_election_seed_sorting(tx, timestamp), @@ -343,6 +348,7 @@ defmodule Archethic.TransactionFactory do validation_stamp = %ValidationStamp{ + genesis_address: "seed" |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_integrity: TransactionChain.proof_of_integrity([tx]), @@ -403,6 +409,7 @@ defmodule Archethic.TransactionFactory do validation_stamp = %ValidationStamp{ + genesis_address: seed |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address(), timestamp: timestamp, proof_of_work: Crypto.origin_node_public_key(), proof_of_election: Election.validation_nodes_election_seed_sorting(tx, timestamp), From 43f8d26628b9ba55ceb5b380857e4cd04904ac30 Mon Sep 17 00:00:00 2001 From: Neylix Date: Wed, 27 Nov 2024 12:28:15 +0100 Subject: [PATCH 2/5] Use Task.Supervisor in list_current_summaries --- lib/archethic/beacon_chain.ex | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/archethic/beacon_chain.ex b/lib/archethic/beacon_chain.ex index ac1d0955d0..897351e1a6 100644 --- a/lib/archethic/beacon_chain.ex +++ b/lib/archethic/beacon_chain.ex @@ -357,13 +357,15 @@ defmodule Archethic.BeaconChain do @spec list_transactions_summaries_from_current_slot(DateTime.t()) :: list(TransactionSummary.t()) def list_transactions_summaries_from_current_slot(datetime = %DateTime{} \\ DateTime.utc_now()) do - get_next_summary_elected_subsets_by_nodes(datetime) - |> Task.async_stream( + Task.Supervisor.async_stream_nolink( + Archethic.task_supervisors(), + get_next_summary_elected_subsets_by_nodes(datetime), fn {node, subsets} -> fetch_current_summaries(node, subsets) end, ordered: false, - max_concurrency: 256 + max_concurrency: 256, + on_timeout: :kill_task ) |> Stream.filter(&match?({:ok, _}, &1)) |> Stream.flat_map(fn {:ok, summaries} -> summaries end) @@ -424,17 +426,20 @@ defmodule Archethic.BeaconChain do end defp fetch_current_summaries(node, subsets) do - subsets - |> Stream.chunk_every(10) - |> Task.async_stream(fn subsets -> - case P2P.send_message(node, %GetCurrentSummaries{subsets: subsets}) do - {:ok, %TransactionSummaryList{transaction_summaries: transaction_summaries}} -> - transaction_summaries - - _ -> - [] - end - end) + Task.Supervisor.async_stream_nolink( + Archethic.task_supervisors(), + Stream.chunk_every(subsets, 10), + fn subsets -> + case P2P.send_message(node, %GetCurrentSummaries{subsets: subsets}) do + {:ok, %TransactionSummaryList{transaction_summaries: transaction_summaries}} -> + transaction_summaries + + _ -> + [] + end + end, + on_timeout: :kill_task + ) |> Stream.filter(&match?({:ok, _}, &1)) |> Stream.flat_map(&elem(&1, 1)) |> Enum.to_list() From 886446ab24092eda1afc6543b3d2b21c8c501ebb Mon Sep 17 00:00:00 2001 From: Neylix Date: Thu, 21 Nov 2024 15:19:49 +0100 Subject: [PATCH 3/5] Update node version and dependencies version --- .tool-versions | 2 +- assets/package-lock.json | 292 ++++----------------------------------- assets/package.json | 2 +- priv/static/.keep | 0 4 files changed, 28 insertions(+), 268 deletions(-) delete mode 100644 priv/static/.keep diff --git a/.tool-versions b/.tool-versions index 622f615a27..473c3a420e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.14.1-otp-25 erlang 25.1.2 -nodejs 18.12.1 \ No newline at end of file +nodejs 22.11.0 diff --git a/assets/package-lock.json b/assets/package-lock.json index 51ab1f87d9..6d3b362c95 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "alpinejs": "^3.12.0", "bulma-tooltip": "^3.0.2", - "chokidar": "^3.5.3", + "chokidar": "^4.0.0", "diff2html": "^3.4.35", "echarts": "^5.4.2", "highlight.js": "^11.8.0", @@ -58,42 +58,12 @@ "@vue/reactivity": "~3.1.1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bulma": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bulma-tooltip": { "version": "3.0.2", @@ -101,29 +71,18 @@ "integrity": "sha512-CsT3APjhlZScskFg38n8HYL8oYNUHQtcu4sz6ERarxkUpBRbk9v0h/5KAvXeKapVSn2dp9l7bOGit5SECP8EWQ==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/diff": { @@ -158,41 +117,6 @@ "zrender": "5.4.4" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/highlight.js": { "version": "11.8.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", @@ -213,44 +137,6 @@ "hulk": "bin/hulk" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/mkdirp": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", @@ -274,14 +160,6 @@ "node": "*" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/phoenix": { "resolved": "../deps/phoenix", "link": true @@ -294,37 +172,17 @@ "resolved": "../deps/phoenix_live_view", "link": true }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" + "node": ">= 14.16.0" }, - "engines": { - "node": ">=8.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/tslib": { @@ -368,28 +226,6 @@ "@vue/reactivity": "~3.1.1" } }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, "bulma": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", @@ -402,18 +238,11 @@ "integrity": "sha512-CsT3APjhlZScskFg38n8HYL8oYNUHQtcu4sz6ERarxkUpBRbk9v0h/5KAvXeKapVSn2dp9l7bOGit5SECP8EWQ==" }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" } }, "diff": { @@ -440,28 +269,6 @@ "zrender": "5.4.4" } }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, "highlight.js": { "version": "11.8.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", @@ -476,32 +283,6 @@ "nopt": "1.0.10" } }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, "mkdirp": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", @@ -515,11 +296,6 @@ "abbrev": "1" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, "phoenix": { "version": "file:../deps/phoenix" }, @@ -529,26 +305,10 @@ "phoenix_live_view": { "version": "file:../deps/phoenix_live_view" }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==" }, "tslib": { "version": "2.3.0", diff --git a/assets/package.json b/assets/package.json index afa2c00e49..bea85a836a 100644 --- a/assets/package.json +++ b/assets/package.json @@ -4,7 +4,7 @@ "dependencies": { "alpinejs": "^3.12.0", "bulma-tooltip": "^3.0.2", - "chokidar": "^3.5.3", + "chokidar": "^4.0.0", "diff2html": "^3.4.35", "echarts": "^5.4.2", "highlight.js": "^11.8.0", diff --git a/priv/static/.keep b/priv/static/.keep deleted file mode 100644 index e69de29bb2..0000000000 From 5c8b26920df65483d791b6c6e9ac84a746b44dbc Mon Sep 17 00:00:00 2001 From: Wassim Mansouri Date: Fri, 15 Nov 2024 14:53:10 +0100 Subject: [PATCH 4/5] Add geopatch to node transactions --- lib/archethic/bootstrap.ex | 55 +++-- lib/archethic/bootstrap/sync.ex | 73 ++++--- .../bootstrap/transaction_handler.ex | 35 ++-- .../mining/pending_transaction_validation.ex | 15 +- lib/archethic/mining/proof_of_work.ex | 3 +- lib/archethic/networking/scheduler.ex | 23 ++- lib/archethic/p2p/mem_table_loader.ex | 8 +- lib/archethic/p2p/node.ex | 74 ++++--- .../shared_secrets/mem_tables_loader.ex | 2 +- .../explorer/live/settings_live.ex | 44 ++-- .../explorer/views/explorer_view.ex | 3 +- .../1.5.14@add_geopatch_node_transactiosn.ex | 193 ++++++++++++++++++ test/archethic/bootstrap/sync_test.exs | 99 +++++++-- .../bootstrap/transaction_handler_test.exs | 10 +- .../mining/distributed_workflow_test.exs | 31 ++- .../pending_transaction_validation_test.exs | 175 ++++++++++++++-- test/archethic/p2p/node_test.exs | 25 +-- .../shared_secrets/mem_tables_loader_test.exs | 42 ++-- 18 files changed, 698 insertions(+), 212 deletions(-) create mode 100644 priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex diff --git a/lib/archethic/bootstrap.ex b/lib/archethic/bootstrap.ex index 9387392dd3..a534350094 100644 --- a/lib/archethic/bootstrap.ex +++ b/lib/archethic/bootstrap.ex @@ -5,6 +5,8 @@ defmodule Archethic.Bootstrap do alias Archethic.Crypto + alias Archethic.P2P.GeoPatch + alias Archethic.Networking alias Archethic.P2P @@ -98,15 +100,17 @@ defmodule Archethic.Bootstrap do ) when is_number(port) and is_list(bootstrapping_seeds) and is_binary(reward_address) do network_patch = get_network_patch(ip) + geo_patch = GeoPatch.from_ip(ip) closest_bootstrapping_nodes = get_closest_nodes(bootstrapping_seeds, network_patch) - if should_bootstrap?(ip, port, http_port, transport, last_sync_date) do + if should_bootstrap?(ip, port, http_port, transport, geo_patch, last_sync_date) do start_bootstrap( ip, port, http_port, transport, + geo_patch, closest_bootstrapping_nodes, reward_address ) @@ -147,12 +151,12 @@ defmodule Archethic.Bootstrap do end end - defp should_bootstrap?(_ip, _port, _http_port, _, nil), do: true + defp should_bootstrap?(_ip, _port, _http_port, _, _, nil), do: true - defp should_bootstrap?(ip, port, http_port, transport, last_sync_date) do + defp should_bootstrap?(ip, port, http_port, transport, geo_patch, last_sync_date) do case P2P.get_node_info(Crypto.first_node_public_key()) do {:ok, _} -> - if Sync.require_update?(ip, port, http_port, transport, last_sync_date) do + if Sync.require_update?(ip, port, http_port, transport, geo_patch, last_sync_date) do Logger.debug("Node chain need to updated") true else @@ -171,6 +175,7 @@ defmodule Archethic.Bootstrap do port, http_port, transport, + geo_patch, closest_bootstrapping_nodes, configured_reward_address ) do @@ -187,7 +192,8 @@ defmodule Archethic.Bootstrap do port, http_port, transport, - configured_reward_address + configured_reward_address, + geo_patch ) Sync.initialize_network(tx) @@ -203,7 +209,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - configured_reward_address + configured_reward_address, + geo_patch ) true -> @@ -215,7 +222,8 @@ defmodule Archethic.Bootstrap do ) {:ok, _ip, _p2p_port, _http_port, _transport, last_reward_address, _origin_public_key, - _key_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _key_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) update_node( ip, @@ -223,7 +231,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - last_reward_address + last_reward_address, + geo_patch ) end end @@ -265,7 +274,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - configured_reward_address + configured_reward_address, + geo_patch ) do # In case node had lose it's DB, we ask the network if the node chain already exists {:ok, length} = @@ -286,7 +296,8 @@ defmodule Archethic.Bootstrap do TransactionChain.fetch_transaction(last_address, closest_bootstrapping_nodes) {:ok, _ip, _p2p_port, _http_port, _transport, last_reward_address, _origin_public_key, - _key_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _key_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) last_reward_address else @@ -294,7 +305,14 @@ defmodule Archethic.Bootstrap do end tx = - TransactionHandler.create_node_transaction(ip, port, http_port, transport, reward_address) + TransactionHandler.create_node_transaction( + ip, + port, + http_port, + transport, + reward_address, + geo_patch + ) {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) @@ -307,18 +325,27 @@ defmodule Archethic.Bootstrap do ) end - defp update_node(_ip, _port, _http_port, _transport, [], _reward_address) do + defp update_node(_ip, _port, _http_port, _transport, [], _reward_address, _geo_patch) do Logger.warning("Not enough nodes in the network. No node update") end - defp update_node(ip, port, http_port, transport, closest_bootstrapping_nodes, reward_address) do + defp update_node( + ip, + port, + http_port, + transport, + closest_bootstrapping_nodes, + reward_address, + geo_patch + ) do tx = TransactionHandler.create_node_transaction( ip, port, http_port, transport, - reward_address + reward_address, + geo_patch ) {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) diff --git a/lib/archethic/bootstrap/sync.ex b/lib/archethic/bootstrap/sync.ex index 1540cf5b18..50c8c2c758 100644 --- a/lib/archethic/bootstrap/sync.ex +++ b/lib/archethic/bootstrap/sync.ex @@ -56,36 +56,63 @@ defmodule Archethic.Bootstrap.Sync do :inet.port_number(), :inet.port_number(), P2P.supported_transport(), + binary(), DateTime.t() | nil ) :: boolean() - def require_update?(_ip, _port, _http_port, _transport, nil), do: false + def require_update?(_ip, _port, _http_port, _transport, _geo_patch, nil), do: false - def require_update?(ip, port, http_port, transport, last_sync_date) do + def require_update?(ip, port, http_port, transport, geo_patch, last_sync_date) do first_node_public_key = Crypto.first_node_public_key() - case P2P.authorized_and_available_nodes() do - [%Node{first_public_key: ^first_node_public_key}] -> - false + if is_node_active?(first_node_public_key) do + false + else + needs_update?( + ip, + port, + http_port, + transport, + geo_patch, + last_sync_date, + first_node_public_key + ) + end + end + + defp is_node_active?(first_node_public_key) do + P2P.authorized_and_available_nodes() + |> Enum.any?(fn %Node{first_public_key: pk} -> pk == first_node_public_key end) + end + + defp needs_update?( + ip, + port, + http_port, + transport, + geo_patch, + last_sync_date, + first_node_public_key + ) do + diff_sync = DateTime.diff(DateTime.utc_now(), last_sync_date, :second) + + case P2P.get_node_info(first_node_public_key) do + {:ok, + %Node{ + ip: prev_ip, + port: prev_port, + http_port: prev_http_port, + transport: prev_transport, + geo_patch: prev_geo_patch + }} -> + ip != prev_ip or + port != prev_port or + http_port != prev_http_port or + geo_patch != prev_geo_patch or + diff_sync > @out_of_sync_date_threshold or + prev_transport != transport _ -> - diff_sync = DateTime.diff(DateTime.utc_now(), last_sync_date, :second) - - case P2P.get_node_info(first_node_public_key) do - {:ok, - %Node{ - ip: prev_ip, - port: prev_port, - http_port: prev_http_port, - transport: prev_transport - }} - when ip != prev_ip or port != prev_port or http_port != prev_http_port or - diff_sync > @out_of_sync_date_threshold or - prev_transport != transport -> - true - - _ -> - false - end + false end end diff --git a/lib/archethic/bootstrap/transaction_handler.ex b/lib/archethic/bootstrap/transaction_handler.ex index 18d22645dd..2d4e2564e9 100644 --- a/lib/archethic/bootstrap/transaction_handler.ex +++ b/lib/archethic/bootstrap/transaction_handler.ex @@ -73,13 +73,21 @@ defmodule Archethic.Bootstrap.TransactionHandler do p2p_port :: :inet.port_number(), http_port :: :inet.port_number(), transport :: P2P.supported_transport(), - reward_address :: Crypto.versioned_hash() + reward_address :: Crypto.versioned_hash(), + geo_patch :: binary() ) :: Transaction.t() - def create_node_transaction(ip = {_, _, _, _}, port, http_port, transport, reward_address) + def create_node_transaction( + ip = {_, _, _, _}, + port, + http_port, + transport, + reward_address, + geo_patch + ) when is_number(port) and port >= 0 and is_binary(reward_address) do origin_public_key = Crypto.origin_node_public_key() - origin_public_key_certificate = Crypto.get_key_certificate(origin_public_key) + origin_public_certificate = Crypto.get_key_certificate(origin_public_key) mining_public_key = Crypto.mining_node_public_key() Transaction.new(:node, %TransactionData{ @@ -94,16 +102,17 @@ defmodule Archethic.Bootstrap.TransactionHandler do ] """, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - reward_address, - origin_public_key, - origin_public_key_certificate, - mining_public_key - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: origin_public_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) }) end end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 20ae219215..f4a1005755 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -17,6 +17,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do alias Archethic.OracleChain alias Archethic.P2P + alias Archethic.P2P.GeoPatch alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GetFirstPublicKey alias Archethic.P2P.Node @@ -317,7 +318,8 @@ defmodule Archethic.Mining.PendingTransactionValidation do }, _ ) do - with {:ok, ip, port, _http_port, _, _, origin_public_key, key_certificate, mining_public_key} <- + with {:ok, ip, port, _http_port, _, _, origin_public_key, key_certificate, mining_public_key, + geo_patch} <- Node.decode_transaction_content(content), {:auth_origin, true} <- {:auth_origin, @@ -338,7 +340,9 @@ defmodule Archethic.Mining.PendingTransactionValidation do {:mining_public_key, true} <- {:mining_public_key, Crypto.valid_public_key?(mining_public_key) and - Crypto.get_public_key_curve(mining_public_key) == :bls} do + Crypto.get_public_key_curve(mining_public_key) == :bls}, + {:geo_patch, true} <- + {:geo_patch, valid_geopatch?(ip, geo_patch)} do :ok else :error -> @@ -362,6 +366,9 @@ defmodule Archethic.Mining.PendingTransactionValidation do {:mining_public_key, false} -> {:error, "Invalid mining public key"} + + {:geo_patch, false} -> + {:error, "Invalid geo patch from IP"} end end @@ -966,6 +973,10 @@ defmodule Archethic.Mining.PendingTransactionValidation do end end + defp valid_geopatch?(ip, calculated_geopatch) do + calculated_geopatch == GeoPatch.from_ip(ip) + end + defp get_allowed_node_key_origins do :archethic |> Application.get_env(__MODULE__, []) diff --git a/lib/archethic/mining/proof_of_work.ex b/lib/archethic/mining/proof_of_work.ex index a3d39e7ec7..d9ef39db27 100644 --- a/lib/archethic/mining/proof_of_work.ex +++ b/lib/archethic/mining/proof_of_work.ex @@ -139,7 +139,8 @@ defmodule Archethic.Mining.ProofOfWork do } }) do {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, - _origin_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _origin_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) [origin_public_key] end diff --git a/lib/archethic/networking/scheduler.ex b/lib/archethic/networking/scheduler.ex index a9eb66974a..95f77980de 100644 --- a/lib/archethic/networking/scheduler.ex +++ b/lib/archethic/networking/scheduler.ex @@ -10,6 +10,7 @@ defmodule Archethic.Networking.Scheduler do alias Archethic.Networking.PortForwarding alias Archethic.P2P + alias(Archethic.P2P.GeoPatch) alias Archethic.P2P.Listener, as: P2PListener alias Archethic.P2P.Node @@ -103,21 +104,23 @@ defmodule Archethic.Networking.Scheduler do origin_public_key = Crypto.origin_node_public_key() mining_public_key = Crypto.mining_node_public_key() key_certificate = Crypto.get_key_certificate(origin_public_key) + new_geo_patch = GeoPatch.from_ip(ip) tx = Transaction.new(:node, %TransactionData{ code: code, content: - Node.encode_transaction_content( - ip, - p2p_port, - web_port, - transport, - reward_address, - origin_public_key, - key_certificate, - mining_public_key - ) + Node.encode_transaction_content(%{ + ip: ip, + port: p2p_port, + http_port: web_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: new_geo_patch + }) }) Archethic.send_new_transaction(tx, forward?: true) diff --git a/lib/archethic/p2p/mem_table_loader.ex b/lib/archethic/p2p/mem_table_loader.ex index 8b1e3fc20c..013ad4d8f0 100644 --- a/lib/archethic/p2p/mem_table_loader.ex +++ b/lib/archethic/p2p/mem_table_loader.ex @@ -105,7 +105,9 @@ defmodule Archethic.P2P.MemTableLoader do first_public_key = TransactionChain.get_first_public_key(previous_public_key) {:ok, ip, port, http_port, transport, reward_address, origin_public_key, _certificate, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, geo_patch} = Node.decode_transaction_content(content) + + geo_patch = if geo_patch == nil, do: GeoPatch.from_ip(ip), else: geo_patch if first_node_change?(first_public_key, previous_public_key) do node = %Node{ @@ -114,7 +116,7 @@ defmodule Archethic.P2P.MemTableLoader do http_port: http_port, first_public_key: first_public_key, last_public_key: previous_public_key, - geo_patch: GeoPatch.from_ip(ip), + geo_patch: geo_patch, transport: transport, last_address: address, reward_address: reward_address, @@ -135,7 +137,7 @@ defmodule Archethic.P2P.MemTableLoader do port: port, http_port: http_port, last_public_key: previous_public_key, - geo_patch: GeoPatch.from_ip(ip), + geo_patch: geo_patch, transport: transport, last_address: address, reward_address: reward_address, diff --git a/lib/archethic/p2p/node.ex b/lib/archethic/p2p/node.ex index 45b9175e18..7d7f2b4f58 100755 --- a/lib/archethic/p2p/node.ex +++ b/lib/archethic/p2p/node.ex @@ -47,7 +47,8 @@ defmodule Archethic.P2P.Node do {:ok, ip_address :: :inet.ip_address(), p2p_port :: :inet.port_number(), http_port :: :inet.port_number(), P2P.supported_transport(), reward_address :: binary(), origin_public_key :: Crypto.key(), - key_certificate :: binary(), mining_public_key :: binary() | nil} + key_certificate :: binary(), mining_public_key :: binary() | nil, + geo_patch :: binary() | nil} | :error def decode_transaction_content( <> @@ -56,18 +57,11 @@ defmodule Archethic.P2P.Node do {reward_address, rest} <- Utils.deserialize_address(rest), {origin_public_key, rest} <- Utils.deserialize_public_key(rest), <> <- rest do - mining_public_key = - case rest do - "" -> - nil - - mining_public_key -> - mining_public_key |> Utils.deserialize_public_key() |> elem(0) - end - + rest::binary>> <- rest, + {mining_public_key, rest} <- extract_mining_public_key(rest), + {geo_patch, _rest} <- extract_geo_patch(rest) do {:ok, {ip0, ip1, ip2, ip3}, port, http_port, deserialize_transport(transport), - reward_address, origin_public_key, key_certificate, mining_public_key} + reward_address, origin_public_key, key_certificate, mining_public_key, geo_patch} else _ -> :error @@ -76,32 +70,46 @@ defmodule Archethic.P2P.Node do def decode_transaction_content(<<>>), do: :error + @spec extract_mining_public_key(binary()) :: {Crypto.key() | nil, binary()} + defp extract_mining_public_key(<<>>), do: {nil, <<>>} + + defp extract_mining_public_key(rest) do + Utils.deserialize_public_key(rest) + end + + @spec extract_geo_patch(binary()) :: {binary() | nil, binary()} + defp extract_geo_patch(<>), do: {geo_patch, rest} + + defp extract_geo_patch(rest), do: {nil, rest} + @doc """ Encode node's transaction content """ - @spec encode_transaction_content( - :inet.ip_address(), - :inet.port_number(), - :inet.port_number(), - P2P.supported_transport(), - reward_address :: binary(), - origin_public_key :: Crypto.key(), - origin_key_certificate :: binary(), - mining_public_key :: Crypto.key() - ) :: binary() - def encode_transaction_content( - {ip1, ip2, ip3, ip4}, - port, - http_port, - transport, - reward_address, - origin_public_key, - key_certificate, - mining_public_key - ) do + @spec encode_transaction_content(%{ + ip: :inet.ip_address(), + port: :inet.port_number(), + http_port: :inet.port_number(), + transport: P2P.supported_transport(), + reward_address: reward_address :: binary(), + origin_public_key: origin_public_key :: Crypto.key(), + key_certificate: origin_key_certificate :: binary(), + mining_public_key: mining_public_key :: Crypto.key(), + geo_patch: geo_patch :: binary() + }) :: binary() + def encode_transaction_content(%{ + ip: {ip1, ip2, ip3, ip4}, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) do <> + key_certificate::binary, mining_public_key::binary, geo_patch::binary-size(3)>> end @type t() :: %__MODULE__{ diff --git a/lib/archethic/shared_secrets/mem_tables_loader.ex b/lib/archethic/shared_secrets/mem_tables_loader.ex index b4f7aedcce..ed7afc4b94 100644 --- a/lib/archethic/shared_secrets/mem_tables_loader.ex +++ b/lib/archethic/shared_secrets/mem_tables_loader.ex @@ -61,7 +61,7 @@ defmodule Archethic.SharedSecrets.MemTablesLoader do } }) do {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, _cert, - _mining_public_key} = Node.decode_transaction_content(content) + _mining_public_key, _geo_patch} = Node.decode_transaction_content(content) <<_::8, origin_id::8, _::binary>> = origin_public_key diff --git a/lib/archethic_web/explorer/live/settings_live.ex b/lib/archethic_web/explorer/live/settings_live.ex index 6f926c0904..73504c4ba2 100644 --- a/lib/archethic_web/explorer/live/settings_live.ex +++ b/lib/archethic_web/explorer/live/settings_live.ex @@ -120,6 +120,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do defp send_new_transaction(next_reward_address) do %Node{ ip: ip, + geo_patch: geo_patch, port: port, http_port: http_port, transport: transport, @@ -149,16 +150,17 @@ defmodule ArchethicWeb.Explorer.SettingsLive do }, code: code, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - next_reward_address, - Crypto.origin_node_public_key(), - Crypto.get_key_certificate(Crypto.origin_node_public_key()), - Crypto.mining_node_public_key() - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: next_reward_address, + origin_public_key: Crypto.origin_node_public_key(), + key_certificate: Crypto.get_key_certificate(Crypto.origin_node_public_key()), + mining_public_key: Crypto.mining_node_public_key(), + geo_patch: geo_patch + }) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) @@ -169,6 +171,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do defp send_noop_transaction() do %Node{ ip: ip, + geo_patch: geo_patch, port: port, http_port: http_port, transport: transport, @@ -184,16 +187,17 @@ defmodule ArchethicWeb.Explorer.SettingsLive do Transaction.new(:node, %TransactionData{ code: code, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - reward_address, - Crypto.origin_node_public_key(), - Crypto.get_key_certificate(Crypto.origin_node_public_key()), - Crypto.mining_node_public_key() - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: Crypto.origin_node_public_key(), + key_certificate: Crypto.get_key_certificate(Crypto.origin_node_public_key()), + mining_public_key: Crypto.mining_node_public_key(), + geo_patch: geo_patch + }) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) diff --git a/lib/archethic_web/explorer/views/explorer_view.ex b/lib/archethic_web/explorer/views/explorer_view.ex index 6d7df2fabe..77f1775d98 100644 --- a/lib/archethic_web/explorer/views/explorer_view.ex +++ b/lib/archethic_web/explorer/views/explorer_view.ex @@ -52,10 +52,11 @@ defmodule ArchethicWeb.Explorer.ExplorerView do def format_transaction_content(:node, content) do {:ok, ip, port, http_port, transport, reward_address, origin_public_key, key_certificate, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, geo_patch} = Node.decode_transaction_content(content) content = """ IP: #{:inet.ntoa(ip)} + GeoPatch: #{geo_patch} P2P Port: #{port} HTTP Port: #{http_port} Transport: #{transport} diff --git a/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex b/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex new file mode 100644 index 0000000000..743925c026 --- /dev/null +++ b/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex @@ -0,0 +1,193 @@ +defmodule Migration_1_5_14 do + @moduledoc """ + Migration script to add geopatch to a node's transaction. + """ + + alias Archethic.Crypto + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.TransactionChain + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.Utils + alias Archethic.PubSub + + require Logger + + def run() do + nodes = P2P.list_nodes() |> Enum.sort_by(& &1.first_public_key) + + execute_migration(nodes) + end + + defp execute_migration([]) do + :ok + end + + defp execute_migration(nodes) do + current_node_pk = Crypto.first_node_public_key() + transaction_cache = %{} + + Enum.reduce_while(nodes, transaction_cache, fn node, transaction_cache -> + node_pk = node.first_public_key + Logger.info("Processing node", node: Base.encode16(node_pk)) + + if geopatch_in_last_transaction?(node_pk) do + Logger.info("Migration not needed for node", node: Base.encode16(node_pk)) + {:cont, Map.delete(transaction_cache, node_pk)} + else + if node_pk == current_node_pk do + Logger.info("Starting migration for node", node: Base.encode16(node_pk)) + + case send_node_transaction() do + :ok -> + Logger.info("Migration complete for node", node: Base.encode16(node_pk)) + {:halt, :ok} + + {:error, reason} -> + Logger.error( + "Migration failed (reason: #{inspect(reason)}) for", + node: Base.encode16(node_pk) + ) + + {:halt, {:error, reason}} + end + else + case Map.fetch(transaction_cache, node_pk) do + {:ok, transaction} -> + {:cont, process_transaction(transaction, Map.delete(transaction_cache, node_pk))} + + :error -> + PubSub.register_to_new_transaction_by_type(:node) + + receive do + {:new_transaction, address, :node, _timestamp} -> + with {:ok, %Transaction{previous_public_key: previous_pk} = transaction} <- + TransactionChain.get_transaction(address) do + first_pk = TransactionChain.get_first_public_key(previous_pk) + + if first_pk == node_pk do + {:cont, process_transaction(transaction, transaction_cache)} + else + updated_cache = Map.put(transaction_cache, first_pk, transaction) + {:cont, updated_cache} + end + else + {:error, reason} -> + Logger.error( + "Failed to fetch transaction: #{inspect(reason)} for address", + address: Base.encode16(address) + ) + + {:cont, transaction_cache} + end + after + 60_000 -> + Logger.error("Timeout waiting for updates from node", + node: Base.encode16(node_pk) + ) + + PubSub.unregister_to_new_transaction_by_type(:node) + {:cont, transaction_cache} + end + end + end + end + end) + end + + defp process_transaction( + %Transaction{data: %TransactionData{content: content}}, + transaction_cache + ) do + case geopatch_in_transaction_content?(content) do + true -> + PubSub.unregister_to_new_transaction_by_type(:node) + transaction_cache + + false -> + transaction_cache + end + end + + defp geopatch_in_last_transaction?(node_pk) do + case P2P.get_node_info(node_pk) do + {:ok, %Node{last_address: last_address}} -> + case TransactionChain.get_transaction(last_address) do + {:ok, %Transaction{data: %TransactionData{content: content}}} -> + geopatch_in_transaction_content?(content) + + {:error, _} -> + false + end + + {:error, _} -> + false + end + end + + defp geopatch_in_transaction_content?(content) do + with {:ok, _ip, _p2p_port, _http_port, _transport, _last_reward_address, _origin_public_key, + _key_certificate, _mining_public_key, + geo_patch} <- Node.decode_transaction_content(content) do + geo_patch != nil + else + error -> + false + end + end + + defp send_node_transaction() do + %Node{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + last_address: last_address + } = P2P.get_node_info() + + geopatch = Archethic.P2P.GeoPatch.from_ip(ip) + + mining_public_key = Crypto.mining_node_public_key() + key_certificate = Crypto.get_key_certificate(origin_public_key) + + {:ok, %Transaction{data: %TransactionData{code: code}}} = + TransactionChain.get_transaction(last_address, data: [:code]) + + tx = + Transaction.new(:node, %TransactionData{ + code: code, + content: + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: geopatch + }) + }) + + :ok = Archethic.send_new_transaction(tx, forward?: true) + + nodes = + P2P.authorized_and_available_nodes() + |> Enum.filter(&P2P.node_connected?/1) + |> P2P.sort_by_nearest_nodes() + + case Utils.await_confirmation(tx.address, nodes) do + {:ok, _} -> + Logger.error("Mining node transaction successful.") + :ok + + {:error, reason} -> + Logger.error("Cannot update node transaction: #{inspect(reason)}") + {:error, reason} + end + end +end diff --git a/test/archethic/bootstrap/sync_test.exs b/test/archethic/bootstrap/sync_test.exs index 94dea38589..5c7f1179d5 100644 --- a/test/archethic/bootstrap/sync_test.exs +++ b/test/archethic/bootstrap/sync_test.exs @@ -146,13 +146,21 @@ defmodule Archethic.Bootstrap.SyncTest do first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), transport: :tcp, + geo_patch: "AAA", authorized?: true, available?: true, authorization_date: DateTime.utc_now() }) assert false == - Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :tcp, DateTime.utc_now()) + Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :tcp, + "AAA", + DateTime.utc_now() + ) end test "should return true when the node ip change" do @@ -161,7 +169,8 @@ defmodule Archethic.Bootstrap.SyncTest do port: 3000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -170,10 +179,18 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) - assert Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :tcp, DateTime.utc_now()) + assert Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :tcp, + "AAA", + DateTime.utc_now() + ) end test "should return true when the node port change" do @@ -182,7 +199,31 @@ defmodule Archethic.Bootstrap.SyncTest do port: 3000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" + }) + + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3050, + http_port: 4000, + first_public_key: "other_node_key", + last_public_key: "other_node_key", + transport: :tcp, + geo_patch: "AAA" + }) + + assert Sync.require_update?({127, 0, 0, 1}, 3010, 4000, :tcp, "AAA", DateTime.utc_now()) + end + + test "should return true when the geopatch changes" do + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: Crypto.first_node_public_key(), + last_public_key: Crypto.last_node_public_key(), + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -191,10 +232,11 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) - assert Sync.require_update?({127, 0, 0, 1}, 3010, 4000, :tcp, DateTime.utc_now()) + assert Sync.require_update?({127, 0, 0, 1}, 3000, 4000, :tcp, "BBB", DateTime.utc_now()) end test "should return true when the last date of sync diff is greater than 3 seconds" do @@ -204,7 +246,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -213,7 +256,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) assert Sync.require_update?( @@ -221,6 +265,7 @@ defmodule Archethic.Bootstrap.SyncTest do 3000, 4000, :tcp, + "AAA", DateTime.utc_now() |> DateTime.add(-10) ) @@ -233,7 +278,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -242,11 +288,19 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) assert true == - Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :sctp, DateTime.utc_now()) + Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :sctp, + "AAA", + DateTime.utc_now() + ) end end @@ -308,16 +362,17 @@ defmodule Archethic.Bootstrap.SyncTest do node_tx = Transaction.new(:node, %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_public_key(), - ArchethicCase.random_public_key(), - :crypto.strong_rand_bytes(64), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_public_key(), + origin_public_key: ArchethicCase.random_public_key(), + key_certificate: :crypto.strong_rand_bytes(64), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) }) :ok = Sync.initialize_network(node_tx) diff --git a/test/archethic/bootstrap/transaction_handler_test.exs b/test/archethic/bootstrap/transaction_handler_test.exs index f0053f2c10..a82cad19cf 100644 --- a/test/archethic/bootstrap/transaction_handler_test.exs +++ b/test/archethic/bootstrap/transaction_handler_test.exs @@ -18,7 +18,7 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do import Mox - test "create_node_transaction/4 should create transaction with ip and port encoded in the content" do + test "create_node_transaction/4 should create transaction with ip, geopatch and port encoded in the content" do assert %Transaction{ data: %TransactionData{ content: content @@ -29,11 +29,12 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do 3000, 4000, :tcp, - <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + "000" ) assert {:ok, {127, 0, 0, 1}, 3000, 4000, :tcp, _reward_address, _origin_public_key, _cert, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, "000"} = Node.decode_transaction_content(content) assert Archethic.Crypto.mining_node_public_key() == mining_public_key end @@ -59,7 +60,8 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do 3000, 4000, :tcp, - "00610F69B6C5C3449659C99F22956E5F37AA6B90B473585216CF4931DAF7A0AB45" + "00610F69B6C5C3449659C99F22956E5F37AA6B90B473585216CF4931DAF7A0AB45", + "000" ) validated_transaction = %Transaction{ diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 3e2d6e76b1..ff532b1119 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -104,19 +104,28 @@ defmodule Archethic.Mining.DistributedWorkflowTest do tx = Transaction.new(:node, %TransactionData{ content: - Node.encode_transaction_content( - {80, 10, 20, 102}, - 3000, - 4000, - MockTransport, - <<0, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, 12, 236, 69, 10, 209, 6, - 234, 172, 97, 188, 240, 207, 70, 115, 64, 117, 44, 82, 132, 186>>, - origin_public_key, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {80, 10, 20, 102}, + port: 3000, + http_port: 4000, + transport: MockTransport, + reward_address: + <<0, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, 12, 236, 69, 10, 209, + 6, 234, 172, 97, 188, 240, 207, 70, 115, 64, 117, 44, 82, 132, 186>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "F1B" + }) }) + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {80, 10, 20, 102} -> + {38.345170, -0.481490} + end + end) + {:ok, %{ genesis: Transaction.previous_address(tx), diff --git a/test/archethic/mining/pending_transaction_validation_test.exs b/test/archethic/mining/pending_transaction_validation_test.exs index 84ca849a01..4f7bef4751 100644 --- a/test/archethic/mining/pending_transaction_validation_test.exs +++ b/test/archethic/mining/pending_transaction_validation_test.exs @@ -7,6 +7,7 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do alias Archethic.Mining.PendingTransactionValidation alias Archethic.P2P + alias Archethic.P2P.GeoPatch alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GenesisAddress alias Archethic.P2P.Message.GetFirstPublicKey @@ -542,17 +543,19 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do certificate = Crypto.ECDSA.sign(:secp256r1, ca_pv, origin_key) content = - Node.encode_transaction_content( - {80, 20, 10, 200}, - 3000, - 4000, - :tcp, - <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, - 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, - origin_public_key, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {88, 22, 30, 229}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "F1B" + }) tx = TransactionFactory.create_non_valided_transaction(type: :node, content: content) @@ -561,9 +564,135 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do address end) + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {88, 22, 30, 229} -> + {38.345170, -0.481490} + end + end) + assert :ok = PendingTransactionValidation.validate_type_rules(tx, DateTime.utc_now()) end + test "should return an error if the geo_patch is not the expected one" do + {origin_public_key, _} = + Crypto.generate_deterministic_keypair(:crypto.strong_rand_bytes(32), :secp256r1) + + {_, ca_pv} = :crypto.generate_key(:ecdh, :secp256r1, "ca_root_key") + <<_::8, _::8, origin_key::binary>> = origin_public_key + certificate = Crypto.ECDSA.sign(:secp256r1, ca_pv, origin_key) + + content = + Node.encode_transaction_content(%{ + ip: {88, 22, 30, 229}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "WRONG" + }) + + tx = TransactionFactory.create_non_valided_transaction(type: :node, content: content) + + MockDB + |> stub(:get_last_chain_address, fn address -> + address + end) + + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {88, 22, 30, 229} -> + {38.345170, -0.481490} + end + end) + + assert {:error, "Invalid geo patch from IP"} = + PendingTransactionValidation.validate_type_rules(tx, DateTime.utc_now()) + end + + test "Should include and validate geopatch in a node transaction" do + ip = {127, 0, 0, 1} + expected_geopatch = GeoPatch.from_ip(ip) + + assert byte_size(expected_geopatch) == 3 + + port = 3000 + http_port = 4000 + transport = :tcp + reward_address = ArchethicCase.random_address() + origin_public_key = ArchethicCase.random_public_key() + key_certificate = "" + mining_public_key = ArchethicCase.random_public_key() + + content = + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: expected_geopatch + }) + + assert {:ok, decoded_ip, decoded_port, decoded_http_port, decoded_transport, + decoded_reward_address, decoded_origin_public_key, decoded_key_certificate, + decoded_mining_public_key, + decoded_geopatch} = Node.decode_transaction_content(content) + + assert decoded_ip == ip + assert decoded_port == port + assert decoded_http_port == http_port + assert decoded_transport == transport + assert decoded_reward_address == reward_address + assert decoded_origin_public_key == origin_public_key + assert decoded_key_certificate == key_certificate + assert decoded_mining_public_key == mining_public_key + assert decoded_geopatch == expected_geopatch + + assert GeoPatch.from_ip(decoded_ip) == decoded_geopatch + end + + test "Should reject invalid geopatch in node transaction" do + ip = {127, 0, 0, 1} + invalid_geopatch = "BAD" + assert byte_size(invalid_geopatch) == 3 + + port = 3000 + http_port = 4000 + transport = :tcp + reward_address = ArchethicCase.random_address() + origin_public_key = ArchethicCase.random_public_key() + key_certificate = "" + mining_public_key = ArchethicCase.random_public_key() + + content = + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: invalid_geopatch + }) + + assert {:ok, decoded_ip, _, _, _, _, _, _, _, decoded_geopatch} = + Node.decode_transaction_content(content) + + assert decoded_ip == ip + refute GeoPatch.from_ip(decoded_ip) == decoded_geopatch + end + test "should return an error when a node transaction public key used on non allowed origin" do Application.put_env(:archethic, Archethic.Mining.PendingTransactionValidation, allowed_node_key_origins: [:tpm] @@ -573,17 +702,19 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do certificate = Crypto.get_key_certificate(public_key) content = - Node.encode_transaction_content( - {80, 20, 10, 200}, - 3000, - 4000, - :tcp, - <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, - 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, - <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {80, 20, 10, 200}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "BBB" + }) tx = TransactionFactory.create_non_valided_transaction( diff --git a/test/archethic/p2p/node_test.exs b/test/archethic/p2p/node_test.exs index 74be43266e..1a40ed1903 100644 --- a/test/archethic/p2p/node_test.exs +++ b/test/archethic/p2p/node_test.exs @@ -41,18 +41,19 @@ defmodule Archethic.P2P.NodeTest do mining_public_key = ArchethicCase.random_public_key() assert {:ok, {127, 0, 0, 1}, 3000, 4000, :tcp, ^reward_address, ^origin_public_key, - ^certificate, - ^mining_public_key} = - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - reward_address, - origin_public_key, - certificate, - mining_public_key - ) + ^certificate, ^mining_public_key, + "000"} = + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: mining_public_key, + geo_patch: "000" + }) |> Node.decode_transaction_content() end end diff --git a/test/archethic/shared_secrets/mem_tables_loader_test.exs b/test/archethic/shared_secrets/mem_tables_loader_test.exs index 21d820c0fe..0e333c01e1 100644 --- a/test/archethic/shared_secrets/mem_tables_loader_test.exs +++ b/test/archethic/shared_secrets/mem_tables_loader_test.exs @@ -40,16 +40,17 @@ defmodule Archethic.SharedSecrets.MemTablesLoaderTest do type: :node, data: %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_address(), - origin_public_key, - :crypto.strong_rand_bytes(32), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_address(), + origin_public_key: origin_public_key, + key_certificate: :crypto.strong_rand_bytes(32), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) } } @@ -152,16 +153,17 @@ defmodule Archethic.SharedSecrets.MemTablesLoaderTest do type: :node, data: %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_address(), - node_origin_public_key, - :crypto.strong_rand_bytes(32), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_address(), + origin_public_key: node_origin_public_key, + key_certificate: :crypto.strong_rand_bytes(32), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) } } From f1c4300be31464d8003f0dde3d29022f3cadd5c6 Mon Sep 17 00:00:00 2001 From: Wassim Mansouri Date: Mon, 2 Dec 2024 15:07:35 +0100 Subject: [PATCH 5/5] Handle geopatch updates in Notifier --- lib/archethic/p2p/mem_table_loader.ex | 37 ++++- lib/archethic/self_repair/notifier.ex | 152 ++++++++++++------- test/archethic/self_repair/notifier_test.exs | 27 ++-- test/archethic/self_repair/sync_test.exs | 133 ++++++++-------- 4 files changed, 209 insertions(+), 140 deletions(-) diff --git a/lib/archethic/p2p/mem_table_loader.ex b/lib/archethic/p2p/mem_table_loader.ex index 013ad4d8f0..381a406160 100644 --- a/lib/archethic/p2p/mem_table_loader.ex +++ b/lib/archethic/p2p/mem_table_loader.ex @@ -8,6 +8,7 @@ defmodule Archethic.P2P.MemTableLoader do alias Archethic.DB + alias Archethic.P2P alias Archethic.P2P.GeoPatch alias Archethic.P2P.MemTable alias Archethic.P2P.Node @@ -85,7 +86,7 @@ defmodule Archethic.P2P.MemTableLoader do end @doc """ - Load the transaction and update the P2P view + Load the transaction and update the P2P view. """ @spec load_transaction(Transaction.t()) :: :ok def load_transaction(%Transaction{ @@ -125,13 +126,20 @@ defmodule Archethic.P2P.MemTableLoader do mining_public_key: mining_public_key } - node - |> Node.enroll(timestamp) - |> MemTable.add_node() + node = Node.enroll(node, timestamp) + MemTable.add_node(node) else {:ok, node} = MemTable.get_node(first_public_key) - MemTable.add_node(%{ + if node.geo_patch != geo_patch do + Logger.info("GeoPatch changed for node", + node: Base.encode16(first_public_key) + ) + + start_notifier_for_geopatch_change(timestamp) + end + + updated_node = %Node{ node | ip: ip, port: port, @@ -144,10 +152,12 @@ defmodule Archethic.P2P.MemTableLoader do origin_public_key: origin_public_key, last_update_date: timestamp, mining_public_key: mining_public_key - }) + } + + MemTable.add_node(updated_node) end - Logger.info("Node loaded into in memory p2p tables", node: Base.encode16(first_public_key)) + Logger.info("Node loaded into in-memory P2P tables", node: Base.encode16(first_public_key)) end def load_transaction(%Transaction{ @@ -193,4 +203,17 @@ defmodule Archethic.P2P.MemTableLoader do MemTable.set_node_unavailable(node_public_key, availability_update) end end + + defp start_notifier_for_geopatch_change(timestamp) do + Logger.info("Starting Notifier for GeoPatch change") + + prev_available_nodes = P2P.authorized_and_available_nodes(timestamp, true) + new_available_nodes = P2P.authorized_and_available_nodes() + + Archethic.SelfRepair.Notifier.start_link(%{ + availability_update: timestamp, + prev_available_nodes: prev_available_nodes, + new_available_nodes: new_available_nodes + }) + end end diff --git a/lib/archethic/self_repair/notifier.ex b/lib/archethic/self_repair/notifier.ex index 3b20b34897..70f3d7d910 100644 --- a/lib/archethic/self_repair/notifier.ex +++ b/lib/archethic/self_repair/notifier.ex @@ -59,15 +59,12 @@ defmodule Archethic.SelfRepair.Notifier do end def handle_info(:start, data) do - unavailable_nodes = Keyword.fetch!(data, :unavailable_nodes) prev_available_nodes = Keyword.fetch!(data, :prev_available_nodes) new_available_nodes = Keyword.fetch!(data, :new_available_nodes) - Logger.info( - "Start Notifier due to a topology change #{inspect(Enum.map(unavailable_nodes, &Base.encode16(&1)))}" - ) + Logger.info("Start Notifier due to a topology change") - repair_transactions(unavailable_nodes, prev_available_nodes, new_available_nodes) + repair_transactions(prev_available_nodes, new_available_nodes) repair_summaries_aggregate(prev_available_nodes, new_available_nodes) {:stop, :normal, data} @@ -77,8 +74,8 @@ defmodule Archethic.SelfRepair.Notifier do For each txn chain in db. Load its genesis address, load its chain, recompute shards , notifiy nodes. Network txns are excluded. """ - @spec repair_transactions(list(Crypto.key()), list(Node.t()), list(Node.t())) :: :ok - def repair_transactions(unavailable_nodes, prev_available_nodes, new_available_nodes) do + @spec repair_transactions(list(Node.t()), list(Node.t())) :: :ok + def repair_transactions(prev_available_nodes, new_available_nodes) do # We fetch all the transactions existing and check if the disconnected nodes were in storage nodes TransactionChain.list_first_addresses() |> Stream.reject(&network_chain?(&1)) @@ -86,7 +83,6 @@ defmodule Archethic.SelfRepair.Notifier do |> Stream.each(fn chunk -> concurrent_txn_processing( chunk, - unavailable_nodes, prev_available_nodes, new_available_nodes ) @@ -103,21 +99,20 @@ defmodule Archethic.SelfRepair.Notifier do defp concurrent_txn_processing( addresses, - unavailable_nodes, prev_available_nodes, new_available_nodes ) do Task.Supervisor.async_stream_nolink( Archethic.task_supervisors(), addresses, - &sync_chain(&1, unavailable_nodes, prev_available_nodes, new_available_nodes), + &sync_chain(&1, prev_available_nodes, new_available_nodes), ordered: false, on_timeout: :kill_task ) |> Stream.run() end - defp sync_chain(address, unavailable_nodes, prev_available_nodes, new_available_nodes) do + defp sync_chain(address, prev_available_nodes, new_available_nodes) do genesis_address = TransactionChain.get_genesis_address(address) address @@ -126,13 +121,45 @@ defmodule Archethic.SelfRepair.Notifier do validation_stamp: [ledger_operations: [:transaction_movements]] ]) |> Stream.map(&get_previous_election(&1, prev_available_nodes, genesis_address)) - |> Stream.filter(&storage_or_io_node?(&1, unavailable_nodes)) + |> Stream.map(&compute_elections(&1, new_available_nodes)) + |> Stream.filter(&election_changed?(&1)) |> Stream.filter(¬ify?(&1)) - |> Stream.map(&new_storage_nodes(&1, new_available_nodes)) + |> Stream.map(&new_storage_nodes(&1)) |> map_last_addresses_for_node() |> notify_nodes(genesis_address) end + defp compute_elections( + {address, resolved_addresses, prev_storage_nodes, prev_io_nodes}, + new_available_nodes + ) do + new_storage_nodes = + Election.chain_storage_nodes(address, new_available_nodes) + |> Enum.map(& &1.first_public_key) + + new_io_nodes = + resolved_addresses + |> Election.io_storage_nodes(new_available_nodes) + |> Enum.map(& &1.first_public_key) + + %{ + address: address, + prev_storage_nodes: prev_storage_nodes, + prev_io_nodes: prev_io_nodes, + new_storage_nodes: new_storage_nodes, + new_io_nodes: new_io_nodes + } + end + + defp election_changed?(%{ + prev_storage_nodes: prev_storage_nodes, + prev_io_nodes: prev_io_nodes, + new_storage_nodes: new_storage_nodes, + new_io_nodes: new_io_nodes + }) do + prev_storage_nodes != new_storage_nodes or prev_io_nodes != new_io_nodes + end + defp get_previous_election( %Transaction{ address: address, @@ -153,36 +180,16 @@ defmodule Archethic.SelfRepair.Notifier do movements_addresses = transaction_movements |> Enum.map(& &1.to) - |> Enum.concat(recipients) - - authorized_nodes = P2P.authorized_and_available_nodes() # Before AEIP-21, resolve movements included only last addresses, # then we have to resolve the genesis address for all the movements resolved_addresses = - if protocol_version <= 7 do - Task.async_stream( - movements_addresses, - fn address -> - storage_nodes = Election.chain_storage_nodes(address, authorized_nodes) - - {:ok, resolved_genesis_address} = - TransactionChain.fetch_genesis_address( - address, - storage_nodes - ) - - [address, resolved_genesis_address] - end, - on_timeout: :kill_task, - max_concurrency: max(System.schedulers_online(), length(movements_addresses)) - ) - |> Stream.filter(&match?({:ok, _}, &1)) - |> Stream.flat_map(fn {:ok, addresses} -> addresses end) - |> Enum.concat([genesis_address]) - else - [genesis_address | movements_addresses] - end + compute_resolved_addresses( + genesis_address, + movements_addresses, + recipients, + protocol_version + ) prev_io_nodes = resolved_addresses @@ -192,14 +199,50 @@ defmodule Archethic.SelfRepair.Notifier do {address, resolved_addresses, prev_storage_nodes, prev_io_nodes -- prev_storage_nodes} end - defp storage_or_io_node?({_, _, prev_storage_nodes, prev_io_nodes}, unavailable_nodes) do - nodes = prev_storage_nodes ++ prev_io_nodes - Enum.any?(unavailable_nodes, &Enum.member?(nodes, &1)) + @spec compute_resolved_addresses( + binary(), + list(binary()), + list(binary()), + integer() + ) :: list(binary()) + def compute_resolved_addresses( + genesis_address, + movements_addresses, + recipients, + protocol_version + ) do + if protocol_version <= 7 do + Task.async_stream( + movements_addresses ++ recipients, + fn address -> + authorized_nodes = P2P.authorized_and_available_nodes() + storage_nodes = Election.chain_storage_nodes(address, authorized_nodes) + + {:ok, resolved_genesis_address} = + TransactionChain.fetch_genesis_address(address, storage_nodes) + + [address, resolved_genesis_address] + end, + on_timeout: :kill_task, + max_concurrency: max(System.schedulers_online(), length(movements_addresses)) + ) + |> Stream.filter(&match?({:ok, _}, &1)) + |> Stream.flat_map(fn {:ok, addresses} -> addresses end) + |> Enum.concat([genesis_address]) + else + [genesis_address | movements_addresses ++ recipients] + end end # Notify only if the current node is part of the previous storage / io nodes # to reduce number of messages - defp notify?({_, _, prev_storage_nodes, prev_io_nodes}) do + defp notify?(%{ + address: _, + new_io_nodes: _, + new_storage_nodes: _, + prev_io_nodes: prev_io_nodes, + prev_storage_nodes: prev_storage_nodes + }) do Enum.member?(prev_storage_nodes ++ prev_io_nodes, Crypto.first_node_public_key()) end @@ -207,26 +250,23 @@ defmodule Archethic.SelfRepair.Notifier do New election is carried out on the set of all authorized omiting unavailable_node. The set of previous storage nodes is subtracted from the set of new storage nodes. """ - @spec new_storage_nodes( - {binary(), list(Crypto.prepended_hash()), list(Crypto.key()), list(Crypto.key())}, - list(Node.t()) - ) :: + @spec new_storage_nodes(map()) :: {binary(), list(Crypto.key()), list(Crypto.key())} - def new_storage_nodes( - {address, resolved_addresses, prev_storage_nodes, prev_io_nodes}, - new_available_nodes - ) do + def new_storage_nodes(%{ + address: address, + new_io_nodes: new_io_nodes, + new_storage_nodes: new_storage_nodes, + prev_io_nodes: prev_io_nodes, + prev_storage_nodes: prev_storage_nodes + }) do new_storage_nodes = - Election.chain_storage_nodes(address, new_available_nodes) - |> Enum.map(& &1.first_public_key) + new_storage_nodes |> Enum.reject(&Enum.member?(prev_storage_nodes, &1)) already_stored_nodes = prev_storage_nodes ++ prev_io_nodes ++ new_storage_nodes new_io_nodes = - resolved_addresses - |> Election.io_storage_nodes(new_available_nodes) - |> Enum.map(& &1.first_public_key) + new_io_nodes |> Enum.reject(&Enum.member?(already_stored_nodes, &1)) {address, new_storage_nodes, new_io_nodes} diff --git a/test/archethic/self_repair/notifier_test.exs b/test/archethic/self_repair/notifier_test.exs index 8e4536005a..fbfed2be37 100644 --- a/test/archethic/self_repair/notifier_test.exs +++ b/test/archethic/self_repair/notifier_test.exs @@ -25,7 +25,7 @@ defmodule Archethic.SelfRepair.NotifierTest do alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations - test "new_storage_nodes/2 should return new election" do + test "new_storage_nodes/1 should return new election" do node1 = %Node{ first_public_key: "node1", last_public_key: "node1", @@ -68,13 +68,20 @@ defmodule Archethic.SelfRepair.NotifierTest do P2P.add_and_connect_node(node4) prev_storage_nodes = ["node2", "node3"] - new_available_nodes = [node1, node2, node3, node4] - - assert {"Alice1", ["node4", "node1"], []} = - Notifier.new_storage_nodes( - {"Alice1", [], prev_storage_nodes, []}, - new_available_nodes - ) + prev_io_nodes = [] + new_storage_nodes = ["node4", "node1"] + new_io_nodes = [] + + result = + Notifier.new_storage_nodes(%{ + address: "Alice1", + new_io_nodes: new_io_nodes, + new_storage_nodes: new_storage_nodes, + prev_io_nodes: prev_io_nodes, + prev_storage_nodes: prev_storage_nodes + }) + + assert {"Alice1", ["node4", "node1"], []} = result end test "map_last_address_for_node/1 should create a map with last address for each node" do @@ -96,7 +103,7 @@ defmodule Archethic.SelfRepair.NotifierTest do assert ^expected = Notifier.map_last_addresses_for_node(tab) end - test "repair_transactions/3 should send message to new storage nodes" do + test "repair_transactions/2 should send message to new storage nodes" do node = %Node{ first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), @@ -179,7 +186,7 @@ defmodule Archethic.SelfRepair.NotifierTest do {:ok, %Ok{}} end) - Notifier.repair_transactions(unavailable_nodes, prev_available_nodes, new_available_nodes) + Notifier.repair_transactions(prev_available_nodes, new_available_nodes) # Expect to receive only 1 new node for Alice2 assert_receive :new_node diff --git a/test/archethic/self_repair/sync_test.exs b/test/archethic/self_repair/sync_test.exs index a9980e229c..27c53b6383 100644 --- a/test/archethic/self_repair/sync_test.exs +++ b/test/archethic/self_repair/sync_test.exs @@ -590,82 +590,81 @@ defmodule Archethic.SelfRepair.SyncTest do assert %Node{network_patch: "AAA"} = P2P.get_node_info() end - describe "process_replication_attestations/2" do - test "should replicate the transactions and their inputs" do - {pub, priv} = Crypto.generate_random_keypair() + test "should replicate the transactions and their inputs" do + fixed_time = ~U[2024-12-04 16:00:00Z] - node1 = %Node{ - first_public_key: pub, - last_public_key: pub, - available?: true, - geo_patch: "BBB", - network_patch: "BBB", - authorized?: true, - reward_address: random_address(), - authorization_date: DateTime.utc_now() |> DateTime.add(-10), - enrollment_date: DateTime.utc_now() - } + {pub, priv} = Crypto.generate_random_keypair() - node2 = %Node{ - first_public_key: Crypto.first_node_public_key(), - last_public_key: Crypto.first_node_public_key(), - available?: true, - geo_patch: "AAA", - network_patch: "AAA", - authorized?: true, - reward_address: random_address(), - authorization_date: DateTime.utc_now() |> DateTime.add(-10), - enrollment_date: DateTime.utc_now() - } + node1 = %Node{ + first_public_key: pub, + last_public_key: pub, + available?: true, + geo_patch: "BBB", + network_patch: "BBB", + authorized?: true, + reward_address: "reward_address_1", + authorization_date: fixed_time |> DateTime.add(-10), + enrollment_date: fixed_time + } - P2P.add_and_connect_node(node1) - P2P.add_and_connect_node(node2) + node2 = %Node{ + first_public_key: Crypto.first_node_public_key(), + last_public_key: Crypto.first_node_public_key(), + available?: true, + geo_patch: "AAA", + network_patch: "AAA", + authorized?: true, + reward_address: "reward_address_2", + authorization_date: fixed_time |> DateTime.add(-10), + enrollment_date: fixed_time + } - tx = TransactionFactory.create_valid_transaction() - tx_address = tx.address - tx_summary = TransactionSummary.from_transaction(tx, Transaction.previous_address(tx)) + P2P.add_and_connect_node(node1) + P2P.add_and_connect_node(node2) + + tx = TransactionFactory.create_valid_transaction() + tx_address = tx.address + tx_summary = TransactionSummary.from_transaction(tx, Transaction.previous_address(tx)) + + transaction_inputs = %TransactionInputList{ + inputs: [ + %VersionedTransactionInput{ + protocol_version: 8, + input: %TransactionInput{ + from: "input_address", + amount: 100_000_000, + type: :UCO, + timestamp: fixed_time + } + } + ], + more?: false, + offset: 0 + } - MockClient - |> expect(:send_message, fn ^node1, %GetTransaction{}, _ -> - {:ok, tx} - end) - |> expect(:send_message, fn ^node1, %GetTransactionInputs{}, _ -> - {:ok, - %TransactionInputList{ - inputs: [ - %VersionedTransactionInput{ - protocol_version: 8, - input: %TransactionInput{ - from: random_address(), - amount: 100_000_000, - type: :UCO, - timestamp: DateTime.utc_now() - } - } - ], - more?: false, - offset: 0 - }} - end) + MockClient + |> stub(:send_message, fn + ^node1, %GetTransaction{}, _ -> {:ok, tx} + ^node1, %GetTransactionInputs{}, _ -> {:ok, transaction_inputs} + end) - MockTransactionLedger - |> expect(:write_inputs, fn ^tx_address, list -> - assert 1 = Enum.count(list) - :ok - end) + MockTransactionLedger + |> stub(:write_inputs, fn ^tx_address, inputs -> + assert length(inputs) == 1 + :ok + end) - tx_summary_bin = TransactionSummary.serialize(tx_summary) - signature = Crypto.sign(tx_summary_bin, priv) + tx_summary_bin = TransactionSummary.serialize(tx_summary) + signature = Crypto.sign(tx_summary_bin, priv) - attestations = [ - %ReplicationAttestation{ - transaction_summary: tx_summary, - confirmations: [{0, signature}] - } - ] + attestations = [ + %ReplicationAttestation{ + transaction_summary: tx_summary, + confirmations: [{0, signature}] + } + ] - assert 1 = Sync.process_replication_attestations(attestations, [node1]) - end + assert 1 == Sync.process_replication_attestations(attestations, [node1]) end defp create_p2p_context do