Skip to content

Commit 10f2acb

Browse files
authored
test: add blockchain DSL for testing purposes (#233)
This is a temprary experimental approach to remove the synced blockchain requirement for running tests. In the near future, once the devmode is enabled, we can turn this DSL into something more imperative (a Ganache-like approach).
1 parent e06296d commit 10f2acb

File tree

3 files changed

+177
-141
lines changed

3 files changed

+177
-141
lines changed

mix.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule AeMdw.MixProject do
3737
:aec_hard_forks,
3838
:aec_hash,
3939
:aec_headers,
40+
:aec_spend_tx,
4041
:aec_sync,
4142
:aec_trees,
4243
:aect_call,
@@ -66,7 +67,8 @@ defmodule AeMdw.MixProject do
6667
:aetx_env,
6768
:aetx_sign,
6869
:aeu_info,
69-
:aeu_mtrees
70+
:aeu_mtrees,
71+
:enacl
7072
]
7173
],
7274
dialyzer: dialyzer(),
Lines changed: 13 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,36 @@
11
defmodule AeMdwWeb.BlockControllerTest do
22
use AeMdwWeb.ConnCase, async: false
33

4-
alias :aeser_api_encoder, as: Enc
5-
alias AeMdw.Validate
6-
alias AeMdw.Db.{Model, Format}
7-
alias AeMdwWeb.TestUtil
8-
alias AeMdw.Error.Input, as: ErrInput
9-
require Model
4+
alias AeMdwWeb.BlockchainSim
105

11-
import AeMdw.Db.Util
126
import Mock
7+
import AeMdwWeb.BlockchainSim
138

149
describe "block" do
1510
test "get key block by hash", %{conn: conn} do
16-
kb_hash = "kh_29Gmo8RMdCD5aJ1UUrKd6Kx2c3tvHQu82HKsnVhbprmQnFy5bn"
17-
bin_kb_hash = AeMdw.Validate.id!(kb_hash)
11+
with_blockchain %{alice: 10_000}, b1: {:kb, :alice} do
12+
kb_hash = blocks[:b1]
1813

19-
with_mocks [
20-
{:aec_chain, [], get_block: fn ^bin_kb_hash -> {:ok, sample_key_block()} end},
21-
{:aec_db, [], get_header: fn ^bin_kb_hash -> sample_key_header() end}
22-
] do
23-
conn = get(conn, "/block/#{kb_hash}")
24-
25-
assert json_response(conn, 200) == TestUtil.handle_input(fn -> get_block(kb_hash) end)
14+
assert %{"hash" => ^kb_hash} = get(conn, "/block/#{kb_hash}") |> json_response(200)
2615
end
2716
end
2817

2918
test "get micro block by hash", %{conn: conn} do
30-
mb_hash = "mh_RuKG8scokoAdhqNN3uvq29VZBsSaZdpeaoyBsXLeGV69sud85"
31-
bin_mb_hash = AeMdw.Validate.id!(mb_hash)
32-
33-
with_mocks [
34-
{:aec_chain, [], get_block: fn ^bin_mb_hash -> {:ok, sample_micro_block()} end},
35-
{:aec_db, [], get_header: fn ^bin_mb_hash -> sample_micro_header() end}
36-
] do
37-
conn = get(conn, "/block/#{mb_hash}")
19+
with_blockchain %{alice: 10_000, bob: 20_000},
20+
b1: [
21+
t1: BlockchainSim.spend_tx(:alice, :bob, 5_000)
22+
] do
23+
mb_hash = blocks[:b1]
3824

39-
assert json_response(conn, 200) == TestUtil.handle_input(fn -> get_block(mb_hash) end)
25+
assert %{"hash" => ^mb_hash} = get(conn, "/block/#{mb_hash}") |> json_response(200)
4026
end
4127
end
4228

4329
test "renders error when the hash is invalid", %{conn: conn} do
4430
hash = "kh_NoSuchHash"
45-
conn = get(conn, "/block/#{hash}")
46-
47-
assert json_response(conn, 400) == %{
48-
"error" => TestUtil.handle_input(fn -> get_block(hash) end)
49-
}
50-
end
51-
end
52-
53-
################
54-
55-
defp get_block(enc_block_hash) when is_binary(enc_block_hash) do
56-
block_hash = Validate.id!(enc_block_hash)
57-
58-
case :aec_chain.get_block(block_hash) do
59-
{:ok, _} ->
60-
Format.to_map({:block, {nil, nil}, nil, block_hash})
61-
62-
:error ->
63-
raise ErrInput.NotFound, value: enc_block_hash
64-
end
65-
end
66-
67-
defp get_block({_, mbi} = block_index) do
68-
case read_block(block_index) do
69-
[block] ->
70-
type = (mbi == -1 && :key_block_hash) || :micro_block_hash
71-
hash = Model.block(block, :hash)
72-
get_block(Enc.encode(type, hash))
7331

74-
[] ->
75-
raise ErrInput.NotFound, value: block_index
32+
assert %{"error" => <<"invalid id: ", _rest::binary>>} =
33+
get(conn, "/block/#{hash}") |> json_response(400)
7634
end
7735
end
78-
79-
defp sample_key_block do
80-
{:key_block, sample_key_header()}
81-
end
82-
83-
defp sample_key_header do
84-
{:key_header, 1,
85-
<<108, 21, 218, 110, 191, 175, 2, 120, 254, 175, 77, 241, 176, 241, 169, 130, 85, 7, 174,
86-
123, 154, 73, 75, 195, 76, 145, 113, 63, 56, 221, 87, 131>>,
87-
<<108, 21, 218, 110, 191, 175, 2, 120, 254, 175, 77, 241, 176, 241, 169, 130, 85, 7, 174,
88-
123, 154, 73, 75, 195, 76, 145, 113, 63, 56, 221, 87, 131>>,
89-
<<52, 183, 229, 249, 54, 69, 51, 88, 116, 15, 122, 6, 182, 198, 8, 237, 95, 88, 152, 76, 53,
90-
115, 239, 229, 75, 84, 120, 17, 7, 73, 153, 49>>, 522_133_279, 7_537_663_592_980_547_537,
91-
1_543_373_685_748, 1,
92-
[
93-
26_922_260,
94-
37_852_188,
95-
59_020_115,
96-
60_279_463,
97-
79_991_400,
98-
85_247_410,
99-
107_259_316,
100-
109_139_865,
101-
110_742_806,
102-
135_064_096,
103-
135_147_996,
104-
168_331_414,
105-
172_261_759,
106-
199_593_922,
107-
202_230_201,
108-
203_701_465,
109-
210_434_810,
110-
231_398_482,
111-
262_809_482,
112-
271_994_744,
113-
272_584_245,
114-
287_928_914,
115-
292_169_553,
116-
362_488_698,
117-
364_101_896,
118-
364_186_805,
119-
373_099_116,
120-
398_793_711,
121-
400_070_528,
122-
409_055_423,
123-
410_928_197,
124-
423_334_086,
125-
423_561_843,
126-
428_130_074,
127-
496_454_011,
128-
501_715_005,
129-
505_858_333,
130-
514_079_183,
131-
522_053_501,
132-
526_239_399,
133-
527_666_844,
134-
532_070_334
135-
],
136-
<<109, 80, 187, 72, 39, 0, 181, 159, 179, 75, 226, 70, 33, 153, 149, 169, 59, 82, 131, 166,
137-
223, 128, 104, 223, 115, 204, 111, 77, 205, 5, 56, 247>>,
138-
<<186, 203, 214, 163, 246, 107, 124, 137, 222, 135, 217, 193, 221, 104, 215, 16, 94, 25, 47,
139-
35, 97, 96, 99, 179, 23, 38, 226, 135, 232, 249, 24, 44>>, "",
140-
%{consensus: :aec_consensus_bitcoin_ng}}
141-
end
142-
143-
defp sample_micro_block do
144-
{:mic_block, sample_micro_header()}
145-
end
146-
147-
defp sample_micro_header do
148-
{:mic_header, 305_488, "",
149-
<<123, 66, 245, 198, 197, 131, 107, 129, 76, 33, 48, 83, 69, 18, 29, 92, 34, 125, 232, 194,
150-
72, 143, 41, 63, 18, 139, 120, 116, 45, 79, 16, 108>>,
151-
<<205, 135, 56, 162, 96, 202, 59, 7, 215, 179, 229, 109, 41, 29, 214, 107, 35, 50, 95, 154,
152-
219, 228, 142, 169, 53, 232, 166, 4, 232, 147, 188, 64>>,
153-
<<159, 240, 58, 171, 83, 153, 27, 217, 82, 171, 254, 252, 207, 84, 95, 53, 51, 74, 232, 74,
154-
71, 119, 195, 119, 76, 151, 185, 56, 200, 189, 193, 78>>,
155-
<<105, 101, 147, 121, 43, 123, 67, 195, 141, 128, 83, 57, 81, 64, 38, 102, 16, 183, 151, 198,
156-
70, 31, 124, 51, 136, 54, 61, 145, 175, 206, 242, 131, 139, 7, 85, 12, 93, 191, 223, 205,
157-
50, 239, 189, 136, 12, 18, 31, 47, 127, 94, 194, 131, 254, 70, 243, 168, 236, 149, 63, 101,
158-
84, 78, 219, 6>>,
159-
<<155, 74, 110, 105, 160, 87, 202, 235, 211, 79, 100, 7, 204, 19, 228, 89, 48, 64, 212, 231,
160-
175, 166, 195, 25, 170, 195, 160, 121, 134, 181, 73, 200>>, 1_598_562_036_727, 4,
161-
%{consensus: :aec_consensus_bitcoin_ng}}
162-
end
16336
end

test/support/blockchain_sim.ex

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
defmodule AeMdwWeb.BlockchainSim do
2+
@moduledoc """
3+
"""
4+
5+
require Mock
6+
7+
@passthrough_functions ~w(module_info)a
8+
9+
@type account_id() :: :aeser_id.id()
10+
11+
defmacro with_blockchain(initial_balances, blocks, do: body) do
12+
aec_db =
13+
:exports
14+
|> :aec_db.module_info()
15+
|> Enum.map(fn {fun_name, arity} ->
16+
args = Macro.generate_arguments(arity, __MODULE__)
17+
18+
{fun_name,
19+
{:fn, [],
20+
quote do
21+
unquote_splicing(args) ->
22+
raise "Unmocked function #{unquote(fun_name)}/#{unquote(arity)}"
23+
end}}
24+
end)
25+
|> Keyword.drop(unquote(@passthrough_functions))
26+
27+
quote do
28+
{
29+
aec_db_mock,
30+
mock_blocks,
31+
mock_transactions,
32+
mock_accounts
33+
} = unquote(__MODULE__).generate_blockchain(unquote(initial_balances), unquote(blocks))
34+
35+
aec_db_mock = Keyword.merge(unquote(aec_db), aec_db_mock)
36+
37+
Mock.with_mocks [{:aec_db, [:passthrough], aec_db_mock}] do
38+
var!(blocks) = mock_blocks
39+
var!(transactions) = mock_transactions
40+
var!(accounts) = mock_accounts
41+
42+
# HACK: To remove unused warnings
43+
{var!(blocks), var!(transactions), var!(accounts)}
44+
45+
unquote(body)
46+
end
47+
end
48+
end
49+
50+
@spec spend_tx(account_id(), account_id(), non_neg_integer()) :: :aetx.t()
51+
def spend_tx(sender_id, recipient_id, amount) do
52+
{:spend_tx, sender_id, recipient_id, amount}
53+
end
54+
55+
def generate_blockchain(initial_balances, blocks) do
56+
mock_accounts =
57+
initial_balances
58+
|> Enum.map(fn {account_id, _balance} ->
59+
%{public: account_pkey} = :enacl.sign_keypair()
60+
61+
{account_id, :aeser_id.create(:account, account_pkey)}
62+
end)
63+
|> Map.new()
64+
65+
{mock_blocks, mock_transactions, _max_height} =
66+
blocks
67+
|> Enum.reduce({%{}, %{}, 1}, fn
68+
{block_id, {:kb, account_id}}, {mock_blocks, mock_transactions, height}
69+
when is_atom(account_id) ->
70+
{
71+
Map.put(mock_blocks, block_id, mock_key_block(account_id, mock_accounts, height)),
72+
mock_transactions,
73+
height + 1
74+
}
75+
76+
{block_id, transactions}, {mock_blocks, mock_transactions, height}
77+
when is_list(transactions) ->
78+
{micro_block, transactions} = mock_micro_block(transactions, mock_accounts, height)
79+
80+
{
81+
Map.put(mock_blocks, block_id, micro_block),
82+
Map.merge(mock_transactions, transactions),
83+
height
84+
}
85+
end)
86+
87+
aec_db_mock = [
88+
find_block: fn hash ->
89+
find_block(hash, mock_blocks)
90+
end,
91+
get_block: fn hash ->
92+
{:value, block} = find_block(hash, mock_blocks)
93+
94+
block
95+
end,
96+
get_header: fn hash ->
97+
{:value, block} = find_block(hash, mock_blocks)
98+
99+
:aec_blocks.to_header(block)
100+
end
101+
]
102+
103+
blocks_pkeys =
104+
mock_blocks
105+
|> Enum.map(fn {block_id, block} ->
106+
header = :aec_blocks.to_header(block)
107+
{:ok, block_hash} = :aec_headers.hash_header(header)
108+
109+
hash_type =
110+
case :aec_blocks.type(block) do
111+
:key -> :key_block_hash
112+
:micro -> :micro_block_hash
113+
end
114+
115+
{block_id, :aeser_api_encoder.encode(hash_type, block_hash)}
116+
end)
117+
|> Map.new()
118+
119+
{aec_db_mock, blocks_pkeys, mock_transactions, mock_accounts}
120+
end
121+
122+
defp mock_key_block(account_id, accounts, height) do
123+
miner_pk = Map.fetch!(accounts, account_id) |> :aeser_id.specialize(:account)
124+
125+
:aec_blocks.new_key(height, <<>>, <<>>, <<>>, 0, 0, 0, :default, 0, miner_pk, miner_pk)
126+
end
127+
128+
defp mock_micro_block(transactions, accounts, height) do
129+
txs =
130+
transactions
131+
|> Enum.map(fn {tx_id, tx} -> {tx_id, serialize_tx(tx, accounts)} end)
132+
|> Map.new()
133+
134+
{:aec_blocks.new_micro(height, <<>>, <<>>, <<>>, <<>>, Map.values(txs), 0, :no_fraud, 0), txs}
135+
end
136+
137+
defp find_block(hash, blocks) do
138+
blocks
139+
|> Enum.find(fn {_block_id, block} ->
140+
header = :aec_blocks.to_header(block)
141+
{:ok, block_hash} = :aec_headers.hash_header(header)
142+
143+
block_hash == hash
144+
end)
145+
|> case do
146+
nil -> :none
147+
{_block_id, block} -> {:value, block}
148+
end
149+
end
150+
151+
defp serialize_tx({:spend_tx, sender_id, recipient_id, amount}, accounts) do
152+
:aec_spend_tx.new(%{
153+
sender_id: Map.fetch!(accounts, sender_id),
154+
recipient_id: Map.fetch!(accounts, recipient_id),
155+
amount: amount,
156+
fee: 0,
157+
nonce: 0,
158+
payload: <<>>
159+
})
160+
end
161+
end

0 commit comments

Comments
 (0)