src/main/generic/network/Network.js
class Network extends Observable {
/**
* @constructor
* @param {IBlockchain} blockchain
* @param {NetworkConfig} networkConfig
* @param {Time} time
* @listens PeerAddressBook#added
* @listens ConnectionPool#peer-joined
* @listens ConnectionPool#peer-left
* @listens ConnectionPool#peers-changed
* @listens ConnectionPool#recycling-request
* @listens ConnectionPool#connect-error
*/
constructor(blockchain, networkConfig, time) {
super();
/**
* @type {IBlockchain}
* @private
*/
this._blockchain = blockchain;
/**
* @type {NetworkConfig}
* @private
*/
this._networkConfig = networkConfig;
/**
* @type {Time}
* @private
*/
this._time = time;
/**
* Flag indicating whether we should actively connect to other peers
* if our peer count is below PEER_COUNT_DESIRED.
* @type {boolean}
* @private
*/
this._autoConnect = false;
/**
* Backoff for peer count check in seconds.
* @type {number}
* @private
*/
this._backoff = Network.CONNECT_BACKOFF_INITIAL;
/**
* Flag indicating whether we already triggered a backoff.
* @type {boolean}
* @private
*/
this._backedOff = false;
/**
* The network's addressbook
* @type {PeerAddressBook}
* @private
*/
this._addresses = new PeerAddressBook(this._networkConfig);
this._addresses.on('added', () => {
setTimeout(this._checkPeerCount.bind(this), Network.CONNECT_THROTTLE);
});
/**
* Peer connections database & operator
* @type {ConnectionPool}
* @private
*/
this._connections = new ConnectionPool(this._addresses, networkConfig, blockchain, time);
this._connections.on('peer-joined', peer => this._onPeerJoined(peer));
this._connections.on('peer-left', peer => this._onPeerLeft(peer));
this._connections.on('peers-changed', () => this._onPeersChanged());
this._connections.on('recycling-request', () => this._onRecyclingRequest());
this._connections.on('connect-error', () => setTimeout(this._checkPeerCount.bind(this), Network.CONNECT_THROTTLE));
/**
* Helper object to pick addresses from PeerAddressBook.
* @type {PeerScorer}
* @private
*/
this._scorer = new PeerScorer(this._networkConfig, this._addresses, this._connections);
/**
* @type {number|null}
* @private
*/
this._houseKeepingIntervalId = null;
/**
* @type {Timers}
*/
this._timers = new Timers();
}
/**
* @returns {void}
*/
connect() {
this._autoConnect = true;
// Setup housekeeping interval.
this._houseKeepingIntervalId = setInterval(() => this._housekeeping(), Network.HOUSEKEEPING_INTERVAL);
// Start connecting to peers.
this._checkPeerCount();
}
/**
* @param {string|*} reason
* @returns {void}
*/
disconnect(reason) {
this._autoConnect = false;
// Clear housekeeping interval.
clearInterval(this._houseKeepingIntervalId);
this._connections.disconnect(reason);
this._connections.allowInboundConnections = false;
}
// XXX For testing
disconnectWebSocket() {
this._autoConnect = false;
this._connections.disconnectWebSocket();
}
/**
* @param {Peer} peer
* @returns {void}
* @fires Network#peer-joined
* @private
*/
_onPeerJoined(peer) {
// Recalculate the network adjusted offset
this._updateTimeOffset();
this.fire('peer-joined', peer);
}
/**
* @param {Peer} peer
* @returns {void}
* @fires Network#peer-left
* @private
*/
_onPeerLeft(peer) {
// Recalculate the network adjusted offset
this._updateTimeOffset();
this.fire('peer-left', peer);
}
/**
* @returns {void}
* @fires Network#peers-changed
* @private
*/
_onPeersChanged() {
setTimeout(this._checkPeerCount.bind(this), Network.CONNECT_THROTTLE);
this.fire('peers-changed');
}
/**
* @returns {void}
* @private
*/
_onRecyclingRequest() {
this._scorer.recycleConnections(1, CloseType.PEER_CONNECTION_RECYCLED_INBOUND_EXCHANGE, 'Peer connection recycled inbound exchange');
// set ability to exchange for new inbound connections
this._connections.allowInboundExchange = this._scorer.lowestConnectionScore !== null
? this._scorer.lowestConnectionScore < Network.SCORE_INBOUND_EXCHANGE
: false;
}
/**
* @returns {void}
* @private
*/
_checkPeerCount() {
if (this._autoConnect
&& !this._scorer.isGoodPeerSet()
&& this._connections.connectingCount < Network.CONNECTING_COUNT_MAX) {
// Pick a peer address that we are not connected to yet.
const peerAddress = this._scorer.pickAddress();
// We can't connect if we don't know any more addresses or only want connections to good peers.
const onlyGoodPeers = this._scorer.needsGoodPeers() && !this._scorer.needsMorePeers();
if (!peerAddress || onlyGoodPeers && !this._scorer.isGoodPeer(peerAddress)) {
// If no backoff has been triggered, trigger one.
// This helps us to check back whether we need more connections.
if (!this._backedOff) {
this._backedOff = true;
const oldBackoff = this._backoff;
this._backoff = Math.min(Network.CONNECT_BACKOFF_MAX, oldBackoff * 2);
setTimeout(() => {
this._backedOff = false;
this._checkPeerCount();
}, oldBackoff);
if (this._connections.count === 0) {
// We are not connected to any peers (anymore) and don't know any more addresses to connect to.
// Tell listeners that we are disconnected. This is primarily useful for tests.
this.fire('disconnected');
// Allow inbound connections. This is important for the first seed node on the network which
// will never establish a consensus and needs to accept incoming connections eventually.
this._connections.allowInboundConnections = true;
}
}
return;
}
// Connect to this address.
if (!this._connections.connectOutbound(peerAddress)) {
this._addresses.close(null, peerAddress, CloseType.CONNECTION_FAILED);
setTimeout(() => this._checkPeerCount(), Network.CONNECT_THROTTLE);
}
}
this._backoff = Network.CONNECT_BACKOFF_INITIAL;
}
/**
* Updates the network time offset by calculating the median offset
* from all our peers.
* @returns {void}
* @private
*/
_updateTimeOffset() {
const peerConnections = this._connections.values();
const offsets = [0]; // Add our own offset.
peerConnections.forEach(peerConnection => {
if (peerConnection.state === PeerConnectionState.ESTABLISHED) {
offsets.push(peerConnection.networkAgent.peer.timeOffset);
}
});
const offsetsLength = offsets.length;
offsets.sort((a, b) => a - b);
let timeOffset;
if ((offsetsLength % 2) === 0) {
timeOffset = Math.round((offsets[(offsetsLength / 2) - 1] + offsets[offsetsLength / 2]) / 2);
} else {
timeOffset = offsets[(offsetsLength - 1) / 2];
}
this._time.offset = Math.max(Math.min(timeOffset, Network.TIME_OFFSET_MAX), -Network.TIME_OFFSET_MAX);
}
/**
* @returns {void}
* @private
*/
_housekeeping() {
this._scorer.scoreConnections();
// recycle
if (this.peerCount > Network.PEER_COUNT_RECYCLING_ACTIVE) {
// recycle 1% at PEER_COUNT_RECYCLING_ACTIVE, 20% at PEER_COUNT_MAX
const percentageToRecycle = (this.peerCount - Network.PEER_COUNT_RECYCLING_ACTIVE) * 0.19 / (Network.PEER_COUNT_MAX - Network.PEER_COUNT_RECYCLING_ACTIVE) + 0.01;
const connectionsToRecycle = Math.ceil(this.peerCount * percentageToRecycle);
this._scorer.recycleConnections(connectionsToRecycle, CloseType.PEER_CONNECTION_RECYCLED, 'Peer connection recycled');
}
// set ability to exchange for new inbound connections
this._connections.allowInboundExchange = this._scorer.lowestConnectionScore !== null
? this._scorer.lowestConnectionScore < Network.SCORE_INBOUND_EXCHANGE
: false;
// Request fresh addresses.
this._refreshAddresses();
}
_refreshAddresses() {
if (this._scorer.connectionScores && this._scorer.connectionScores.length > 0) {
const cutoff = Math.min(this._connections.peerCountWs * 2, Network.ADDRESS_REQUEST_CUTOFF);
const length = Math.min(this._scorer.connectionScores.length, cutoff);
for (let i = 0; i < Math.min(Network.ADDRESS_REQUEST_PEERS, this._scorer.connectionScores.length); i++) {
const index = Math.floor(Math.random() * length);
const peerConnection = this._scorer.connectionScores[index];
Log.v(Network, () => `Requesting addresses from ${peerConnection.peerAddress} (score idx ${index})`);
peerConnection.networkAgent.requestAddresses();
}
} else {
const index = Math.floor(Math.random() * Math.min(this._connections.count, 10));
/** @type {PeerConnection} */
let peerConnection;
let i = 0;
for (const conn of this._connections.valueIterator()) {
if (conn.state === PeerConnectionState.ESTABLISHED) {
peerConnection = conn;
}
if (i >= index && peerConnection) {
break;
}
i++;
}
if (peerConnection) {
Log.v(Network, () => `Requesting addresses from ${peerConnection.peerAddress} (rand idx ${index})`);
peerConnection.networkAgent.requestAddresses();
}
}
}
/** @type {Time} */
get time() {
return this._time;
}
/** @type {number} */
get peerCount() {
return this._connections.peerCount;
}
/** @type {number} */
get peerCountWebSocket() {
return this._connections.peerCountWs;
}
/** @type {number} */
get peerCountWebRtc() {
return this._connections.peerCountRtc;
}
/** @type {number} */
get peerCountDumb() {
return this._connections.peerCountDumb;
}
/** @type {number} */
get peerCountConnecting() {
return this._connections.connectingCount;
}
/** @type {number} */
get knownAddressesCount() {
return this._addresses.knownAddressesCount;
}
/** @type {number} */
get bytesSent() {
return this._connections.bytesSent;
}
/** @type {number} */
get bytesReceived() {
return this._connections.bytesReceived;
}
/** @type {boolean} */
get allowInboundConnections() {
return this._connections.allowInboundConnections;
}
/** @param {boolean} allowInboundConnections */
set allowInboundConnections(allowInboundConnections) {
this._connections.allowInboundConnections = allowInboundConnections;
}
/** @type {PeerAddressBook} */
get addresses() {
return this._addresses;
}
/** @type {ConnectionPool} */
get connections() {
return this._connections;
}
}
/**
* @type {number}
* @constant
*/
Network.PEER_COUNT_MAX = PlatformUtils.isBrowser() ? 15 : 10000;
/**
* @type {number}
* @constant
*/
Network.INBOUND_PEER_COUNT_PER_SUBNET_MAX = PlatformUtils.isBrowser() ? 2 : 100;
/**
* @type {number}
* @constant
*/
Network.OUTBOUND_PEER_COUNT_PER_SUBNET_MAX = 2;
/**
* @type {number}
* @constant
*/
Network.PEER_COUNT_PER_IP_MAX = PlatformUtils.isBrowser() ? 1 : 20;
/**
* @type {number}
* @constant
*/
Network.IPV4_SUBNET_MASK = 24;
/**
* @type {number}
* @constant
*/
Network.IPV6_SUBNET_MASK = 96;
/**
* @type {number}
* @constant
*/
Network.PEER_COUNT_RECYCLING_ACTIVE = PlatformUtils.isBrowser() ? 5 : 1000;
/**
* @type {number}
* @constant
*/
Network.CONNECTING_COUNT_MAX = 2;
/**
* @type {number}
* @constant
*/
Network.SIGNAL_TTL_INITIAL = 3;
/**
* @type {number}
* @constant
*/
Network.CONNECT_BACKOFF_INITIAL = 2000; // 2 seconds
/**
* @type {number}
* @constant
*/
Network.CONNECT_BACKOFF_MAX = 10 * 60 * 1000; // 10 minutes
/**
* @type {number}
* @constant
*/
Network.TIME_OFFSET_MAX = 15 * 60 * 1000; // 15 minutes
/**
* @type {number}
* @constant
*/
Network.HOUSEKEEPING_INTERVAL = 5 * 60 * 1000; // 5 minutes
/**
* @type {number}
* @constant
*/
Network.SCORE_INBOUND_EXCHANGE = 0.5;
/**
* @type {number}
* @constant
*/
Network.CONNECT_THROTTLE = 1000; // 1 second
Network.ADDRESS_REQUEST_CUTOFF = 250;
Network.ADDRESS_REQUEST_PEERS = 2;
Class.register(Network);