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; 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