Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add New Types of Transaction #726 #791

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 181 additions & 127 deletions lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ defmodule Archethic.Mining.PendingTransactionValidation do
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@code_max_size Application.compile_env!(:archethic, :transaction_data_code_max_size)
apoorv-2204 marked this conversation as resolved.
Show resolved Hide resolved

@tx_max_size Application.compile_env!(:archethic, :transaction_data_content_max_size)

@doc """
Determines if the transaction is accepted into the network
"""
Expand All @@ -66,10 +70,11 @@ defmodule Archethic.Mining.PendingTransactionValidation do
) do
start = System.monotonic_time()

with :ok <- valid_not_exists(tx),
with :ok <- validate_size(tx),
:ok <- valid_not_exists(tx),
:ok <- valid_previous_signature(tx),
:ok <- validate_contract(tx),
:ok <- validate_content_size(tx),
:ok <- validate_ownerships(tx),
:ok <- do_accept_transaction(tx, validation_time),
:ok <- validate_previous_transaction_type?(tx) do
:telemetry.execute(
Expand All @@ -80,25 +85,26 @@ defmodule Archethic.Mining.PendingTransactionValidation do

:ok
else
{:error, :invalid_tx_type} ->
Logger.error("Invalid Transaction Type",
{:error, reason} = e ->
Logger.info("Invalid Transaction: #{reason}",
transaction_address: Base.encode16(address),
transaction_type: type
)

{:error, "Invalid Transaction Type"}

false ->
Logger.error("Invalid previous signature",
transaction_address: Base.encode16(address),
transaction_type: type
)
e
end
end

{:error, "Invalid previous signature"}
defp validate_size(%Transaction{data: data, version: tx_version}) do
tx_size =
data
|> TransactionData.serialize(tx_version)
|> byte_size()

{:error, reason} = e ->
Logger.info(reason, transaction_address: Base.encode16(address), transaction_type: type)
e
if tx_size >= @tx_max_size do
{:error, "Transaction data exceeds limit"}
else
:ok
end
end

Expand All @@ -123,7 +129,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do
conflict_resolver
) do
{:ok, %TransactionSummary{address: ^address}} ->
{:error, "transaction already exists"}
{:error, "Transaction already exists"}

{:ok, %NotFound{}} ->
:ok
Expand All @@ -133,6 +139,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end
end

@spec valid_previous_signature(tx :: Transaction.t()) :: :ok | {:error, any()}
defp valid_previous_signature(tx = %Transaction{}) do
if Transaction.verify_previous_signature?(tx) do
:ok
Expand All @@ -141,121 +148,12 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end
end

def validate_previous_transaction_type?(tx) do
case Transaction.network_type?(tx.type) do
false ->
# not a network tx, no need to validate with last tx
:ok

true ->
# when network tx, check with previous transaction
if validate_network_chain?(tx.type, tx), do: :ok, else: {:error, :invalid_tx_type}
end
end

@spec validate_network_chain?(
:code_approval
| :code_proposal
| :mint_rewards
| :node
| :node_rewards
| :node_shared_secrets
| :oracle
| :oracle_summary
| :origin,
Archethic.TransactionChain.Transaction.t()
) :: boolean
def validate_network_chain?(type, tx = %Transaction{})
when type in [:oracle, :oracle_summary] do
# mulitpe txn chain based on summary date

case OracleChain.genesis_address() do
nil ->
false

gen_addr ->
# first adddress found by looking up in the db
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

# 1: tx gen & validated in current summary cycle, gen_addr.current must match
# 2: tx gen in prev summary cycle & validated in current summary cycle, gen_addr.prev must match
# situation: the shift b/w summary A to summary B
gen_addr.current |> elem(0) == first_addr ||
gen_addr.prev |> elem(0) == first_addr
end
end

def validate_network_chain?(type, tx = %Transaction{})
when type in [:mint_rewards, :node_rewards] do
# singleton tx chain in network lifespan
case Reward.genesis_address() do
nil ->
false

gen_addr ->
# first addr located in db by prev_address from tx
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

gen_addr == first_addr
end
end

def validate_network_chain?(:node_shared_secrets, tx = %Transaction{}) do
# singleton tx chain in network lifespan
case SharedSecrets.genesis_address(:node_shared_secrets) do
nil ->
false

gen_addr ->
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

first_addr == gen_addr
end
end

def validate_network_chain?(:origin, tx = %Transaction{}) do
# singleton tx chain in network lifespan
# not parsing orgin pub key for origin family
case SharedSecrets.genesis_address(:origin) do
nil ->
false

origin_gen_addr_list ->
first_addr_from_prev_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

first_addr_from_prev_addr in origin_gen_addr_list
end
end

def validate_network_chain?(_type, _tx), do: true

defp validate_content_size(%Transaction{data: %TransactionData{content: content}}) do
content_max_size = Application.get_env(:archethic, :transaction_data_content_max_size)

if byte_size(content) >= content_max_size do
{:error, "Invalid node transaction with content size greaterthan content_max_size"}
else
:ok
end
end

defp validate_contract(%Transaction{data: %TransactionData{code: ""}}), do: :ok

defp validate_contract(%Transaction{
data: %TransactionData{code: code, ownerships: ownerships}
}) do
})
when byte_size(code) <= @code_max_size do
case Contracts.parse(code) do
{:ok, %Contract{triggers: triggers}} when map_size(triggers) > 0 ->
if Enum.any?(
Expand All @@ -275,6 +173,51 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end
end

defp validate_contract(%Transaction{data: %TransactionData{code: code}})
when byte_size(code) > @code_max_size do
{:error, "Invalid contract type transaction , code exceed max size"}
end

@spec validate_ownerships(Transaction.t()) :: :ok | {:error, any()}
defp validate_ownerships(%Transaction{data: %TransactionData{ownerships: []}}), do: :ok

defp validate_ownerships(%Transaction{data: %TransactionData{ownerships: ownerships}}) do
Enum.reduce_while(ownerships, :ok, fn
%Ownership{secret: secret, authorized_keys: authorized_keys}, :ok ->
cond do
secret == "" ->
{:halt, {:error, "Ownership: secret is empty"}}

authorized_keys == %{} ->
{:halt, {:error, "Ownership: authorized keys are empty"}}

true ->
verify_authorized_keys(authorized_keys)
end
end)
end

@spec verify_authorized_keys(authorized_keys :: map()) ::
{:cont, :ok} | {:halt, {:error, any()}}
defp verify_authorized_keys(authorized_keys) do
# authorized_keys: %{(public_key :: Crypto.key()) => encrypted_key }
apoorv-2204 marked this conversation as resolved.
Show resolved Hide resolved

Enum.reduce_while(authorized_keys, {:cont, :ok}, fn
{"", _}, _ ->
e = {:halt, {:error, "Ownership: public key is empty"}}
{:halt, e}

{_, ""}, _ ->
e = {:halt, {:error, "Ownership: encrypted key is empty"}}
{:halt, e}

{public_key, _}, acc ->
if Crypto.valid_public_key?(public_key),
do: {:cont, acc},
else: {:halt, {:halt, {:error, "Ownership: invalid public key"}}}
end)
end

defp do_accept_transaction(
%Transaction{
type: :transfer,
Expand Down Expand Up @@ -723,8 +666,119 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end
end

defp do_accept_transaction(%Transaction{type: :contract, data: %TransactionData{code: ""}}, _),
do: {:error, "Invalid contract type transaction - code is empty"}

defp do_accept_transaction(
%Transaction{type: :data, data: %TransactionData{content: "", ownerships: []}},
_
),
do: {:error, "Invalid data type transaction - Both content & ownership are empty"}

defp do_accept_transaction(_, _), do: :ok

defp validate_previous_transaction_type?(tx) do
case Transaction.network_type?(tx.type) do
false ->
# not a network tx, no need to validate with last tx
:ok

true ->
# when network tx, check with previous transaction
if validate_network_chain?(tx.type, tx),
do: :ok,
else: {:error, "Invalid Transaction Type"}
end
end

@spec validate_network_chain?(
:code_approval
| :code_proposal
| :mint_rewards
| :node
| :node_rewards
| :node_shared_secrets
| :oracle
| :oracle_summary
| :origin,
Archethic.TransactionChain.Transaction.t()
) :: boolean
defp validate_network_chain?(type, tx = %Transaction{})
when type in [:oracle, :oracle_summary] do
# mulitpe txn chain based on summary date

case OracleChain.genesis_address() do
nil ->
false

gen_addr ->
# first adddress found by looking up in the db
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

# 1: tx gen & validated in current summary cycle, gen_addr.current must match
# 2: tx gen in prev summary cycle & validated in current summary cycle, gen_addr.prev must match
# situation: the shift b/w summary A to summary B
gen_addr.current |> elem(0) == first_addr ||
gen_addr.prev |> elem(0) == first_addr
end
end

defp validate_network_chain?(type, tx = %Transaction{})
when type in [:mint_rewards, :node_rewards] do
# singleton tx chain in network lifespan
case Reward.genesis_address() do
nil ->
false

gen_addr ->
# first addr located in db by prev_address from tx
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

gen_addr == first_addr
end
end

defp validate_network_chain?(:node_shared_secrets, tx = %Transaction{}) do
# singleton tx chain in network lifespan
case SharedSecrets.genesis_address(:node_shared_secrets) do
nil ->
false

gen_addr ->
first_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

first_addr == gen_addr
end
end

defp validate_network_chain?(:origin, tx = %Transaction{}) do
# singleton tx chain in network lifespan
# not parsing orgin pub key for origin family
case SharedSecrets.genesis_address(:origin) do
nil ->
false

origin_gen_addr_list ->
first_addr_from_prev_addr =
tx
|> Transaction.previous_address()
|> TransactionChain.get_genesis_address()

first_addr_from_prev_addr in origin_gen_addr_list
end
end

defp validate_network_chain?(_type, _tx), do: true

defp verify_token_creation(content) do
with {:ok, json_token} <- Jason.decode(content),
:ok <- ExJsonSchema.Validator.validate(@token_schema, json_token),
Expand Down
Loading