Skip to content

Commit

Permalink
Improve Transaction API validation (#371)
Browse files Browse the repository at this point in the history
* Added Enforcing of transaction validation sized in API
  • Loading branch information
prix-uniris committed Jun 13, 2022
1 parent cbec338 commit 4c7b023
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 13 deletions.
4 changes: 4 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ config :archethic, :marker, "-=%=-=%=-=%=-"
# size represents in bytes binary
config :archethic, :transaction_data_content_max_size, 3_145_728

# size represents in bytes binary
# 24KB Max
config :archethic, :transaction_data_code_max_size, 24576

config :archethic, Archethic.Crypto,
supported_curves: [
:ed25519,
Expand Down
4 changes: 4 additions & 0 deletions lib/archethic_web/controllers/api/schema/nft_ledger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ defmodule ArchethicWeb.API.Schema.NFTLedger do
changeset
|> cast(params, [])
|> cast_embed(:transfers, with: &changeset_transfers/2)
|> validate_length(:transfers,
max: 256,
message: "maximum nft transfers in a transaction can be 256"
)
end

defp changeset_transfers(changeset, params) do
Expand Down
4 changes: 4 additions & 0 deletions lib/archethic_web/controllers/api/schema/ownership.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ defmodule ArchethicWeb.API.Schema.Ownership do
|> cast(params, [:secret])
|> cast_embed(:authorizedKeys)
|> validate_required([:secret, :authorizedKeys])
|> validate_length(:authorizedKeys,
max: 256,
message: "maximum number of authorized keys can be 256"
)
|> format_authorized_keys()
end

Expand Down
24 changes: 11 additions & 13 deletions lib/archethic_web/controllers/api/schema/transaction_data.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ArchethicWeb.API.Schema.TransactionData do
@moduledoc false
@content_max_size Application.compile_env!(:archethic, :transaction_data_content_max_size)
@code_max_size Application.compile_env!(:archethic, :transaction_data_code_max_size)

use Ecto.Schema
import Ecto.Changeset
Expand All @@ -23,18 +24,15 @@ defmodule ArchethicWeb.API.Schema.TransactionData do
|> cast(params, [:code, :content, :recipients])
|> cast_embed(:ledger)
|> cast_embed(:ownerships)
|> validate_content_size()
end

defp validate_content_size(changeset = %Ecto.Changeset{}) do
validate_change(changeset, :content, fn field, content ->
content_size = byte_size(content)

if content_size >= @content_max_size do
[{field, "content size must be lessthan content_max_size"}]
else
[]
end
end)
|> validate_length(:content,
max: @content_max_size,
message: "content size must be lessthan content_max_size"
)
|> validate_length(:code,
max: @code_max_size,
message: "code size can't be more than #{Integer.to_string(@code_max_size)} bytes"
)
|> validate_length(:ownerships, max: 256, message: "ownerships can not be more that 256")
|> validate_length(:recipients, max: 256, message: "maximum number of recipients can be 256")
end
end
4 changes: 4 additions & 0 deletions lib/archethic_web/controllers/api/schema/uco_ledger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ defmodule ArchethicWeb.API.Schema.UCOLedger do
changeset
|> cast(params, [])
|> cast_embed(:transfers, with: &changeset_transfers/2)
|> validate_length(:transfers,
max: 256,
message: "maximum uco transfers in a transaction can be 256"
)
end

defp changeset_transfers(changeset, params) do
Expand Down
193 changes: 193 additions & 0 deletions test/archethic_web/controllers/api/transaction_payload_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do
assert {"is invalid", _} = Keyword.get(errors, :code)
end

test "should return an error if the code length is more than 24KB" do
%Ecto.Changeset{
valid?: false,
changes: %{data: %{errors: errors}}
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{"code" => Base.encode16(:crypto.strong_rand_bytes(24 * 1024 + 1))}
})

{error_message, _} = Keyword.get(errors, :code)
assert String.starts_with?(error_message, "code size can't be more than ")
end

test "should return an error if the uco ledger transfer address is invalid" do
changeset =
%Ecto.Changeset{
Expand Down Expand Up @@ -176,6 +197,41 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do
changeset |> get_errors() |> get_in([:data, :ledger, :uco, :transfers])
end

test "should return an error if the uco ledger transfers are more than 256" do
changeset =
%Ecto.Changeset{
valid?: false
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{
"ledger" => %{
"uco" => %{
"transfers" =>
1..257
|> Enum.map(fn _ ->
%{
"to" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"amount" => Enum.random(1..100)
}
end)
}
}
}
})

assert %{transfers: ["maximum uco transfers in a transaction can be 256"]} =
changeset |> get_errors() |> get_in([:data, :ledger, :uco])
end

test "should return an error if the nft ledger transfer address is invalid" do
changeset =
%Ecto.Changeset{
Expand Down Expand Up @@ -280,6 +336,43 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do
changeset |> get_errors |> get_in([:data, :ledger, :nft, :transfers])
end

test "should return an error if the nft ledger transfers are more than 256" do
changeset =
%Ecto.Changeset{
valid?: false
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{
"ledger" => %{
"nft" => %{
"transfers" =>
1..257
|> Enum.map(fn _ ->
%{
"to" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"amount" => Enum.random(1..100),
"nft" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>)
}
end)
}
}
}
})

assert %{transfers: ["maximum nft transfers in a transaction can be 256"]} =
changeset |> get_errors |> get_in([:data, :ledger, :nft])
end

test "should return an error if the encrypted secret is not an hexadecimal" do
changeset =
%Ecto.Changeset{
Expand Down Expand Up @@ -413,6 +506,79 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do
changeset |> get_errors |> get_in([:data, :ownerships])
end

test "should return an error ownerships are more than 256." do
changeset =
%Ecto.Changeset{
valid?: false
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{
"ownerships" =>
1..257
|> Enum.map(fn _ ->
%{
"authorizedKeys" =>
Enum.map(1..2, fn _ ->
%{
"publicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"encryptedSecretKey" => Base.encode16(:crypto.strong_rand_bytes(45))
}
end),
"secret" => Base.encode16(<<:crypto.strong_rand_bytes(64)::binary>>)
}
end)
}
})

{msg, _} = changeset.errors[:ownerships]
assert "ownerships can not be more that 256" == msg
end

test "should return an error authorized keys in a ownership can't be more than 256" do
changeset =
%Ecto.Changeset{
valid?: false
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{
"ownerships" => [
%{
"authorizedKeys" =>
1..257
|> Enum.map(fn _ ->
%{
"publicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"encryptedSecretKey" => Base.encode16(:crypto.strong_rand_bytes(64))
}
end),
"secret" => Base.encode16(:crypto.strong_rand_bytes(64))
}
]
}
})

assert [%{authorizedKeys: ["maximum number of authorized keys can be 256"]}] =
changeset |> get_errors |> get_in([:data, :ownerships])
end

test "should return an error if the recipients are invalid" do
changeset =
%Ecto.Changeset{
Expand Down Expand Up @@ -456,6 +622,33 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do
end
end

test "should return an error if the recipients are more that 256" do
changeset =
%Ecto.Changeset{
valid?: false
} =
TransactionPayload.changeset(%{
"version" => 1,
"address" => Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"type" => "transfer",
"timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond),
"previousPublicKey" =>
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>),
"previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)),
"data" => %{
"recipients" =>
1..257
|> Enum.map(fn _ ->
Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>)
end)
}
})

assert ["maximum number of recipients can be 256"] =
changeset |> get_errors() |> get_in([:data, :recipients])
end

test "to_map/1 should return a map of the changeset" do
address = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>
previous_public_key = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>
Expand Down

0 comments on commit 4c7b023

Please sign in to comment.