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

Improve Transaction API validation #371

Merged
5 commits merged into from
Jun 13, 2022
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
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