Current section
Files
Jump to
Current section
Files
src/libp2p_peer.erl
%% @copyright Helium Systems, Inc.
%%
%% @doc A peeer record represents the current state of a peer on the libp2p network.
-module(libp2p_peer).
-include("pb/libp2p_peer_pb.hrl").
-type nat_type() :: libp2p_peer_pb:nat_type().
-type peer_map() :: #{ pubkey_bin => libp2p_crypto:pubkey_bin(),
listen_addrs => [string()],
connected => [binary()],
nat_type => nat_type(),
network_id => binary(),
signed_metadata => #{binary() => binary()}
}.
-type peer() :: #libp2p_signed_peer_pb{}.
-type metadata() :: [{string(), binary()}].
-export_type([peer/0, peer_map/0, nat_type/0]).
-export([from_map/2, encode/2, decode/1, verify/1,
pubkey_bin/1, listen_addrs/1, connected_peers/1, nat_type/1, timestamp/1,
supersedes/2, is_stale/2, network_id/1, network_id_allowable/2]).
%% signed metadata
-export([signed_metadata/1, signed_metadata_get/3]).
%% metadata (unsigned!)
-export([metadata/1, metadata_set/2, metadata_put/3, metadata_get/3]).
%% blacklist (unsigned!)
-export([blacklist/1, is_blacklisted/2,
blacklist_set/2, blacklist_add/2,
cleared_listen_addrs/1]).
%% @doc Create a signed peer from a given map of fields.
-spec from_map(peer_map(), fun((binary()) -> binary())) -> {ok, peer()} | {error, term()}.
from_map(Map, SigFun) ->
Timestamp = case maps:get(timestamp, Map, no_entry) of
no_entry -> erlang:system_time(millisecond);
V -> V
end,
Peer = #libp2p_peer_pb{pubkey=maps:get(pubkey_bin, Map),
listen_addrs=[multiaddr:new(L) || L <- maps:get(listen_addrs, Map)],
connected = maps:get(connected, Map, []),
nat_type=maps:get(nat_type, Map),
network_id=maps:get(network_id, Map, <<>>),
timestamp=Timestamp},
case encode_map(maps:get(signed_metadata, Map, #{})) of
{error, Error} ->
{error, Error};
{ok, MD} ->
sign_peer(Peer#libp2p_peer_pb{signed_metadata = MD}, SigFun)
end.
%% @doc Gets the public key for the given peer.
-spec pubkey_bin(peer()) -> libp2p_crypto:pubkey_bin().
pubkey_bin(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{pubkey=PubKeyBin}}) ->
PubKeyBin.
%% @doc Gets the list of peer multiaddrs that the given peer is
%% listening on.
-spec listen_addrs(peer()) -> [string()].
listen_addrs(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{listen_addrs=Addrs}}) ->
[multiaddr:to_string(A) || A <- Addrs].
%% @doc Gets the list of peer crypto addresses that the given peer was last
%% known to be connected to.
-spec connected_peers(peer()) -> [libp2p_crypto:pubkey_bin()].
connected_peers(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{connected=Conns}}) ->
Conns.
%% @doc Gets the NAT type of the given peer.
-spec nat_type(peer()) -> nat_type().
nat_type(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{nat_type=NatType}}) ->
NatType.
%% @doc Gets the timestamp of the given peer.
-spec timestamp(peer()) -> integer().
timestamp(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{timestamp=Timestamp}}) ->
Timestamp.
%% @doc Gets the signed metadata of the given peer
-spec signed_metadata(peer()) -> map().
signed_metadata(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{signed_metadata=undefined}}) ->
#{};
signed_metadata(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{signed_metadata=MD}}) ->
lists:foldl(fun({K, #libp2p_metadata_value_pb{value = {_Type, V}}}, Acc) ->
maps:put(list_to_binary(K), V, Acc)
end, #{}, MD).
%% @doc Gets a key from the signed metadata of the given peer
-spec signed_metadata_get(peer(), Key::binary(), Default::any()) -> any().
signed_metadata_get(Peer, Key, Default) ->
maps:get(Key, signed_metadata(Peer), Default).
%% @doc Gets the metadata map from the given peer. The metadata for a
%% peer is `NOT' part of the signed peer since it can be read and
%% updated by anyone to annotate the given peer with extra information
-spec metadata(peer()) -> metadata().
metadata(#libp2p_signed_peer_pb{metadata=Metadata}) ->
Metadata.
%% @doc Replaces the full metadata for a given peer
-spec metadata_set(peer(), metadata()) -> {ok, peer()} | {error, term()}.
metadata_set(Peer=#libp2p_signed_peer_pb{}, Metadata) when is_list(Metadata) ->
{ok, Peer#libp2p_signed_peer_pb{metadata=Metadata}}.
%% @doc Updates the metadata for a given peer with the given key/value
%% pair. The `Key' is expected to be a string, while `Value' is
%% expected to be a binary.
-spec metadata_put(peer(), string(), binary()) -> {ok, peer()} | {error, term()}.
metadata_put(Peer=#libp2p_signed_peer_pb{}, Key, Value) when is_list(Key), is_binary(Value) ->
Metadata = lists:keystore(Key, 1, metadata(Peer), {Key, Value}),
metadata_set(Peer, Metadata).
%% @doc Gets the value for a stored `Key' in metadata. If not found,
%% the `Default' is returned.
-spec metadata_get(peer(), Key::string(), Default::any()) -> any().
metadata_get(Peer=#libp2p_signed_peer_pb{}, Key, Default) ->
case lists:keyfind(Key, 1, metadata(Peer)) of
false -> Default;
{_, Value} -> Value
end.
%% @doc Returns whether a given `Target' is more recent than `Other'
-spec supersedes(Target::peer(), Other::peer()) -> boolean().
supersedes(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{timestamp=ThisTimestamp}},
#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{timestamp=OtherTimestamp}}) ->
ThisTimestamp > OtherTimestamp.
%% @doc Returns the declared network id for the peer, if any
-spec network_id(peer()) -> binary() | undefined.
network_id(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{network_id = <<>>}}) ->
undefined;
network_id(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{network_id=ID}}) ->
ID.
%% @doc Returns whether a givne network id is compatible with this peer.
%%
%% A network id is compatible with the network id of this peer if they
%% are equal or if either of them is `undefined'
network_id_allowable(Peer, MyNetworkID) ->
network_id(Peer) == MyNetworkID
orelse libp2p_peer:network_id(Peer) == undefined
orelse MyNetworkID == undefined.
%% @doc Returns whether a given peer is stale relative to a given
%% stale delta time in milliseconds.
-spec is_stale(peer(), integer()) -> boolean().
is_stale(#libp2p_signed_peer_pb{peer=#libp2p_peer_pb{timestamp=Timestamp}}, StaleMS) ->
Now = erlang:system_time(millisecond),
(Timestamp + StaleMS) < Now.
%% @doc Gets the blacklist for this peer. This is a metadata based
%% feature that enables listen addresses to be blacklisted so they
%% will not be connected to until that address is removed from the
%% blacklist.
-spec blacklist(peer()) -> [string()].
blacklist(Peer=#libp2p_signed_peer_pb{}) ->
case metadata_get(Peer, "blacklist", false) of
false -> [];
Bin -> binary_to_term(Bin)
end.
%% @doc Returns whether a given listen address is blacklisted. Note
%% that a blacklisted address may not actually appear in the
%% listen_addrs for this peer.
-spec is_blacklisted(peer(), string()) -> boolean().
is_blacklisted(Peer=#libp2p_signed_peer_pb{}, ListenAddr) ->
lists:member(ListenAddr, blacklist(Peer)).
%% @doc Sets the blacklist for a given peer. Note that currently no
%% validation is done against the existing listen addresses stored in
%% the peer. Blacklisting an address that the peer is not listening to
%% will have no effect anyway.
-spec blacklist_set(peer(), [string()]) -> {ok, peer()} | {error, term()}.
blacklist_set(Peer=#libp2p_signed_peer_pb{}, BlackList) when is_list(BlackList) ->
metadata_put(Peer, "blacklist", term_to_binary(BlackList)).
%% @doc Add a given listen address to the blacklist for the given
%% peer.
-spec blacklist_add(#libp2p_signed_peer_pb{}, ListenAddr::string()) -> {ok, peer()} | {error, term()}.
blacklist_add(Peer=#libp2p_signed_peer_pb{}, ListenAddr) ->
BlackList = blacklist(Peer),
NewBlackList = case lists:member(ListenAddr, BlackList) of
true -> BlackList;
false -> [ListenAddr | BlackList]
end,
blacklist_set(Peer, NewBlackList).
%% @doc Returns the listen addrs for this peer filtered using the
%% blacklist for the peer, if one is present. This is just a
%% convenience function to clear the listen adddresses for a peer
%% with the blacklist stored in metadata.
-spec cleared_listen_addrs(peer()) -> [string()].
cleared_listen_addrs(Peer=#libp2p_signed_peer_pb{}) ->
sets:to_list(sets:subtract(sets:from_list(listen_addrs(Peer)),
sets:from_list(blacklist(Peer)))).
%% @doc Encodes the given peer into its binary form. The peer is
%% stripped from its metadata before encoding if `Strip' is `true'.
-spec encode(peer(), Strip::boolean()) -> binary().
encode(Msg=#libp2p_signed_peer_pb{}, true) ->
{ok, Stripped} = metadata_set(Msg, []),
libp2p_peer_pb:encode_msg(Stripped);
encode(Msg=#libp2p_signed_peer_pb{}, false) ->
libp2p_peer_pb:encode_msg(Msg).
%% @doc Decodes a given binary into a peer. Note that a decoded peer
%% may not verify, so ensure to call `verify' before actually using
%% peer content
-spec decode(binary()) -> {ok, peer()} | {error, term()}.
decode(Bin) ->
{ok, libp2p_peer_pb:decode_msg(Bin, libp2p_signed_peer_pb)}.
%% @doc Cryptographically verifies a given peer and it's
%% associations. Returns true if the given peer can be verified, false
%% otherwise.
-spec verify(peer()) -> boolean().
verify(Msg=#libp2p_signed_peer_pb{peer=Peer0=#libp2p_peer_pb{signed_metadata=MD}, signature=Signature}) ->
Peer = Peer0#libp2p_peer_pb{signed_metadata=lists:usort(MD)},
EncodedPeer = libp2p_peer_pb:encode_msg(Peer),
PubKey = libp2p_crypto:bin_to_pubkey(pubkey_bin(Msg)),
libp2p_crypto:verify(EncodedPeer, Signature, PubKey).
%%
%% Internal
%%
-spec sign_peer(#libp2p_peer_pb{}, libp2p_crypto:sig_fun()) -> {ok, peer()} | {error, term()}.
sign_peer(Peer0 = #libp2p_peer_pb{signed_metadata=MD}, SigFun) ->
Peer = Peer0#libp2p_peer_pb{signed_metadata=lists:usort(MD)},
EncodedPeer = libp2p_peer_pb:encode_msg(Peer),
case SigFun(EncodedPeer) of
{error, Error} ->
{error, Error};
Signature ->
{ok, #libp2p_signed_peer_pb{peer=Peer, signature=Signature}}
end.
encode_map(Map) ->
Encode = fun(_, _, {error, Error}) ->
{error, Error};
(K, V, Acc) when is_binary(K), is_integer(V) ->
[{binary_to_list(K), #libp2p_metadata_value_pb{value = {int, V}}}|Acc];
(K, V, Acc) when is_binary(K), is_float(V) ->
[{binary_to_list(K), #libp2p_metadata_value_pb{value = {flt, V}}}|Acc];
(K, V, Acc) when is_binary(K), is_binary(V) ->
[{binary_to_list(K), #libp2p_metadata_value_pb{value = {bin, V}}}|Acc];
(K, V, Acc) when is_binary(K), (V == true orelse V == false) ->
[{binary_to_list(K), #libp2p_metadata_value_pb{value = {boolean, V}}}|Acc];
(K, V, _Acc) when is_binary(K) ->
{error, {invalid_value, V}};
(K, _V, _Acc) ->
{error, {invalid_key, K}}
end,
case maps:fold(Encode, [], Map) of
{error, Error} -> {error, Error};
List -> {ok, lists:sort(List)}
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
mk_peer(MapOveride) ->
#{public := PubKey, secret := PrivKey} = libp2p_crypto:generate_keys(ecc_compact),
SigFun = libp2p_crypto:mk_sig_fun(PrivKey),
mk_peer(MapOveride, libp2p_crypto:pubkey_to_bin(PubKey), SigFun).
mk_peer(MapOverride, PubKeyBin, SigFun) ->
PeerMap = maps:merge(#{pubkey_bin => PubKeyBin,
listen_addrs => ["/ip4/8.8.8.8/tcp/1234"],
nat_type => static
}, MapOverride),
libp2p_peer:from_map(PeerMap, SigFun).
coding_test() ->
#{public := PubKey2, secret := PrivKey2} = libp2p_crypto:generate_keys(ecc_compact),
SigFun2 = libp2p_crypto:mk_sig_fun(PrivKey2),
{ok, Peer1} = mk_peer(#{connected => [libp2p_crypto:pubkey_to_bin(PubKey2)]}),
{ok, DecodedPeer} = libp2p_peer:decode(libp2p_peer:encode(Peer1, false)),
%% check if decoded is the same as original
?assert(libp2p_peer:pubkey_bin(Peer1) == libp2p_peer:pubkey_bin(DecodedPeer)),
?assert(libp2p_peer:timestamp(Peer1) == libp2p_peer:timestamp(DecodedPeer)),
?assert(libp2p_peer:listen_addrs(Peer1) == libp2p_peer:listen_addrs(DecodedPeer)),
?assert(libp2p_peer:nat_type(Peer1) == libp2p_peer:nat_type(DecodedPeer)),
?assert(libp2p_peer:connected_peers(Peer1) == libp2p_peer:connected_peers(DecodedPeer)),
?assert(libp2p_peer:metadata(Peer1) == libp2p_peer:metadata(DecodedPeer)),
?assert(libp2p_peer:network_id(Peer1) == libp2p_peer:network_id(DecodedPeer)),
?assert(libp2p_peer:signed_metadata(Peer1) == libp2p_peer:signed_metadata(DecodedPeer)),
%% Check signature verify
?assert(libp2p_peer:verify(Peer1)),
%% ensure signing with a different sigfun invalidates the verify
{ok, InvalidPeer} = mk_peer(#{}, libp2p_peer:pubkey_bin(Peer1), SigFun2),
?assert(not libp2p_peer:verify(InvalidPeer)),
%% ensure timestamp override workds
{ok, Peer2} = mk_peer(#{timestamp => 22}),
?assertEqual(22, libp2p_peer:timestamp(Peer2)),
%% Try creating a peer with a sigfun that returns an error
?assertMatch({error, _}, mk_peer(#{}, libp2p_crypto:pubkey_to_bin(PubKey2),
fun(_) -> {error, no_bueno} end)),
ok.
blacklist_test() ->
BlackListAddr = "/ip4/8.8.8.8/tcp/1234",
ListenAddrs = [BlackListAddr, "/ip4/9.9.9.9/tcp/1234"],
{ok, Peer1} = mk_peer(#{listen_addrs => ListenAddrs}),
?assertEqual(ListenAddrs, libp2p_peer:cleared_listen_addrs(Peer1)),
{ok, Peer2} = libp2p_peer:blacklist_add(Peer1, BlackListAddr),
?assertEqual(lists:delete(BlackListAddr, ListenAddrs), libp2p_peer:cleared_listen_addrs(Peer2)),
%% check blacklist membership
?assert(libp2p_peer:is_blacklisted(Peer2, BlackListAddr)),
%% check blacklist
?assertEqual([BlackListAddr], libp2p_peer:blacklist(Peer2)),
%% blacklist is deduped
{ok, Peer3} = libp2p_peer:blacklist_add(Peer2, BlackListAddr),
?assertEqual([BlackListAddr], libp2p_peer:blacklist(Peer3)),
%% Ensure metadata like blacklist is stripped on list encode
{ok, DecodedPeer} = libp2p_peer:decode(libp2p_peer:encode(Peer2, true)),
?assertEqual([], libp2p_peer:blacklist(DecodedPeer)),
ok.
network_id_test() ->
{ok, Peer1} = mk_peer(#{}),
{ok, Peer2} = mk_peer(#{network_id => <<"hello">>}),
?assertEqual(undefined, libp2p_peer:network_id(Peer1)),
?assertEqual(<<"hello">>, libp2p_peer:network_id(Peer2)),
%% undefined network id is always allowed
?assert(libp2p_peer:network_id_allowable(Peer1, undefined)),
?assert(libp2p_peer:network_id_allowable(Peer2, undefined)),
%% a network id is allowed if the peer has undefined or a matching network id
?assert(libp2p_peer:network_id_allowable(Peer1, <<"hello">>)),
?assert(libp2p_peer:network_id_allowable(Peer2, <<"hello">>)),
?assert(not libp2p_peer:network_id_allowable(Peer2, <<"not hello">>)),
ok.
signed_metadata_test() ->
MD = #{ <<"int">> => 22,
<<"double">> => 42.2,
<<"bytes">> => <<"hello">>,
<<"boolean">> => true
},
{ok, Peer1} = mk_peer(#{signed_metadata => MD}),
?assertEqual(MD, libp2p_peer:signed_metadata(Peer1)),
?assertEqual(<<"hello">>, libp2p_peer:signed_metadata_get(Peer1, <<"bytes">>, false)),
?assertEqual(false, libp2p_peer:signed_metadata_get(Peer1, <<"unknown">>, false)),
?assertMatch({error, {invalid_key, _}}, mk_peer(#{signed_metadata => MD#{"foo" => 22}})),
?assertMatch({error, {invalid_value, _}}, mk_peer(#{signed_metadata => MD#{<<"foo">> => foo}})),
ok.
stale_test() ->
{ok, Peer1} = mk_peer(#{}),
%% a peer is stale if it's older than x millis. 0 should really
%% always be stale, a ways from the peer's creation time isn't
?assert(libp2p_peer:is_stale(Peer1, -10)),
?assert(not libp2p_peer:is_stale(Peer1, 10000)),
{ok, Peer2} = mk_peer(#{}),
?assert(libp2p_peer:supersedes(Peer2, Peer1)),
ok.
-endif.