Skip to content

Commit

Permalink
Bitcoin parent chain hyperchain connector (#4124)
Browse files Browse the repository at this point in the history
* Bitcoin parent chain hyperchain connector

* Made Fee and Amount configurable for commitment Tx

* Added Dogecoin parent support

* Abstract encoding of commitment to one place

* Switch to secure and anonymous Commitment format
  • Loading branch information
seanhinde committed May 26, 2023
1 parent 8fe1aa1 commit ce4c347
Show file tree
Hide file tree
Showing 16 changed files with 2,080 additions and 467 deletions.
52 changes: 30 additions & 22 deletions apps/aecontract/test/aecontract_staking_contract_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@

-define(GENESIS_HEIGHT, 5).

-define(NETWORK_ID, <<"hc_stake_test">>).

-define(ALICE, {
<<177,181,119,188,211,39,203,57,229,94,108,2,107,214, 167,74,27,
53,222,108,6,80,196,174,81,239,171,117,158,65,91,102>>,
Expand Down Expand Up @@ -1867,7 +1869,7 @@ entropy_impacts_leader_election(_Config) ->
{ok, Trees3, {tuple, {}}} = set_validator_online_(Bob, TxEnv, Trees2),
Entropy1 = hash($A),
TopHash = aetx_env:key_hash(TxEnv),
Commitments = commitments(#{TopHash => [pubkey(?ALICE), pubkey(?BOB)]}),
Commitments = commitments([{TopHash, ?ALICE}, {TopHash, ?BOB}]),
{ok, Trees4, {tuple, {{address, Bob}, _}}} = hc_elect_(Entropy1, Commitments, ?OWNER_PUBKEY, TxEnv, Trees3),
{ok, _, {address, Bob}} = leader_(?OWNER_PUBKEY, TxEnv, Trees4),
%% same context, different entropy leads to different leader
Expand Down Expand Up @@ -1897,19 +1899,19 @@ commitments_determine_who_participates(_Config) ->
{ok, Trees3, {tuple, {}}} = set_validator_online_(Bob, TxEnv, Trees2),
TopHash = aetx_env:key_hash(TxEnv),
Test =
fun(Pubkey) ->
fun({Pubkey, PrivKey}) ->
lists:foreach(
fun(Char) ->
Entropy = hash(Char),
Commitments = commitments(#{TopHash => [Pubkey]}),
Commitments = commitments([{TopHash, {Pubkey, PrivKey}}]),
{ok, Trees4, {tuple, {{address, Pubkey}, _}}} = hc_elect_(Entropy, Commitments, ?OWNER_PUBKEY, TxEnv, Trees3),
{ok, _, {address, Pubkey}} = leader_(?OWNER_PUBKEY, TxEnv, Trees4),
ok
end,
lists:seq(65, 122)) %% A to z
end,
Test(pubkey(?ALICE)),
Test(pubkey(?BOB)),
Test(?ALICE),
Test(?BOB),
ok.

added_stake_power(_Config) ->
Expand All @@ -1934,12 +1936,12 @@ added_stake_power(_Config) ->
{ok, Trees3, {tuple, {}}} = set_validator_online_(Bob, TxEnv, Trees2),
TopHash = aetx_env:key_hash(TxEnv),
TestOnlyOneCommiter =
fun(Pubkey) ->
fun({Pubkey, PrivKey}) ->
lists:foreach(
fun(Char) ->
Entropy = hash(Char),
TopHash = aetx_env:key_hash(TxEnv),
Commitments = commitments(#{TopHash => [Pubkey]}),
Commitments = commitments([{TopHash, {Pubkey, PrivKey}}]),
Expected = [{tuple, {{address, Bob}, BobAmt}}, {tuple, {{address, Alice}, AliceAmt}}],
{ok, _, Expected} = sorted_validators_(Alice, TxEnv, Trees3),
{ok, Trees4, {tuple, {{address, Pubkey}, StakePower}}} = hc_elect_(Entropy, Commitments, ?OWNER_PUBKEY, TxEnv, Trees3),
Expand All @@ -1954,14 +1956,14 @@ added_stake_power(_Config) ->
end,
lists:seq(65, 122)) %% A to z
end,
TestOnlyOneCommiter(pubkey(?ALICE)),
TestOnlyOneCommiter(pubkey(?BOB)),
TestOnlyOneCommiter(?ALICE),
TestOnlyOneCommiter(?BOB),
%% test two commiters
lists:foreach(
fun(Char) ->
Entropy = hash(Char),
TopHash = aetx_env:key_hash(TxEnv),
Commitments = commitments(#{TopHash => [Alice, Bob]}),
Commitments = commitments([{TopHash, ?ALICE}, {TopHash, ?BOB}]),
Expected = [{tuple, {{address, Bob}, BobAmt}}, {tuple, {{address, Alice}, AliceAmt}}],
{ok, _, Expected} = sorted_validators_(Alice, TxEnv, Trees3),
{ok, Trees4, {tuple, {{address, Pubkey}, StakePower}}} = hc_elect_(Entropy, Commitments, ?OWNER_PUBKEY, TxEnv, Trees3),
Expand Down Expand Up @@ -2200,9 +2202,11 @@ elect_(Caller, TxEnv, Trees0) ->

hc_elect_(Entropy, Commitments, Caller, TxEnv, Trees0) ->
ContractPubkey = election_contract_address(),
NetworkId = aec_parent_chain_block:encode_network_id(?NETWORK_ID),
{ok, CallData} = aeb_fate_abi:create_calldata("elect",
[aefa_fate_code:encode_arg({string, Entropy}),
Commitments]),
Commitments,
aefa_fate_code:encode_arg({bytes, NetworkId})]),
call_contract(ContractPubkey, Caller, CallData, 0, TxEnv, Trees0).

leader_(Caller, TxEnv, Trees0) ->
Expand Down Expand Up @@ -2341,19 +2345,23 @@ assert_equal_states(State1, State2) ->
?assertEqual(Shares1, Shares2),
ok.

commitments(CommitmentsMap) ->
commitments(CommitmentsList) ->
Commitments =
maps:fold(
fun(Commitment, Froms, Accum) ->
Froms1 =
lists:map(
fun(F) -> aefa_fate_code:encode_arg({address, F}) end,
Froms),
maps:put(aefa_fate_code:encode_arg({hash, Commitment}), Froms1, Accum)
lists:map(
fun({TopHash, {PubKey, PrivKey}}) ->
StakerPubKeyFate = aeb_fate_encoding:serialize(aeb_fate_data:make_address(PubKey)),
<<StakerHash:8/binary, _/binary>> = aec_hash:sha256_hash(StakerPubKeyFate),
<<TopKeyHash:7/binary, _/binary>> = aec_hash:sha256_hash(TopHash),
NetworkId = aec_parent_chain_block:encode_network_id(?NETWORK_ID),
Msg = aec_hash:sha256_hash(<<TopHash/binary, NetworkId/binary>>),
<<Signature:64/binary>> = enacl:sign_detached(Msg, PrivKey),
Sig = aefa_fate_code:encode_arg({signature, Signature}),
Staker = aefa_fate_code:encode_arg({bytes, StakerHash}),
TopHashEnc = aefa_fate_code:encode_arg({bytes, TopKeyHash}),
aeb_fate_data:make_tuple({Sig, Staker, TopHashEnc})
end,
#{},
CommitmentsMap),
aeb_fate_data:make_map(Commitments).
CommitmentsList),
aeb_fate_data:make_list(Commitments).

hash(Str) when is_list(Str), length(Str) =:= 32 ->
list_to_binary(Str);
Expand Down
151 changes: 103 additions & 48 deletions apps/aecore/src/aec_consensus_hc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -92,40 +92,16 @@ start(Config, #{block_production := BlockProduction}) ->
<<"consensus">> :=
#{ <<"type">> := PCType,
<<"network_id">> := NetworkId,
<<"spend_address">> := PCSpendAddress
<<"spend_address">> := PCSpendAddress,
<<"fee">> := Fee,
<<"amount">> := Amount
},
<<"polling">> :=
#{ <<"fetch_interval">> := FetchInterval,
<<"nodes">> := Nodes0
} = Polling
}} = Config,
CacheSize = maps:get(<<"cache_size">>, Polling, 200),
Stakers =
lists:map(
fun(#{<<"hyper_chain_account">> := #{<<"pub">> := EncodedPubkey,
<<"priv">> := EncodedPrivkey},
<<"parent_chain_account">> := #{<<"pub">> := _EncodedPubkey,
<<"priv">> := _EncodedPrivkey}
}) ->
{ok, Pubkey} = aeser_api_encoder:safe_decode(account_pubkey,
EncodedPubkey),
Privkey = aeu_hex:hex_to_bin(EncodedPrivkey),
case aec_keys:check_sign_keys(Pubkey, Privkey) of
true -> pass;
false -> throw({error, invalid_staker_pair, {EncodedPubkey, EncodedPrivkey}})
end,
{Pubkey, Privkey}
end,
StakersEncoded),
StakersMap = maps:from_list(Stakers),
%% TODO: ditch this after we move beyond OTP24
_Mod = aec_preset_keys,
start_dependency(aec_preset_keys, [StakersMap]),
lager:debug("Stakers: ~p", [StakersMap]),
ParentConnMod =
case PCType of
<<"AE2AE">> -> aehttpc_aeternity
end,
ParentHosts =
lists:map(
fun(#{<<"host">> := Host,
Expand All @@ -139,15 +115,84 @@ start(Config, #{block_production := BlockProduction}) ->
password => Pass}
end,
Nodes0),
SignModule = get_sign_module(),
{ok, PCSpendPubkey} = aeser_api_encoder:safe_decode(account_pubkey, PCSpendAddress),
{ParentConnMod, PCSpendPubkey, HCPCPairs, SignModule} =
case PCType of
<<"AE2AE">> -> start_ae(StakersEncoded, PCSpendAddress);
<<"AE2BTC">> -> start_btc(StakersEncoded, PCSpendAddress, aehttpc_btc);
<<"AE2DOGE">> -> start_btc(StakersEncoded, PCSpendAddress, aehttpc_doge)
end,
start_dependency(aec_parent_connector, [ParentConnMod, FetchInterval,
ParentHosts, NetworkId,
SignModule, PCSpendPubkey]),
SignModule, HCPCPairs, PCSpendPubkey, Fee, Amount]),
start_dependency(aec_parent_chain_cache, [StartHeight, CacheSize,
Confirmations, BlockProduction]),
ok.

start_btc(StakersEncoded, PCSpendAddress, ParentConnMod) ->
Stakers =
lists:map(
fun(#{<<"hyper_chain_account">> := #{<<"pub">> := EncodedPubkey,
<<"priv">> := EncodedPrivkey}
}) ->
{HCPubkey, HCPrivkey} = validate_keypair(EncodedPubkey, EncodedPrivkey),
{HCPubkey, HCPrivkey}
end,
StakersEncoded),
StakersMap = maps:from_list(Stakers),
start_dependency(aec_preset_keys, [StakersMap]),
HCPCPairs = lists:map(
fun(#{<<"hyper_chain_account">> := #{<<"pub">> := EncodedPubkey},
<<"parent_chain_account">> := #{<<"pub">> := BTCPubkey}
}) ->
{ok, HCPubkey} = aeser_api_encoder:safe_decode(account_pubkey,
EncodedPubkey),
{HCPubkey, BTCPubkey}
end,
StakersEncoded),
SignModule = undefined,
{ParentConnMod, PCSpendAddress, HCPCPairs, SignModule}.

start_ae(StakersEncoded, PCSpendAddress) ->
Stakers =
lists:flatmap(
fun(#{<<"hyper_chain_account">> := #{<<"pub">> := HCEncodedPubkey,
<<"priv">> := HCEncodedPrivkey},
<<"parent_chain_account">> := #{<<"pub">> := PCEncodedPubkey,
<<"priv">> := PCEncodedPrivkey}
}) ->
{HCPubkey, HCPrivkey} = validate_keypair(HCEncodedPubkey, HCEncodedPrivkey),
{PCPubkey, PCPrivkey} = validate_keypair(PCEncodedPubkey, PCEncodedPrivkey),
[{HCPubkey, HCPrivkey}, {PCPubkey, PCPrivkey}]
end,
StakersEncoded),
StakersMap = maps:from_list(Stakers),
%% TODO: ditch this after we move beyond OTP24
_Mod = aec_preset_keys,
start_dependency(aec_preset_keys, [StakersMap]),
HCPCPairs = lists:map(
fun(#{<<"hyper_chain_account">> := #{<<"pub">> := HCEncodedPubkey},
<<"parent_chain_account">> := #{<<"pub">> := PCEncodedPubkey}
}) ->
{ok, HCPubkey} = aeser_api_encoder:safe_decode(account_pubkey,
HCEncodedPubkey),
{HCPubkey, PCEncodedPubkey}
end,
StakersEncoded),
ParentConnMod = aehttpc_aeternity,
SignModule = get_sign_module(),
{ok, PCSpendPubkey} = aeser_api_encoder:safe_decode(account_pubkey, PCSpendAddress),
{ParentConnMod, PCSpendPubkey, HCPCPairs, SignModule}.

validate_keypair(EncodedPubkey, EncodedPrivkey) ->
{ok, Pubkey} = aeser_api_encoder:safe_decode(account_pubkey,
EncodedPubkey),
Privkey = aeu_hex:hex_to_bin(EncodedPrivkey),
case aec_keys:check_sign_keys(Pubkey, Privkey) of
true -> pass;
false -> throw({error, invalid_staker_pair, {EncodedPubkey, EncodedPrivkey}})
end,
{Pubkey, Privkey}.

start_dependency(Mod, Args) ->
%% TODO: ditch this after we move beyond OTP24
OldSpec =
Expand Down Expand Up @@ -200,10 +245,12 @@ state_pre_transform_key_node(_Node, Trees) ->
aec_conductor:throw_error({not_enough_confirmations, aec_parent_chain_block:height(Block)});
{ok, Block} ->
Entropy = aec_parent_chain_block:hash(Block),
CommitmentsSophia = encode_commtiments(Block),
CommitmentsSophia = encode_commitments(Block),
NetworkId = aec_parent_chain_block:encode_network_id(aec_governance:get_network_id()),
{ok, CD} = aeb_fate_abi:create_calldata("elect",
[aefa_fate_code:encode_arg({string, Entropy}),
CommitmentsSophia
CommitmentsSophia,
aefa_fate_code:encode_arg({bytes, NetworkId})
]),
CallData = aeser_api_encoder:encode(contract_bytearray, CD),
case call_consensus_contract_(?ELECTION_CONTRACT, TxEnv, Trees, CallData, "elect", 0) of
Expand Down Expand Up @@ -318,11 +365,13 @@ generate_key_header_seal(_, Candidate, PCHeight, #{expected_key_block_rate := _E
case aec_parent_chain_cache:get_block_by_height(PCHeight) of
{ok, Block} ->
Entropy = aec_parent_chain_block:hash(Block),
CommitmentsSophia = encode_commtiments(Block),
CommitmentsSophia = encode_commitments(Block),
NetworkId = aec_parent_chain_block:encode_network_id(aec_governance:get_network_id()),
{TxEnv, Trees} = aetx_env:tx_env_and_trees_from_top(aetx_transaction),
{ok, CD} = aeb_fate_abi:create_calldata("elect_next",
[aefa_fate_code:encode_arg({string, Entropy}),
CommitmentsSophia
CommitmentsSophia,
aefa_fate_code:encode_arg({bytes, NetworkId})
]),
CallData = aeser_api_encoder:encode(contract_bytearray, CD),
{ok, _Trees1, Call} = call_consensus_contract_(?ELECTION_CONTRACT,
Expand Down Expand Up @@ -359,10 +408,12 @@ set_key_block_seal(KeyBlock0, Seal) ->
PCHeight = pc_height(Height),
{ok, Block} = aec_parent_chain_cache:get_block_by_height(PCHeight),
Entropy = aec_parent_chain_block:hash(Block),
CommitmentsSophia = encode_commtiments(Block),
CommitmentsSophia = encode_commitments(Block),
NetworkId = aec_parent_chain_block:encode_network_id(aec_governance:get_network_id()),
{ok, CD} = aeb_fate_abi:create_calldata("elect_next",
[aefa_fate_code:encode_arg({string, Entropy}),
CommitmentsSophia
CommitmentsSophia,
aefa_fate_code:encode_arg({bytes, NetworkId})
]),
CallData = aeser_api_encoder:encode(contract_bytearray, CD),
{ok, _Trees1, Call} = call_consensus_contract_(?ELECTION_CONTRACT,
Expand Down Expand Up @@ -480,7 +531,7 @@ genesis_protocol_version() ->
log_consensus_call(TxEnv, FunName, EncodedCallData, Amount) ->
Height = aetx_env:height(TxEnv),
lager:debug("Height ~p, calling ~s with amount ~p aettos, encoded ~p",
[Height, FunName, Amount, EncodedCallData]),
[Height, FunName, Amount, EncodedCallData]),
ok.

call_consensus_contract(Contract, Node, Trees, EncodedCallData, Keyword, Amount) ->
Expand Down Expand Up @@ -529,6 +580,9 @@ call_consensus_contract_(ContractType, TxEnv, Trees, EncodedCallData, Keyword, A
Calls),
case aect_call:return_type(Call) of
ok -> pass;
revert ->
lager:debug("consensus contract call failed ~s~n", [aect_call:return_value(Call)]),
error({consensus_call_failed, aect_call:return_value(Call)});
error -> error({consensus_call_failed, aect_call:return_value(Call)})
end,
%% prune the call being produced. If not done, the fees for it
Expand Down Expand Up @@ -570,10 +624,12 @@ next_beneficiary() ->
{ok, Block} ->

Entropy = aec_parent_chain_block:hash(Block),
CommitmentsSophia = encode_commtiments(Block),
CommitmentsSophia = encode_commitments(Block),
NetworkId = aec_parent_chain_block:encode_network_id(aec_governance:get_network_id()),
{ok, CD} = aeb_fate_abi:create_calldata("elect_next",
[aefa_fate_code:encode_arg({string, Entropy}),
CommitmentsSophia
CommitmentsSophia,
aefa_fate_code:encode_arg({bytes, NetworkId})
]),
CallData = aeser_api_encoder:encode(contract_bytearray, CD),
{ok, _Trees1, Call} = call_consensus_contract_(?ELECTION_CONTRACT,
Expand Down Expand Up @@ -728,16 +784,15 @@ seal_padding_size() ->
pc_height(ChildHeight) ->
ChildHeight + pc_start_height() - 1.%% child starts pinning from height 1, not genesis

encode_commtiments(Block) ->
encode_commitments(Block) ->
{ok, Commitments} = aec_parent_chain_block:commitments(Block),
Commitments1 =
lists:foldl(
fun({From0, Commitment0}, Accum) ->
{ok, Commitment} = aeser_api_encoder:safe_decode(key_block_hash, Commitment0),
{ok, From1} = aeser_api_encoder:safe_decode(account_pubkey, From0),
From = aefa_fate_code:encode_arg({address, From1}),
maps:update_with(aefa_fate_code:encode_arg({hash, Commitment}), fun(Fs) -> [From | Fs] end, [From], Accum)
lists:map(
fun({Signature, StakerHash, TopKeyHash}) ->
Sig = aefa_fate_code:encode_arg({signature, Signature}),
Staker = aefa_fate_code:encode_arg({bytes, StakerHash}),
TopHash = aefa_fate_code:encode_arg({bytes, TopKeyHash}),
aeb_fate_data:make_tuple({Sig, Staker, TopHash})
end,
#{},
Commitments),
aeb_fate_data:make_map(Commitments1).
aeb_fate_data:make_list(Commitments1).

0 comments on commit ce4c347

Please sign in to comment.