From 5873aa4a7ae3be62a4c0f7250e3fa906953ca497 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 21 Jan 2022 16:39:53 -0700 Subject: [PATCH 1/3] State Channel Dispute Versioning A flood of disputes coming in from all over the place can cause enough trouble reconciling how to pay out rewards and txns that things start to go haywire. - consistent arity for txn callback - xor filters don't like to be empty - set default sc_handler if you're not running tests with a group - helper for adding traces - add sc_dispute test - break out of valid check early if the ledger state channel is already disputed - add sc_dispute_prevention chaing var --boolean() - add sc_dispute_prevention eunit test for rewards - make sure dispute_prevention is active before rejecting close disputes - Zero out when dispute prevention is active and sc is dipusted, otherwise carry on as usual - better searchable name for the test - more explicit naming for internal test halpers - add sc_dispute_prevention chain var to test suite - Some notes and questions about this test - grab sc_dispute_prevention from passed in Vars - don't use assertMatch, allows empty maps to match any map - more descriptive variable - use rewards v2 Update src/transactions/v2/blockchain_txn_rewards_v2.erl Co-authored-by: Andrew Thompson sc_dispute_prevention -> sc_dispute_strategy_version - Allows for evolution of handling state channel close disputes - Add sc_dispute_strategy_version to rewards_vars() map - get back metadata for rewards - allow versions higher than 1 without code updates in these paths --- include/blockchain_vars.hrl | 5 +- .../blockchain_txn_state_channel_close_v1.erl | 92 +++--- .../v1/blockchain_txn_vars_v1.erl | 2 + .../v2/blockchain_txn_rewards_v2.erl | 107 +++++-- test/blockchain_state_channel_SUITE.erl | 284 +++++++++++++++--- test/meck_test_util.erl | 2 +- 6 files changed, 383 insertions(+), 109 deletions(-) diff --git a/include/blockchain_vars.hrl b/include/blockchain_vars.hrl index fcf4a2c4d5..fc8c084fff 100644 --- a/include/blockchain_vars.hrl +++ b/include/blockchain_vars.hrl @@ -341,7 +341,10 @@ %% The "new" behavior (i.e., `true') is to count state channels %% that are only in the open state and ignore closed channels. -define(sc_only_count_open_active, sc_only_count_open_active). - +%% How to handle rewards when a state channel close is disputed. +%% - 0 :: reconcile as much as possible +%% - 1 :: accept first dispute, drop all DC from opener, no rewards +-define(sc_dispute_strategy_version, sc_dispute_strategy_version). %% ------------------------------------------------------------------ %% snapshot vars diff --git a/src/transactions/v1/blockchain_txn_state_channel_close_v1.erl b/src/transactions/v1/blockchain_txn_state_channel_close_v1.erl index 1eecb921a5..54a438031b 100644 --- a/src/transactions/v1/blockchain_txn_state_channel_close_v1.erl +++ b/src/transactions/v1/blockchain_txn_state_channel_close_v1.erl @@ -151,6 +151,10 @@ is_valid(Txn, Chain) -> _ -> 0 end, + SCDisputeStrategy = case blockchain:config(?sc_dispute_strategy_version, Ledger) of + {ok, V2} -> V2; + _ -> 0 + end, %% first check if it's time to expire case SCVersion == 0 orelse (LedgerHeight >= ExpiresAt andalso @@ -176,50 +180,54 @@ is_valid(Txn, Chain) -> {error, _Reason} -> {error, state_channel_not_open}; {ok, LedgerSC} -> - case Owner == Closer of - %% check the owner's close conditions - %% the owner is not allowed to update if the channel is in dispute - %% and must provide a causally newer version of the channel if there's already a close on file - true -> - case blockchain_ledger_state_channel_v2:is_v2(LedgerSC) of - false -> - ok; + CloseState = blockchain_ledger_state_channel_v2:close_state(LedgerSC), + case {CloseState, SCDisputeStrategy} of + {dispute, Ver} when Ver >= 1 -> {error, already_disputed}; + _ -> + case Owner == Closer of + %% check the owner's close conditions + %% the owner is not allowed to update if the channel is in dispute + %% and must provide a causally newer version of the channel if there's already a close on file true -> - LSC = blockchain_ledger_state_channel_v2:state_channel(LedgerSC), - CloseState = blockchain_ledger_state_channel_v2:close_state(LedgerSC), - lager:info("close state was ~p", [CloseState]), - %% check this new SC is newer than the current one, if any - case LSC == undefined orelse (CloseState /= dispute andalso blockchain_state_channel_v1:compare_causality(LSC, SC) == caused) of - true -> - ok; + case blockchain_ledger_state_channel_v2:is_v2(LedgerSC) of false -> - {error, redundant} - end - end; - false -> - case blockchain_state_channel_v1:get_summary(Closer, SC) of - {error, _Reason}=E -> - E; - {ok, _Summary} -> - case check_close_updates(LedgerSC, Txn, Ledger) of - ok -> - %% This closer was part of the state channel - %% Is therefore allowed to close said state channel - %% Verify they can afford the fee - - AreFeesEnabled = blockchain_ledger_v1:txn_fees_active(Ledger), - TxnFee = ?MODULE:fee(Txn), - %% NOTE: TMP removing fee check as SC close fees are hardcoded to zero atm and the check breaks dialyzer - %% ExpectedTxnFee = ?MODULE:calculate_fee(Txn, Chain), - %% case ExpectedTxnFee =< TxnFee orelse not AreFeesEnabled of - %% false -> - %% {error, {wrong_txn_fee, ExpectedTxnFee, TxnFee}}; - %% true -> - %% blockchain_ledger_v1:check_dc_or_hnt_balance(Closer, TxnFee, Ledger, AreFeesEnabled) - %% end - blockchain_ledger_v1:check_dc_or_hnt_balance(Closer, TxnFee, Ledger, AreFeesEnabled); - E -> - E + ok; + true -> + LSC = blockchain_ledger_state_channel_v2:state_channel(LedgerSC), + lager:info("close state was ~p", [CloseState]), + %% check this new SC is newer than the current one, if any + case LSC == undefined orelse (CloseState /= dispute andalso blockchain_state_channel_v1:compare_causality(LSC, SC) == caused) of + true -> + ok; + false -> + {error, redundant} + end + end; + false -> + case blockchain_state_channel_v1:get_summary(Closer, SC) of + {error, _Reason}=E -> + E; + {ok, _Summary} -> + case check_close_updates(LedgerSC, Txn, Ledger) of + ok -> + %% This closer was part of the state channel + %% Is therefore allowed to close said state channel + %% Verify they can afford the fee + + AreFeesEnabled = blockchain_ledger_v1:txn_fees_active(Ledger), + TxnFee = ?MODULE:fee(Txn), + %% NOTE: TMP removing fee check as SC close fees are hardcoded to zero atm and the check breaks dialyzer + %% ExpectedTxnFee = ?MODULE:calculate_fee(Txn, Chain), + %% case ExpectedTxnFee =< TxnFee orelse not AreFeesEnabled of + %% false -> + %% {error, {wrong_txn_fee, ExpectedTxnFee, TxnFee}}; + %% true -> + %% blockchain_ledger_v1:check_dc_or_hnt_balance(Closer, TxnFee, Ledger, AreFeesEnabled) + %% end + blockchain_ledger_v1:check_dc_or_hnt_balance(Closer, TxnFee, Ledger, AreFeesEnabled); + E -> + E + end end end end diff --git a/src/transactions/v1/blockchain_txn_vars_v1.erl b/src/transactions/v1/blockchain_txn_vars_v1.erl index 5aefc7c29d..01e7a63cf7 100644 --- a/src/transactions/v1/blockchain_txn_vars_v1.erl +++ b/src/transactions/v1/blockchain_txn_vars_v1.erl @@ -1095,6 +1095,8 @@ validate_var(?sc_only_count_open_active, Value) -> false -> ok; Other -> throw({error, {invalid_sc_only_count_open_active_value, Other}}) end; +validate_var(?sc_dispute_strategy_version, Value) -> + validate_int(Value, "sc_dispute_strategy_version", 0, 1, false); %% txn snapshot vars validate_var(?snapshot_version, Value) -> diff --git a/src/transactions/v2/blockchain_txn_rewards_v2.erl b/src/transactions/v2/blockchain_txn_rewards_v2.erl index 322f0d6cdb..dbd662162b 100644 --- a/src/transactions/v2/blockchain_txn_rewards_v2.erl +++ b/src/transactions/v2/blockchain_txn_rewards_v2.erl @@ -688,6 +688,10 @@ get_reward_vars(Start, End, Ledger) -> _ -> 1 end, + SCDisputeStrategyVersion = case blockchain:config(?sc_dispute_strategy_version, Ledger) of + {ok, SCDV} -> SCDV; + _ -> 0 + end, POCVersion = case blockchain:config(?poc_version, Ledger) of {ok, V} -> V; _ -> 1 @@ -746,6 +750,7 @@ get_reward_vars(Start, End, Ledger) -> dc_percent => DCPercent, sc_grace_blocks => SCGrace, sc_version => SCVersion, + sc_dispute_strategy_version => SCDisputeStrategyVersion, poc_version => POCVersion, reward_version => RewardVersion, witness_redundancy => WitnessRedundancy, @@ -1377,26 +1382,39 @@ dc_reward(Txn, End, AccIn, Ledger, #{ sc_grace_blocks := GraceBlocks, %% pull out the final version of the state channel FinalSC = blockchain_ledger_state_channel_v2:state_channel(SC), RewardVersion = maps:get(reward_version, Vars, 1), - - Summaries = case RewardVersion > 3 of - %% reward version 4 normalizes payouts - true -> blockchain_state_channel_v1:summaries(blockchain_state_channel_v1:normalize(FinalSC)); - false -> blockchain_state_channel_v1:summaries(FinalSC) - end, - %% check the dispute status - Bonus = case blockchain_ledger_state_channel_v2:close_state(SC) of - %% Reward version 4 or higher just slashes overcommit - dispute when RewardVersion < 4 -> - %% the owner of the state channel - %% did a naughty thing, divide - %% their overcommit between the - %% participants - OverCommit = blockchain_ledger_state_channel_v2:amount(SC) - - blockchain_ledger_state_channel_v2:original(SC), - OverCommit div length(Summaries); - _ -> - 0 - end, + CloseState = blockchain_ledger_state_channel_v2:close_state(SC), + SCDisputeStrategy = maps:get(?sc_dispute_strategy_version, Vars, 0), + {Summaries, Bonus} = + case {SCDisputeStrategy, CloseState} of + {Ver, dispute} when Ver >= 1 -> + %% When sc_dispute_strategy_version is 1 we want to zero out as much as possible. + %% No Bonuses, no summaries. All slashed. + {[], 0}; + {_, _} -> + + InnerSummaries = case RewardVersion > 3 of + %% reward version 4 normalizes payouts + true -> blockchain_state_channel_v1:summaries(blockchain_state_channel_v1:normalize(FinalSC)); + false -> blockchain_state_channel_v1:summaries(FinalSC) + end, + + %% check the dispute status + InnerBonus = case blockchain_ledger_state_channel_v2:close_state(SC) of + %% Reward version 4 or higher just slashes overcommit + dispute when RewardVersion < 4 -> + %% the owner of the state channel + %% did a naughty thing, divide + %% their overcommit between the + %% participants + + OverCommit = blockchain_ledger_state_channel_v2:amount(SC) + - blockchain_ledger_state_channel_v2:original(SC), + OverCommit div length(InnerSummaries); + _ -> + 0 + end, + {InnerSummaries, InnerBonus} + end, lists:foldl(fun(Summary, A) -> Key = blockchain_state_channel_summary_v1:client_pubkeybin(Summary), @@ -1768,6 +1786,54 @@ poc_witnesses_rewards_test() -> ?assertEqual(Rewards, normalize_witness_rewards(WitnessShares, EpochVars)), test_utils:cleanup_tmp_dir(BaseDir). +dc_rewards_sc_dispute_strategy_test() -> + BaseDir = test_utils:tmp_dir("dc_rewards_dispute_sc_test"), + Ledger = blockchain_ledger_v1:new(BaseDir), + Ledger1 = blockchain_ledger_v1:new_context(Ledger), + + Vars = #{ + epoch_reward => 100000, + dc_percent => 0.3, + consensus_percent => 0.06 + 0.025, + poc_challengees_percent => 0.18, + poc_challengers_percent => 0.0095, + poc_witnesses_percent => 0.0855, + securities_percent => 0.34, + sc_version => 2, + sc_grace_blocks => 5, + reward_version => 4, + oracle_price => 100000000, %% 1 dollar + consensus_members => [<<"c">>, <<"d">>], + %% This is the important part of what's being tested here. + sc_dispute_strategy_version => 1 + }, + + LedgerVars = maps:merge(#{?poc_version => 5, ?sc_version => 2, ?sc_grace_blocks => 5}, common_poc_vars()), + ok = blockchain_ledger_v1:vars(LedgerVars, [], Ledger1), + + {SC0, _} = blockchain_state_channel_v1:new(<<"id">>, <<"owner">>, 100, <<"blockhash">>, 10), + SCValid = blockchain_state_channel_v1:summaries([blockchain_state_channel_summary_v1:new(<<"a">>, 1, 1), blockchain_state_channel_summary_v1:new(<<"b">>, 2, 2)], SC0), + SCDispute = blockchain_state_channel_v1:summaries([blockchain_state_channel_summary_v1:new(<<"a">>, 2, 2), blockchain_state_channel_summary_v1:new(<<"b">>, 3, 3)], SC0), + + ok = blockchain_ledger_v1:add_state_channel(<<"id">>, <<"owner">>, 10, 1, 100, 200, Ledger1), + {ok, _} = blockchain_ledger_v1:find_state_channel(<<"id">>, <<"owner">>, Ledger1), + + ok = blockchain_ledger_v1:close_state_channel(<<"owner">>, <<"owner">>, SCValid, <<"id">>, false, Ledger1), + {ok, _} = blockchain_ledger_v1:find_state_channel(<<"id">>, <<"owner">>, Ledger1), + + ok = blockchain_ledger_v1:close_state_channel(<<"owner">>, <<"a">>, SCDispute, <<"id">>, true, Ledger1), + + SCClose = blockchain_txn_state_channel_close_v1:new(SCValid, <<"owner">>), + {ok, _DCsInEpochAsHNT} = blockchain_ledger_v1:dc_to_hnt(3, 100000000), %% 3 DCs burned at HNT price of 1 dollar + + DCShares = dc_reward(SCClose, 100, #{}, Ledger1, Vars), + + %% We only care that no rewards are generated when sc_dispute_strategy_version is active. + {Res, M} = normalize_dc_rewards(DCShares, Vars), + ?assertEqual(#{}, M, "no summaries in rewards map"), + ?assertEqual(30000, Res), + test_utils:cleanup_tmp_dir(BaseDir). + dc_rewards_v3_test() -> BaseDir = test_utils:tmp_dir("dc_rewards_v3_test"), Ledger = blockchain_ledger_v1:new(BaseDir), @@ -1783,6 +1849,7 @@ dc_rewards_v3_test() -> securities_percent => 0.34, sc_version => 2, sc_grace_blocks => 5, + sc_dispute_strategy_version => 0, reward_version => 3, oracle_price => 100000000, %% 1 dollar consensus_members => [<<"c">>, <<"d">>] diff --git a/test/blockchain_state_channel_SUITE.erl b/test/blockchain_state_channel_SUITE.erl index 3d68a21e6a..913ff691c3 100644 --- a/test/blockchain_state_channel_SUITE.erl +++ b/test/blockchain_state_channel_SUITE.erl @@ -9,6 +9,7 @@ all/0, test_cases/0, init_per_group/2, end_per_group/2, + init_per_suite/1, end_per_suite/1, init_per_testcase/2, end_per_testcase/2 ]). @@ -26,6 +27,7 @@ open_without_oui_test/1, max_scs_open_test/1, max_scs_open_v2_test/1, + sc_dispute_prevention_test/1, oui_not_found_test/1, unknown_owner_test/1, crash_single_sc_test/1, @@ -70,6 +72,7 @@ test_cases() -> open_without_oui_test, max_scs_open_test, max_scs_open_v2_test, + sc_dispute_prevention_test, oui_not_found_test, unknown_owner_test, crash_single_sc_test, @@ -83,6 +86,9 @@ test_cases() -> %% TEST CASE SETUP %%-------------------------------------------------------------------- +init_per_suite(Config) -> + [{sc_client_transport_handler, blockchain_state_channel_handler} | Config]. + %% NOTE: If you're running individual tests 'sc_client_transport_handler` will be unset. %% Run with --group=(sc_libp2p | sc_grpc) init_per_group(sc_libp2p, Config) -> @@ -90,6 +96,18 @@ init_per_group(sc_libp2p, Config) -> init_per_group(sc_grpc, Config) -> [{sc_client_transport_handler, blockchain_grpc_sc_client_test_handler} | Config]. +debug_modules_for_node(_, _, []) -> + ok; +debug_modules_for_node(Node, Filename, [Module | Rest]) -> + {ok, _} = ct_rpc:call( + Node, + lager, + trace_file, + [Filename, [{module, Module}], debug] + ), + debug_modules_for_node(Node, Filename, Rest). + + init_per_testcase(Test, Config) -> application:ensure_all_started(throttle), application:ensure_all_started(lager), @@ -103,49 +121,23 @@ init_per_testcase(Test, Config) -> [RouterNode, GatewayNode1|_] = Nodes, Dir = os:getenv("SC_DIR", ""), - {ok, _} = ct_rpc:call( - RouterNode, - lager, - trace_file, - [Dir ++ "sc_server.log", [{module, blockchain_state_channels_server}], debug] - ), - {ok, _} = ct_rpc:call( - RouterNode, - lager, - trace_file, - [Dir ++ "sc_server.log", [{module, blockchain_state_channel_handler}], debug] - ), - {ok, _} = ct_rpc:call( - RouterNode, - lager, - trace_file, - [Dir ++ "sc_server.log", [{module, blockchain_state_channel_v1}], debug] - ), - {ok, _} = ct_rpc:call( - RouterNode, - lager, - trace_file, - [Dir ++ "sc_server.log", [{module, blockchain_state_channels_worker}], debug] - ), - {ok, _} = ct_rpc:call( - RouterNode, - lager, - trace_file, - [Dir ++ "sc_server.log", [{module, blockchain_state_channels_cache}], debug] - ), - - {ok, _} = ct_rpc:call( - GatewayNode1, - lager, - trace_file, - [Dir ++ "sc_client.log", [{module, blockchain_state_channels_client}], debug] - ), - {ok, _} = ct_rpc:call( - GatewayNode1, - lager, - trace_file, - [Dir ++ "sc_client.log", [{module, blockchain_state_channel_handler}], debug] - ), + debug_modules_for_node( + RouterNode, + Dir ++ "sc_server.log", + [blockchain_state_channel_v1, + blockchain_state_channels_cache, + blockchain_state_channels_handler, + blockchain_state_channels_server, + blockchain_state_channels_worker, + blockchain_txn_state_channel_close_v1] + ), + debug_modules_for_node( + GatewayNode1, + Dir ++ "sc_client_1.log", + [blockchain_state_channel_v1, + blockchain_state_channels_client, + blockchain_state_channels_handler] + ), %% accumulate the address of each node Addrs = lists:foldl(fun(Node, Acc) -> @@ -183,6 +175,10 @@ init_per_testcase(Test, Config) -> end, [{RouterNode, GatewayNodeAddr}, {GatewayNode, RouterNodeAddr}]) end, 200, 150), + SCDisputeStrat = case Test == sc_dispute_prevention_test of + false -> 0; + true -> 1 + end, DefaultVars = #{num_consensus_members => NumConsensusMembers}, ExtraVars = #{ max_open_sc => 2, @@ -195,7 +191,8 @@ init_per_testcase(Test, Config) -> sc_grace_blocks => 5, dc_payload_size => 24, sc_max_actors => 100, - sc_version => 2 %% we are focring 2 for all test as 1 is just rly old now + sc_version => 2, %% we are focring 2 for all test as 1 is just rly old now + sc_dispute_strategy_version => SCDisputeStrat }, {InitialVars, {master_key, MasterKey}} = blockchain_ct_utils:create_vars(maps:merge(DefaultVars, ExtraVars)), @@ -252,6 +249,9 @@ end_per_testcase(Test, Config) -> end_per_group(_, _Config) -> ok. +end_per_suite(_) -> + ok. + %%-------------------------------------------------------------------- %% TEST CASES %%-------------------------------------------------------------------- @@ -1623,6 +1623,197 @@ max_scs_open_v2_test(Config) -> {ok, _Block31} = ct_rpc:call(RouterNode, test_utils, create_block, [ConsensusMembers, [SignedSCOpenTxn3]]), ok. +sc_dispute_prevention_test(Config) -> + [RouterNode, GatewayNode1, GatewayNode2 |_] = ?config(nodes, Config), + ConsensusMembers = ?config(consensus_members, Config), + + %% NOTE: sc_dispute_strategy_version chain var is toggled for this test in init_per_test_case/2 + + Self = self(), + ok = setup_meck_txn_forwarding(RouterNode, Self), + ok = setup_meck_txn_forwarding(GatewayNode1, Self), + + %% Get router chain, swarm and pubkey_bin + RouterChain = ct_rpc:call(RouterNode, blockchain_worker, blockchain, []), + RouterLedger = blockchain:ledger(RouterChain), + RouterSwarm = ct_rpc:call(RouterNode, blockchain_swarm, swarm, []), + + {ok, RouterPubkey, RouterSigFun, _} = ct_rpc:call(RouterNode, blockchain_swarm, keys, []), + RouterPubkeyBin = libp2p_crypto:pubkey_to_bin(RouterPubkey), + + {ok, Gateway1Pubkey, Gateway1SigFun, _} = ct_rpc:call(GatewayNode1, blockchain_swarm, keys, []), + Gateway1PubkeyBin = libp2p_crypto:pubkey_to_bin(Gateway1Pubkey), + + {ok, Gateway2Pubkey, Gateway2SigFun, _} = ct_rpc:call(GatewayNode2, blockchain_swarm, keys, []), + Gateway2PubkeyBin = libp2p_crypto:pubkey_to_bin(Gateway2Pubkey), + + ct:pal("Pubkeys: ~n~p", + [[ + {routernode, RouterPubkeyBin}, + {gateway_1, Gateway1PubkeyBin}, + {gateway_2, Gateway2PubkeyBin} + ]]), + + %% Create OUI txn + SignedOUITxn = create_oui_txn(1, RouterNode, [], 8), + ct:pal("SignedOUITxn: ~p", [SignedOUITxn]), + + %% =================================================================== + %% - open state channel + + ID1 = crypto:strong_rand_bytes(24), + Nonce1 = 1, + SignedSCOpenTxn1 = create_sc_open_txn(RouterNode, ID1, 12, 1, Nonce1, 99), + + %% Adding block with state channels + {ok, B2} = add_block(RouterNode, RouterChain, ConsensusMembers, [SignedOUITxn, SignedSCOpenTxn1]), + ok = ct_rpc:call(RouterNode, blockchain_gossip_handler, add_block, [B2, RouterChain, Self, RouterSwarm]), + + ok = blockchain_ct_utils:wait_until_height(RouterNode, 2), + + %% sanity check + OpenSCCountForOwner0 = ct_rpc:call(RouterNode, blockchain_ledger_v1, count_open_scs_for_owner, [[ID1], RouterPubkeyBin, RouterLedger]), + ?assertEqual(1, OpenSCCountForOwner0), + + + %% Helpers + AddFakeBlocksFn = + fun(NumBlocks, ExpectedBlock, Nodes) -> + ok = add_and_gossip_fake_blocks(NumBlocks, ConsensusMembers, RouterNode, RouterSwarm, RouterChain, Self), + lists:foreach(fun(Node) -> + ok = blockchain_ct_utils:wait_until_height(Node, ExpectedBlock) + end, Nodes) + end, + + SendPacketsFn = fun(NumPackets, Gateway) -> + lists:foreach( + fun(_) -> + DevNonce0 = crypto:strong_rand_bytes(2), + Packet0 = blockchain_ct_utils:join_packet(?APPKEY, DevNonce0, 0.0), + ok = ct_rpc:call(Gateway, blockchain_state_channels_client, packet, [Packet0, [], 'US915']) + end, + lists:seq(1, NumPackets) + ) + end, + + %% Wait until Gateways have gotten blocks with OUI txn to send packets + AddFakeBlocksFn(3, 5, [RouterNode, GatewayNode1]), + + %% =================================================================== + %% Sending 10 packet from first gateway + SendPacketsFn(20, GatewayNode1), + AddFakeBlocksFn(1, 6, [RouterNode, GatewayNode1]), + + %% Send packets from another gateway + %% Gateway2 needs to be involved state channel to dispute + SendPacketsFn(20, GatewayNode2), + AddFakeBlocksFn(1, 7, [RouterNode, GatewayNode1, GatewayNode2]), + + %% =================================================================== + %% Wait until we can get a state channel with both summaries + %% Failures to dial during this test can cause failures here + ok = test_utils:wait_until( + fun() -> + case get_active_state_channel(RouterNode, ID1) of + worker_not_started -> {false, worker_not_started}; + SC -> + case length(blockchain_state_channel_v1:summaries(SC)) of + 2 -> true; + C -> {false, summary_count, C} + end + end + end, 100, 100), + + SC0 = get_active_state_channel(RouterNode, ID1), + ct:pal("Routernode SC: ~p", [lager:pr(SC0, blockchain_state_channel_v1)]), + + %% =================================================================== + %% Let the state channel expire and add to the chain + AddFakeBlocksFn(8, 15, [RouterNode]), + + %% Adding the close txn to the chain + receive + {txn, Txn} -> + %% routernode closing the state channel + {ok, B18} = ct_rpc:call(RouterNode, test_utils, create_block, [ConsensusMembers, [Txn], #{}, false]), + ok = ct_rpc:call(RouterNode, blockchain_gossip_handler, add_block, [B18, RouterChain, Self, RouterSwarm]) + after 10000 -> + ct:fail("close txn timeout") + end, + + %% =================================================================== + %% We've added the state channel to the chain. Rewards should be able to be + %% determined, and there should be some for the gateways involved. + + ok = blockchain_ct_utils:wait_until_height(RouterNode, 16), + + %% REVIEW: How can I assert something here about the rewards? + %% Nothing has been disputed yet. + {ok, Rewards1} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 16, RouterChain]), + ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), + ct:pal("potential Rewards: ~p", [lager:pr(Rewards1, blockchain_txn_rewards_v2)]), + + %% =================================================================== + %% Make two disputes that are both valid before they are submitted + + {SC1, true} = blockchain_state_channel_v1:update_summary_for( + Gateway2PubkeyBin, + blockchain_state_channel_summary_v1:new(Gateway2PubkeyBin, 11, 22), + SC0, + 90), + + SignedSC1 = blockchain_state_channel_v1:sign(SC1, RouterSigFun), + Dispute1 = blockchain_txn_state_channel_close_v1:new(SC0, SignedSC1, Gateway1PubkeyBin), + SignedTxn1 = blockchain_txn_state_channel_close_v1:sign(Dispute1, Gateway1SigFun), + + %% ---- + {SC2, true} = blockchain_state_channel_v1:update_summary_for( + Gateway2PubkeyBin, + blockchain_state_channel_summary_v1:new(Gateway2PubkeyBin, 22, 33), + SC0, + 90), + + SignedSC2 = blockchain_state_channel_v1:sign(SC2, RouterSigFun), + Dispute2 = blockchain_txn_state_channel_close_v1:new(SC0, SignedSC2, Gateway2PubkeyBin), + SignedTxn2 = blockchain_txn_state_channel_close_v1:sign(Dispute2, Gateway2SigFun), + + Res1 = ct_rpc:call(RouterNode, blockchain_txn_state_channel_close_v1, is_valid, [SignedTxn1, RouterChain]), + ?assertEqual(ok, Res1, "Our first dispute close is valid"), + + Res2 = ct_rpc:call(RouterNode, blockchain_txn_state_channel_close_v1, is_valid, [SignedTxn2, RouterChain]), + ?assertEqual(ok, Res2, "Our second dispute close is valid"), + + %% Should not be able to create a block with more than 1 dispute + %% REVIEW: Even with sc_dispute_prevention `false' this line passes + {error, {invalid_txns, [_]}} = add_block(RouterNode, RouterChain, ConsensusMembers, [SignedTxn1, SignedTxn1]), + + %% =================================================================== + %% Submit one fo the close txns to put SC0 in dispute + {ok, B3} = add_block(RouterNode, RouterChain, ConsensusMembers, [SignedTxn1]), + ok = ct_rpc:call(RouterNode, blockchain_gossip_handler, add_block, [B3, RouterChain, Self, RouterSwarm]), + + %% wait until this block has made it everywhere + AddFakeBlocksFn(1, 18, [RouterNode, GatewayNode1, GatewayNode1]), + + %% REVIEW: How can I assert something here about the rewards? + {ok, Rewards2} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 18, RouterChain]), + ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), + ct:pal("disputed Rewards: ~p", [lager:pr(Rewards2, blockchain_txn_rewards_v2)]), + + %% Check that the state that was first closed by routernode, is in dispute + {ok, LedgerSC} = ct_rpc:call(RouterNode, blockchain_ledger_v1, find_state_channel, [ID1, RouterPubkeyBin, RouterLedger]), + ct:pal("Ledger SC: ~p", [lager:pr(LedgerSC, ledger_state_channel_v2)]), + ?assertEqual(dispute, blockchain_ledger_state_channel_v2:close_state(LedgerSC)), + + %% =================================================================== + %% The unsubmitted close dispute is no longer valid + Res3 = ct_rpc:call(RouterNode, blockchain_txn_state_channel_close_v1, is_valid, [SignedTxn2, RouterChain]), + ct:pal("Trying to create block with bad txn: ~p", [Res3]), + ?assertEqual({error, already_disputed}, Res3, "Our second dispute close is not valid"), + + ok. + + oui_not_found_test(Config) -> [RouterNode |_] = ?config(nodes, Config), ConsensusMembers = ?config(consensus_members, Config), @@ -2326,6 +2517,9 @@ get_consensus_members(Config, ConsensusAddrs) -> end end, [], Nodes)). + +create_oui_txn(OUI, RouterNode, [], SubnetSize) -> + create_oui_txn(OUI, RouterNode, [{16#deadbeef, 16#deadc0de}], SubnetSize); create_oui_txn(OUI, RouterNode, EUIs, SubnetSize) -> {ok, RouterPubkey, RouterSigFun, _} = ct_rpc:call(RouterNode, blockchain_swarm, keys, []), RouterPubkeyBin = libp2p_crypto:pubkey_to_bin(RouterPubkey), @@ -2387,7 +2581,7 @@ add_and_gossip_fake_blocks(NumFakeBlocks, ConsensusMembers, Node, Swarm, Chain, setup_meck_txn_forwarding(Node, From) -> ok = ct_rpc:call(Node, meck_test_util, forward_submit_txn, [From]), - ok = ct_rpc:call(Node, blockchain_txn_mgr, submit, [fake_txn, fun(_, _) -> ok end]), + ok = ct_rpc:call(Node, blockchain_txn_mgr, submit, [fake_txn, fun(_) -> ok end]), receive {txn, fake_txn} -> ct:pal("Got fake_txn test"), diff --git a/test/meck_test_util.erl b/test/meck_test_util.erl index 584c747135..f93c0363a1 100644 --- a/test/meck_test_util.erl +++ b/test/meck_test_util.erl @@ -5,7 +5,7 @@ forward_submit_txn(Pid) -> meck:expect(blockchain_txn_mgr, submit, fun(T, F) -> Pid ! {txn, T}, - F(ok, T), + F(ok), ok end), ok. From a093ef361f7c4aba7e8dfe0653e0d33b1f7d4690 Mon Sep 17 00:00:00 2001 From: Andrew Thompson Date: Mon, 7 Feb 2022 15:26:53 -0800 Subject: [PATCH 2/3] Address some review comments --- test/blockchain_state_channel_SUITE.erl | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/blockchain_state_channel_SUITE.erl b/test/blockchain_state_channel_SUITE.erl index 913ff691c3..4fc96ce5f2 100644 --- a/test/blockchain_state_channel_SUITE.erl +++ b/test/blockchain_state_channel_SUITE.erl @@ -200,13 +200,14 @@ init_per_testcase(Test, Config) -> % Create genesis block GenPaymentTxs = [blockchain_txn_coinbase_v1:new(Addr, Balance) || Addr <- Addrs], GenDCsTxs = [blockchain_txn_dc_coinbase_v1:new(Addr, Balance) || Addr <- Addrs], + GenPriceOracle = blockchain_txn_gen_price_oracle_v1:new(100000000), % 1 dollar GenConsensusGroupTx = blockchain_txn_consensus_group_v1:new(ConsensusAddrs, <<"proof">>, 1, 0), %% Make one consensus member the owner of all gateways GenGwTxns = [blockchain_txn_gen_gateway_v1:new(Addr, hd(ConsensusAddrs), h3:from_geo({37.780586, -122.469470}, 13), 0) || Addr <- Addrs], - Txs = InitialVars ++ GenPaymentTxs ++ GenDCsTxs ++ GenGwTxns ++ [GenConsensusGroupTx], + Txs = InitialVars ++ [GenPriceOracle] ++ GenPaymentTxs ++ GenDCsTxs ++ GenGwTxns ++ [GenConsensusGroupTx], GenesisBlock = blockchain_block:new_genesis_block(Txs), %% tell each node to integrate the genesis block @@ -1749,9 +1750,10 @@ sc_dispute_prevention_test(Config) -> %% REVIEW: How can I assert something here about the rewards? %% Nothing has been disputed yet. - {ok, Rewards1} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 16, RouterChain]), - ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), - ct:pal("potential Rewards: ~p", [lager:pr(Rewards1, blockchain_txn_rewards_v2)]), + %{ok, Rewards1} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 16, RouterChain]), + %?assertNotEqual(#{}, maps:get(dc_rewards, Rewards1)), + %ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), + %ct:pal("potential Rewards: ~p", [lager:pr(Rewards1, blockchain_txn_rewards_v2)]), %% =================================================================== %% Make two disputes that are both valid before they are submitted @@ -1784,8 +1786,7 @@ sc_dispute_prevention_test(Config) -> ?assertEqual(ok, Res2, "Our second dispute close is valid"), %% Should not be able to create a block with more than 1 dispute - %% REVIEW: Even with sc_dispute_prevention `false' this line passes - {error, {invalid_txns, [_]}} = add_block(RouterNode, RouterChain, ConsensusMembers, [SignedTxn1, SignedTxn1]), + {error, {invalid_txns, [_]}} = add_block(RouterNode, RouterChain, ConsensusMembers, [SignedTxn1, SignedTxn2]), %% =================================================================== %% Submit one fo the close txns to put SC0 in dispute @@ -1793,10 +1794,11 @@ sc_dispute_prevention_test(Config) -> ok = ct_rpc:call(RouterNode, blockchain_gossip_handler, add_block, [B3, RouterChain, Self, RouterSwarm]), %% wait until this block has made it everywhere - AddFakeBlocksFn(1, 18, [RouterNode, GatewayNode1, GatewayNode1]), + AddFakeBlocksFn(3, 20, [RouterNode, GatewayNode1, GatewayNode1]), - %% REVIEW: How can I assert something here about the rewards? - {ok, Rewards2} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 18, RouterChain]), + {ok, Rewards2} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 20, RouterChain]), + %% there should be no rewards here + ?assertEqual(#{}, maps:get(dc_rewards, Rewards2)), ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), ct:pal("disputed Rewards: ~p", [lager:pr(Rewards2, blockchain_txn_rewards_v2)]), From 4adbe67731fa5347a8902a6b60735f8c410a8267 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 7 Feb 2022 17:10:32 -0700 Subject: [PATCH 3/3] move rewards check a couple blocks later is_valid check has to be done within the grace period, rewards can't be calculated until after the grace period. having the rewards check before will cause the test to always fail on the is_valid check with a cannot_expire result. --- test/blockchain_state_channel_SUITE.erl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/blockchain_state_channel_SUITE.erl b/test/blockchain_state_channel_SUITE.erl index 4fc96ce5f2..04185df51f 100644 --- a/test/blockchain_state_channel_SUITE.erl +++ b/test/blockchain_state_channel_SUITE.erl @@ -1794,13 +1794,7 @@ sc_dispute_prevention_test(Config) -> ok = ct_rpc:call(RouterNode, blockchain_gossip_handler, add_block, [B3, RouterChain, Self, RouterSwarm]), %% wait until this block has made it everywhere - AddFakeBlocksFn(3, 20, [RouterNode, GatewayNode1, GatewayNode1]), - - {ok, Rewards2} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 20, RouterChain]), - %% there should be no rewards here - ?assertEqual(#{}, maps:get(dc_rewards, Rewards2)), - ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), - ct:pal("disputed Rewards: ~p", [lager:pr(Rewards2, blockchain_txn_rewards_v2)]), + AddFakeBlocksFn(1, 18, [RouterNode, GatewayNode1, GatewayNode1]), %% Check that the state that was first closed by routernode, is in dispute {ok, LedgerSC} = ct_rpc:call(RouterNode, blockchain_ledger_v1, find_state_channel, [ID1, RouterPubkeyBin, RouterLedger]), @@ -1813,6 +1807,16 @@ sc_dispute_prevention_test(Config) -> ct:pal("Trying to create block with bad txn: ~p", [Res3]), ?assertEqual({error, already_disputed}, Res3, "Our second dispute close is not valid"), + %% =================================================================== + %% Move past the grace period and check that the no rewards are generated + AddFakeBlocksFn(2, 20, [RouterNode, GatewayNode1, GatewayNode2]), + {ok, Rewards2} = ct_rpc:call(RouterNode, blockchain_txn_rewards_v2, calculate_rewards_metadata, [5, 20, RouterChain]), + %% there should be no rewards here + ?assertEqual(#{}, maps:get(dc_rewards, Rewards2)), + ct:pal("PubkeyBins: ~n~p", [[{routernode, RouterPubkeyBin}, {gateway_1, Gateway1PubkeyBin}, {gateway_2, Gateway2PubkeyBin}]]), + ct:pal("disputed Rewards: ~p", [lager:pr(Rewards2, blockchain_txn_rewards_v2)]), + + ok.