Skip to content

Conversation

Jonny-Ringo
Copy link

@Jonny-Ringo Jonny-Ringo commented Aug 13, 2025

Online Ping Device ([email protected])

Enables HyperBEAM nodes to announce their presence and connectivity to the AO network. Each ping is cryptographically signed and includes metadata and the node URL(if populated in the config) for network discovery and monitoring.

Note for Operators: Built for node discovery and uptime monitoring. Recommend 12-hour intervals to stay listed on explorers like eye-of-ao.ar.io. Longer intervals may cause nodes to fall off uptime listings.

Features

  • Cryptographically Signed - Wallet-signed pings for authenticity
  • Network Discovery - Includes node URL for connectivity mapping
  • GraphQL Indexable - Uses "Online: Yes" tag for easy querying
  • Cron Integration - Works with existing cron device for scheduling
  • Protocol Smart - Auto-handles HTTP/HTTPS URL construction

Quick Start

Set up 12-hour recurring pings:

curl "http://localhost:10000/[email protected]/every?cron-path=/[email protected]/ping_once&interval=12-hours"

Test with manual ping:

curl http://localhost:10000/[email protected]/ping_once

Pings take 10-20 minutes to index before appearing in GraphQL.

Configuration

Add to your config.flat file:

# Simple hostname
host: mynode.example.com
port: 10000
# Result: "Node-URL": "http://mynode.example.com:10000"

# HTTPS domain  
host: https://ao-node.company.com
port: 10000
# Result: "Node-URL": "https://ao-node.company.com"

Message Format

{
  "data": "Node online ping from HyperBEAM",
  "Online": "Yes",
  "Action": "Ping", 
  "Node-URL": "https://ao-node.example.com",
  "Timestamp": "1703123456789"
}

Network Benefits

For Operators: Announce availability, participate in network topology, enable monitoring

For AO Ecosystem: Network discovery, topology mapping, health monitoring, load balancing

GraphQL Example

query {
  transactions(tags: [
    { name: "Online", values: ["Yes"] }
    { name: "Action", values: ["Ping"] }
  ]) {
    edges {
      node {
        id
        owner { address }
        tags { name value }
        block { timestamp }
      }
    }
  }
}

Implementation Notes

  • Uses existing HyperBEAM patterns (cron device, config.flat, wallet signing)
  • Minimal dependencies, leverages current infrastructure
  • Handles error cases gracefully (unknown host, upload failures)

added device to edge branch
#{<<"name">> => <<"[email protected]">>, <<"module">> => dev_wasm}
#{<<"name">> => <<"[email protected]">>, <<"module">> => dev_online_ping}
removed Hanlders from info
info(_) ->
    #{
        exports => [info, ping_once]
    }.
update wallet routing

send_ping(Opts) ->
    ?event({debug_send_ping_start, "Function called"}),
    % Get the node's wallet for signing - fall back to hb:wallet() if not in Opts
    Wallet = case hb_opts:get(priv_wallet, undefined, Opts) of
        undefined -> hb:wallet();
        W -> W
    end,
    % Get the node's address from the wallet
    NodeAddress = hb_util:id(ar_wallet:to_address(Wallet)),
fix send_ping function

%% @doc Send a ping message to the network with the tags.
%% This properly signs the message with the node's wallet before sending.
send_ping(Opts) ->
    ?event({debug_send_ping_start, "Function called"}),
    % Get the node's wallet for signing - fall back to hb:wallet() if not in Opts
        Wallet = case hb_opts:get(priv_wallet, undefined, Opts) of
        undefined -> hb:wallet();
        W -> W
    end,
    % Get the node's address from the wallet
    NodeAddress = hb_util:id(ar_wallet:to_address(Wallet)),
            
            % Get host and port information
            Host = hb_opts:get(host, <<"unknown">>, Opts),
            Port = hb_opts:get(port, 10000, Opts),

            % Build the URL (handling the unknown case and protocol detection)
            NodeUrl = case Host of
                <<"unknown">> ->
                    <<"unknown">>;  % Don't build a URL if host is unknown
                _ ->
                    % Check if host already includes a protocol
                    case binary:match(Host, [<<"://">>]) of
                        nomatch ->
                            % No protocol specified, use http with port
                            iolist_to_binary(io_lib:format("http://~s:~p", [Host, Port]));
                        _ ->
                            % Protocol already specified, use as-is (assume port is included if needed)
                            Host
                    end
            end,

            % Create a ping message wiht node details from config file
            UnsignedPingMessage = #{
                <<"data">> => <<"Node online ping from HyperBEAM">>,
                <<"Online">> => <<"Yes">>,
                <<"Action">> => <<"Ping">>,
                <<"Node-URL">> => NodeUrl,  % Will be "unknown" or actual URL
                <<"Timestamp">> => integer_to_binary(hb:now()),
                <<"codec-device">> => <<"[email protected]">>
            },
            
            try
                ?event({debug_start_of_try_block, "Starting ping process"}),
                % Sign the message with the node's wallet using ans104 commitment device
                CommitmentDevice = hb_opts:get(commitment_device, <<"[email protected]">>, Opts),
                {ok, SignedMessage} = dev_message:commit(
                    UnsignedPingMessage,
                    #{ <<"commitment-device">> => CommitmentDevice },
                    Opts
                ),
                ?event({debug_signed_message, SignedMessage}),

                % Let's see what the conversion produces step by step
                ?event({debug_about_to_convert, "Converting to [email protected]"}),
                Converted = hb_message:convert(SignedMessage, <<"[email protected]">>, Opts),
                ?event({debug_converted_tx, Converted}),
                
                % Check if ar_bundles can verify it before serialization
                case ar_bundles:verify_item(Converted) of
                    true -> 
                        ?event({debug_verify_success, "TX verifies locally"});
                    false -> 
                        ?event({debug_verify_failed, "TX does NOT verify locally"})
                end,
                
                % See the serialization
                Serialized = ar_bundles:serialize(Converted),
                ?event({debug_serialized, {size, byte_size(Serialized)}, {first_100_bytes, binary:part(Serialized, 0, min(100, byte_size(Serialized)))}}),
                % END OF DEBUG LINES

                ?event({online_ping_signed, {node_address, NodeAddress}, {message_id, hb_message:id(SignedMessage, all)}}),
                
                % Log what we're about to upload for debugging (upload directly without codec-device)
                ?event({online_ping_uploading, {message_size, byte_size(term_to_binary(SignedMessage))}, {commitment_device, CommitmentDevice}}),
                
                % Now submit the signed message to the Arweave network (upload directly)
                case hb_client:upload(SignedMessage, Opts) of
                    {ok, UploadResult} ->
                        ?event({online_ping_uploaded, {upload_result, UploadResult}}),
                        {ok, #{
                            <<"message">> => <<"ping_sent_to_network">>,
                            <<"message_id">> => hb_message:id(SignedMessage, all),
                            <<"node_address">> => NodeAddress,
                            <<"commitment_device">> => CommitmentDevice,
                            <<"upload_result">> => UploadResult
                        }};
                    {error, UploadError} ->
                        ?event({online_ping_upload_error, {error, UploadError}, {bundler_response_details, UploadError}}),
                        % Still return success for signing, but note upload failed
                        {ok, #{
                            <<"message">> => <<"ping_signed_but_upload_failed">>,
                            <<"message_id">> => hb_message:id(SignedMessage, all),
                            <<"node_address">> => NodeAddress,
                            <<"commitment_device">> => CommitmentDevice,
                            <<"upload_error">> => UploadError,
                            <<"signed_message">> => SignedMessage
                        }}
                end
            catch
                Class:Reason:Stacktrace ->
                    ?event({online_ping_error, {class, Class}, {reason, Reason}, {stacktrace, Stacktrace}}),
                    {error, #{
                        <<"error">> => <<"Failed to sign ping message">>,
                        <<"class">> => Class,
                        <<"reason">> => Reason
                    }}
            end.
opts to OptsWithWallet
% Always use ans104 commitment for this device
CommitmentDevice = <<"[email protected]">>,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant