namespace obelisk { using namespace bc; namespace posix_time = boost::posix_time; using posix_time::seconds; using posix_time::microsec_clock; constexpr size_t request_retries = 3; const posix_time::time_duration request_timeout_init = seconds(30); backend_cluster::backend_cluster(threadpool& pool, zmq::context_t& context, const std::string& connection) : context_(context), socket_(context_, ZMQ_DEALER), strand_(pool) { socket_.connect(connection.c_str()); // Configure socket to not wait at close time. int linger = 0; socket_.setsockopt(ZMQ_LINGER, &linger, sizeof (linger)); } void backend_cluster::request( const std::string& command, const data_chunk& data, response_handler handle, const worker_uuid& dest) { request_container request{ microsec_clock::universal_time(), request_timeout_init, request_retries, outgoing_message(dest, command, data)}; auto insert_request = [this, handle, request] { handlers_[request.message.id()] = handle; retry_queue_[request.message.id()] = request; send(request.message); }; strand_.randomly_queue(insert_request); } void backend_cluster::send(const outgoing_message& message) { message.send(socket_); } void backend_cluster::update() { // Poll socket for a reply, with timeout zmq::pollitem_t items[] = { { socket_, 0, ZMQ_POLLIN, 0 } }; zmq::poll(&items[0], 1, 0); // If we got a reply, process it if (items[0].revents & ZMQ_POLLIN) receive_incoming(); // Finally resend any expired requests that we didn't get // a response to yet. strand_.randomly_queue( &backend_cluster::resend_expired, this); } void backend_cluster::append_filter( const std::string& command, response_handler filter) { strand_.randomly_queue( [this, command, filter] { filters_[command] = filter; }); } void backend_cluster::receive_incoming() { incoming_message response; if (!response.recv(socket_)) return; strand_.randomly_queue( &backend_cluster::process, this, response); } void backend_cluster::process(const incoming_message& response) { if (process_filters(response)) return; if (process_as_reply(response)) return; } bool backend_cluster::process_filters(const incoming_message& response) { auto filter_it = filters_.find(response.command()); if (filter_it == filters_.end()) return false; filter_it->second(response.data(), response.origin()); return true; } bool backend_cluster::process_as_reply(const incoming_message& response) { auto handle_it = handlers_.find(response.id()); // Unknown response. Not in our map. if (handle_it == handlers_.end()) return false; handle_it->second(response.data(), response.origin()); handlers_.erase(handle_it); size_t n_erased = retry_queue_.erase(response.id()); BITCOIN_ASSERT(n_erased == 1); return true; } void backend_cluster::resend_expired() { const posix_time::ptime now = microsec_clock::universal_time(); for (auto& retry: retry_queue_) { request_container& request = retry.second; if (now - request.timestamp < request.timeout) continue; if (request.retries_left == 0) { // Unhandled... Appears there's a problem with the server. request.retries_left = request_retries; return; } request.timeout *= 2; --request.retries_left; // Resend request again. request.timestamp = now; send(request.message); } } } // namespace obelisk
namespace obelisk { using namespace bc; using std::placeholders::_1; using std::placeholders::_2; namespace posix_time = boost::posix_time; using posix_time::minutes; using posix_time::second_clock; const posix_time::time_duration sub_expiry = minutes(10); void register_with_node(subscribe_manager& manager, node_impl& node) { auto recv_blk = [&manager](size_t height, const block_type& blk) { const hash_digest blk_hash = hash_block_header(blk.header); for (const transaction_type& tx: blk.transactions) manager.submit(height, blk_hash, tx); }; auto recv_tx = [&manager](const transaction_type& tx) { manager.submit(0, null_hash, tx); }; node.subscribe_blocks(recv_blk); node.subscribe_transactions(recv_tx); } subscribe_manager::subscribe_manager(node_impl& node) : strand_(node.memory_related_threadpool()) { // subscribe to blocks and txs -> submit register_with_node(*this, node); } bool deserialize_address(payment_address& addr, const data_chunk& data) { auto deserial = make_deserializer(data.begin(), data.end()); try { uint8_t version_byte = deserial.read_byte(); short_hash hash = deserial.read_short_hash(); addr.set(version_byte, hash); } catch (end_of_stream) { return false; } if (deserial.iterator() != data.end()) return false; return true; } void subscribe_manager::subscribe( const incoming_message& request, queue_send_callback queue_send) { strand_.queue( &subscribe_manager::do_subscribe, this, request, queue_send); } std::error_code subscribe_manager::add_subscription( const incoming_message& request, queue_send_callback queue_send) { payment_address addr_key; if (!deserialize_address(addr_key, request.data())) { log_warning(LOG_SUBSCRIBER) << "Incorrect format for subscribe data."; return error::bad_stream; } // Now create subscription. const posix_time::ptime now = second_clock::universal_time(); // Limit absolute number of subscriptions to prevent exhaustion attacks. if (subs_.size() >= subscribe_limit_) return error::pool_filled; subs_.emplace(addr_key, subscription{ now + sub_expiry, request.origin(), queue_send}); return std::error_code(); } void subscribe_manager::do_subscribe( const incoming_message& request, queue_send_callback queue_send) { std::error_code ec = add_subscription(request, queue_send); // Send response. data_chunk result(4); auto serial = make_serializer(result.begin()); write_error_code(serial, ec); outgoing_message response(request, result); queue_send(response); } void subscribe_manager::renew( const incoming_message& request, queue_send_callback queue_send) { strand_.randomly_queue( &subscribe_manager::do_renew, this, request, queue_send); } void subscribe_manager::do_renew( const incoming_message& request, queue_send_callback queue_send) { payment_address addr_key; if (!deserialize_address(addr_key, request.data())) { log_warning(LOG_SUBSCRIBER) << "Incorrect format for subscribe renew."; return; } const posix_time::ptime now = second_clock::universal_time(); // Find entry and update expiry_time. auto range = subs_.equal_range(addr_key); for (auto it = range.first; it != range.second; ++it) { subscription& sub = it->second; // Only update subscriptions which were created by // the same client as this request originated from. if (sub.client_origin != request.origin()) continue; // Future expiry time. sub.expiry_time = now + sub_expiry; } // Send response. data_chunk result(4); auto serial = make_serializer(result.begin()); write_error_code(serial, std::error_code()); outgoing_message response(request, result); queue_send(response); } void subscribe_manager::submit( size_t height, const bc::hash_digest& block_hash, const bc::transaction_type& tx) { strand_.queue( &subscribe_manager::do_submit, this, height, block_hash, tx); } void subscribe_manager::do_submit( size_t height, const bc::hash_digest& block_hash, const bc::transaction_type& tx) { for (const transaction_input_type& input: tx.inputs) { payment_address addr; if (!extract(addr, input.script)) continue; post_updates(addr, height, block_hash, tx); } for (const transaction_output_type& output: tx.outputs) { payment_address addr; if (!extract(addr, output.script)) continue; post_updates(addr, height, block_hash, tx); } // Periodicially sweep old expired entries. // Use the block 10 minute window as a periodic trigger. if (height) sweep_expired(); } void subscribe_manager::post_updates(const payment_address& address, size_t height, const bc::hash_digest& block_hash, const bc::transaction_type& tx) { auto range = subs_.equal_range(address); // Avoid expensive serialization if not needed. if (range.first == range.second) return; // [ addr,version ] (1 byte) // [ addr.hash ] (20 bytes) // [ height ] (4 bytes) // [ block_hash ] (32 bytes) // [ tx ] constexpr size_t info_size = 1 + short_hash_size + 4 + hash_size; data_chunk data(info_size + satoshi_raw_size(tx)); auto serial = make_serializer(data.begin()); serial.write_byte(address.version()); serial.write_short_hash(address.hash()); serial.write_4_bytes(height); serial.write_hash(block_hash); BITCOIN_ASSERT(serial.iterator() == data.begin() + info_size); // Now write the tx part. auto rawtx_end_it = satoshi_save(tx, serial.iterator()); BITCOIN_ASSERT(rawtx_end_it == data.end()); // Send the result to everyone interested. for (auto it = range.first; it != range.second; ++it) { const subscription& sub_detail = it->second; outgoing_message update( sub_detail.client_origin, "address.update", data); sub_detail.queue_send(update); } } void subscribe_manager::sweep_expired() { // Delete entries that have expired. const posix_time::ptime now = second_clock::universal_time(); for (auto it = subs_.begin(); it != subs_.end(); ) { const subscription& sub_detail = it->second; // Already expired? If so, then erase. if (sub_detail.expiry_time < now) { log_debug(LOG_SUBSCRIBER) << "Deleting expired subscription: " << it->first.encoded() << " from " << sub_detail.client_origin; it = subs_.erase(it); } else ++it; } } } // namespace obelisk
namespace obelisk { using namespace bc; using std::placeholders::_1; namespace posix_time = boost::posix_time; using posix_time::milliseconds; using posix_time::seconds; using posix_time::microsec_clock; const posix_time::time_duration heartbeat_interval = milliseconds(4000); // Milliseconds constexpr long poll_sleep_interval = 1000; auto now = []() { return microsec_clock::universal_time(); }; send_worker::send_worker(czmqpp::context& context) : context_(context) { } void send_worker::queue_send(const outgoing_message& message) { czmqpp::socket socket(context_, ZMQ_PUSH); BITCOIN_ASSERT(socket.self()); int rc = socket.connect("inproc://trigger-send"); BITCOIN_ASSERT(rc == 0); message.send(socket); socket.destroy(context_); } request_worker::request_worker() : socket_(context_, ZMQ_ROUTER), auth_(context_), wakeup_socket_(context_, ZMQ_PULL), heartbeat_socket_(context_, ZMQ_PUB), sender_(context_) { BITCOIN_ASSERT(socket_.self()); BITCOIN_ASSERT(wakeup_socket_.self()); BITCOIN_ASSERT(heartbeat_socket_.self()); int rc = wakeup_socket_.bind("inproc://trigger-send"); BITCOIN_ASSERT(rc != -1); } bool request_worker::start(config_type& config) { // Load config values. log_requests_ = config.log_requests; if (log_requests_) auth_.set_verbose(true); if (!config.whitelist.empty()) whitelist(config.whitelist); if (config.certificate.empty()) socket_.set_zap_domain("global"); else enable_crypto(config); // Start ZeroMQ sockets. create_new_socket(config); log_debug(LOG_WORKER) << "Heartbeat: " << config.heartbeat; heartbeat_socket_.bind(config.heartbeat); // Timer stuff heartbeat_at_ = now() + heartbeat_interval; return true; } void request_worker::stop() { } void request_worker::whitelist(config_type::ipaddress_list& addrs) { for (const std::string& ip_address: addrs) auth_.allow(ip_address); } void request_worker::enable_crypto(config_type& config) { if (config.client_allowed_certs == "ALLOW_ALL_CERTS") auth_.configure_curve("*", CURVE_ALLOW_ANY); else auth_.configure_curve("*", config.client_allowed_certs); cert_.reset(czmqpp::load_cert(config.certificate)); cert_.apply(socket_); socket_.set_curve_server(1); } void request_worker::create_new_socket(config_type& config) { log_debug(LOG_WORKER) << "Listening: " << config.service; // Set the socket identity name. if (!config.name.empty()) socket_.set_identity(config.name.c_str()); // Connect... socket_.bind(config.service); // Configure socket to not wait at close time socket_.set_linger(0); // Tell queue we're ready for work log_info(LOG_WORKER) << "worker ready"; } void request_worker::attach( const std::string& command, command_handler handler) { handlers_[command] = handler; } void request_worker::update() { poll(); } void request_worker::poll() { // Poll for network updates. czmqpp::poller poller(socket_, wakeup_socket_); BITCOIN_ASSERT(poller.self()); czmqpp::socket which = poller.wait(poll_sleep_interval); BITCOIN_ASSERT(socket_.self() && wakeup_socket_.self()); if (which == socket_) { // Get message // 6-part envelope + content -> request incoming_message request; request.recv(socket_); auto it = handlers_.find(request.command()); // Perform request if found. if (it != handlers_.end()) { if (log_requests_) log_debug(LOG_REQUEST) << request.command() << " from " << request.origin(); it->second(request, std::bind(&send_worker::queue_send, &sender_, _1)); } else { log_warning(LOG_WORKER) << "Unhandled request: " << request.command() << " from " << request.origin(); } } else if (which == wakeup_socket_) { // Send queued message. czmqpp::message message; message.receive(wakeup_socket_); message.send(socket_); } // Publish heartbeat. if (now() > heartbeat_at_) { heartbeat_at_ = now() + heartbeat_interval; log_debug(LOG_WORKER) << "Sending heartbeat"; publish_heartbeat(); } } void request_worker::publish_heartbeat() { static uint32_t counter = 0; czmqpp::message message; data_chunk raw_counter = to_data_chunk(to_little_endian(counter)); message.append(raw_counter); message.send(heartbeat_socket_); ++counter; } } // namespace obelisk
int main() { using namespace boost; using namespace local_time; using namespace gregorian; using posix_time::time_duration; /***** custom_time_zone *****/ // create the dependent objects for a custom_time_zone time_zone_names tzn("Eastern Standard Time", "EST", "Eastern Daylight Time", "EDT"); time_duration utc_offset(-5,0,0); dst_adjustment_offsets adj_offsets(time_duration(1,0,0), time_duration(2,0,0), time_duration(2,0,0)); // rules for this zone are: // start on first Sunday of April at 2 am // end on last Sunday of October at 2 am // so we use a first_last_dst_rule first_day_of_the_week_in_month start_rule(Sunday, Apr); last_day_of_the_week_in_month end_rule(Sunday, Oct); shared_ptr<dst_calc_rule> nyc_rules(new first_last_dst_rule(start_rule, end_rule)); // create more dependent objects for a non-dst custom_time_zone time_zone_names tzn2("Mountain Standard Time", "MST", "", ""); // no dst means empty dst strings time_duration utc_offset2(-7,0,0); dst_adjustment_offsets adj_offsets2(time_duration(0,0,0), time_duration(0,0,0), time_duration(0,0,0)); // no dst means we need a null pointer to the rules shared_ptr<dst_calc_rule> phx_rules; // create the custom_time_zones time_zone_ptr nyc_1(new custom_time_zone(tzn, utc_offset, adj_offsets, nyc_rules)); time_zone_ptr phx_1(new custom_time_zone(tzn2, utc_offset2, adj_offsets2, phx_rules)); /***** posix_time_zone *****/ // create posix_time_zones that are the duplicates of the // custom_time_zones created above. See posix_time_zone documentation // for details on full zone names. std::string nyc_string, phx_string; nyc_string = "EST-05:00:00EDT+01:00:00,M4.1.0/02:00:00,M10.5.0/02:00:00"; // nyc_string = "EST-05EDT,M4.1.0,M10.5.0"; // shorter when defaults used phx_string = "MST-07"; // no-dst time_zone_ptr nyc_2(new posix_time_zone(nyc_string)); time_zone_ptr phx_2(new posix_time_zone(phx_string)); /***** show the sets are equal *****/ std::cout << "The first zone is in daylight savings from:\n " << nyc_1->dst_local_start_time(2004) << " through " << nyc_1->dst_local_end_time(2004) << std::endl; std::cout << "The second zone is in daylight savings from:\n " << nyc_2->dst_local_start_time(2004) << " through " << nyc_2->dst_local_end_time(2004) << std::endl; std::cout << "The third zone (no daylight savings):\n " << phx_1->std_zone_abbrev() << " and " << phx_1->base_utc_offset() << std::endl; std::cout << "The fourth zone (no daylight savings):\n " << phx_2->std_zone_abbrev() << " and " << phx_2->base_utc_offset() << std::endl; return 0; }
namespace obelisk { using namespace bc; using std::placeholders::_1; using std::placeholders::_2; namespace posix_time = boost::posix_time; using posix_time::minutes; using posix_time::second_clock; #define LOG_SUBSCRIBER "subscriber" const posix_time::time_duration sub_renew = minutes(2); subscriber_part::subscriber_part(czmqpp::context& context) : socket_block_(context, ZMQ_SUB), socket_tx_(context, ZMQ_SUB) { } bool subscriber_part::setup_socket( const std::string& connection, czmqpp::socket socket) { if (!socket.connect(connection)) { log_warning(LOG_SUBSCRIBER) << "Subscriber failed to connect: " << connection; return false; } socket.set_subscribe(""); return true; } bool subscriber_part::subscribe_blocks(const std::string& connection, block_notify_callback notify_block) { if (!setup_socket(connection, socket_block_)) return false; notify_block_ = notify_block; return true; } bool subscriber_part::subscribe_transactions(const std::string& connection, transaction_notify_callback notify_tx) { if (!setup_socket(connection, socket_tx_)) return false; notify_tx_ = notify_tx; return true; } void subscriber_part::update() { czmqpp::poller poller; if (socket_tx_.self()) poller.add(socket_tx_); if (socket_block_.self()) poller.add(socket_block_); czmqpp::socket which = poller.wait(0); // Poll socket for a reply, with timeout if (socket_tx_.self() && which == socket_tx_) recv_tx(); if (socket_block_.self() && which == socket_block_) recv_block(); } bool read_hash(hash_digest& hash, const data_chunk& raw_hash) { if (raw_hash.size() != hash.size()) { log_warning(LOG_SUBSCRIBER) << "Wrong size for hash. Dropping."; return false; } std::copy(raw_hash.begin(), raw_hash.end(), hash.begin()); return true; } void subscriber_part::recv_tx() { czmqpp::message message; bool success = message.receive(socket_tx_); BITCOIN_ASSERT(success); // [ tx hash ] // [ raw tx ] const data_stack& parts = message.parts(); if (parts.size() != 2) { log_warning(LOG_SUBSCRIBER) << "Malformed tx response. Dropping."; return; } hash_digest tx_hash; if (!read_hash(tx_hash, parts[0])) return; const data_chunk& raw_tx = parts[1]; transaction_type tx; satoshi_load(raw_tx.begin(), raw_tx.end(), tx); if (hash_transaction(tx) != tx_hash) { log_warning(LOG_SUBSCRIBER) << "Tx hash and actual tx unmatched. Dropping."; return; } // Everything OK! notify_tx_(tx); } void subscriber_part::recv_block() { czmqpp::message message; bool success = message.receive(socket_block_); BITCOIN_ASSERT(success); // [ block hash ] // [ height ] // [ block data ] const data_stack& parts = message.parts(); if (parts.size() != 3) { log_warning(LOG_SUBSCRIBER) << "Malformed block response. Dropping."; return; } hash_digest blk_hash; if (!read_hash(blk_hash, parts[0])) return; uint32_t height = cast_chunk<uint32_t>(parts[1]); const data_chunk& raw_blk = parts[2]; block_type blk; satoshi_load(raw_blk.begin(), raw_blk.end(), blk); if (hash_block_header(blk.header) != blk_hash) { log_warning(LOG_SUBSCRIBER) << "Block hash and actual block unmatched. Dropping."; return; } // Everything OK! notify_block_(height, blk); } address_subscriber::address_subscriber( threadpool& pool, backend_cluster& backend) : backend_(backend), strand_(pool), last_renew_(second_clock::universal_time()) { backend_.append_filter("address.update", strand_.wrap(&address_subscriber::receive_update, this, _1, _2)); } void address_subscriber::subscribe(const payment_address& address, update_handler handle_update, subscribe_handler handle_subscribe) { data_chunk data(1 + short_hash_size); auto serial = make_serializer(data.begin()); serial.write_byte(address.version()); serial.write_short_hash(address.hash()); BITCOIN_ASSERT(serial.iterator() == data.end()); backend_.request("address.subscribe", data, strand_.wrap(&address_subscriber::receive_subscribe_result, this, _1, _2, address, handle_update, handle_subscribe)); } void address_subscriber::receive_subscribe_result( const data_chunk& data, const worker_uuid& worker, const payment_address& address, update_handler handle_update, subscribe_handler handle_subscribe) { // Insert listener into backend. subs_.emplace(address, subscription{worker, handle_update}); // We will periodically send subscription // update messages with the Bitcoin address. // Decode std::error_code indicating success. decode_reply(data, worker, handle_subscribe); } void address_subscriber::decode_reply( const data_chunk& data, const worker_uuid& worker, subscribe_handler handle_subscribe) { std::error_code ec; BITCOIN_ASSERT(data.size() == 4); auto deserial = make_deserializer(data.begin(), data.end()); if (!read_error_code(deserial, data.size(), ec)) return; BITCOIN_ASSERT(deserial.iterator() == data.end()); handle_subscribe(ec, worker); } void address_subscriber::receive_update( const data_chunk& data, const worker_uuid& worker) { // Deserialize data -> address, height, block hash, tx constexpr size_t info_size = 1 + short_hash_size + 4 + hash_digest_size; auto deserial = make_deserializer(data.begin(), data.begin() + info_size); // [ addr,version ] (1 byte) uint8_t version_byte = deserial.read_byte(); // [ addr.hash ] (20 bytes) short_hash addr_hash = deserial.read_short_hash(); payment_address address(version_byte, addr_hash); // [ height ] (4 bytes) uint32_t height = deserial.read_4_bytes(); // [ block_hash ] (32 bytes) const hash_digest blk_hash = deserial.read_hash(); // [ tx ] BITCOIN_ASSERT(deserial.iterator() == data.begin() + info_size); transaction_type tx; satoshi_load(deserial.iterator(), data.end(), tx); post_updates(address, worker, height, blk_hash, tx); } void address_subscriber::post_updates( const bc::payment_address& address, const worker_uuid& worker, size_t height, const bc::hash_digest& blk_hash, const bc::transaction_type& tx) { auto it = subs_.find(address); if (it == subs_.end()) return; const subscription& sub = it->second; if (sub.worker != worker) { log_error(LOG_SUBSCRIBER) << "Server sent update from a different worker than expected."; return; } sub.handle_update(std::error_code(), height, blk_hash, tx); } void address_subscriber::update() { auto renewal_sent = [](const data_chunk&, const worker_uuid&) {}; // Loop through subscriptions, send renew packets. auto send_renew = [this, renewal_sent]( const payment_address& address, const worker_uuid& worker) { data_chunk data(1 + short_hash_size); auto serial = make_serializer(data.begin()); serial.write_byte(address.version()); serial.write_short_hash(address.hash()); BITCOIN_ASSERT(serial.iterator() == data.end()); backend_.request("address.renew", data, renewal_sent, worker); }; auto loop_subs = [this, send_renew] { for (const auto& keyvalue_pair: subs_) send_renew(keyvalue_pair.first, keyvalue_pair.second.worker); }; const posix_time::ptime now = second_clock::universal_time(); // Send renews... if (now - last_renew_ > sub_renew) { strand_.randomly_queue(loop_subs); last_renew_ = now; } } void address_subscriber::fetch_history(const payment_address& address, blockchain::fetch_handler_history handle_fetch, size_t from_height, const worker_uuid& worker) { data_chunk data; wrap_fetch_history_args(data, address, from_height); backend_.request("address.fetch_history", data, std::bind(receive_history_result, _1, handle_fetch), worker); } fullnode_interface::fullnode_interface( threadpool& pool, const std::string& connection, const std::string& cert_filename, const std::string& server_pubkey) : backend_(pool, context_, connection, cert_filename, server_pubkey), blockchain(backend_), transaction_pool(backend_), protocol(backend_), address(pool, backend_), subscriber_(context_) { } void fullnode_interface::update() { backend_.update(); subscriber_.update(); // Address subcomponent. address.update(); } bool fullnode_interface::subscribe_blocks(const std::string& connection, subscriber_part::block_notify_callback notify_block) { return subscriber_.subscribe_blocks(connection, notify_block); } bool fullnode_interface::subscribe_transactions(const std::string& connection, subscriber_part::transaction_notify_callback notify_tx) { return subscriber_.subscribe_transactions(connection, notify_tx); } } // namespace obelisk