mirror of
https://github.com/yzernik/squeaknode.git
synced 2026-08-13 12:33:25 +02:00
Use http for peer connection (#2002)
* Got basic web server running for peer * Got squeak route and lookup route working * Got secret key route working * Got offer endpoint working * Remove old comment * Move peer server code to server subdir * Create client subdir for peer client * Return empty list for lookup when pubkeys is empty * Add peer client and network controller class * Remove old network code * Remove old block subscription worker * Remove network handler class * Remove more old classes * Add back twitter forwarder worker * Remove download methods from controller * Add websocket endpoint * Add websocket subscribe method * Add websocket client library * Fetch squeaks from peer (slowly) on UI timeline load * Fetch squeaks from peer (slowly) on UI pubkey squeaks load * Got download squeaks working with HTTP requests * Fixed external address method in controller * Remove download pubkey squeak button and action * Remove unused connection displays from peer ana peers page * Fix 404 not found for offer endpoint * Better handling of null cases in peer client downloader * Simplify peer downloader * Create subclass for range download * Got single squeak download working * Fix itest for get external address
This commit is contained in:
parent
51f39427a9
commit
68e69e55c8
50 changed files with 790 additions and 3638 deletions
|
|
@ -744,15 +744,3 @@ export const downloadSqueak = (squeakHash) => {
|
|||
deser: deser,
|
||||
});
|
||||
}
|
||||
|
||||
export const downloadPubkeySqueaks = (pubkey) => {
|
||||
console.log('Calling downloadPubkeySqueaks');
|
||||
const request = new DownloadPubKeySqueaksRequest();
|
||||
request.setPubkey(pubkey);
|
||||
const deser = DownloadPubKeySqueaksReply.deserializeBinary;
|
||||
return baseRequest({
|
||||
url: '/downloadaddresssqueaks',
|
||||
req: request,
|
||||
deser: deser,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import {
|
|||
selectMakeSqueakStatus,
|
||||
selectBuySqueakStatus,
|
||||
selectDownloadSqueakStatus,
|
||||
selectDownloadPubkeySqueakStatus,
|
||||
} from '../../features/squeaks/squeaksSlice'
|
||||
|
||||
const Nav = ({history}) => {
|
||||
|
|
@ -303,18 +302,6 @@ const Nav = ({history}) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block screen with modal when download pubkey squeaks is waiting. */}
|
||||
<div style={{display: useSelector(selectDownloadPubkeySqueakStatus) === 'loading' ? 'block' : 'none'}} className="modal-edit">
|
||||
<div style={{minHeight: '270px', height: 'initial'}} className="modal-content">
|
||||
<div className="modal-header">
|
||||
<p className="modal-title">Downloading pubkey squeaks...</p>
|
||||
</div>
|
||||
<div style={{marginTop:'5px'}} className="modal-body">
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,8 @@ import { useDispatch } from 'react-redux'
|
|||
|
||||
import {
|
||||
fetchPeer,
|
||||
fetchConnectedPeers,
|
||||
selectCurrentPeer,
|
||||
selectCurrentPeerStatus,
|
||||
selectConnectedPeers,
|
||||
selectPeerConnectionByAddress,
|
||||
setConnectPeer,
|
||||
setDisconnectPeer,
|
||||
setSavePeer,
|
||||
setDeletePeer,
|
||||
setPeerAutoconnectEnabled,
|
||||
|
|
@ -43,12 +38,6 @@ const [bannerLoading, setBannerLoading] = useState(false)
|
|||
const [styleBody, setStyleBody] = useState(false)
|
||||
|
||||
const peer = useSelector(selectCurrentPeer);
|
||||
const connectedPeers = useSelector(selectConnectedPeers);
|
||||
const peerConnection = useSelector(state => selectPeerConnectionByAddress(state, {
|
||||
network: props.match.params.network,
|
||||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
}));
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
|
|
@ -59,7 +48,6 @@ useEffect(() => {
|
|||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
}));
|
||||
dispatch(fetchConnectedPeers());
|
||||
}, [])
|
||||
|
||||
const isInitialMount = useRef(true);
|
||||
|
|
@ -73,22 +61,6 @@ useEffect(() => {
|
|||
useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
|
||||
|
||||
|
||||
const connectPeer = (e) => {
|
||||
dispatch(setConnectPeer({
|
||||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
network: props.match.params.network,
|
||||
}));
|
||||
}
|
||||
|
||||
const disconnectPeer = (e) => {
|
||||
dispatch(setDisconnectPeer({
|
||||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
network: props.match.params.network,
|
||||
}));
|
||||
}
|
||||
|
||||
const deletePeer = () => {
|
||||
let values = {
|
||||
peerId: peer.getPeerId(),
|
||||
|
|
@ -187,18 +159,6 @@ return(
|
|||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="profile-options">
|
||||
<div onClick={(e)=>
|
||||
peerConnection ?
|
||||
disconnectPeer(e) :
|
||||
connectPeer(e)
|
||||
}
|
||||
className={peerConnection ? 'disconnect peer-connect-button' : 'peer-connect-button'}>
|
||||
<span><span>{ peerConnection ? 'Connected' : 'Connect'}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-options">
|
||||
{peer &&
|
||||
<div onClick={(e)=>
|
||||
|
|
@ -215,34 +175,20 @@ return(
|
|||
</div>
|
||||
|
||||
<div className="feed-wrapper">
|
||||
{peerConnection &&
|
||||
<div className="feed-trending-card">
|
||||
<div className="feed-card-trend">
|
||||
<div>Connection Time</div>
|
||||
<div>{moment(peerConnection.getConnectTimeS() * 1000).fromNow(true)}</div>
|
||||
<div>Number of downloads</div>
|
||||
<div>TODO</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Bytes received</div>
|
||||
<div>{peerConnection.getNumberBytesReceived()}</div>
|
||||
<div>Number of purchases</div>
|
||||
<div>TODO</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Messages received</div>
|
||||
<div>{peerConnection.getNumberMessagesReceived()}</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Last Message Received Time</div>
|
||||
<div>{moment(peerConnection.getLastMessageReceivedTimeS() * 1000).fromNow(true)}</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Bytes sent</div>
|
||||
<div>{peerConnection.getNumberBytesSent()}</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Messages sent</div>
|
||||
<div>{peerConnection.getNumberMessagesSent()}</div>
|
||||
<div>Last connection time</div>
|
||||
<div>TODO</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,8 @@ import { useSelector } from 'react-redux'
|
|||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import {
|
||||
fetchConnectedPeers,
|
||||
selectConnectedPeers,
|
||||
selectConnectedPeersStatus,
|
||||
selectSavedPeers,
|
||||
fetchSavedPeers,
|
||||
setConnectPeer,
|
||||
setSavePeer,
|
||||
} from '../../features/peers/peersSlice'
|
||||
import {
|
||||
|
|
@ -38,14 +34,12 @@ const Peers = (props) => {
|
|||
|
||||
const externalAddress = useSelector(selectExternalAddress);
|
||||
const peers = useSelector(selectSavedPeers);
|
||||
const connectedPeers = useSelector(selectConnectedPeers);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0)
|
||||
dispatch(fetchExternalAddress());
|
||||
dispatch(fetchConnectedPeers());
|
||||
dispatch(fetchSavedPeers());
|
||||
}, [])
|
||||
|
||||
|
|
@ -61,12 +55,6 @@ const Peers = (props) => {
|
|||
return `${peerAddress.getNetwork()}/${peerAddress.getHost()}:${peerAddress.getPort()}`;
|
||||
}
|
||||
|
||||
const isPeerConnected = (peerAddress) => {
|
||||
const peerAddressStr = peerAddressToStr(peerAddress);
|
||||
const connectedPeerAddresses = connectedPeers.map((p) => peerAddressToStr(p.getPeerAddress()));
|
||||
return connectedPeerAddresses.includes(peerAddressStr);
|
||||
};
|
||||
|
||||
const toggleSavePeerModal = (param, type) => {
|
||||
setStyleBody(!styleBody)
|
||||
setTimeout(()=>{ setSavePeerModalOpen(!savePeerModalOpen) },20)
|
||||
|
|
@ -135,10 +123,7 @@ const Peers = (props) => {
|
|||
<div>
|
||||
<div className="explore-nav-menu">
|
||||
<div onClick={()=>setTab('Saved Peers')} className={tab === 'Saved Peers' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||
Saved Peers
|
||||
</div>
|
||||
<div onClick={()=>setTab('Connected Peers')} className={tab === 'Connected Peers' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||
Connected Peers
|
||||
Peers
|
||||
</div>
|
||||
</div>
|
||||
{tab === 'Saved Peers' ?
|
||||
|
|
@ -149,13 +134,9 @@ const Peers = (props) => {
|
|||
const host = peerAddress.getHost();
|
||||
const port = peerAddress.getPort();
|
||||
const addrStr = host + ':' + port;
|
||||
const isConnected = isPeerConnected(peerAddress);
|
||||
|
||||
return <div onClick={()=>goToPeer(peerAddress)} key={peerId} className="search-result-wapper">
|
||||
{isConnected ?
|
||||
<ICON_LAPTOPFILL styles={{fill:"rgb(0,128,0)", width:'48px', height:"48px"}} /> :
|
||||
<ICON_LAPTOPFILL styles={{fill:"rgb(255,0,0)", width:'48px', height:"48px"}} />
|
||||
}
|
||||
<ICON_LAPTOPFILL styles={{fill:"rgb(0,128,0)", width:'48px', height:"48px"}} /> :
|
||||
<div className="search-user-details">
|
||||
<div className="search-user-warp">
|
||||
<div className="search-user-info">
|
||||
|
|
@ -171,28 +152,7 @@ const Peers = (props) => {
|
|||
})
|
||||
:
|
||||
tab === 'Connected Peers' ?
|
||||
connectedPeers.map(p=>{
|
||||
const peerAddress = p.getPeerAddress();
|
||||
const host = peerAddress.getHost();
|
||||
const port = peerAddress.getPort();
|
||||
const addrStr = host + ':' + port;
|
||||
const savedPeer = p.getSavedPeer();
|
||||
const savedPeerName = savedPeer && savedPeer.getPeerName();
|
||||
return <div onClick={()=>goToPeer(peerAddress)} key={addrStr} className="search-result-wapper">
|
||||
<ICON_LAPTOPFILL styles={{fill:"rgb(0,128,0)", width:'48px', height:"48px"}} />
|
||||
<div className="search-user-details">
|
||||
<div className="search-user-warp">
|
||||
<div className="search-user-info">
|
||||
<div className="search-user-name">{savedPeerName}</div>
|
||||
<div className="search-user-username">{addrStr}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="search-user-bio">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
<></>
|
||||
: <div className="try-searching">
|
||||
Nothing to see here ..
|
||||
<div/>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import {
|
|||
selectLastProfileSqueak,
|
||||
selectProfileSqueaksStatus,
|
||||
clearProfileSqueaks,
|
||||
setDownloadPubkeySqueaks,
|
||||
} from '../../features/squeaks/squeaksSlice'
|
||||
|
||||
|
||||
|
|
@ -118,14 +117,6 @@ const Profile = (props) => {
|
|||
});
|
||||
}
|
||||
|
||||
const downloadUserSqueaks = () => {
|
||||
dispatch(setDownloadPubkeySqueaks(props.match.params.username))
|
||||
.then(() => {
|
||||
console.log('Finished downloading pubkey squeaks.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const createContactProfile = () => {
|
||||
dispatch(setCreateContactProfile({
|
||||
pubkey: userParam,
|
||||
|
|
@ -311,13 +302,6 @@ const Profile = (props) => {
|
|||
</div>
|
||||
}
|
||||
|
||||
<div onClick={(e)=>
|
||||
downloadUserSqueaks()
|
||||
}
|
||||
className={'profile-edit-button'}>
|
||||
<span><span>{'Download Squeaks'}</span></span>
|
||||
</div>
|
||||
|
||||
{user &&
|
||||
<div onClick={(e)=>
|
||||
user.getFollowing() ?
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
getSqueakOffers,
|
||||
buySqueak,
|
||||
downloadSqueak,
|
||||
downloadPubkeySqueaks,
|
||||
} from '../../api/client'
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -40,7 +39,6 @@ const initialState = {
|
|||
squeakOffers: [],
|
||||
buySqueakStatus: 'idle',
|
||||
downloadSqueakStatus: 'idle',
|
||||
downloadPubkeySqueakStatus: 'idle',
|
||||
}
|
||||
|
||||
// Thunk functions
|
||||
|
|
@ -199,15 +197,6 @@ export const setDownloadSqueak = createAsyncThunk(
|
|||
}
|
||||
)
|
||||
|
||||
export const setDownloadPubkeySqueaks = createAsyncThunk(
|
||||
'squeaks/setDownloadPubkeySqueaks',
|
||||
async (pubkey) => {
|
||||
console.log('Downloading pubkey squeaks');
|
||||
const response = await downloadPubkeySqueaks(pubkey);
|
||||
return response;
|
||||
}
|
||||
)
|
||||
|
||||
const updatedSqueakInArray = (squeakArr, newSqueak) => {
|
||||
const currentIndex = squeakArr.findIndex(squeak => squeak.getSqueakHash() === newSqueak.getSqueakHash());
|
||||
if (currentIndex != -1) {
|
||||
|
|
@ -394,16 +383,6 @@ const squeaksSlice = createSlice({
|
|||
// TODO: check if current squeak is the one that got downloaded.
|
||||
state.currentSqueak = newSqueak;
|
||||
})
|
||||
.addCase(setDownloadPubkeySqueaks.pending, (state, action) => {
|
||||
state.downloadPubkeySqueakStatus = 'loading'
|
||||
})
|
||||
.addCase(setDownloadPubkeySqueaks.rejected, (state, action) => {
|
||||
state.downloadPubkeySqueakStatus = 'idle'
|
||||
})
|
||||
.addCase(setDownloadPubkeySqueaks.fulfilled, (state, action) => {
|
||||
console.log(action);
|
||||
state.downloadPubkeySqueakStatus = 'idle';
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -466,5 +445,3 @@ export const selectSqueakOffersStatus = state => state.squeaks.squeakOffersStatu
|
|||
export const selectBuySqueakStatus = state => state.squeaks.buySqueakStatus
|
||||
|
||||
export const selectDownloadSqueakStatus = state => state.squeaks.downloadSqueakStatus
|
||||
|
||||
export const selectDownloadPubkeySqueakStatus = state => state.squeaks.downloadPubkeySqueakStatus
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ rpc_pass=devpass
|
|||
rpc_use_ssl=true
|
||||
rpc_ssl_cert=/rpc/rpc.cert
|
||||
|
||||
[server]
|
||||
external_address=myexternaladdress.com
|
||||
port=8765
|
||||
|
||||
[postgresql]
|
||||
host=db
|
||||
user=postgres
|
||||
|
|
|
|||
|
|
@ -104,8 +104,10 @@ def test_get_external_address(admin_stub):
|
|||
external_address = get_external_address(admin_stub)
|
||||
|
||||
print(external_address)
|
||||
assert external_address.host is not None and len(external_address.host) > 0
|
||||
assert external_address.port > 0
|
||||
# assert external_address.host is not None and len(external_address.host) > 0
|
||||
assert external_address.host == 'myexternaladdress.com'
|
||||
# assert external_address.port > 0
|
||||
assert external_address.port == 8765
|
||||
|
||||
|
||||
def test_get_default_peer_port(admin_stub):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ alembic==1.7.1
|
|||
Flask==2.0.1
|
||||
flask-cors==3.0.10
|
||||
flask-login==0.5.0
|
||||
flask-sock==0.5.1
|
||||
Flask-WTF==0.15.1
|
||||
googleapis-common-protos==1.53.0
|
||||
importlib_resources==1.4.0
|
||||
|
|
@ -14,3 +15,4 @@ requests==2.26.0
|
|||
SQLAlchemy==1.4.25
|
||||
squeaklib==0.11.0
|
||||
typed-config==0.2.5
|
||||
websocket-client==1.3.1
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from squeak.core.keys import SqueakPublicKey
|
|||
from proto import squeak_admin_pb2
|
||||
from squeaknode.admin.profile_image_util import bytes_to_base64_string
|
||||
from squeaknode.admin.profile_image_util import load_default_profile_image
|
||||
from squeaknode.core.connected_peer import ConnectedPeer
|
||||
from squeaknode.core.download_result import DownloadResult
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
|
|
@ -160,24 +159,6 @@ def payment_summary_to_message(
|
|||
)
|
||||
|
||||
|
||||
def connected_peer_to_message(connected_peer: ConnectedPeer) -> squeak_admin_pb2.ConnectedPeer:
|
||||
return squeak_admin_pb2.ConnectedPeer(
|
||||
peer_address=peer_address_to_message(
|
||||
connected_peer.peer.remote_address),
|
||||
connect_time_s=connected_peer.peer.connect_time,
|
||||
last_message_received_time_s=connected_peer.peer.last_msg_revc_time,
|
||||
number_messages_received=connected_peer.peer.num_msgs_received,
|
||||
number_bytes_received=connected_peer.peer.num_bytes_received,
|
||||
number_messages_sent=connected_peer.peer.num_msgs_sent,
|
||||
number_bytes_sent=connected_peer.peer.num_bytes_sent,
|
||||
is_peer_saved=(connected_peer.saved_peer is not None),
|
||||
saved_peer=(
|
||||
squeak_peer_to_message(connected_peer.saved_peer)
|
||||
if connected_peer.saved_peer else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def peer_address_to_message(peer_address: PeerAddress) -> squeak_admin_pb2.PeerAddress:
|
||||
return squeak_admin_pb2.PeerAddress(
|
||||
network=peer_address.network.name,
|
||||
|
|
@ -300,9 +281,3 @@ def optional_sent_payment_to_message(sent_payment: Optional[SentPayment]) -> Opt
|
|||
if sent_payment is None:
|
||||
return None
|
||||
return sent_payment_to_message(sent_payment)
|
||||
|
||||
|
||||
def optional_connected_peer_to_message(connected_peer: Optional[ConnectedPeer]) -> Optional[squeak_admin_pb2.ConnectedPeer]:
|
||||
if connected_peer is None:
|
||||
return None
|
||||
return connected_peer_to_message(connected_peer)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,11 @@ from squeak.core.keys import SqueakPrivateKey
|
|||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from proto import squeak_admin_pb2
|
||||
from squeaknode.admin.messages import connected_peer_to_message
|
||||
from squeaknode.admin.messages import download_result_to_message
|
||||
from squeaknode.admin.messages import message_to_peer_address
|
||||
from squeaknode.admin.messages import message_to_received_payment
|
||||
from squeaknode.admin.messages import message_to_sent_payment
|
||||
from squeaknode.admin.messages import message_to_squeak_entry
|
||||
from squeaknode.admin.messages import optional_connected_peer_to_message
|
||||
from squeaknode.admin.messages import optional_received_offer_to_message
|
||||
from squeaknode.admin.messages import optional_sent_payment_to_message
|
||||
from squeaknode.admin.messages import optional_squeak_entry_to_message
|
||||
|
|
@ -884,35 +882,6 @@ class SqueakAdminServerHandler(object):
|
|||
self.squeak_controller.connect_peer(peer_address)
|
||||
return squeak_admin_pb2.ConnectPeerReply()
|
||||
|
||||
def handle_get_connected_peers(self, request):
|
||||
logger.info("Handle get connected peers.")
|
||||
connected_peers = self.squeak_controller.get_connected_peers()
|
||||
logger.info("Connected peers: {}".format(
|
||||
connected_peers,
|
||||
))
|
||||
connected_peers_display_msgs = [
|
||||
connected_peer_to_message(peer) for peer in connected_peers
|
||||
]
|
||||
return squeak_admin_pb2.GetConnectedPeersReply(
|
||||
connected_peers=connected_peers_display_msgs
|
||||
)
|
||||
|
||||
def handle_get_connected_peer(self, request):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
logger.info("Handle get connected peer for address: {}".format(
|
||||
peer_address,
|
||||
))
|
||||
connected_peer = self.squeak_controller.get_connected_peer(
|
||||
peer_address)
|
||||
logger.info("Connected peer: {}".format(
|
||||
connected_peer,
|
||||
))
|
||||
connected_peers_display_msg = optional_connected_peer_to_message(
|
||||
connected_peer)
|
||||
return squeak_admin_pb2.GetConnectedPeerReply(
|
||||
connected_peer=connected_peers_display_msg
|
||||
)
|
||||
|
||||
def handle_disconnect_peer(self, request):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
logger.info(
|
||||
|
|
@ -920,34 +889,6 @@ class SqueakAdminServerHandler(object):
|
|||
self.squeak_controller.disconnect_peer(peer_address)
|
||||
return squeak_admin_pb2.DisconnectPeerReply()
|
||||
|
||||
def handle_subscribe_connected_peers(self, request, stopped):
|
||||
logger.info("Handle subscribe connected peers")
|
||||
connected_peers_stream = self.squeak_controller.subscribe_connected_peers(
|
||||
stopped,
|
||||
)
|
||||
for connected_peers in connected_peers_stream:
|
||||
connected_peers_display_msgs = [
|
||||
connected_peer_to_message(peer) for peer in connected_peers
|
||||
]
|
||||
yield squeak_admin_pb2.GetConnectedPeersReply(
|
||||
connected_peers=connected_peers_display_msgs
|
||||
)
|
||||
|
||||
def handle_subscribe_connected_peer(self, request, stopped):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
logger.info(
|
||||
"Handle subscribe connected peer with peer address: {}".format(peer_address))
|
||||
connected_peer_stream = self.squeak_controller.subscribe_connected_peer(
|
||||
peer_address,
|
||||
stopped,
|
||||
)
|
||||
for connected_peer in connected_peer_stream:
|
||||
connected_peer_display_msg = optional_connected_peer_to_message(
|
||||
connected_peer)
|
||||
yield squeak_admin_pb2.GetConnectedPeerReply(
|
||||
connected_peer=connected_peer_display_msg,
|
||||
)
|
||||
|
||||
def handle_subscribe_buy_offers(self, request, stopped):
|
||||
squeak_hash_str = request.squeak_hash
|
||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||
|
|
|
|||
|
|
@ -481,18 +481,6 @@ def create_app(handler, username, password):
|
|||
def getlikedsqueakdisplays(msg):
|
||||
return handler.handle_get_liked_squeak_display_entries(msg)
|
||||
|
||||
@app.route("/getconnectedpeers", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetConnectedPeersRequest())
|
||||
def getconnectedpeers(msg):
|
||||
return handler.handle_get_connected_peers(msg)
|
||||
|
||||
@app.route("/getconnectedpeer", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetConnectedPeerRequest())
|
||||
def getconnectedpeer(msg):
|
||||
return handler.handle_get_connected_peer(msg)
|
||||
|
||||
@app.route("/connectpeer", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.ConnectPeerRequest())
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import binascii
|
||||
import logging
|
||||
import struct
|
||||
from typing import Iterable
|
||||
|
||||
import zmq.asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BitcoinBlockSubscriptionClient:
|
||||
"""Get an iterator of new bitcoin blocks."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
) -> None:
|
||||
self.zmqContext = zmq.Context()
|
||||
self.zmqSubSocket = self.zmqContext.socket(zmq.SUB)
|
||||
self.zmqSubSocket.setsockopt(zmq.RCVHWM, 0)
|
||||
self.zmqSubSocket.setsockopt_string(zmq.SUBSCRIBE, "hashblock")
|
||||
self.zmqSubSocket.connect("tcp://{}:{}".format(host, port))
|
||||
|
||||
def get_blocks(self) -> Iterable[bytes]:
|
||||
while True:
|
||||
topic, body, seq = self.zmqSubSocket.recv_multipart()
|
||||
sequence = "Unknown"
|
||||
if len(seq) == 4:
|
||||
sequence = str(struct.unpack('<I', seq)[-1])
|
||||
if topic == b"hashblock":
|
||||
logger.debug('- HASH BLOCK (' + sequence + ') -')
|
||||
logger.debug(binascii.hexlify(body))
|
||||
logger.debug('body type: {}'.format(type(body)))
|
||||
logger.debug('body: {}'.format(body))
|
||||
yield body
|
||||
79
squeaknode/client/network_controller.py
Normal file
79
squeaknode/client/network_controller.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from squeaknode.client.peer_downloader import RangeDownloader
|
||||
from squeaknode.client.peer_downloader import SingleDownloader
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOWNLOAD_TIMEOUT_S = 10
|
||||
|
||||
|
||||
class NetworkController:
|
||||
|
||||
def __init__(self, squeak_store: SqueakStore, config):
|
||||
self.squeak_store = squeak_store
|
||||
self.config = config
|
||||
|
||||
def download_timeline(self) -> None:
|
||||
min_block = 0 # TODO
|
||||
max_block = 999999999999 # TODO
|
||||
followed_public_keys = self.squeak_store.get_followed_public_keys()
|
||||
peers = self.squeak_store.get_autoconnect_peers()
|
||||
for peer in peers:
|
||||
downloader = RangeDownloader(
|
||||
peer,
|
||||
self.squeak_store,
|
||||
min_block,
|
||||
max_block,
|
||||
followed_public_keys,
|
||||
)
|
||||
downloader.download()
|
||||
|
||||
def download_pubkey_squeaks(self, pubkey: SqueakPublicKey) -> None:
|
||||
min_block = 0 # TODO
|
||||
max_block = 999999999999 # TODO
|
||||
peers = self.squeak_store.get_autoconnect_peers()
|
||||
for peer in peers:
|
||||
downloader = RangeDownloader(
|
||||
peer,
|
||||
self.squeak_store,
|
||||
min_block,
|
||||
max_block,
|
||||
[pubkey],
|
||||
)
|
||||
downloader.download()
|
||||
|
||||
def download_single_squeak(self, squeak_hash: bytes) -> None:
|
||||
peers = self.squeak_store.get_autoconnect_peers()
|
||||
for peer in peers:
|
||||
downloader = SingleDownloader(
|
||||
peer,
|
||||
self.squeak_store,
|
||||
squeak_hash,
|
||||
)
|
||||
downloader.download()
|
||||
100
squeaknode/client/peer_client.py
Normal file
100
squeaknode/client/peer_client.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from squeak.core import CSqueak
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.core.squeak_peer import SqueakPeer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOWNLOAD_TIMEOUT_S = 10
|
||||
|
||||
|
||||
class PeerClient:
|
||||
|
||||
def __init__(self, peer: SqueakPeer):
|
||||
self.peer = peer
|
||||
self.base_url = f"http://{peer.address.host}:{peer.address.port}"
|
||||
|
||||
def lookup(
|
||||
self,
|
||||
min_block: int,
|
||||
max_block: int,
|
||||
pubkeys: List[SqueakPublicKey],
|
||||
) -> List[bytes]:
|
||||
pubkeys_str = [
|
||||
pubkey.to_bytes().hex()
|
||||
for pubkey in pubkeys
|
||||
]
|
||||
payload = {
|
||||
'minblock': min_block,
|
||||
'maxblock': max_block,
|
||||
'pubkeys': pubkeys_str,
|
||||
}
|
||||
url = f"{self.base_url}/lookup"
|
||||
r = requests.get(url, params=payload) # type: ignore
|
||||
squeak_hashes_str = r.json()
|
||||
return [
|
||||
bytes.fromhex(squeak_hash_str)
|
||||
for squeak_hash_str in squeak_hashes_str
|
||||
]
|
||||
|
||||
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
|
||||
squeak_hash_str = squeak_hash.hex()
|
||||
url = f"{self.base_url}/squeak/{squeak_hash_str}"
|
||||
r = requests.get(url)
|
||||
if r.status_code != requests.codes.ok:
|
||||
return None
|
||||
squeak_bytes = r.content
|
||||
return CSqueak.deserialize(squeak_bytes)
|
||||
|
||||
def get_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
||||
squeak_hash_str = squeak_hash.hex()
|
||||
url = f"{self.base_url}/secretkey/{squeak_hash_str}"
|
||||
r = requests.get(url)
|
||||
if r.status_code != requests.codes.ok:
|
||||
return None
|
||||
secret_key = r.content
|
||||
return secret_key
|
||||
|
||||
def get_offer(self, squeak_hash: bytes) -> Optional[Offer]:
|
||||
squeak_hash_str = squeak_hash.hex()
|
||||
url = f"{self.base_url}/offer/{squeak_hash_str}"
|
||||
r = requests.get(url)
|
||||
if r.status_code != requests.codes.ok:
|
||||
return None
|
||||
offer_json = r.json()
|
||||
offer = Offer(
|
||||
squeak_hash=bytes.fromhex(offer_json['squeak_hash']),
|
||||
nonce=bytes.fromhex(offer_json['nonce']),
|
||||
payment_request=offer_json['payment_request'],
|
||||
host=offer_json['host'],
|
||||
port=int(offer_json['port']),
|
||||
)
|
||||
return offer
|
||||
146
squeaknode/client/peer_downloader.py
Normal file
146
squeaknode/client/peer_downloader.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from typing import List
|
||||
|
||||
from squeak.core import CSqueak
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from squeaknode.client.peer_client import PeerClient
|
||||
from squeaknode.core.squeak_peer import SqueakPeer
|
||||
from squeaknode.core.squeaks import get_hash
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOWNLOAD_TIMEOUT_S = 10
|
||||
|
||||
|
||||
class PeerDownloader(ABC):
|
||||
|
||||
def __init__(self, peer: SqueakPeer, squeak_store: SqueakStore):
|
||||
self.peer = peer
|
||||
self.client = PeerClient(peer)
|
||||
self.squeak_store = squeak_store
|
||||
|
||||
@abstractmethod
|
||||
def get_hashes(self) -> List[bytes]:
|
||||
"""Get list of squeak hashes to download.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_squeak_wanted(self, squeak: CSqueak) -> bool:
|
||||
"""Return true if squeak is supposed to be downloaded.
|
||||
"""
|
||||
|
||||
def download(self) -> None:
|
||||
squeak_hashes = self.get_hashes()
|
||||
for squeak_hash in squeak_hashes:
|
||||
# Download the squeak if not already owned.
|
||||
self.download_squeak(squeak_hash)
|
||||
# Download the secret key if not already unlocked.
|
||||
self.download_secret_key(squeak_hash)
|
||||
# Download the offer if not already unlocked.
|
||||
self.download_offer(squeak_hash)
|
||||
|
||||
def download_squeak(self, squeak_hash: bytes) -> None:
|
||||
# Download the squeak if not already owned.
|
||||
if self.squeak_store.get_squeak(squeak_hash):
|
||||
return
|
||||
squeak = self.client.get_squeak(squeak_hash)
|
||||
if squeak and self.is_squeak_wanted(squeak):
|
||||
self.squeak_store.save_squeak(squeak)
|
||||
|
||||
def download_secret_key(self, squeak_hash: bytes) -> None:
|
||||
# Get the squeak from the database.
|
||||
squeak = self.squeak_store.get_squeak(squeak_hash)
|
||||
if squeak and self.is_squeak_wanted(squeak):
|
||||
# Download the secret key is not already unlocked.
|
||||
if self.squeak_store.get_squeak_secret_key(squeak_hash):
|
||||
return
|
||||
secret_key = self.client.get_secret_key(squeak_hash)
|
||||
if secret_key:
|
||||
self.squeak_store.save_secret_key(squeak_hash, secret_key)
|
||||
|
||||
def download_offer(self, squeak_hash: bytes) -> None:
|
||||
# Get the squeak from the database.
|
||||
squeak = self.squeak_store.get_squeak(squeak_hash)
|
||||
if squeak and self.is_squeak_wanted(squeak):
|
||||
# Download the secret key is not already unlocked.
|
||||
if self.squeak_store.get_squeak_secret_key(squeak_hash):
|
||||
return
|
||||
offer = self.client.get_offer(squeak_hash)
|
||||
if offer:
|
||||
self.squeak_store.handle_offer(
|
||||
squeak,
|
||||
offer,
|
||||
self.peer.address,
|
||||
)
|
||||
|
||||
|
||||
class RangeDownloader(PeerDownloader):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
peer: SqueakPeer,
|
||||
squeak_store: SqueakStore,
|
||||
min_block: int,
|
||||
max_block: int,
|
||||
pubkeys: List[SqueakPublicKey],
|
||||
):
|
||||
super().__init__(peer, squeak_store)
|
||||
self.min_block = min_block
|
||||
self.max_block = max_block
|
||||
self.pubkeys = pubkeys
|
||||
|
||||
def get_hashes(self) -> List[bytes]:
|
||||
return self.client.lookup(
|
||||
self.min_block,
|
||||
self.max_block,
|
||||
self.pubkeys,
|
||||
)
|
||||
|
||||
def is_squeak_wanted(self, squeak: CSqueak) -> bool:
|
||||
return squeak.nBlockHeight >= self.min_block and \
|
||||
squeak.nBlockHeight <= self.max_block and \
|
||||
squeak.GetPubKey() in self.pubkeys
|
||||
|
||||
|
||||
class SingleDownloader(PeerDownloader):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
peer: SqueakPeer,
|
||||
squeak_store: SqueakStore,
|
||||
squeak_hash: bytes,
|
||||
):
|
||||
super().__init__(peer, squeak_store)
|
||||
self.squeak_hash = squeak_hash
|
||||
|
||||
def get_hashes(self) -> List[bytes]:
|
||||
return [self.squeak_hash]
|
||||
|
||||
def is_squeak_wanted(self, squeak: CSqueak) -> bool:
|
||||
return get_hash(squeak) == self.squeak_hash
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
|
||||
from squeaknode.core.squeak_peer import SqueakPeer
|
||||
from squeaknode.network.peer import Peer
|
||||
|
||||
|
||||
class ConnectedPeer(NamedTuple):
|
||||
"""Represents another node in the network."""
|
||||
peer: Peer
|
||||
saved_peer: Optional[SqueakPeer]
|
||||
|
|
@ -1,365 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from squeak.messages import msg_addr
|
||||
from squeak.messages import msg_getaddr
|
||||
from squeak.messages import msg_getdata
|
||||
from squeak.messages import msg_inv
|
||||
from squeak.messages import msg_notfound
|
||||
from squeak.messages import msg_ping
|
||||
from squeak.messages import msg_pong
|
||||
from squeak.messages import MSG_SECRET_KEY
|
||||
from squeak.messages import MSG_SQUEAK
|
||||
from squeak.messages import msg_squeak
|
||||
|
||||
from squeaknode.core.crypto import generate_ping_nonce
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.network.peer import Peer
|
||||
from squeaknode.node.network_handler import NetworkHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
HANDSHAKE_TIMEOUT = 30
|
||||
PING_TIMEOUT = 60
|
||||
PONG_TIMEOUT = 30
|
||||
|
||||
|
||||
class Connection(object):
|
||||
"""Handles lifecycle of a connection to a peer.
|
||||
"""
|
||||
|
||||
def __init__(self, peer: Peer, network_handler: NetworkHandler):
|
||||
self.peer = peer
|
||||
self.network_handler = network_handler
|
||||
self.handshake_timer = HandshakeTimer(self)
|
||||
self.ping_timer = PingTimer(self)
|
||||
self.pong_timer = PongTimer(self)
|
||||
|
||||
def handshake(self):
|
||||
"""Do a handshake with a peer.
|
||||
"""
|
||||
self.handshake_timer.start_timer()
|
||||
|
||||
if self.peer.outgoing:
|
||||
self.peer.send_version()
|
||||
self.peer.receive_version()
|
||||
if not self.peer.outgoing:
|
||||
self.peer.send_version()
|
||||
|
||||
self.peer.set_connected()
|
||||
self.handshake_timer.cancel()
|
||||
logger.debug("HANDSHAKE COMPLETE-----------")
|
||||
|
||||
def shutdown(self):
|
||||
logger.info("Peer shutting down...")
|
||||
self.peer.stop()
|
||||
self.handshake_timer.cancel()
|
||||
self.ping_timer.cancel()
|
||||
self.pong_timer.cancel()
|
||||
|
||||
def handle_connection(self):
|
||||
try:
|
||||
self.initial_sync()
|
||||
self.handle_msgs()
|
||||
except Exception:
|
||||
logger.exception("Error in handle_connection")
|
||||
finally:
|
||||
self.shutdown()
|
||||
# self._stopped.set()
|
||||
|
||||
def initial_sync(self):
|
||||
self.send_ping()
|
||||
self.update_addrs()
|
||||
self.update_subscription()
|
||||
|
||||
def send_ping(self):
|
||||
ping_msg = msg_ping()
|
||||
ping_msg.nonce = generate_ping_nonce()
|
||||
self.pong_timer.start_timer(ping_msg.nonce)
|
||||
self.peer.send_msg(ping_msg)
|
||||
|
||||
def start_ping_timer(self):
|
||||
self.ping_timer.start_timer()
|
||||
|
||||
def update_subscription(self):
|
||||
locator = self.network_handler.get_interested_locator()
|
||||
self.peer.update_local_subscription(locator)
|
||||
|
||||
def update_addrs(self):
|
||||
getaddr_msg = msg_getaddr()
|
||||
self.peer.send_msg(getaddr_msg)
|
||||
|
||||
def handle_msgs(self):
|
||||
"""Handles messages from the peer if there are any available.
|
||||
|
||||
This method blocks when the peer has not sent any messages.
|
||||
"""
|
||||
msg = self.peer.recv_msg()
|
||||
while msg is not None:
|
||||
self.handle_peer_message(msg)
|
||||
msg = self.peer.recv_msg()
|
||||
logger.info("Finished handle_msgs")
|
||||
|
||||
def handle_peer_message(self, msg):
|
||||
"""Handle messages from a peer with completed handshake."""
|
||||
if msg.command == b'ping':
|
||||
self.handle_ping(msg)
|
||||
elif msg.command == b'pong':
|
||||
self.handle_pong(msg)
|
||||
elif msg.command == b'addr':
|
||||
self.handle_addr(msg)
|
||||
elif msg.command == b'getaddr':
|
||||
self.handle_getaddr(msg)
|
||||
elif msg.command == b'inv':
|
||||
self.handle_inv(msg)
|
||||
elif msg.command == b'getsqueaks':
|
||||
self.handle_getsqueaks(msg)
|
||||
elif msg.command == b'squeak':
|
||||
self.handle_squeak(msg)
|
||||
elif msg.command == b'getdata':
|
||||
self.handle_getdata(msg)
|
||||
elif msg.command == b'notfound':
|
||||
self.handle_notfound(msg)
|
||||
elif msg.command == b'secretkey':
|
||||
self.handle_secret_key(msg)
|
||||
elif msg.command == b'subscribe':
|
||||
self.handle_subscribe(msg)
|
||||
else:
|
||||
raise Exception("Unrecognized message: {}".format(
|
||||
msg.command
|
||||
))
|
||||
|
||||
def handle_ping(self, msg):
|
||||
nonce = msg.nonce
|
||||
pong = msg_pong()
|
||||
pong.nonce = nonce
|
||||
self.peer.set_last_recv_ping()
|
||||
self.peer.send_msg(pong)
|
||||
|
||||
def handle_pong(self, msg):
|
||||
nonce = msg.nonce
|
||||
self.pong_timer.stop_timer(nonce)
|
||||
|
||||
def handle_addr(self, msg):
|
||||
# TODO: Save new address in table.
|
||||
for addr in msg.addrs:
|
||||
pass
|
||||
|
||||
def handle_getaddr(self, msg):
|
||||
# TODO: Get known peers from table in database.
|
||||
addr_msg = msg_addr(addrs=[])
|
||||
self.peer.send_msg(addr_msg)
|
||||
|
||||
def handle_inv(self, msg):
|
||||
invs = msg.inv
|
||||
unknown_invs = self.network_handler.get_unknown_invs(invs)
|
||||
if unknown_invs:
|
||||
getdata_msg = msg_getdata(inv=unknown_invs)
|
||||
self.peer.send_msg(getdata_msg)
|
||||
|
||||
def handle_getdata(self, msg):
|
||||
invs = msg.inv
|
||||
for inv in invs:
|
||||
if inv.type == MSG_SQUEAK:
|
||||
squeak = self.network_handler.get_squeak(inv.hash)
|
||||
if squeak is None:
|
||||
reply_msg = msg_notfound(inv=[inv])
|
||||
self.peer.send_msg(reply_msg)
|
||||
else:
|
||||
reply_msg = msg_squeak(squeak=squeak)
|
||||
self.peer.send_msg(reply_msg)
|
||||
if inv.type == MSG_SECRET_KEY:
|
||||
reply = self.network_handler.get_secret_key_reply(
|
||||
inv.hash,
|
||||
self.peer.remote_address,
|
||||
)
|
||||
if reply is None:
|
||||
reply_msg = msg_notfound(inv=[inv])
|
||||
self.peer.send_msg(reply_msg)
|
||||
else:
|
||||
reply_msg = reply.get_msg()
|
||||
self.peer.send_msg(reply_msg)
|
||||
|
||||
def handle_notfound(self, msg):
|
||||
pass
|
||||
|
||||
def handle_getsqueaks(self, msg):
|
||||
locator = msg.locator
|
||||
for interest in locator.vInterested:
|
||||
reply_invs = self.network_handler.get_reply_invs(interest)
|
||||
inv_msg = msg_inv(inv=reply_invs)
|
||||
self.peer.send_msg(inv_msg)
|
||||
|
||||
def handle_squeak(self, msg):
|
||||
squeak = msg.squeak
|
||||
saved_squeak_hash = self.network_handler.save_squeak(squeak)
|
||||
if saved_squeak_hash is not None:
|
||||
self.network_handler.request_offers(saved_squeak_hash)
|
||||
|
||||
def handle_secret_key(self, msg):
|
||||
if msg.has_secret_key():
|
||||
self.network_handler.unlock_squeak(
|
||||
msg.hashSqk,
|
||||
msg.secretKey,
|
||||
)
|
||||
elif msg.has_offer():
|
||||
offer = Offer(
|
||||
squeak_hash=msg.hashSqk,
|
||||
nonce=msg.offer.nonce,
|
||||
payment_request=msg.offer.strPaymentInfo.decode('utf-8'),
|
||||
host=msg.offer.host.decode('utf-8'),
|
||||
port=msg.offer.port,
|
||||
)
|
||||
self.network_handler.handle_received_offer(
|
||||
offer,
|
||||
self.peer.remote_address,
|
||||
)
|
||||
|
||||
def handle_subscribe(self, msg):
|
||||
self.peer.set_remote_subscription(msg.locator)
|
||||
|
||||
|
||||
class HandshakeTimer:
|
||||
"""Stop the peer if handshake is not complete before timeout.
|
||||
"""
|
||||
|
||||
def __init__(self, connection: Connection):
|
||||
self.connection = connection
|
||||
self.timer = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def start_timer(self):
|
||||
with self._lock:
|
||||
self.timer = threading.Timer(
|
||||
HANDSHAKE_TIMEOUT,
|
||||
self.shutdown,
|
||||
)
|
||||
self.timer.name = "handshake_timer_thread_{}".format(
|
||||
self.connection.peer)
|
||||
self.timer.start()
|
||||
|
||||
def cancel(self):
|
||||
logger.debug("Cancelling handshake timer.")
|
||||
with self._lock:
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
def shutdown(self):
|
||||
logger.info("Shutdown connection triggered by handshake timer.")
|
||||
self.connection.shutdown()
|
||||
|
||||
|
||||
class PingTimer:
|
||||
"""Send a ping message when the timer expires.
|
||||
"""
|
||||
|
||||
def __init__(self, connection: Connection):
|
||||
self.connection = connection
|
||||
self.timer = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def start_timer(self):
|
||||
logger.debug("Starting ping timer.")
|
||||
with self._lock:
|
||||
# Cancel the existing timer.
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
# Start a new timer.
|
||||
self.timer = threading.Timer(
|
||||
PING_TIMEOUT,
|
||||
self.send_ping,
|
||||
)
|
||||
self.timer.name = "ping_timer_thread_{}".format(
|
||||
self.connection.peer)
|
||||
self.timer.start()
|
||||
|
||||
def cancel(self):
|
||||
logger.debug("Cancelling ping timer.")
|
||||
with self._lock:
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
def send_ping(self):
|
||||
logger.debug("Sending ping triggered by timer.")
|
||||
self.connection.send_ping()
|
||||
|
||||
|
||||
class PongTimer:
|
||||
"""Shut down the connection when the timer expires.
|
||||
"""
|
||||
|
||||
def __init__(self, connection: Connection):
|
||||
self.connection = connection
|
||||
self.timer = None
|
||||
self.expected_nonce = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def start_timer(self, nonce):
|
||||
logger.debug("Starting pong timer.")
|
||||
with self._lock:
|
||||
# Cancel the existing timer.
|
||||
if self.timer:
|
||||
return
|
||||
|
||||
# Start a new timer.
|
||||
self.expected_nonce = nonce
|
||||
self.timer = threading.Timer(
|
||||
PONG_TIMEOUT,
|
||||
self.shutdown,
|
||||
)
|
||||
self.timer.name = "pong_timer_thread_{}".format(
|
||||
self.connection.peer)
|
||||
self.timer.start()
|
||||
|
||||
def stop_timer(self, nonce):
|
||||
logger.debug("Stopping pong timer.")
|
||||
with self._lock:
|
||||
if nonce != self.expected_nonce:
|
||||
self.shutdown()
|
||||
|
||||
# Cancel the existing timer.
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = None
|
||||
self.expected_nonce = None
|
||||
|
||||
# Start a new ping timer.
|
||||
self.start_ping_timer()
|
||||
|
||||
def cancel(self):
|
||||
logger.debug("Cancelling pong timer.")
|
||||
with self._lock:
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
def shutdown(self):
|
||||
logger.info("Shutdown connection triggered by pong timer.")
|
||||
self.connection.shutdown()
|
||||
|
||||
def start_ping_timer(self):
|
||||
logger.debug("Starting ping timer triggered by pong response.")
|
||||
self.connection.start_ping_timer()
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.network.connection import Connection
|
||||
from squeaknode.network.peer import Peer
|
||||
from squeaknode.network.peer_client import ConnectPeerResult
|
||||
from squeaknode.node.listener_subscription_client import EventListener
|
||||
from squeaknode.node.network_handler import NetworkHandler
|
||||
|
||||
|
||||
MIN_PEERS = 5
|
||||
MAX_PEERS = 10
|
||||
UPDATE_THREAD_SLEEP_TIME = 10
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConnectionManager(object):
|
||||
"""Maintains connections to other peers in the network.
|
||||
"""
|
||||
|
||||
def __init__(self, local_address):
|
||||
self._peers: Dict[PeerAddress, Peer] = {}
|
||||
self.peers_lock = threading.Lock()
|
||||
self.peer_changed_listener = EventListener()
|
||||
self.single_peer_changed_listener = EventListener()
|
||||
self.accept_connections = True
|
||||
self.local_address = local_address
|
||||
|
||||
@contextmanager
|
||||
def connect(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
address: PeerAddress,
|
||||
outgoing: bool,
|
||||
network_handler: NetworkHandler,
|
||||
result_queue: queue.Queue,
|
||||
):
|
||||
try:
|
||||
peer = Peer(
|
||||
peer_socket,
|
||||
self.local_address,
|
||||
address,
|
||||
outgoing,
|
||||
self.single_peer_changed_listener,
|
||||
)
|
||||
connection = Connection(peer, network_handler)
|
||||
logger.debug("Doing handshake.")
|
||||
connection.handshake()
|
||||
logger.debug("Adding peer.")
|
||||
self._add_peer(peer)
|
||||
result_queue.put(
|
||||
ConnectPeerResult.from_success(peer.remote_address))
|
||||
logger.debug("Yielding connection.")
|
||||
yield connection
|
||||
logger.debug("Removing peer.")
|
||||
self._remove_peer(peer)
|
||||
except Exception as e:
|
||||
logger.exception("Error in connection.")
|
||||
result_queue.put(ConnectPeerResult.from_failure(e))
|
||||
raise
|
||||
finally:
|
||||
logger.info("Disconnected peer.")
|
||||
peer.stop()
|
||||
|
||||
@property
|
||||
def peers(self) -> List[Peer]:
|
||||
return list(self._peers.values())
|
||||
|
||||
def has_connection(self, address):
|
||||
"""Return True if the address is already connected."""
|
||||
return address in self._peers
|
||||
|
||||
def _on_peers_changed(self):
|
||||
peers = self.peers
|
||||
logger.info('Current number of peers {}'.format(len(peers)))
|
||||
logger.info('Current peers:--------')
|
||||
for peer in peers:
|
||||
logger.info(peer)
|
||||
logger.info('--------------')
|
||||
self.peer_changed_listener.handle_new_item(peers)
|
||||
|
||||
def _is_duplicate_nonce(self, peer):
|
||||
for other_peer in self.peers:
|
||||
if other_peer.local_version:
|
||||
if peer.remote_version == other_peer.local_version.nNonce:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _add_peer(self, peer: Peer):
|
||||
"""Add a peer.
|
||||
"""
|
||||
with self.peers_lock:
|
||||
if not self.accept_connections:
|
||||
raise NotAcceptingConnectionsError()
|
||||
if self._is_duplicate_nonce(peer):
|
||||
logger.debug('Failed to add peer {}'.format(peer))
|
||||
raise DuplicateNonceError()
|
||||
if self.has_connection(peer.remote_address):
|
||||
logger.debug('Failed to add peer {}'.format(peer))
|
||||
raise DuplicatePeerError()
|
||||
self._peers[peer.remote_address] = peer
|
||||
logger.debug('Added peer {}'.format(peer))
|
||||
self._on_peers_changed()
|
||||
|
||||
def _remove_peer(self, peer: Peer):
|
||||
"""Remove a peer.
|
||||
"""
|
||||
with self.peers_lock:
|
||||
if not self.has_connection(peer.remote_address):
|
||||
return
|
||||
del self._peers[peer.remote_address]
|
||||
logger.debug('Removed peer {}'.format(peer))
|
||||
self._on_peers_changed()
|
||||
|
||||
def get_peer(self, address) -> Optional[Peer]:
|
||||
"""Get a peer info by address.
|
||||
"""
|
||||
return self._peers.get(address)
|
||||
|
||||
def stop_connection(self, address):
|
||||
"""Stop peer connections for address.
|
||||
"""
|
||||
with self.peers_lock:
|
||||
peer = self.get_peer(address)
|
||||
if peer is not None:
|
||||
peer.stop()
|
||||
|
||||
def stop_all_connections(self):
|
||||
"""Stop all peer connections.
|
||||
"""
|
||||
self.accept_connections = False
|
||||
with self.peers_lock:
|
||||
for peer in self.peers:
|
||||
peer.stop()
|
||||
|
||||
def yield_peers_changed(self, stopped: threading.Event):
|
||||
yield from self.peer_changed_listener.yield_items(stopped)
|
||||
|
||||
def yield_single_peer_changed(self, peer_address: PeerAddress, stopped: threading.Event):
|
||||
for peer in self.single_peer_changed_listener.yield_items(stopped):
|
||||
logger.debug('yield_single_peer_changed: {}'.format(peer))
|
||||
if peer.remote_address == peer_address:
|
||||
if peer.connect_time is None:
|
||||
yield None
|
||||
else:
|
||||
yield peer
|
||||
|
||||
|
||||
class DuplicatePeerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateNonceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotAcceptingConnectionsError(Exception):
|
||||
pass
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import socket
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import CSqueakLocator
|
||||
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.network.connection_manager import ConnectionManager
|
||||
from squeaknode.network.peer import Peer
|
||||
from squeaknode.network.peer_client import PeerClient
|
||||
from squeaknode.network.peer_handler import PeerHandler
|
||||
from squeaknode.network.peer_server import PeerServer
|
||||
|
||||
|
||||
MIN_PEERS = 5
|
||||
MAX_PEERS = 10
|
||||
UPDATE_THREAD_SLEEP_TIME = 10
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkManager(object):
|
||||
"""Interface for doing things involving the network.
|
||||
"""
|
||||
|
||||
def __init__(self, config, default_port):
|
||||
self.config = config
|
||||
self.external_host = self.config.server.external_address
|
||||
self.local_ip = socket.gethostbyname('localhost')
|
||||
self.local_port = self.config.server.port or default_port
|
||||
self.peer_server = None
|
||||
self.peer_client = None
|
||||
self.tor_proxy_ip = self.config.tor.proxy_ip
|
||||
self.tor_proxy_port = self.config.tor.proxy_port
|
||||
self.connection_manager = ConnectionManager(self.local_address)
|
||||
|
||||
def start(self, network_handler):
|
||||
peer_handler = PeerHandler(
|
||||
self.connection_manager,
|
||||
network_handler,
|
||||
)
|
||||
self.peer_server = PeerServer(
|
||||
peer_handler,
|
||||
self.local_port,
|
||||
)
|
||||
self.peer_client = PeerClient(
|
||||
peer_handler,
|
||||
self.tor_proxy_ip,
|
||||
self.tor_proxy_port,
|
||||
)
|
||||
self.peer_server.start()
|
||||
|
||||
def stop(self):
|
||||
self.peer_server.stop()
|
||||
self.connection_manager.stop_all_connections()
|
||||
|
||||
def connect_peer_sync(self, peer_address: PeerAddress) -> None:
|
||||
if self.connection_manager.has_connection(peer_address):
|
||||
raise Exception("Already connected to: {}".format(peer_address))
|
||||
self.peer_client.connect_address(peer_address)
|
||||
|
||||
def connect_peer_async(self, peer_address: PeerAddress) -> None:
|
||||
if self.connection_manager.has_connection(peer_address):
|
||||
return
|
||||
self.peer_client.connect_address_async(peer_address)
|
||||
|
||||
def disconnect_peer(self, peer_address: PeerAddress) -> None:
|
||||
self.connection_manager.stop_connection(peer_address)
|
||||
|
||||
def get_connected_peer(self, peer_address: PeerAddress) -> Optional[Peer]:
|
||||
return self.connection_manager.get_peer(peer_address)
|
||||
|
||||
def get_connected_peers(self) -> List[Peer]:
|
||||
return self.connection_manager.peers
|
||||
|
||||
def broadcast_msg(self, msg: MsgSerializable) -> int:
|
||||
"""Send a message to all connected peers.
|
||||
|
||||
Returns:
|
||||
int: the number of peers message was sent to.
|
||||
"""
|
||||
count = 0
|
||||
for peer in self.connection_manager.peers:
|
||||
try:
|
||||
peer.send_msg(msg)
|
||||
count += 1
|
||||
except Exception:
|
||||
logger.exception("Failed to send msg to peer: {}".format(
|
||||
peer,
|
||||
))
|
||||
return count
|
||||
|
||||
def update_local_subscriptions(self, locator: CSqueakLocator) -> None:
|
||||
for peer in self.connection_manager.peers:
|
||||
try:
|
||||
peer.update_local_subscription(locator)
|
||||
except Exception:
|
||||
logger.exception("Failed to update local subcription with peer: {}".format(
|
||||
peer,
|
||||
))
|
||||
|
||||
@property
|
||||
def local_address(self) -> PeerAddress:
|
||||
return PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=self.local_ip,
|
||||
port=self.local_port,
|
||||
)
|
||||
|
||||
@property
|
||||
def external_address(self) -> PeerAddress:
|
||||
return PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=self.external_host or self.local_ip,
|
||||
port=self.local_port,
|
||||
)
|
||||
|
||||
def subscribe_connected_peers(self, stopped) -> Iterable[List[Peer]]:
|
||||
# yield from self.connection_manager.yield_peers_changed(stopped)
|
||||
for item in self.connection_manager.yield_peers_changed(stopped):
|
||||
logger.info("subscribe_connected_peers yielding item: {}".format(
|
||||
item,
|
||||
))
|
||||
yield item
|
||||
|
||||
def subscribe_connected_peer(self, peer_address: PeerAddress, stopped) -> Iterable[Peer]:
|
||||
yield from self.connection_manager.yield_single_peer_changed(
|
||||
peer_address,
|
||||
stopped,
|
||||
)
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
from bitcoin.core.serialize import SerializationTruncationError
|
||||
from bitcoin.net import CAddress
|
||||
from squeak.core import CSqueak
|
||||
from squeak.messages import msg_getsqueaks
|
||||
from squeak.messages import msg_subscribe
|
||||
from squeak.messages import msg_verack
|
||||
from squeak.messages import msg_version
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import CSqueakLocator
|
||||
|
||||
from squeaknode.core.crypto import generate_version_nonce
|
||||
from squeaknode.core.interests import get_differential_squeaks
|
||||
from squeaknode.core.interests import squeak_matches_interest
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.network.util import time_now
|
||||
from squeaknode.node.listener_subscription_client import EventListener
|
||||
|
||||
|
||||
MAX_MESSAGE_LEN = 1048576
|
||||
SOCKET_READ_LEN = 1024
|
||||
LAST_MESSAGE_TIMEOUT = 600
|
||||
PING_TIMEOUT = 10
|
||||
PING_INTERVAL = 60
|
||||
|
||||
UPDATE_TIME_INTERVAL = 10
|
||||
HANDSHAKE_VERSION = 70002
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Peer(object):
|
||||
"""Maintains the internal state of a peer connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
local_address: PeerAddress,
|
||||
remote_address: PeerAddress,
|
||||
outgoing: bool,
|
||||
peer_changed_listener: EventListener,
|
||||
):
|
||||
self._peer_socket = peer_socket
|
||||
self._peer_socket_lock = threading.Lock()
|
||||
self._local_address = local_address
|
||||
self._remote_address = remote_address
|
||||
self._outgoing = outgoing
|
||||
self._connect_time = 0
|
||||
self._local_version = None
|
||||
self._remote_version = None
|
||||
self._last_msg_revc_time = None
|
||||
self._last_sent_ping_nonce = None
|
||||
self._last_sent_ping_time = None
|
||||
self._last_recv_ping_time = None
|
||||
self._num_msgs_received = 0
|
||||
self._num_bytes_received = 0
|
||||
self._num_msgs_sent = 0
|
||||
self._num_bytes_sent = 0
|
||||
|
||||
self._remote_subscription = None
|
||||
self._local_subscription = None
|
||||
|
||||
self.msg_receiver = MessageReceiver(
|
||||
self._peer_socket,
|
||||
)
|
||||
self.received_msgs_iter = self.msg_receiver.recv_msgs()
|
||||
|
||||
self.peer_changed_listener = peer_changed_listener
|
||||
|
||||
@property
|
||||
def nVersion(self):
|
||||
remote_version = self._remote_version
|
||||
if remote_version:
|
||||
return remote_version.nVersion
|
||||
|
||||
@property
|
||||
def local_address(self):
|
||||
return self._local_address
|
||||
|
||||
@property
|
||||
def remote_address(self):
|
||||
return self._remote_address
|
||||
|
||||
@property
|
||||
def remote_subscription(self):
|
||||
return self._remote_subscription
|
||||
|
||||
@property
|
||||
def local_subscription(self):
|
||||
return self._local_subscription
|
||||
|
||||
@property
|
||||
def local_caddress(self):
|
||||
caddress = CAddress()
|
||||
caddress.nTime = self.connect_time
|
||||
caddress.ip = self.local_address.host
|
||||
caddress.port = self.local_address.port
|
||||
return caddress
|
||||
|
||||
@property
|
||||
def remote_caddress(self):
|
||||
caddress = CAddress()
|
||||
caddress.nTime = self.connect_time
|
||||
# TODO: Set the remote address ip
|
||||
# caddress.ip =
|
||||
caddress.port = self.remote_address.port
|
||||
return caddress
|
||||
|
||||
@property
|
||||
def outgoing(self):
|
||||
return self._outgoing
|
||||
|
||||
@property
|
||||
def connect_time(self):
|
||||
return self._connect_time
|
||||
|
||||
@property
|
||||
def num_msgs_received(self):
|
||||
return self._num_msgs_received
|
||||
|
||||
@property
|
||||
def num_bytes_received(self):
|
||||
return self._num_bytes_received
|
||||
|
||||
@property
|
||||
def num_msgs_sent(self):
|
||||
return self._num_msgs_sent
|
||||
|
||||
@property
|
||||
def num_bytes_sent(self):
|
||||
return self._num_bytes_sent
|
||||
|
||||
@property
|
||||
def local_version(self):
|
||||
return self._local_version
|
||||
|
||||
@local_version.setter
|
||||
def local_version(self, local_version):
|
||||
self._local_version = local_version
|
||||
|
||||
@property
|
||||
def remote_version(self):
|
||||
return self._remote_version
|
||||
|
||||
@remote_version.setter
|
||||
def remote_version(self, remote_version):
|
||||
self._remote_version = remote_version
|
||||
|
||||
@property
|
||||
def last_msg_revc_time(self):
|
||||
return self._last_msg_revc_time
|
||||
|
||||
@property
|
||||
def last_sent_ping_time(self):
|
||||
return self._last_sent_ping_time
|
||||
|
||||
def set_last_sent_ping(self, nonce, timestamp=None):
|
||||
timestamp = timestamp or time.time()
|
||||
self._last_sent_ping_nonce = nonce
|
||||
self._last_sent_ping_time = time.time()
|
||||
|
||||
@property
|
||||
def last_recv_ping_time(self):
|
||||
return self._last_recv_ping_time
|
||||
|
||||
def set_last_recv_ping(self, timestamp=None):
|
||||
timestamp = timestamp or time.time()
|
||||
self._last_recv_ping_time = timestamp
|
||||
|
||||
# @property
|
||||
# def peer_state(self):
|
||||
# return ConnectedPeer(
|
||||
# peer_address=self.remote_address,
|
||||
# connect_time_s=self.connect_time,
|
||||
# outgoing=self.outgoing,
|
||||
# sent_bytes=0,
|
||||
# sent_messages=0,
|
||||
# received_bytes=0,
|
||||
# received_messages=0,
|
||||
# )
|
||||
|
||||
def recv_msg(self):
|
||||
"""Read data from the peer socket, and yield messages as they are decoded.
|
||||
|
||||
This method blocks when the socket has no data to read.
|
||||
"""
|
||||
try:
|
||||
msg = next(self.received_msgs_iter)
|
||||
except Exception:
|
||||
return None
|
||||
self.record_msg_received(msg)
|
||||
self.on_peer_updated()
|
||||
logger.info('Received msg {} from {}'.format(msg, self))
|
||||
return msg
|
||||
|
||||
def stop(self):
|
||||
logger.info("Stopping peer socket: {}".format(self._peer_socket))
|
||||
try:
|
||||
self._peer_socket.shutdown(socket.SHUT_RDWR)
|
||||
self._peer_socket.stop()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.set_disconnected()
|
||||
self.on_peer_updated()
|
||||
|
||||
def send_msg(self, msg):
|
||||
logger.info('Sending msg {} to {}'.format(msg, self))
|
||||
data = msg.to_bytes()
|
||||
try:
|
||||
with self._peer_socket_lock:
|
||||
self._peer_socket.send(data)
|
||||
self.record_msg_sent(msg)
|
||||
self.on_peer_updated()
|
||||
except Exception:
|
||||
logger.info('Failed to send msg to {}'.format(self))
|
||||
self.stop()
|
||||
|
||||
def send_version(self):
|
||||
local_version = self.version_pkt()
|
||||
self.local_version = local_version
|
||||
self.send_msg(local_version)
|
||||
verack = self.recv_msg()
|
||||
if not isinstance(verack, msg_verack):
|
||||
raise Exception('Expected verack response: {}'.format(
|
||||
verack,
|
||||
))
|
||||
|
||||
def receive_version(self):
|
||||
remote_version = self.recv_msg()
|
||||
if not isinstance(remote_version, msg_version):
|
||||
raise Exception('Expected version message. Received: {}'.format(
|
||||
remote_version,
|
||||
))
|
||||
self.remote_version = remote_version
|
||||
verack = msg_verack()
|
||||
self.send_msg(verack)
|
||||
|
||||
def version_pkt(self):
|
||||
"""Get the version message for this peer."""
|
||||
msg = msg_version()
|
||||
msg.nVersion = HANDSHAKE_VERSION
|
||||
msg.addrTo = self.remote_caddress
|
||||
msg.addrFrom = self.local_caddress
|
||||
msg.nNonce = generate_version_nonce()
|
||||
return msg
|
||||
|
||||
def set_connected(self):
|
||||
self._connect_time = time_now()
|
||||
|
||||
def set_disconnected(self):
|
||||
self._connect_time = None
|
||||
|
||||
def set_remote_subscription(self, locator: Optional[CSqueakLocator]):
|
||||
self._remote_subscription = locator
|
||||
|
||||
def set_local_subscription(self, locator: Optional[CSqueakLocator]):
|
||||
self._local_subscription = locator
|
||||
|
||||
def is_remote_subscribed(self, squeak: CSqueak):
|
||||
if self.remote_subscription is None:
|
||||
return False
|
||||
for interest in self.remote_subscription.vInterested:
|
||||
if squeak_matches_interest(squeak, interest):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_local_subscription(self, locator: CSqueakLocator):
|
||||
assert len(locator.vInterested) <= 1
|
||||
if len(locator.vInterested) == 0:
|
||||
locator = None
|
||||
|
||||
# Send the subscribe message
|
||||
subscribe_msg = msg_subscribe(
|
||||
locator=locator,
|
||||
)
|
||||
self.send_msg(subscribe_msg)
|
||||
|
||||
# Send the getsqueaks messages for differential interests
|
||||
if locator is not None:
|
||||
for interest in self.get_differential_interests(locator):
|
||||
diff_locator = CSqueakLocator(
|
||||
vInterested=[interest]
|
||||
)
|
||||
getsqueaks_msg = msg_getsqueaks(
|
||||
locator=diff_locator,
|
||||
)
|
||||
self.send_msg(getsqueaks_msg)
|
||||
|
||||
# Set the local subscription with the new value
|
||||
self.set_local_subscription(locator)
|
||||
|
||||
def get_differential_interests(self, locator: CSqueakLocator):
|
||||
if self._local_subscription is None:
|
||||
yield locator.vInterested[0]
|
||||
else:
|
||||
new_interest = locator.vInterested[0]
|
||||
old_interest = self._local_subscription.vInterested[0]
|
||||
for interest in get_differential_squeaks(new_interest, old_interest):
|
||||
yield interest
|
||||
|
||||
def on_peer_updated(self):
|
||||
logger.debug('on_peer_updated: {}'.format(self))
|
||||
self.peer_changed_listener.handle_new_item(self)
|
||||
|
||||
def record_msg_received(self, msg):
|
||||
self._num_msgs_received += 1
|
||||
self._num_bytes_received += len(msg.to_bytes())
|
||||
self._last_msg_revc_time = time_now()
|
||||
|
||||
def record_msg_sent(self, msg):
|
||||
if msg:
|
||||
self._num_msgs_sent += 1
|
||||
self._num_bytes_sent += len(msg.to_bytes())
|
||||
|
||||
# def subscribe_peer_state(self, stopped):
|
||||
# for result in self.peer_changed_listener.yield_items(stopped):
|
||||
# yield result
|
||||
|
||||
def __repr__(self):
|
||||
return "Peer(%s)" % (str(self.remote_address))
|
||||
|
||||
|
||||
class MessageDecoder:
|
||||
"""Handles the incoming binary data from a peer and buffers and decodes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.recv_data_buffer = BytesIO()
|
||||
|
||||
def process_recv_data(self, recv_data):
|
||||
data = self.read_data_buffer() + recv_data
|
||||
try:
|
||||
while data:
|
||||
self.set_data_buffer(data)
|
||||
msg = MsgSerializable.stream_deserialize(self.recv_data_buffer)
|
||||
if msg is None:
|
||||
raise Exception('Invalid data')
|
||||
else:
|
||||
yield msg
|
||||
data = self.read_data_buffer()
|
||||
except SerializationTruncationError:
|
||||
self.set_data_buffer(data)
|
||||
|
||||
def read_data_buffer(self):
|
||||
return self.recv_data_buffer.read()
|
||||
|
||||
def set_data_buffer(self, data):
|
||||
if len(data) > MAX_MESSAGE_LEN:
|
||||
raise Exception('Message size too large')
|
||||
self.recv_data_buffer = BytesIO(data)
|
||||
|
||||
|
||||
class MessageReceiver:
|
||||
"""Reads bytes from the socket and return decoded messages in an iterator.
|
||||
"""
|
||||
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
self.decoder = MessageDecoder()
|
||||
|
||||
def recv_msgs(self):
|
||||
while True:
|
||||
try:
|
||||
recv_data = self.socket.recv(SOCKET_READ_LEN)
|
||||
except Exception:
|
||||
logger.error("Error in recv")
|
||||
return
|
||||
if not recv_data:
|
||||
logger.error("revc_data is None")
|
||||
return
|
||||
|
||||
for msg in self.decoder.process_recv_data(recv_data):
|
||||
yield msg
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
|
||||
import socks
|
||||
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
|
||||
|
||||
SOCKET_CONNECT_TIMEOUT = 30
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PeerClient(object):
|
||||
"""Creates outgoing connections to other peers in the network.
|
||||
"""
|
||||
|
||||
def __init__(self, peer_handler, tor_proxy_ip, tor_proxy_port):
|
||||
self.peer_handler = peer_handler
|
||||
self.tor_proxy_ip = tor_proxy_ip
|
||||
self.tor_proxy_port = tor_proxy_port
|
||||
|
||||
def connect_address(self, address: PeerAddress):
|
||||
logger.info('Making connection to {}'.format(address))
|
||||
result_queue: queue.Queue = queue.Queue()
|
||||
threading.Thread(
|
||||
target=self.make_connection,
|
||||
args=(address, result_queue,),
|
||||
).start()
|
||||
|
||||
# Wait for connect result from the queue.
|
||||
connect_result = result_queue.get()
|
||||
logger.debug("connect_result: {}".format(connect_result))
|
||||
if connect_result.failure is not None:
|
||||
raise connect_result.failure
|
||||
|
||||
def connect_address_async(self, address: PeerAddress):
|
||||
logger.info('Making connection async to {}'.format(address))
|
||||
result_queue: queue.Queue = queue.Queue()
|
||||
threading.Thread(
|
||||
target=self.make_connection,
|
||||
args=(address, result_queue,),
|
||||
).start()
|
||||
|
||||
def make_connection(self, address: PeerAddress, result_queue: queue.Queue):
|
||||
logger.info('Conecting to address: {}'.format(address))
|
||||
try:
|
||||
peer_socket = self.get_socket(address)
|
||||
peer_socket.settimeout(SOCKET_CONNECT_TIMEOUT)
|
||||
connect_address = (address.host, address.port)
|
||||
peer_socket.connect(connect_address)
|
||||
peer_socket.setblocking(True)
|
||||
self.handle_connection(
|
||||
peer_socket,
|
||||
address,
|
||||
result_queue,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception('Failed to connect to {}'.format(address))
|
||||
result_queue.put(ConnectPeerResult.from_failure(e))
|
||||
|
||||
def handle_connection(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
peer_address: PeerAddress,
|
||||
result_queue: queue.Queue,
|
||||
):
|
||||
"""Handle a newly connected peer socket."""
|
||||
self.peer_handler.handle_connection(
|
||||
peer_socket,
|
||||
peer_address,
|
||||
outgoing=True,
|
||||
result_queue=result_queue,
|
||||
)
|
||||
|
||||
def get_socket(self, address: PeerAddress):
|
||||
if address.network not in [
|
||||
Network.IPV4,
|
||||
Network.IPV6,
|
||||
Network.TORV3,
|
||||
]:
|
||||
raise Exception("Unsupported network: {}".format(address.network))
|
||||
if address.network == Network.TORV3 and self.tor_proxy_ip is None:
|
||||
raise Exception(
|
||||
"Unable to connect to tor address without tor proxy ip configured.")
|
||||
if address.network == Network.TORV3 and self.tor_proxy_port is None:
|
||||
raise Exception(
|
||||
"Unable to connect to tor address without tor proxy port configured.")
|
||||
if address.network == Network.TORV3:
|
||||
s = socks.socksocket() # Same API as socket.socket in the standard lib
|
||||
s.set_proxy(socks.SOCKS5, self.tor_proxy_ip, self.tor_proxy_port)
|
||||
return s
|
||||
return socket.socket()
|
||||
|
||||
|
||||
class ConnectPeerResult(object):
|
||||
"""Result of a connect peer attempt.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
success: PeerAddress = None,
|
||||
failure: Exception = None,
|
||||
):
|
||||
self.success = success
|
||||
self.failure = failure
|
||||
|
||||
@classmethod
|
||||
def from_success(cls, success):
|
||||
return cls(success=success)
|
||||
|
||||
@classmethod
|
||||
def from_failure(cls, failure):
|
||||
return cls(failure=failure)
|
||||
|
||||
def __repr__(self):
|
||||
return "ConnectPeerResult: \
|
||||
success: {} \
|
||||
failure: {}".format(
|
||||
self.success,
|
||||
self.failure,
|
||||
)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PeerHandler():
|
||||
"""Handles new peer connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection_manager,
|
||||
network_handler,
|
||||
):
|
||||
self.connection_manager = connection_manager
|
||||
self.network_handler = network_handler
|
||||
|
||||
def handle_connection(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
address: PeerAddress,
|
||||
outgoing: bool,
|
||||
result_queue: Optional[queue.Queue] = None,
|
||||
):
|
||||
"""Handle a new socket connection.
|
||||
|
||||
This method blocks until the peer connection is established.
|
||||
"""
|
||||
# Create a dummy queue if not needed.
|
||||
if result_queue is None:
|
||||
result_queue = queue.Queue()
|
||||
|
||||
threading.Thread(
|
||||
target=self.start_connection,
|
||||
args=(
|
||||
peer_socket,
|
||||
address,
|
||||
outgoing,
|
||||
result_queue,
|
||||
),
|
||||
).start()
|
||||
|
||||
def start_connection(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
address: PeerAddress,
|
||||
outgoing: bool,
|
||||
result_queue: queue.Queue,
|
||||
):
|
||||
"""Start a connection
|
||||
"""
|
||||
with self.connection_manager.connect(
|
||||
peer_socket,
|
||||
address,
|
||||
outgoing,
|
||||
self.network_handler,
|
||||
result_queue,
|
||||
) as connection:
|
||||
connection.handle_connection()
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
|
||||
|
||||
MIN_PEERS = 5
|
||||
MAX_PEERS = 10
|
||||
UPDATE_THREAD_SLEEP_TIME = 10
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PeerServer(object):
|
||||
"""Accepts incoming connections from other peers in the network.
|
||||
"""
|
||||
|
||||
def __init__(self, peer_handler, port):
|
||||
self.peer_handler = peer_handler
|
||||
self.port = port
|
||||
self.listen_socket = socket.socket()
|
||||
|
||||
def start(self):
|
||||
logger.info("Starting peer server with port: {}".format(
|
||||
self.port,
|
||||
))
|
||||
# Start Listen thread
|
||||
threading.Thread(
|
||||
target=self.accept_connections,
|
||||
name="peer_server_listen_thread",
|
||||
).start()
|
||||
|
||||
def stop(self):
|
||||
logger.info("Stopping peer server listener thread...")
|
||||
self.listen_socket.shutdown(socket.SHUT_RDWR)
|
||||
self.listen_socket.close()
|
||||
|
||||
def accept_connections(self):
|
||||
try:
|
||||
self.listen_socket.bind(('', self.port))
|
||||
self.listen_socket.listen()
|
||||
while True:
|
||||
peer_socket, address = self.listen_socket.accept()
|
||||
host, port = address
|
||||
peer_address = PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
peer_socket.setblocking(True)
|
||||
self.handle_connection(
|
||||
peer_socket,
|
||||
peer_address,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Accept peer connections failed.")
|
||||
|
||||
def handle_connection(
|
||||
self,
|
||||
peer_socket: socket.socket,
|
||||
peer_address: PeerAddress,
|
||||
):
|
||||
"""Handle a newly connected peer socket."""
|
||||
threading.Thread(
|
||||
target=self.peer_handler.handle_connection,
|
||||
args=(peer_socket, peer_address, False),
|
||||
).start()
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent.futures import TimeoutError
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from squeak.messages import msg_getdata
|
||||
from squeak.messages import msg_getsqueaks
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import CInterested
|
||||
from squeak.net import CInv
|
||||
from squeak.net import CSqueakLocator
|
||||
|
||||
from squeaknode.core.download_result import DownloadResult
|
||||
from squeaknode.node.downloaded_object import DownloadedObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOWNLOAD_TIMEOUT_S = 10
|
||||
|
||||
|
||||
class ActiveDownload(ABC):
|
||||
|
||||
def __init__(self, limit: int):
|
||||
self.limit = limit
|
||||
self.count = 0
|
||||
self._lock = threading.Lock()
|
||||
self.stopped = threading.Event()
|
||||
self.num_peers = 0
|
||||
self.start_time_ms: Optional[int] = None
|
||||
|
||||
@abstractmethod
|
||||
def is_interested(self, downloaded_object: DownloadedObject) -> bool:
|
||||
"""Return True if the given squeak matches the download interest."""
|
||||
|
||||
@abstractmethod
|
||||
def get_download_msg(self) -> MsgSerializable:
|
||||
"""Get the message to send to peers to get download response."""
|
||||
|
||||
def initiate_download(self, broadcast_fn) -> None:
|
||||
self.start_time_ms = int(time.time() * 1000)
|
||||
msg = self.get_download_msg()
|
||||
self.num_peers = broadcast_fn(msg)
|
||||
if self.num_peers == 0:
|
||||
self.mark_complete()
|
||||
|
||||
def increment(self) -> None:
|
||||
with self._lock:
|
||||
self.count += 1
|
||||
if self.count >= self.limit:
|
||||
self.mark_complete()
|
||||
|
||||
def mark_complete(self):
|
||||
self.stopped.set()
|
||||
|
||||
def cancel(self):
|
||||
self.stopped.set()
|
||||
|
||||
def get_elapsed_time_ms(self):
|
||||
if self.start_time_ms is None:
|
||||
return 0
|
||||
end_time_ms = int(time.time() * 1000)
|
||||
return end_time_ms - self.start_time_ms
|
||||
|
||||
def wait_for_complete(self, timeout_s: int) -> None:
|
||||
self.stopped.wait(timeout=timeout_s)
|
||||
|
||||
def get_result(self) -> DownloadResult:
|
||||
return DownloadResult(
|
||||
number_downloaded=self.count,
|
||||
number_requested=self.limit,
|
||||
elapsed_time_ms=self.get_elapsed_time_ms(),
|
||||
number_peers=self.num_peers,
|
||||
)
|
||||
|
||||
|
||||
class InterestDownload(ActiveDownload):
|
||||
|
||||
def __init__(self, limit: int, interest: CInterested):
|
||||
self.interest = interest
|
||||
super().__init__(limit)
|
||||
|
||||
def is_interested(self, downloaded_object: DownloadedObject) -> bool:
|
||||
return downloaded_object.matches_requested_squeak_range(self.interest)
|
||||
|
||||
def get_download_msg(self) -> MsgSerializable:
|
||||
locator = CSqueakLocator(
|
||||
vInterested=[self.interest],
|
||||
)
|
||||
return msg_getsqueaks(
|
||||
locator=locator,
|
||||
)
|
||||
|
||||
|
||||
class HashDownload(ActiveDownload):
|
||||
|
||||
def __init__(self, squeak_hash: bytes):
|
||||
self.squeak_hash = squeak_hash
|
||||
super().__init__(1)
|
||||
|
||||
def is_interested(self, downloaded_object: DownloadedObject) -> bool:
|
||||
return downloaded_object.matches_requested_squeak_hash(self.squeak_hash)
|
||||
|
||||
def get_download_msg(self) -> MsgSerializable:
|
||||
invs = [
|
||||
CInv(type=1, hash=self.squeak_hash)
|
||||
]
|
||||
return msg_getdata(
|
||||
inv=invs,
|
||||
)
|
||||
|
||||
|
||||
class OffersDownload(ActiveDownload):
|
||||
|
||||
def __init__(self, limit: int, squeak_hash: bytes):
|
||||
self.squeak_hash = squeak_hash
|
||||
super().__init__(limit)
|
||||
|
||||
def is_interested(self, downloaded_object: DownloadedObject) -> bool:
|
||||
return downloaded_object.matches_requested_offer_hash(self.squeak_hash)
|
||||
|
||||
def get_download_msg(self) -> MsgSerializable:
|
||||
invs = [
|
||||
CInv(type=2, hash=self.squeak_hash)
|
||||
]
|
||||
return msg_getdata(inv=invs)
|
||||
|
||||
|
||||
class ActiveDownloadManager:
|
||||
|
||||
def __init__(self, network_manager):
|
||||
self.network_manager = network_manager
|
||||
self.downloads: Dict[str, ActiveDownload] = dict()
|
||||
self.executor = None
|
||||
|
||||
def start(self):
|
||||
logger.info("Starting Download Manager...")
|
||||
self.executor = ThreadPoolExecutor(max_workers=10)
|
||||
|
||||
def stop(self):
|
||||
for download in self.downloads.values():
|
||||
download.cancel()
|
||||
logger.info("Stopping Download Manager...")
|
||||
self.executor.shutdown(wait=True)
|
||||
logger.info("Stopped Download Manager.")
|
||||
|
||||
def lookup_counter(self, downloaded_object: DownloadedObject) -> Optional[ActiveDownload]:
|
||||
for name, interest in self.downloads.items():
|
||||
if interest.is_interested(downloaded_object):
|
||||
return interest
|
||||
return None
|
||||
|
||||
def run_download(self, download: ActiveDownload) -> DownloadResult:
|
||||
name_key = "download_key_{}".format(uuid.uuid1())
|
||||
self.downloads[name_key] = download
|
||||
future = self.executor.submit(self.download_task, download)
|
||||
try:
|
||||
return future.result()
|
||||
except TimeoutError:
|
||||
return download.get_result()
|
||||
finally:
|
||||
del self.downloads[name_key]
|
||||
|
||||
def download_task(self, download: ActiveDownload) -> DownloadResult:
|
||||
broadcast_fn = self.network_manager.broadcast_msg
|
||||
download.initiate_download(broadcast_fn)
|
||||
download.wait_for_complete(DOWNLOAD_TIMEOUT_S)
|
||||
return download.get_result()
|
||||
|
||||
def download_interest(self, limit: int, interest: CInterested) -> DownloadResult:
|
||||
download = InterestDownload(limit, interest)
|
||||
return self.run_download(download)
|
||||
|
||||
def download_hash(self, squeak_hash: bytes) -> DownloadResult:
|
||||
download = HashDownload(squeak_hash)
|
||||
return self.run_download(download)
|
||||
|
||||
def download_offers(self, limit: int, squeak_hash: bytes) -> DownloadResult:
|
||||
download = OffersDownload(limit, squeak_hash)
|
||||
return self.run_download(download)
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from squeak.core import CSqueak
|
||||
from squeak.net import CInterested
|
||||
|
||||
from squeaknode.core.interests import squeak_matches_interest
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.core.squeaks import get_hash
|
||||
|
||||
|
||||
class DownloadedObject:
|
||||
|
||||
def matches_requested_squeak_range(self, interest: CInterested) -> bool:
|
||||
"""Return True if the object matches the requested interest.
|
||||
"""
|
||||
|
||||
def matches_requested_squeak_hash(self, squeak_hash: bytes) -> bool:
|
||||
"""Return True if the object matches the requested offer hash.
|
||||
"""
|
||||
|
||||
def matches_requested_offer_hash(self, squeak_hash: bytes) -> bool:
|
||||
"""Return True if the object matches the requested squeak hash.
|
||||
"""
|
||||
|
||||
|
||||
class DownloadedSqueak(DownloadedObject):
|
||||
|
||||
def __init__(self, squeak: CSqueak):
|
||||
self.squeak = squeak
|
||||
|
||||
def matches_requested_squeak_range(self, interest: CInterested) -> bool:
|
||||
return squeak_matches_interest(self.squeak, interest)
|
||||
|
||||
def matches_requested_squeak_hash(self, squeak_hash: bytes) -> bool:
|
||||
return squeak_hash == get_hash(self.squeak)
|
||||
|
||||
def matches_requested_offer_hash(self, squeak_hash: bytes) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class DownloadedOffer(DownloadedObject):
|
||||
|
||||
def __init__(self, offer: Offer):
|
||||
self.offer = offer
|
||||
|
||||
def matches_requested_squeak_range(self, interest: CInterested) -> bool:
|
||||
return False
|
||||
|
||||
def matches_requested_squeak_hash(self, squeak_hash: bytes) -> bool:
|
||||
return False
|
||||
|
||||
def matches_requested_offer_hash(self, squeak_hash: bytes) -> bool:
|
||||
return self.offer.squeak_hash == squeak_hash
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from squeak.core import CSqueak
|
||||
from squeak.messages import msg_getdata
|
||||
from squeak.messages import MSG_SECRET_KEY
|
||||
from squeak.messages import MSG_SQUEAK
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import CInterested
|
||||
from squeak.net import CInv
|
||||
from squeak.net import CSqueakLocator
|
||||
|
||||
from squeaknode.core.block_range import BlockRange
|
||||
from squeaknode.core.interests import squeak_matches_interest
|
||||
from squeaknode.core.lightning_address import LightningAddressHostPort
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.core.sent_offer import SentOffer
|
||||
from squeaknode.core.squeak_core import SqueakCore
|
||||
from squeaknode.node.active_download_manager import ActiveDownload
|
||||
from squeaknode.node.downloaded_object import DownloadedOffer
|
||||
from squeaknode.node.downloaded_object import DownloadedSqueak
|
||||
from squeaknode.node.price_policy import PricePolicy
|
||||
from squeaknode.node.secret_key_reply import FreeSecretKeyReply
|
||||
from squeaknode.node.secret_key_reply import OfferReply
|
||||
from squeaknode.node.secret_key_reply import SecretKeyReply
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EMPTY_HASH = b'\x00' * 32
|
||||
|
||||
|
||||
class NetworkHandler:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
squeak_store: SqueakStore,
|
||||
squeak_core: SqueakCore,
|
||||
network_manager,
|
||||
download_manager,
|
||||
node_settings,
|
||||
config,
|
||||
):
|
||||
self.squeak_store = squeak_store
|
||||
self.squeak_core = squeak_core
|
||||
self.network_manager = network_manager
|
||||
self.active_download_manager = download_manager
|
||||
self.node_settings = node_settings
|
||||
self.config = config
|
||||
|
||||
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
|
||||
return self.squeak_store.get_squeak(squeak_hash)
|
||||
|
||||
def get_squeak_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
||||
return self.squeak_store.get_squeak_secret_key(squeak_hash)
|
||||
|
||||
def get_unknown_invs(self, invs):
|
||||
unknown_squeak_invs = self.get_unknown_squeaks(invs)
|
||||
unknown_secret_key_invs = self.get_unknown_secret_keys(invs)
|
||||
return unknown_squeak_invs + unknown_secret_key_invs
|
||||
|
||||
def get_unknown_squeaks(self, invs):
|
||||
return [
|
||||
inv for inv in invs
|
||||
if inv.type == MSG_SQUEAK
|
||||
and self.get_squeak(inv.hash) is None
|
||||
]
|
||||
|
||||
def get_unknown_secret_keys(self, invs):
|
||||
return [
|
||||
inv for inv in invs
|
||||
if inv.type == MSG_SECRET_KEY
|
||||
and self.get_squeak(inv.hash) is not None
|
||||
and self.get_squeak_secret_key(inv.hash) is None
|
||||
]
|
||||
|
||||
def save_squeak(self, squeak: CSqueak) -> Optional[bytes]:
|
||||
return self.save_active_download_squeak(squeak) or \
|
||||
self.save_followed_squeak(squeak)
|
||||
|
||||
def save_active_download_squeak(self, squeak: CSqueak) -> Optional[bytes]:
|
||||
"""Save the given squeak as an active download.
|
||||
|
||||
Returns:
|
||||
bytes: the hash of the saved squeak.
|
||||
"""
|
||||
counter = self.get_download_squeak_counter(squeak)
|
||||
if counter is None:
|
||||
return None
|
||||
saved_squeak_hash = self.squeak_store.save_squeak(squeak)
|
||||
if saved_squeak_hash is None:
|
||||
return None
|
||||
counter.increment()
|
||||
return saved_squeak_hash
|
||||
|
||||
def save_followed_squeak(self, squeak: CSqueak) -> Optional[bytes]:
|
||||
"""Save the given squeak because it matches the followed
|
||||
interest criteria.
|
||||
|
||||
Returns:
|
||||
bytes: the hash of the saved squeak.
|
||||
"""
|
||||
if not self.squeak_matches_interest(squeak):
|
||||
return None
|
||||
# TODO: catch exception if save_squeak fails (because of rate limit, for example).
|
||||
return self.squeak_store.save_squeak(squeak)
|
||||
|
||||
def squeak_matches_interest(self, squeak: CSqueak) -> bool:
|
||||
locator = self.get_interested_locator()
|
||||
for interest in locator.vInterested:
|
||||
if squeak_matches_interest(squeak, interest):
|
||||
return True
|
||||
return False
|
||||
|
||||
def unlock_squeak(self, squeak_hash: bytes, secret_key: bytes):
|
||||
self.squeak_store.save_secret_key(
|
||||
squeak_hash,
|
||||
secret_key,
|
||||
)
|
||||
|
||||
def get_secret_key_reply(self, squeak_hash: bytes, peer_address: PeerAddress) -> Optional[SecretKeyReply]:
|
||||
squeak = self.get_squeak(squeak_hash)
|
||||
price_msat = self.get_price_for_squeak(squeak, peer_address)
|
||||
lnd_external_address: Optional[LightningAddressHostPort] = None
|
||||
if self.config.lnd.external_host:
|
||||
lnd_external_address = LightningAddressHostPort(
|
||||
host=self.config.lnd.external_host,
|
||||
port=self.config.lnd.port,
|
||||
)
|
||||
if price_msat == 0:
|
||||
return self.get_free_squeak_secret_key_reply(
|
||||
squeak_hash,
|
||||
)
|
||||
else:
|
||||
return self.get_offer_reply(
|
||||
squeak_hash,
|
||||
lnd_external_address,
|
||||
peer_address,
|
||||
price_msat,
|
||||
)
|
||||
|
||||
def get_offer_reply(
|
||||
self,
|
||||
squeak_hash: bytes,
|
||||
lnd_external_address: Optional[LightningAddressHostPort],
|
||||
peer_address: PeerAddress,
|
||||
price_msat: int,
|
||||
) -> Optional[OfferReply]:
|
||||
sent_offer = self.get_sent_offer_for_peer(
|
||||
squeak_hash,
|
||||
peer_address,
|
||||
price_msat,
|
||||
)
|
||||
if sent_offer is None:
|
||||
return None
|
||||
try:
|
||||
offer = self.squeak_core.package_offer(
|
||||
sent_offer,
|
||||
lnd_external_address,
|
||||
)
|
||||
return OfferReply(
|
||||
squeak_hash=squeak_hash,
|
||||
offer=offer,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_free_squeak_secret_key_reply(self, squeak_hash: bytes) -> Optional[FreeSecretKeyReply]:
|
||||
secret_key = self.squeak_store.get_squeak_secret_key(squeak_hash)
|
||||
if secret_key is None:
|
||||
return None
|
||||
return FreeSecretKeyReply(
|
||||
squeak_hash=squeak_hash,
|
||||
secret_key=secret_key,
|
||||
)
|
||||
|
||||
def get_sent_offer_for_peer(
|
||||
self,
|
||||
squeak_hash: bytes,
|
||||
peer_address: PeerAddress,
|
||||
price_msat: int,
|
||||
) -> Optional[SentOffer]:
|
||||
# Check if there is an existing offer for the hash/peer_address combination
|
||||
sent_offer = self.squeak_store.get_sent_offer_by_squeak_hash_and_peer(
|
||||
squeak_hash,
|
||||
peer_address,
|
||||
)
|
||||
if sent_offer:
|
||||
return sent_offer
|
||||
squeak = self.squeak_store.get_squeak(squeak_hash)
|
||||
secret_key = self.squeak_store.get_squeak_secret_key(squeak_hash)
|
||||
if squeak is None or secret_key is None:
|
||||
return None
|
||||
try:
|
||||
sent_offer = self.squeak_core.create_offer(
|
||||
squeak,
|
||||
secret_key,
|
||||
peer_address,
|
||||
price_msat,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to create offer.")
|
||||
return None
|
||||
self.squeak_store.save_sent_offer(sent_offer)
|
||||
return sent_offer
|
||||
|
||||
def save_received_offer(self, offer: Offer, peer_address: PeerAddress) -> Optional[int]:
|
||||
squeak = self.squeak_store.get_squeak(offer.squeak_hash)
|
||||
secret_key = self.squeak_store.get_squeak_secret_key(offer.squeak_hash)
|
||||
if squeak is None or secret_key is not None:
|
||||
return None
|
||||
try:
|
||||
# TODO: Call unpack_offer with check_payment_point=True.
|
||||
received_offer = self.squeak_core.unpack_offer(
|
||||
squeak,
|
||||
offer,
|
||||
peer_address,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to save received offer.")
|
||||
return None
|
||||
return self.squeak_store.save_received_offer(received_offer)
|
||||
|
||||
def get_reply_invs(self, interest):
|
||||
squeak_hashes = self.get_local_squeaks(interest)
|
||||
secret_key_hashes = self.get_local_secret_keys(interest)
|
||||
squeak_invs = [
|
||||
CInv(type=MSG_SQUEAK, hash=squeak_hash)
|
||||
for squeak_hash in squeak_hashes]
|
||||
secret_key_invs = [
|
||||
CInv(type=MSG_SECRET_KEY, hash=squeak_hash)
|
||||
for squeak_hash in secret_key_hashes]
|
||||
return squeak_invs + secret_key_invs
|
||||
|
||||
def get_local_squeaks(self, interest: CInterested):
|
||||
min_block = interest.nMinBlockHeight if interest.nMinBlockHeight != -1 else None
|
||||
max_block = interest.nMaxBlockHeight if interest.nMaxBlockHeight != -1 else None
|
||||
reply_to_hash = interest.hashReplySqk if interest.hashReplySqk != EMPTY_HASH else None
|
||||
return self.squeak_store.lookup_squeaks(
|
||||
interest.pubkeys,
|
||||
min_block,
|
||||
max_block,
|
||||
reply_to_hash,
|
||||
)
|
||||
|
||||
def get_local_secret_keys(self, interest: CInterested):
|
||||
min_block = interest.nMinBlockHeight if interest.nMinBlockHeight != -1 else None
|
||||
max_block = interest.nMaxBlockHeight if interest.nMaxBlockHeight != -1 else None
|
||||
reply_to_hash = interest.hashReplySqk if interest.hashReplySqk != EMPTY_HASH else None
|
||||
return self.squeak_store.lookup_secret_keys(
|
||||
interest.pubkeys,
|
||||
min_block,
|
||||
max_block,
|
||||
reply_to_hash,
|
||||
)
|
||||
|
||||
def request_offers(self, squeak_hash: bytes):
|
||||
logger.info("Requesting offers for squeak: {}".format(
|
||||
squeak_hash.hex(),
|
||||
))
|
||||
invs = [
|
||||
CInv(type=2, hash=squeak_hash)
|
||||
]
|
||||
getdata_msg = msg_getdata(inv=invs)
|
||||
self.broadcast_msg(getdata_msg)
|
||||
|
||||
def handle_received_offer(self, offer: Offer, peer_address: PeerAddress) -> Optional[int]:
|
||||
received_offer_id = self.save_received_offer(
|
||||
offer,
|
||||
peer_address,
|
||||
)
|
||||
if received_offer_id is None:
|
||||
return None
|
||||
counter = self.get_download_offer_counter(offer)
|
||||
if counter is not None:
|
||||
counter.increment()
|
||||
return received_offer_id
|
||||
|
||||
def get_download_offer_counter(self, offer: Offer) -> Optional[ActiveDownload]:
|
||||
downloaded_offer = DownloadedOffer(offer)
|
||||
return self.active_download_manager.lookup_counter(downloaded_offer)
|
||||
|
||||
def get_download_squeak_counter(self, squeak: CSqueak) -> Optional[ActiveDownload]:
|
||||
downloaded_squeak = DownloadedSqueak(squeak)
|
||||
return self.active_download_manager.lookup_counter(downloaded_squeak)
|
||||
|
||||
def get_price_for_squeak(self, squeak: CSqueak, peer_address: PeerAddress) -> int:
|
||||
price_policy = PricePolicy(
|
||||
self.squeak_store,
|
||||
self.config,
|
||||
self.node_settings,
|
||||
)
|
||||
return price_policy.get_price(squeak, peer_address)
|
||||
|
||||
def broadcast_msg(self, msg: MsgSerializable) -> int:
|
||||
return self.network_manager.broadcast_msg(msg)
|
||||
|
||||
def get_interested_locator(self) -> CSqueakLocator:
|
||||
block_range = self.get_interested_block_range()
|
||||
followed_public_keys = self.squeak_store.get_followed_public_keys()
|
||||
if len(followed_public_keys) == 0:
|
||||
return CSqueakLocator(
|
||||
vInterested=[],
|
||||
)
|
||||
interests = [
|
||||
CInterested(
|
||||
pubkeys=followed_public_keys,
|
||||
nMinBlockHeight=block_range.min_block,
|
||||
nMaxBlockHeight=block_range.max_block,
|
||||
)
|
||||
]
|
||||
return CSqueakLocator(
|
||||
vInterested=interests,
|
||||
)
|
||||
|
||||
def get_interested_block_range(self) -> BlockRange:
|
||||
max_block = self.squeak_core.get_best_block_height()
|
||||
min_block = max(
|
||||
0,
|
||||
# TODO: rename this.
|
||||
max_block - self.config.node.interest_block_interval,
|
||||
)
|
||||
return BlockRange(min_block, max_block)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.periodic_worker import PeriodicWorker
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PeerConnectionWorker(PeriodicWorker):
|
||||
def __init__(
|
||||
self,
|
||||
squeak_store: SqueakStore,
|
||||
network_manager: NetworkManager,
|
||||
connect_interval_s: int,
|
||||
):
|
||||
self.squeak_store = squeak_store
|
||||
self.network_manager = network_manager
|
||||
self.connect_interval_s = connect_interval_s
|
||||
|
||||
def work_fn(self):
|
||||
peers = self.squeak_store.get_autoconnect_peers()
|
||||
for peer in peers:
|
||||
self.network_manager.connect_peer_async(
|
||||
peer.address,
|
||||
)
|
||||
|
||||
def get_interval_s(self):
|
||||
return self.connect_interval_s
|
||||
|
||||
def get_name(self):
|
||||
return "peer_connection_worker"
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from squeaknode.bitcoin.bitcoin_block_subscription_client import BitcoinBlockSubscriptionClient
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.network_handler import NetworkHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PeerSubscriptionUpdateWorker:
|
||||
def __init__(
|
||||
self,
|
||||
network_manager: NetworkManager,
|
||||
network_handler: NetworkHandler,
|
||||
block_subscription_client: BitcoinBlockSubscriptionClient,
|
||||
):
|
||||
self.network_manager = network_manager
|
||||
self.network_handler = network_handler
|
||||
self.block_subscription_client = block_subscription_client
|
||||
|
||||
def start_running(self):
|
||||
threading.Thread(
|
||||
target=self.subscribe_blocks,
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def subscribe_blocks(self):
|
||||
for block_hash in self.block_subscription_client.get_blocks():
|
||||
logger.info("Got block from zeromq: {}".format(block_hash.hex()))
|
||||
locator = self.network_handler.get_interested_locator()
|
||||
self.network_manager.update_local_subscriptions(locator)
|
||||
|
|
@ -22,8 +22,6 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from squeak.core import CSqueak
|
||||
|
||||
from squeaknode.config.config import SqueaknodeConfig
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.core.squeak_peer import SqueakPeer
|
||||
|
|
@ -42,14 +40,10 @@ class PricePolicy:
|
|||
self.config = config
|
||||
self.node_settings = node_settings
|
||||
|
||||
def get_price(self, squeak: CSqueak, peer_address: PeerAddress) -> int:
|
||||
def get_price(self) -> int:
|
||||
"""Get the price to sell this squeak to this peer.
|
||||
|
||||
"""
|
||||
# Return zero for price if peer is configured to be share for free.
|
||||
peer = self.get_peer(peer_address)
|
||||
if peer is not None and peer.share_for_free:
|
||||
return 0
|
||||
sell_price_msat = self.get_sell_price_msat()
|
||||
if sell_price_msat is None:
|
||||
return self.get_default_price()
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from squeak.messages import msg_secretkey
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import COffer
|
||||
|
||||
from squeaknode.core.offer import Offer
|
||||
|
||||
|
||||
class SecretKeyReply:
|
||||
|
||||
def get_msg(self) -> MsgSerializable:
|
||||
"""Return the message to reply to the other peer.
|
||||
"""
|
||||
|
||||
|
||||
class FreeSecretKeyReply(SecretKeyReply):
|
||||
|
||||
def __init__(self, squeak_hash: bytes, secret_key: bytes):
|
||||
self.squeak_hash = squeak_hash
|
||||
self.secret_key = secret_key
|
||||
|
||||
def get_msg(self) -> MsgSerializable:
|
||||
return msg_secretkey(
|
||||
hashSqk=self.squeak_hash,
|
||||
secretKey=self.secret_key,
|
||||
)
|
||||
|
||||
|
||||
class OfferReply(SecretKeyReply):
|
||||
|
||||
def __init__(self, squeak_hash: bytes, offer: Offer):
|
||||
self.squeak_hash = squeak_hash
|
||||
self.offer = offer
|
||||
|
||||
def get_msg(self) -> MsgSerializable:
|
||||
offer = COffer(
|
||||
nonce=self.offer.nonce,
|
||||
strPaymentInfo=self.offer.payment_request.encode(
|
||||
'utf-8'),
|
||||
host=self.offer.host.encode('utf-8'),
|
||||
port=self.offer.port,
|
||||
)
|
||||
return msg_secretkey(
|
||||
hashSqk=self.squeak_hash,
|
||||
offer=offer,
|
||||
)
|
||||
|
|
@ -21,20 +21,16 @@
|
|||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from squeak.core import CSqueak
|
||||
from squeak.core.keys import SqueakPrivateKey
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
from squeak.messages import msg_getdata
|
||||
from squeak.messages import MsgSerializable
|
||||
from squeak.net import CInterested
|
||||
from squeak.net import CInv
|
||||
|
||||
from squeaknode.core.connected_peer import ConnectedPeer
|
||||
from squeaknode.client.network_controller import NetworkController
|
||||
from squeaknode.core.download_result import DownloadResult
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.core.received_offer import ReceivedOffer
|
||||
from squeaknode.core.received_payment import ReceivedPayment
|
||||
|
|
@ -64,8 +60,6 @@ class SqueakController:
|
|||
squeak_store: SqueakStore,
|
||||
squeak_core: SqueakCore,
|
||||
payment_processor,
|
||||
network_manager,
|
||||
download_manager,
|
||||
tweet_forwarder,
|
||||
node_settings,
|
||||
config,
|
||||
|
|
@ -74,8 +68,6 @@ class SqueakController:
|
|||
self.squeak_store = squeak_store
|
||||
self.squeak_core = squeak_core
|
||||
self.payment_processor = payment_processor
|
||||
self.network_manager = network_manager
|
||||
self.active_download_manager = download_manager
|
||||
self.tweet_forwarder = tweet_forwarder
|
||||
self.node_settings = node_settings
|
||||
self.config = config
|
||||
|
|
@ -279,11 +271,21 @@ class SqueakController:
|
|||
def get_squeak_entry(self, squeak_hash: bytes) -> Optional[SqueakEntry]:
|
||||
return self.squeak_store.get_squeak_entry(squeak_hash)
|
||||
|
||||
def download_single_squeak(self, squeak_hash: bytes) -> DownloadResult:
|
||||
network_controller = NetworkController(self.squeak_store, self.config)
|
||||
network_controller.download_single_squeak(squeak_hash)
|
||||
return DownloadResult(1, 1, 0, 9999)
|
||||
|
||||
def get_timeline_squeak_entries(
|
||||
self,
|
||||
limit: int,
|
||||
last_entry: Optional[SqueakEntry],
|
||||
) -> List[SqueakEntry]:
|
||||
# TODO: remove this temporary hack, after converting this to websockets.
|
||||
logger.info('Start downloading timeline...')
|
||||
network_controller = NetworkController(self.squeak_store, self.config)
|
||||
network_controller.download_timeline()
|
||||
logger.info('Finished downloading timeline.')
|
||||
return self.squeak_store.get_timeline_squeak_entries(limit, last_entry)
|
||||
|
||||
def get_liked_squeak_entries(
|
||||
|
|
@ -299,6 +301,11 @@ class SqueakController:
|
|||
limit: int,
|
||||
last_entry: Optional[SqueakEntry],
|
||||
) -> List[SqueakEntry]:
|
||||
# TODO: remove this temporary hack, after converting this to websockets.
|
||||
logger.info('Start downloading pubkey squeaks...')
|
||||
network_controller = NetworkController(self.squeak_store, self.config)
|
||||
network_controller.download_pubkey_squeaks(public_key)
|
||||
logger.info('Finished downloading pubkey squeaks.')
|
||||
return self.squeak_store.get_squeak_entries_for_public_key(
|
||||
public_key,
|
||||
limit,
|
||||
|
|
@ -351,125 +358,6 @@ class SqueakController:
|
|||
def unlike_squeak(self, squeak_hash: bytes):
|
||||
return self.squeak_store.unlike_squeak(squeak_hash)
|
||||
|
||||
def connect_peer(self, peer_address: PeerAddress) -> None:
|
||||
logger.info("Connect to peer: {}".format(
|
||||
peer_address,
|
||||
))
|
||||
self.network_manager.connect_peer_sync(peer_address)
|
||||
|
||||
def get_connected_peer(self, peer_address: PeerAddress) -> Optional[ConnectedPeer]:
|
||||
peer = self.network_manager.get_connected_peer(peer_address)
|
||||
if peer is None:
|
||||
return None
|
||||
return ConnectedPeer(
|
||||
peer=peer,
|
||||
saved_peer=self.squeak_store.get_peer_by_address(
|
||||
peer_address,
|
||||
),
|
||||
)
|
||||
|
||||
def get_connected_peers(self) -> List[ConnectedPeer]:
|
||||
peers = self.network_manager.get_connected_peers()
|
||||
return [
|
||||
ConnectedPeer(
|
||||
peer=peer,
|
||||
saved_peer=self.squeak_store.get_peer_by_address(
|
||||
peer.remote_address,
|
||||
),
|
||||
) for peer in peers
|
||||
]
|
||||
|
||||
def download_squeaks(
|
||||
self,
|
||||
public_keys: List[SqueakPublicKey],
|
||||
min_block: int,
|
||||
max_block: int,
|
||||
replyto_hash: Optional[bytes],
|
||||
) -> DownloadResult:
|
||||
interest = CInterested(
|
||||
pubkeys=public_keys,
|
||||
nMinBlockHeight=min_block,
|
||||
nMaxBlockHeight=max_block,
|
||||
replyto_squeak_hash=replyto_hash,
|
||||
) if replyto_hash else CInterested(
|
||||
pubkeys=public_keys,
|
||||
nMinBlockHeight=min_block,
|
||||
nMaxBlockHeight=max_block,
|
||||
)
|
||||
return self.active_download_manager.download_interest(10, interest)
|
||||
|
||||
def download_single_squeak(self, squeak_hash: bytes) -> DownloadResult:
|
||||
logger.info("Downloading single squeak: {}".format(
|
||||
squeak_hash.hex(),
|
||||
))
|
||||
return self.active_download_manager.download_hash(squeak_hash)
|
||||
|
||||
def download_offers(self, squeak_hash: bytes) -> DownloadResult:
|
||||
logger.info("Downloading offers for squeak: {}".format(
|
||||
squeak_hash.hex(),
|
||||
))
|
||||
return self.active_download_manager.download_offers(10, squeak_hash)
|
||||
|
||||
def request_offers(self, squeak_hash: bytes):
|
||||
logger.info("Requesting offers for squeak: {}".format(
|
||||
squeak_hash.hex(),
|
||||
))
|
||||
invs = [
|
||||
CInv(type=2, hash=squeak_hash)
|
||||
]
|
||||
getdata_msg = msg_getdata(inv=invs)
|
||||
self.broadcast_msg(getdata_msg)
|
||||
|
||||
def download_replies(self, squeak_hash: bytes) -> DownloadResult:
|
||||
logger.info("Downloading replies for squeak: {}".format(
|
||||
squeak_hash.hex(),
|
||||
))
|
||||
interest = CInterested(
|
||||
hashReplySqk=squeak_hash,
|
||||
)
|
||||
return self.active_download_manager.download_interest(10, interest)
|
||||
|
||||
def download_public_key_squeaks(self, public_key: SqueakPublicKey) -> DownloadResult:
|
||||
logger.info("Downloading squeaks for public key: {}".format(
|
||||
public_key,
|
||||
))
|
||||
interest = CInterested(
|
||||
pubkeys=[public_key],
|
||||
)
|
||||
return self.active_download_manager.download_interest(10, interest)
|
||||
|
||||
def broadcast_msg(self, msg: MsgSerializable) -> int:
|
||||
return self.network_manager.broadcast_msg(msg)
|
||||
|
||||
def disconnect_peer(self, peer_address: PeerAddress) -> None:
|
||||
logger.info("Disconnect to peer: {}".format(
|
||||
peer_address,
|
||||
))
|
||||
self.network_manager.disconnect_peer(peer_address)
|
||||
|
||||
def subscribe_connected_peers(self, stopped: threading.Event) -> Iterable[List[ConnectedPeer]]:
|
||||
for peers in self.network_manager.subscribe_connected_peers(stopped):
|
||||
yield [
|
||||
ConnectedPeer(
|
||||
peer=peer,
|
||||
saved_peer=self.squeak_store.get_peer_by_address(
|
||||
peer.remote_address,
|
||||
)
|
||||
) for peer in peers
|
||||
]
|
||||
|
||||
def subscribe_connected_peer(self, peer_address: PeerAddress, stopped: threading.Event) -> Iterable[Optional[ConnectedPeer]]:
|
||||
for peer in self.network_manager.subscribe_connected_peer(peer_address, stopped):
|
||||
if peer is None:
|
||||
yield None
|
||||
else:
|
||||
yield ConnectedPeer(
|
||||
peer=peer,
|
||||
saved_peer=self.squeak_store.get_peer_by_address(
|
||||
peer.remote_address,
|
||||
)
|
||||
)
|
||||
|
||||
def subscribe_new_squeaks(self, stopped: threading.Event):
|
||||
yield from self.squeak_store.subscribe_new_squeaks(stopped)
|
||||
|
||||
|
|
@ -520,7 +408,11 @@ class SqueakController:
|
|||
yield self.get_squeak_entry(squeak_hash)
|
||||
|
||||
def get_external_address(self) -> PeerAddress:
|
||||
return self.network_manager.external_address
|
||||
return PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=self.config.server.external_address or '',
|
||||
port=self.config.server.port or 0,
|
||||
)
|
||||
|
||||
def get_default_peer_port(self) -> int:
|
||||
return self.default_port
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from squeak.params import SelectParams
|
|||
from squeaknode.admin.squeak_admin_server_handler import SqueakAdminServerHandler
|
||||
from squeaknode.admin.squeak_admin_server_servicer import SqueakAdminServerServicer
|
||||
from squeaknode.admin.webapp.app import SqueakAdminWebServer
|
||||
from squeaknode.bitcoin.bitcoin_block_subscription_client import BitcoinBlockSubscriptionClient
|
||||
from squeaknode.bitcoin.bitcoin_core_client import BitcoinCoreClient
|
||||
from squeaknode.config.config import SqueaknodeConfig
|
||||
from squeaknode.core.squeak_core import SqueakCore
|
||||
|
|
@ -35,22 +34,16 @@ from squeaknode.db.db_engine import get_connection_string
|
|||
from squeaknode.db.db_engine import get_engine
|
||||
from squeaknode.db.squeak_db import SqueakDb
|
||||
from squeaknode.lightning.lnd_lightning_client import LNDLightningClient
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.active_download_manager import ActiveDownloadManager
|
||||
from squeaknode.node.network_handler import NetworkHandler
|
||||
from squeaknode.node.node_settings import NodeSettings
|
||||
from squeaknode.node.payment_processor import PaymentProcessor
|
||||
from squeaknode.node.peer_connection_worker import PeerConnectionWorker
|
||||
from squeaknode.node.peer_subscription_update_worker import PeerSubscriptionUpdateWorker
|
||||
from squeaknode.node.process_forward_tweets_worker import ProcessForwardTweetsWorker
|
||||
from squeaknode.node.process_received_payments_worker import ProcessReceivedPaymentsWorker
|
||||
from squeaknode.node.squeak_controller import SqueakController
|
||||
from squeaknode.node.squeak_deletion_worker import SqueakDeletionWorker
|
||||
from squeaknode.node.squeak_offer_expiry_worker import SqueakOfferExpiryWorker
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
from squeaknode.node.update_follows_worker import UpdateFollowsWorker
|
||||
from squeaknode.node.update_subscribed_secret_key_worker import UpdateSubscribedSecretKeysWorker
|
||||
from squeaknode.node.update_subscribed_squeak_worker import UpdateSubscribedSqueaksWorker
|
||||
from squeaknode.server.app import SqueakPeerWebServer
|
||||
from squeaknode.server.squeak_peer_server_handler import SqueakPeerServerHandler
|
||||
from squeaknode.twitter.twitter_forwarder import TwitterForwarder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -65,56 +58,43 @@ class SqueakNode:
|
|||
self.create_node_settings()
|
||||
self.create_lightning_client()
|
||||
self.create_bitcoin_client()
|
||||
self.create_bitcoin_block_subscription_client()
|
||||
self.create_squeak_core()
|
||||
self.create_squeak_store()
|
||||
self.create_payment_processor()
|
||||
self.create_twitter_forwarder()
|
||||
self.create_network_manager()
|
||||
self.create_download_manager()
|
||||
self.create_squeak_controller()
|
||||
self.create_network_handler()
|
||||
|
||||
self.create_peer_handler()
|
||||
self.create_peer_web_server()
|
||||
|
||||
self.create_admin_handler()
|
||||
self.create_admin_rpc_server()
|
||||
self.create_admin_web_server()
|
||||
self.create_received_payment_processor_worker()
|
||||
self.create_forward_tweets_processor_worker()
|
||||
self.create_peer_connection_worker()
|
||||
self.create_squeak_deletion_worker()
|
||||
self.create_offer_expiry_worker()
|
||||
self.create_new_squeak_worker()
|
||||
self.create_new_secret_key_worker()
|
||||
self.create_new_follow_worker()
|
||||
self.create_peer_subscription_update_worker()
|
||||
self.create_forward_tweets_processor_worker()
|
||||
|
||||
def start_running(self):
|
||||
self.squeak_db.init_with_retries()
|
||||
self.lightning_client.init()
|
||||
|
||||
self.network_manager.start(self.network_handler)
|
||||
if self.config.rpc.enabled:
|
||||
self.admin_rpc_server.start()
|
||||
if self.config.webadmin.enabled:
|
||||
self.admin_web_server.start()
|
||||
self.peer_web_server.start()
|
||||
self.received_payment_processor_worker.start_running()
|
||||
self.forward_tweets_processor_worker.start_running()
|
||||
self.peer_connection_worker.start()
|
||||
self.squeak_deletion_worker.start()
|
||||
self.offer_expiry_worker.start()
|
||||
self.new_squeak_worker.start_running()
|
||||
self.new_secret_key_worker.start_running()
|
||||
self.new_follow_worker.start_running()
|
||||
self.new_bitcoin_block_worker.start_running()
|
||||
self.download_manager.start()
|
||||
self.forward_tweets_processor_worker.start_running()
|
||||
|
||||
def stop_running(self):
|
||||
self.download_manager.stop()
|
||||
self.admin_web_server.stop()
|
||||
self.admin_rpc_server.stop()
|
||||
self.network_manager.stop()
|
||||
self.peer_web_server.stop()
|
||||
self.received_payment_processor_worker.stop_running()
|
||||
self.forward_tweets_processor_worker.stop_running()
|
||||
self.new_squeak_worker.stop_running()
|
||||
|
||||
def set_network_params(self):
|
||||
SelectParams(self.config.node.network)
|
||||
|
|
@ -150,12 +130,6 @@ class SqueakNode:
|
|||
self.config.bitcoin.rpc_ssl_cert,
|
||||
)
|
||||
|
||||
def create_bitcoin_block_subscription_client(self):
|
||||
self.bitcoin_block_subscription_client = BitcoinBlockSubscriptionClient(
|
||||
self.config.bitcoin.rpc_host,
|
||||
self.config.bitcoin.zeromq_hashblock_port,
|
||||
)
|
||||
|
||||
def create_squeak_core(self):
|
||||
self.squeak_core = SqueakCore(
|
||||
self.bitcoin_client,
|
||||
|
|
@ -187,41 +161,30 @@ class SqueakNode:
|
|||
self.config.twitter.forward_tweets_retry_s,
|
||||
)
|
||||
|
||||
def create_network_manager(self):
|
||||
self.network_manager = NetworkManager(
|
||||
self.config,
|
||||
squeak.params.params.DEFAULT_PORT,
|
||||
)
|
||||
|
||||
def create_squeak_controller(self):
|
||||
self.squeak_controller = SqueakController(
|
||||
self.squeak_store,
|
||||
self.squeak_core,
|
||||
self.payment_processor,
|
||||
self.network_manager,
|
||||
self.download_manager,
|
||||
self.twitter_forwarder,
|
||||
self.node_settings,
|
||||
self.config,
|
||||
squeak.params.params.DEFAULT_PORT,
|
||||
)
|
||||
|
||||
def create_network_handler(self):
|
||||
self.network_handler = NetworkHandler(
|
||||
self.squeak_store,
|
||||
self.squeak_core,
|
||||
self.network_manager,
|
||||
self.download_manager,
|
||||
self.node_settings,
|
||||
self.config,
|
||||
)
|
||||
|
||||
def create_admin_handler(self):
|
||||
self.admin_handler = SqueakAdminServerHandler(
|
||||
self.lightning_client,
|
||||
self.squeak_controller,
|
||||
)
|
||||
|
||||
def create_peer_handler(self):
|
||||
self.peer_handler = SqueakPeerServerHandler(
|
||||
self.squeak_store,
|
||||
self.node_settings,
|
||||
self.config,
|
||||
)
|
||||
|
||||
def create_admin_rpc_server(self):
|
||||
self.admin_rpc_server = SqueakAdminServerServicer(
|
||||
self.config.rpc.host,
|
||||
|
|
@ -241,23 +204,18 @@ class SqueakNode:
|
|||
self.admin_handler,
|
||||
)
|
||||
|
||||
def create_peer_web_server(self):
|
||||
self.peer_web_server = SqueakPeerWebServer(
|
||||
self.config.server.host,
|
||||
squeak.params.params.DEFAULT_PORT,
|
||||
self.peer_handler,
|
||||
)
|
||||
|
||||
def create_received_payment_processor_worker(self):
|
||||
self.received_payment_processor_worker = ProcessReceivedPaymentsWorker(
|
||||
self.payment_processor,
|
||||
)
|
||||
|
||||
def create_forward_tweets_processor_worker(self):
|
||||
self.forward_tweets_processor_worker = ProcessForwardTweetsWorker(
|
||||
self.twitter_forwarder,
|
||||
)
|
||||
|
||||
def create_peer_connection_worker(self):
|
||||
self.peer_connection_worker = PeerConnectionWorker(
|
||||
self.squeak_store,
|
||||
self.network_manager,
|
||||
self.config.node.peer_autoconnect_interval_s,
|
||||
)
|
||||
|
||||
def create_squeak_deletion_worker(self):
|
||||
self.squeak_deletion_worker = SqueakDeletionWorker(
|
||||
self.squeak_store,
|
||||
|
|
@ -270,33 +228,7 @@ class SqueakNode:
|
|||
self.config.node.offer_deletion_interval_s,
|
||||
)
|
||||
|
||||
def create_new_squeak_worker(self):
|
||||
self.new_squeak_worker = UpdateSubscribedSqueaksWorker(
|
||||
self.squeak_store,
|
||||
self.network_manager,
|
||||
)
|
||||
|
||||
def create_new_secret_key_worker(self):
|
||||
self.new_secret_key_worker = UpdateSubscribedSecretKeysWorker(
|
||||
self.squeak_store,
|
||||
self.network_manager,
|
||||
)
|
||||
|
||||
def create_new_follow_worker(self):
|
||||
self.new_follow_worker = UpdateFollowsWorker(
|
||||
self.squeak_store,
|
||||
self.network_manager,
|
||||
self.network_handler,
|
||||
)
|
||||
|
||||
def create_peer_subscription_update_worker(self):
|
||||
self.new_bitcoin_block_worker = PeerSubscriptionUpdateWorker(
|
||||
self.network_manager,
|
||||
self.network_handler,
|
||||
self.bitcoin_block_subscription_client,
|
||||
)
|
||||
|
||||
def create_download_manager(self):
|
||||
self.download_manager = ActiveDownloadManager(
|
||||
self.network_manager,
|
||||
def create_forward_tweets_processor_worker(self):
|
||||
self.forward_tweets_processor_worker = ProcessForwardTweetsWorker(
|
||||
self.twitter_forwarder,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ from squeak.core import CSqueak
|
|||
from squeak.core.keys import SqueakPrivateKey
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from squeaknode.core.lightning_address import LightningAddressHostPort
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.core.peers import create_saved_peer
|
||||
from squeaknode.core.profiles import create_contact_profile
|
||||
|
|
@ -189,6 +191,55 @@ class SqueakStore:
|
|||
def save_sent_offer(self, sent_offer: SentOffer) -> int:
|
||||
return self.squeak_db.insert_sent_offer(sent_offer)
|
||||
|
||||
def get_sent_offer_for_peer(
|
||||
self,
|
||||
squeak_hash: bytes,
|
||||
peer_address: PeerAddress,
|
||||
price_msat: int,
|
||||
) -> Optional[SentOffer]:
|
||||
# Check if there is an existing offer for the hash/peer_address combination
|
||||
sent_offer = self.get_sent_offer_by_squeak_hash_and_peer(
|
||||
squeak_hash,
|
||||
peer_address,
|
||||
)
|
||||
if sent_offer:
|
||||
return sent_offer
|
||||
squeak = self.get_squeak(squeak_hash)
|
||||
secret_key = self.get_squeak_secret_key(squeak_hash)
|
||||
if squeak is None or secret_key is None:
|
||||
return None
|
||||
try:
|
||||
sent_offer = self.squeak_core.create_offer(
|
||||
squeak,
|
||||
secret_key,
|
||||
peer_address,
|
||||
price_msat,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to create offer.")
|
||||
return None
|
||||
self.save_sent_offer(sent_offer)
|
||||
return sent_offer
|
||||
|
||||
def get_packaged_offer(
|
||||
self,
|
||||
squeak_hash: bytes,
|
||||
peer_address: PeerAddress,
|
||||
price_msat: int,
|
||||
lnd_external_address: Optional[LightningAddressHostPort],
|
||||
) -> Optional[Offer]:
|
||||
sent_offer = self.get_sent_offer_for_peer(
|
||||
squeak_hash,
|
||||
peer_address,
|
||||
price_msat,
|
||||
)
|
||||
if sent_offer is None:
|
||||
return None
|
||||
return self.squeak_core.package_offer(
|
||||
sent_offer,
|
||||
lnd_external_address,
|
||||
)
|
||||
|
||||
def create_signing_profile(self, profile_name: str) -> int:
|
||||
squeak_profile = create_signing_profile(
|
||||
profile_name,
|
||||
|
|
@ -427,6 +478,14 @@ class SqueakStore:
|
|||
self.new_received_offer_listener.handle_new_item(received_offer)
|
||||
return received_offer_id
|
||||
|
||||
def handle_offer(self, squeak: CSqueak, offer: Offer, peer_address: PeerAddress):
|
||||
received_offer = self.squeak_core.unpack_offer(
|
||||
squeak,
|
||||
offer,
|
||||
peer_address,
|
||||
)
|
||||
self.save_received_offer(received_offer)
|
||||
|
||||
def get_followed_public_keys(self) -> List[SqueakPublicKey]:
|
||||
followed_profiles = self.squeak_db.get_following_profiles()
|
||||
return [profile.public_key for profile in followed_profiles]
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.network_handler import NetworkHandler
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateFollowsWorker:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
squeak_store: SqueakStore,
|
||||
network_manager: NetworkManager,
|
||||
network_handler: NetworkHandler,
|
||||
):
|
||||
self.squeak_store = squeak_store
|
||||
self.network_manager = network_manager
|
||||
self.network_handler = network_handler
|
||||
self.stopped = threading.Event()
|
||||
|
||||
def start_running(self):
|
||||
threading.Thread(
|
||||
target=self.handle_new_follow,
|
||||
name="new_follows_thread",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def stop_running(self):
|
||||
self.stopped.set()
|
||||
|
||||
def handle_new_follow(self):
|
||||
logger.debug("Starting UpdateFollowsWorker...")
|
||||
for _ in self.squeak_store.subscribe_follows(
|
||||
self.stopped,
|
||||
):
|
||||
logger.debug("Handling update subscriptions event")
|
||||
locator = self.network_handler.get_interested_locator()
|
||||
self.network_manager.update_local_subscriptions(locator)
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from squeak.messages import msg_inv
|
||||
from squeak.messages import MSG_SECRET_KEY
|
||||
from squeak.net import CInv
|
||||
|
||||
from squeaknode.core.squeaks import get_hash
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateSubscribedSecretKeysWorker:
|
||||
|
||||
def __init__(self, squeak_store: SqueakStore, network_manager: NetworkManager):
|
||||
self.squeak_store = squeak_store
|
||||
self.network_manager = network_manager
|
||||
self.stopped = threading.Event()
|
||||
|
||||
def start_running(self):
|
||||
threading.Thread(
|
||||
target=self.handle_new_secret_keys,
|
||||
name="new_secret_keys_worker_thread",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def stop_running(self):
|
||||
self.stopped.set()
|
||||
|
||||
def handle_new_secret_keys(self):
|
||||
logger.debug("Starting UpdateSubscribedSecretKeysWorker...")
|
||||
for squeak in self.squeak_store.subscribe_new_secret_keys(
|
||||
self.stopped,
|
||||
):
|
||||
logger.debug("Handling new secret key for squeak hash: {!r}".format(
|
||||
get_hash(squeak).hex(),
|
||||
))
|
||||
self.forward_secret_key(squeak)
|
||||
|
||||
def forward_secret_key(self, squeak):
|
||||
logger.debug("Forward new secret key for hash: {!r}".format(
|
||||
get_hash(squeak).hex(),
|
||||
))
|
||||
for peer in self.network_manager.get_connected_peers():
|
||||
if peer.is_remote_subscribed(squeak):
|
||||
logger.debug("Forwarding to peer: {}".format(
|
||||
peer,
|
||||
))
|
||||
squeak_hash = get_hash(squeak)
|
||||
inv = CInv(type=MSG_SECRET_KEY, hash=squeak_hash)
|
||||
inv_msg = msg_inv(inv=[inv])
|
||||
peer.send_msg(inv_msg)
|
||||
logger.debug("Finished checking peers to forward.")
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from squeak.messages import msg_inv
|
||||
from squeak.messages import MSG_SQUEAK
|
||||
from squeak.net import CInv
|
||||
|
||||
from squeaknode.core.squeaks import get_hash
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateSubscribedSqueaksWorker:
|
||||
|
||||
def __init__(self, squeak_store: SqueakStore, network_manager: NetworkManager):
|
||||
self.squeak_store = squeak_store
|
||||
self.network_manager = network_manager
|
||||
self.stopped = threading.Event()
|
||||
|
||||
def start_running(self):
|
||||
threading.Thread(
|
||||
target=self.handle_new_squeaks,
|
||||
name="new_squeaks_worker_thread",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def stop_running(self):
|
||||
self.stopped.set()
|
||||
|
||||
def handle_new_squeaks(self):
|
||||
logger.debug("Starting UpdateSubscribedSqueaksWorker...")
|
||||
for squeak in self.squeak_store.subscribe_new_squeaks(
|
||||
self.stopped,
|
||||
):
|
||||
logger.debug("Handling new squeak: {!r}".format(
|
||||
get_hash(squeak).hex(),
|
||||
))
|
||||
self.forward_squeak(squeak)
|
||||
|
||||
def forward_squeak(self, squeak):
|
||||
logger.debug("Forward new squeak: {!r}".format(
|
||||
get_hash(squeak).hex(),
|
||||
))
|
||||
for peer in self.network_manager.get_connected_peers():
|
||||
if peer.is_remote_subscribed(squeak):
|
||||
logger.debug("Forwarding to peer: {}".format(
|
||||
peer,
|
||||
))
|
||||
squeak_hash = get_hash(squeak)
|
||||
inv = CInv(type=MSG_SQUEAK, hash=squeak_hash)
|
||||
inv_msg = msg_inv(inv=[inv])
|
||||
peer.send_msg(inv_msg)
|
||||
logger.debug("Finished checking peers to forward.")
|
||||
|
|
@ -19,8 +19,3 @@
|
|||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import time
|
||||
|
||||
|
||||
def time_now():
|
||||
return int(time.time())
|
||||
199
squeaknode/server/app.py
Normal file
199
squeaknode/server/app.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from flask import Flask
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
from flask_sock import Sock
|
||||
from werkzeug.serving import make_server
|
||||
|
||||
from squeaknode.server.squeak_peer_server_handler import NotFoundError
|
||||
from squeaknode.server.squeak_peer_server_handler import PaymentRequiredError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app(handler):
|
||||
# create and configure the app
|
||||
logger.debug("Starting flask app from directory: {}".format(os.getcwd()))
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_url_path="/",
|
||||
static_folder="static/build",
|
||||
)
|
||||
app.config.from_mapping(
|
||||
SECRET_KEY="dev",
|
||||
)
|
||||
logger.debug("Starting flask with app.root_path: {}".format(app.root_path))
|
||||
logger.debug("Files in root path: {}".format(os.listdir(app.root_path)))
|
||||
sock = Sock(app)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
logger.info("Getting index route.")
|
||||
return "Hello, Index!"
|
||||
|
||||
@app.route("/hello")
|
||||
def hello_world():
|
||||
logger.info("Getting hello route.")
|
||||
return "Hello, World!"
|
||||
|
||||
@app.route('/squeak/<hash>')
|
||||
def squeak(hash):
|
||||
logger.info("Getting squeak route.")
|
||||
logger.info(hash)
|
||||
squeak_bytes = handler.handle_get_squeak_bytes(hash)
|
||||
logger.info(squeak_bytes)
|
||||
return squeak_bytes
|
||||
|
||||
@app.route('/secretkey/<hash>')
|
||||
def secret_key(hash):
|
||||
logger.info("Getting secretkey route.")
|
||||
logger.info(hash)
|
||||
try:
|
||||
secret_key_bytes = handler.handle_get_secret_key(hash)
|
||||
except NotFoundError:
|
||||
return "Not found", 404
|
||||
except PaymentRequiredError:
|
||||
return "Payment required", 402
|
||||
logger.info(secret_key_bytes)
|
||||
return secret_key_bytes
|
||||
|
||||
@app.route('/offer/<hash>')
|
||||
def offer(hash):
|
||||
logger.info("Getting offer route.")
|
||||
logger.info(hash)
|
||||
client_host = request.remote_addr
|
||||
logger.info(client_host)
|
||||
try:
|
||||
offer = handler.handle_get_offer(hash, client_host)
|
||||
except NotFoundError:
|
||||
return "Not found", 404
|
||||
return jsonify({
|
||||
'squeak_hash': offer.squeak_hash.hex(),
|
||||
'nonce': offer.nonce.hex(),
|
||||
'payment_request': offer.payment_request,
|
||||
'host': offer.host,
|
||||
'port': offer.port,
|
||||
})
|
||||
|
||||
@app.route("/lookup")
|
||||
def lookup():
|
||||
logger.info("Getting lookup route.")
|
||||
min_block = request.args.get('minblock')
|
||||
max_block = request.args.get('maxblock')
|
||||
pubkeys = request.args.getlist('pubkeys')
|
||||
logger.info("Hello, lookup! Min block {}, Max block {}, pubkeys: {}".format(
|
||||
min_block,
|
||||
max_block,
|
||||
pubkeys,
|
||||
))
|
||||
if len(pubkeys) == 0:
|
||||
squeak_hashes = []
|
||||
else:
|
||||
squeak_hashes = handler.handle_lookup_squeaks(
|
||||
pubkeys,
|
||||
min_block,
|
||||
max_block,
|
||||
)
|
||||
squeak_hashes_str = [
|
||||
squeak_hash.hex()
|
||||
for squeak_hash in squeak_hashes
|
||||
]
|
||||
logger.info(squeak_hashes_str)
|
||||
return jsonify(squeak_hashes_str)
|
||||
|
||||
@sock.route('/echo')
|
||||
def echo(ws):
|
||||
count = 0
|
||||
while True:
|
||||
data = f'hello_{count}'
|
||||
logger.info(data)
|
||||
ws.send(data)
|
||||
count += 1
|
||||
import time
|
||||
time.sleep(5)
|
||||
|
||||
@sock.route('/subscribetimeline')
|
||||
def subscribe_timeline(ws):
|
||||
logger.info("Getting lookup route.")
|
||||
min_block = request.args.get('minblock')
|
||||
max_block = request.args.get('maxblock')
|
||||
pubkeys = request.args.getlist('pubkeys')
|
||||
logger.info("Hello, lookup! Min block {}, Max block {}, pubkeys: {}".format(
|
||||
min_block,
|
||||
max_block,
|
||||
pubkeys,
|
||||
))
|
||||
# TODO: This special case should not need to be handled here.
|
||||
if len(pubkeys) == 0:
|
||||
squeak_hashes = []
|
||||
else:
|
||||
squeak_hashes = handler.handle_lookup_squeaks(
|
||||
pubkeys,
|
||||
min_block,
|
||||
max_block,
|
||||
)
|
||||
for squeak_hash in squeak_hashes:
|
||||
ws.send(squeak_hash.hex())
|
||||
|
||||
return app
|
||||
|
||||
|
||||
class SqueakPeerWebServer:
|
||||
def __init__(
|
||||
self,
|
||||
host,
|
||||
port,
|
||||
handler,
|
||||
):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = create_app(handler)
|
||||
self.server = None
|
||||
|
||||
def get_app(self):
|
||||
return self.app
|
||||
|
||||
def start(self):
|
||||
self.server = make_server(
|
||||
self.host,
|
||||
self.port,
|
||||
self.get_app(),
|
||||
threaded=True,
|
||||
ssl_context=None,
|
||||
)
|
||||
|
||||
logger.info("Starting SqueakPeerWebServer...")
|
||||
threading.Thread(
|
||||
target=self.server.serve_forever,
|
||||
).start()
|
||||
|
||||
def stop(self):
|
||||
if self.server is None:
|
||||
return
|
||||
logger.info("Stopping SqueakPeerWebServer....")
|
||||
self.server.shutdown()
|
||||
logger.info("Stopped SqueakPeerWebServer.")
|
||||
136
squeaknode/server/squeak_peer_server_handler.py
Normal file
136
squeaknode/server/squeak_peer_server_handler.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from squeak.core.keys import SqueakPublicKey
|
||||
|
||||
from squeaknode.core.lightning_address import LightningAddressHostPort
|
||||
from squeaknode.core.offer import Offer
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.node.price_policy import PricePolicy
|
||||
from squeaknode.node.squeak_store import SqueakStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentRequiredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SqueakPeerServerHandler(object):
|
||||
"""Handles peer server commands."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
squeak_store: SqueakStore,
|
||||
node_settings,
|
||||
config,
|
||||
):
|
||||
self.squeak_store = squeak_store
|
||||
self.node_settings = node_settings
|
||||
self.config = config
|
||||
|
||||
def handle_get_squeak_bytes(self, squeak_hash_str) -> Optional[bytes]:
|
||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||
logger.info("Handle get squeak for hash: {}".format(squeak_hash_str))
|
||||
squeak = self.squeak_store.get_squeak(squeak_hash)
|
||||
if not squeak:
|
||||
raise NotFoundError()
|
||||
return squeak.serialize()
|
||||
|
||||
def handle_get_secret_key(self, squeak_hash_str) -> bytes:
|
||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||
logger.info(
|
||||
"Handle get secret key for hash: {}".format(squeak_hash_str))
|
||||
price_msat = self.get_price_for_squeak()
|
||||
if price_msat > 0:
|
||||
raise PaymentRequiredError()
|
||||
secret_key = self.squeak_store.get_squeak_secret_key(squeak_hash)
|
||||
if not secret_key:
|
||||
raise NotFoundError()
|
||||
return secret_key
|
||||
|
||||
def handle_get_offer(self, squeak_hash_str, client_host) -> Offer:
|
||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||
logger.info("Handle get offer for hash: {}, client_host: {}".format(
|
||||
squeak_hash_str, client_host))
|
||||
client_addr = PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=client_host,
|
||||
port=0,
|
||||
)
|
||||
logger.info("client_addr: {}".format(client_addr))
|
||||
price_msat = self.get_price_for_squeak()
|
||||
if price_msat == 0:
|
||||
raise NotFoundError()
|
||||
# TODO: lnd_external_address should be configured inside SqueakStore.
|
||||
lnd_external_address: Optional[LightningAddressHostPort] = None
|
||||
if self.config.lnd.external_host:
|
||||
lnd_external_address = LightningAddressHostPort(
|
||||
host=self.config.lnd.external_host,
|
||||
port=self.config.lnd.port,
|
||||
)
|
||||
logger.info(lnd_external_address)
|
||||
offer = self.squeak_store.get_packaged_offer(
|
||||
squeak_hash,
|
||||
client_addr,
|
||||
price_msat,
|
||||
lnd_external_address,
|
||||
)
|
||||
if not offer:
|
||||
raise NotFoundError()
|
||||
return offer
|
||||
|
||||
def handle_lookup_squeaks(
|
||||
self,
|
||||
pubkey_strs: List[str],
|
||||
min_block: Optional[int],
|
||||
max_block: Optional[int],
|
||||
) -> List[bytes]:
|
||||
pubkeys = [
|
||||
SqueakPublicKey.from_bytes(bytes.fromhex(pubkey_str))
|
||||
for pubkey_str in pubkey_strs
|
||||
]
|
||||
|
||||
# Add separate endpoint for replies.
|
||||
# reply_to_hash = interest.hashReplySqk if interest.hashReplySqk != EMPTY_HASH else None
|
||||
return self.squeak_store.lookup_squeaks(
|
||||
pubkeys,
|
||||
min_block,
|
||||
max_block,
|
||||
None,
|
||||
)
|
||||
|
||||
def get_price_for_squeak(self) -> int:
|
||||
price_policy = PricePolicy(
|
||||
self.squeak_store,
|
||||
self.config,
|
||||
self.node_settings,
|
||||
)
|
||||
return price_policy.get_price()
|
||||
|
|
@ -218,21 +218,6 @@ def payment_summary_msg(
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connected_peer_msg(peer_address_message, peer_msg):
|
||||
yield squeak_admin_pb2.ConnectedPeer(
|
||||
peer_address=peer_address_message,
|
||||
connect_time_s=0,
|
||||
last_message_received_time_s=0,
|
||||
number_messages_received=0,
|
||||
number_bytes_received=0,
|
||||
number_messages_sent=0,
|
||||
number_bytes_sent=0,
|
||||
is_peer_saved=True,
|
||||
saved_peer=peer_msg,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_result_msg(
|
||||
download_result,
|
||||
|
|
|
|||
|
|
@ -19,13 +19,11 @@
|
|||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from squeaknode.admin.messages import connected_peer_to_message
|
||||
from squeaknode.admin.messages import download_result_to_message
|
||||
from squeaknode.admin.messages import message_to_peer_address
|
||||
from squeaknode.admin.messages import message_to_received_payment
|
||||
from squeaknode.admin.messages import message_to_sent_payment
|
||||
from squeaknode.admin.messages import message_to_squeak_entry
|
||||
from squeaknode.admin.messages import optional_connected_peer_to_message
|
||||
from squeaknode.admin.messages import optional_received_offer_to_message
|
||||
from squeaknode.admin.messages import optional_sent_payment_to_message
|
||||
from squeaknode.admin.messages import optional_squeak_entry_to_message
|
||||
|
|
@ -145,12 +143,6 @@ def test_payment_summary_to_message(
|
|||
assert msg == payment_summary_msg
|
||||
|
||||
|
||||
def test_connected_peer_to_message(connected_peer, connected_peer_msg):
|
||||
msg = connected_peer_to_message(connected_peer)
|
||||
|
||||
assert msg == connected_peer_msg
|
||||
|
||||
|
||||
def test_optional_profile_to_message_none():
|
||||
msg = optional_squeak_profile_to_message(None)
|
||||
|
||||
|
|
@ -221,15 +213,3 @@ def test_optional_sent_payment_to_message(sent_payment, sent_payment_msg):
|
|||
msg = optional_sent_payment_to_message(sent_payment)
|
||||
|
||||
assert msg == sent_payment_msg
|
||||
|
||||
|
||||
def test_optional_connected_peer_to_message_none():
|
||||
msg = optional_connected_peer_to_message(None)
|
||||
|
||||
assert msg is None
|
||||
|
||||
|
||||
def test_optional_connected_peer_to_message(connected_peer, connected_peer_msg):
|
||||
msg = optional_connected_peer_to_message(connected_peer)
|
||||
|
||||
assert msg == connected_peer_msg
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes
|
|||
from squeak.core.keys import SqueakPrivateKey
|
||||
|
||||
from squeaknode.bitcoin.block_info import BlockInfo
|
||||
from squeaknode.core.connected_peer import ConnectedPeer
|
||||
from squeaknode.core.download_result import DownloadResult
|
||||
from squeaknode.core.lightning_address import LightningAddressHostPort
|
||||
from squeaknode.core.offer import Offer
|
||||
|
|
@ -44,7 +43,6 @@ from squeaknode.core.squeak_peer import SqueakPeer
|
|||
from squeaknode.core.squeaks import get_hash
|
||||
from squeaknode.core.squeaks import make_squeak_with_block
|
||||
from squeaknode.core.user_config import UserConfig
|
||||
from squeaknode.network.peer import Peer
|
||||
from tests.utils import gen_contact_profile
|
||||
from tests.utils import gen_signing_profile
|
||||
from tests.utils import sha256
|
||||
|
|
@ -557,25 +555,6 @@ def sent_payment_summary(
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def peer_object(peer_address):
|
||||
yield Peer(
|
||||
peer_socket=None,
|
||||
local_address=peer_address,
|
||||
remote_address=peer_address,
|
||||
outgoing=True,
|
||||
peer_changed_listener=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connected_peer(peer, peer_object):
|
||||
yield ConnectedPeer(
|
||||
peer=peer_object,
|
||||
saved_peer=peer,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_result():
|
||||
yield DownloadResult(
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
from util import yield_inbound_socket_and_outbound_socket
|
||||
|
||||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.network.connection_manager import ConnectionManager
|
||||
from squeaknode.node.squeak_controller import SqueakController
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_address():
|
||||
local_ip = socket.gethostbyname('localhost')
|
||||
local_port = 56789
|
||||
yield PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host=local_ip,
|
||||
port=local_port,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inbound_socket_and_outbound_socket():
|
||||
yield from yield_inbound_socket_and_outbound_socket()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inbound_socket(inbound_socket_and_outbound_socket):
|
||||
inbound_socket, _ = inbound_socket_and_outbound_socket
|
||||
yield inbound_socket
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def outbound_socket(inbound_socket_and_outbound_socket):
|
||||
_, outbound_socket = inbound_socket_and_outbound_socket
|
||||
yield outbound_socket
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inbound_local_address():
|
||||
yield PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host='inbound.com',
|
||||
port=56789,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def outbound_local_address():
|
||||
yield PeerAddress(
|
||||
network=Network.IPV4,
|
||||
host='outbound.com',
|
||||
port=4321,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inbound_connection_manager(local_address):
|
||||
yield ConnectionManager(local_address)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def outbound_connection_manager(local_address):
|
||||
yield ConnectionManager(local_address)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def squeak_controller():
|
||||
return mock.Mock(spec=SqueakController)
|
||||
|
||||
|
||||
def start_connection(
|
||||
connection_manager,
|
||||
connected_event,
|
||||
disconnect_event,
|
||||
disconnected_event,
|
||||
socket,
|
||||
remote_address,
|
||||
is_outbound,
|
||||
squeak_controller,
|
||||
result_q,
|
||||
):
|
||||
if is_outbound:
|
||||
print('------Starting outbound connection------')
|
||||
else:
|
||||
print('------Starting inbound connection------')
|
||||
with connection_manager.connect(
|
||||
socket,
|
||||
remote_address,
|
||||
is_outbound,
|
||||
squeak_controller,
|
||||
result_q,
|
||||
):
|
||||
connected_event.set()
|
||||
disconnect_event.wait()
|
||||
disconnected_event.set()
|
||||
|
||||
|
||||
def test_connect_peers(
|
||||
inbound_socket,
|
||||
outbound_socket,
|
||||
inbound_connection_manager,
|
||||
outbound_connection_manager,
|
||||
inbound_local_address,
|
||||
outbound_local_address,
|
||||
squeak_controller,
|
||||
caplog,
|
||||
):
|
||||
import logging
|
||||
caplog.set_level(logging.INFO)
|
||||
inbound_q = queue.Queue()
|
||||
outbound_q = queue.Queue()
|
||||
|
||||
assert inbound_socket is not None
|
||||
assert outbound_socket is not None
|
||||
|
||||
assert len(inbound_connection_manager.peers) == 0
|
||||
assert len(outbound_connection_manager.peers) == 0
|
||||
|
||||
inbound_connected_event = threading.Event()
|
||||
inbound_disconnect_event = threading.Event()
|
||||
inbound_disconnected_event = threading.Event()
|
||||
# Start the inbound connection in a thread.
|
||||
inbound_connection_thread = threading.Thread(
|
||||
target=start_connection,
|
||||
args=(
|
||||
inbound_connection_manager,
|
||||
inbound_connected_event,
|
||||
inbound_disconnect_event,
|
||||
inbound_disconnected_event,
|
||||
inbound_socket,
|
||||
outbound_local_address,
|
||||
False,
|
||||
squeak_controller,
|
||||
inbound_q,
|
||||
))
|
||||
inbound_connection_thread.start()
|
||||
|
||||
outbound_connected_event = threading.Event()
|
||||
outbound_disconnect_event = threading.Event()
|
||||
outbound_disconnected_event = threading.Event()
|
||||
# Start the outbound connection in a thread.
|
||||
outbound_connection_thread = threading.Thread(
|
||||
target=start_connection,
|
||||
args=(
|
||||
outbound_connection_manager,
|
||||
outbound_connected_event,
|
||||
outbound_disconnect_event,
|
||||
outbound_disconnected_event,
|
||||
outbound_socket,
|
||||
inbound_local_address,
|
||||
True,
|
||||
squeak_controller,
|
||||
outbound_q,
|
||||
))
|
||||
outbound_connection_thread.start()
|
||||
|
||||
# import time
|
||||
# time.sleep(5)
|
||||
# assert False
|
||||
|
||||
# Wait for both sides of connection to be connected
|
||||
inbound_connected_event.wait()
|
||||
outbound_connected_event.wait()
|
||||
|
||||
print('Opened connection.')
|
||||
assert len(inbound_connection_manager.peers) == 1
|
||||
assert len(outbound_connection_manager.peers) == 1
|
||||
|
||||
inbound_disconnect_event.set()
|
||||
outbound_disconnect_event.set()
|
||||
|
||||
inbound_disconnected_event.wait()
|
||||
outbound_disconnected_event.wait()
|
||||
|
||||
print('Closed connection.')
|
||||
assert len(inbound_connection_manager.peers) == 0
|
||||
assert len(outbound_connection_manager.peers) == 0
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import socket
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import closing
|
||||
|
||||
|
||||
TEST_SOCKET_PORT = 19999
|
||||
SOCKET_CONNECT_TIMEOUT = 10
|
||||
|
||||
|
||||
def accept_connections(listen_socket, started_event):
|
||||
try:
|
||||
print('Trying to bind and listen on port: {}'.format(
|
||||
TEST_SOCKET_PORT), flush=True)
|
||||
listen_socket.bind(('', TEST_SOCKET_PORT))
|
||||
listen_socket.listen()
|
||||
started_event.set()
|
||||
print('Started event set.', flush=True)
|
||||
peer_socket, address = listen_socket.accept()
|
||||
# host, port = address
|
||||
# peer_address = PeerAddress(
|
||||
# host=host,
|
||||
# port=port,
|
||||
# )
|
||||
peer_socket.setblocking(True)
|
||||
return peer_socket
|
||||
except Exception as e:
|
||||
print(e)
|
||||
started_event.set()
|
||||
return e
|
||||
|
||||
|
||||
def make_connection(peer_socket, started):
|
||||
address = ('localhost', TEST_SOCKET_PORT)
|
||||
started.wait()
|
||||
print('Conecting to address: {}'.format(address))
|
||||
try:
|
||||
peer_socket.settimeout(SOCKET_CONNECT_TIMEOUT)
|
||||
peer_socket.connect(address)
|
||||
peer_socket.setblocking(True)
|
||||
return peer_socket
|
||||
except Exception:
|
||||
print('Failed to connect to {}'.format(address))
|
||||
|
||||
|
||||
def yield_inbound_socket_and_outbound_socket():
|
||||
# TODO: set up inbound and outbound sockets
|
||||
started = threading.Event()
|
||||
|
||||
# Use futures
|
||||
with closing(socket.socket()) as listen_socket, \
|
||||
closing(socket.socket()) as peer_socket, \
|
||||
ThreadPoolExecutor(max_workers=2) as executor:
|
||||
inbound_future = executor.submit(
|
||||
accept_connections, listen_socket, started)
|
||||
outbound_future = executor.submit(
|
||||
make_connection, peer_socket, started)
|
||||
|
||||
inbound_socket = inbound_future.result()
|
||||
print(inbound_socket)
|
||||
if isinstance(inbound_socket, Exception):
|
||||
raise inbound_socket
|
||||
outbound_socket = outbound_future.result()
|
||||
print(outbound_socket)
|
||||
|
||||
yield inbound_socket, outbound_socket
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import mock
|
||||
import pytest
|
||||
from squeak.messages import msg_getdata
|
||||
from squeak.messages import msg_getsqueaks
|
||||
from squeak.net import CInterested
|
||||
from squeak.net import CInv
|
||||
from squeak.net import CSqueakLocator
|
||||
|
||||
from squeaknode.core.download_result import DownloadResult
|
||||
from squeaknode.node.active_download_manager import HashDownload
|
||||
from squeaknode.node.active_download_manager import InterestDownload
|
||||
from squeaknode.node.downloaded_object import DownloadedSqueak
|
||||
from tests.utils import gen_squeak
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def download_hash(squeak_hash):
|
||||
yield HashDownload(squeak_hash=squeak_hash)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def interest(public_key, block_count):
|
||||
yield CInterested(
|
||||
pubkeys=(public_key,),
|
||||
nMinBlockHeight=block_count - 100,
|
||||
nMaxBlockHeight=block_count + 100,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def download_interest(interest):
|
||||
yield InterestDownload(limit=10, interest=interest)
|
||||
|
||||
|
||||
def test_download_hash_is_interested(download_hash, squeak):
|
||||
downloaded_squeak = DownloadedSqueak(squeak)
|
||||
|
||||
assert download_hash.is_interested(downloaded_squeak)
|
||||
|
||||
|
||||
def test_download_hash_is_not_interested(download_hash, private_key, block_count):
|
||||
other_squeak = gen_squeak(private_key, block_count)
|
||||
downloaded_squeak = DownloadedSqueak(other_squeak)
|
||||
|
||||
assert not download_hash.is_interested(downloaded_squeak)
|
||||
|
||||
|
||||
def test_download_interest_is_interested(download_interest, squeak):
|
||||
downloaded_squeak = DownloadedSqueak(squeak)
|
||||
|
||||
assert download_interest.is_interested(downloaded_squeak)
|
||||
|
||||
|
||||
def test_download_interest_is_not_interested(download_interest, private_key, block_count):
|
||||
other_squeak = gen_squeak(private_key, block_count + 200)
|
||||
downloaded_squeak = DownloadedSqueak(other_squeak)
|
||||
|
||||
assert not download_interest.is_interested(downloaded_squeak)
|
||||
|
||||
|
||||
def test_download_hash_initiate(download_hash, squeak_hash):
|
||||
broadcast_fn = mock.Mock()
|
||||
download_hash.initiate_download(broadcast_fn)
|
||||
|
||||
expected_msg = msg_getdata(
|
||||
inv=[CInv(type=1, hash=squeak_hash)]
|
||||
)
|
||||
|
||||
broadcast_fn.assert_called_once_with(expected_msg)
|
||||
|
||||
|
||||
def test_download_interest_initiate(download_interest, interest):
|
||||
broadcast_fn = mock.Mock()
|
||||
download_interest.initiate_download(broadcast_fn)
|
||||
|
||||
expected_msg = msg_getsqueaks(
|
||||
locator=CSqueakLocator(
|
||||
vInterested=[interest],
|
||||
)
|
||||
)
|
||||
|
||||
broadcast_fn.assert_called_once_with(expected_msg)
|
||||
|
||||
|
||||
def test_download_hash_mark_complete(download_hash, squeak):
|
||||
with mock.patch.object(download_hash, 'mark_complete', autospec=True) as mock_mark_complete:
|
||||
mock_mark_complete.return_value = None
|
||||
download_hash.increment()
|
||||
|
||||
mock_mark_complete.assert_called_once_with()
|
||||
|
||||
|
||||
def test_download_hash_mark_complete_not_called(download_hash, squeak):
|
||||
with mock.patch.object(download_hash, 'mark_complete', autospec=True) as mock_mark_complete:
|
||||
mock_mark_complete.return_value = None
|
||||
|
||||
mock_mark_complete.assert_not_called()
|
||||
|
||||
|
||||
def test_download_hash_wait_for_complete(download_hash):
|
||||
download_hash.mark_complete()
|
||||
|
||||
download_hash.wait_for_complete(50)
|
||||
|
||||
|
||||
def test_download_hash_get_result(download_hash, squeak):
|
||||
download_hash.increment()
|
||||
download_result = download_hash.get_result()
|
||||
|
||||
assert download_result == DownloadResult(
|
||||
number_downloaded=1,
|
||||
number_requested=1,
|
||||
elapsed_time_ms=0,
|
||||
number_peers=0,
|
||||
)
|
||||
|
|
@ -30,32 +30,10 @@ def price_policy():
|
|||
yield PricePolicy(None, None, None)
|
||||
|
||||
|
||||
def test_get_price(price_policy, squeak, peer_address, user_config):
|
||||
def test_get_price(price_policy, user_config):
|
||||
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
|
||||
mock.patch.object(price_policy, 'get_sell_price_msat', autospec=True) as mock_get_sell_price_msat:
|
||||
mock_get_peer.return_value = None
|
||||
mock_get_sell_price_msat.return_value = 555
|
||||
|
||||
assert price_policy.get_price(squeak, peer_address) == 555
|
||||
|
||||
|
||||
def test_get_price_profile_share_free_peer(price_policy, squeak, peer_address, peer, user_config):
|
||||
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
|
||||
mock.patch.object(price_policy, 'get_sell_price_msat', autospec=True) as mock_get_sell_price_msat:
|
||||
mock_get_peer.return_value = peer._replace(
|
||||
share_for_free=True,
|
||||
)
|
||||
mock_get_sell_price_msat.return_value = 555
|
||||
|
||||
assert price_policy.get_price(squeak, peer_address) == 0
|
||||
|
||||
|
||||
def test_get_price_profile_no_share_free_peer(price_policy, squeak, peer_address, peer, user_config):
|
||||
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
|
||||
mock.patch.object(price_policy, 'get_sell_price_msat', autospec=True) as mock_get_sell_price_msat:
|
||||
mock_get_peer.return_value = peer._replace(
|
||||
share_for_free=False,
|
||||
)
|
||||
mock_get_sell_price_msat.return_value = 555
|
||||
|
||||
assert price_policy.get_price(squeak, peer_address) == 555
|
||||
assert price_policy.get_price() == 555
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2020 Jonathan Zernik
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import pytest
|
||||
from squeak.messages import msg_secretkey
|
||||
from squeak.net import COffer
|
||||
|
||||
from squeaknode.node.secret_key_reply import FreeSecretKeyReply
|
||||
from squeaknode.node.secret_key_reply import OfferReply
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def free_secret_key_reply(squeak_hash, secret_key):
|
||||
yield FreeSecretKeyReply(
|
||||
squeak_hash=squeak_hash,
|
||||
secret_key=secret_key,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def offer_reply(squeak_hash, offer):
|
||||
yield OfferReply(
|
||||
squeak_hash=squeak_hash,
|
||||
offer=offer,
|
||||
)
|
||||
|
||||
|
||||
def test_free_secret_key_reply_msg(squeak_hash, secret_key, free_secret_key_reply):
|
||||
reply_msg = free_secret_key_reply.get_msg()
|
||||
|
||||
assert reply_msg == msg_secretkey(
|
||||
hashSqk=squeak_hash,
|
||||
secretKey=secret_key,
|
||||
)
|
||||
|
||||
|
||||
def test_offer_reply_msg(squeak_hash, offer, offer_reply):
|
||||
reply_msg = offer_reply.get_msg()
|
||||
|
||||
assert reply_msg == msg_secretkey(
|
||||
hashSqk=squeak_hash,
|
||||
offer=COffer(
|
||||
nonce=offer.nonce,
|
||||
strPaymentInfo=offer.payment_request.encode(
|
||||
'utf-8'),
|
||||
host=offer.host.encode('utf-8'),
|
||||
port=offer.port,
|
||||
)
|
||||
)
|
||||
|
|
@ -27,8 +27,6 @@ from squeaknode.core.lightning_address import LightningAddressHostPort
|
|||
from squeaknode.core.peer_address import Network
|
||||
from squeaknode.core.peer_address import PeerAddress
|
||||
from squeaknode.core.squeak_core import SqueakCore
|
||||
from squeaknode.network.network_manager import NetworkManager
|
||||
from squeaknode.node.active_download_manager import ActiveDownloadManager
|
||||
from squeaknode.node.node_settings import NodeSettings
|
||||
from squeaknode.node.payment_processor import PaymentProcessor
|
||||
from squeaknode.node.squeak_controller import SqueakController
|
||||
|
|
@ -62,11 +60,6 @@ def node_settings():
|
|||
return mock.Mock(spec=NodeSettings)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def network_manager():
|
||||
return mock.Mock(spec=NetworkManager)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def squeak_core():
|
||||
return mock.Mock(spec=SqueakCore)
|
||||
|
|
@ -110,11 +103,6 @@ def payment_processor():
|
|||
return mock.Mock(spec=PaymentProcessor)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_manager():
|
||||
return mock.Mock(spec=ActiveDownloadManager)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def twitter_forwarder():
|
||||
return mock.Mock(spec=TwitterForwarder)
|
||||
|
|
@ -125,8 +113,6 @@ def squeak_controller(
|
|||
squeak_store,
|
||||
squeak_core,
|
||||
payment_processor,
|
||||
network_manager,
|
||||
download_manager,
|
||||
twitter_forwarder,
|
||||
node_settings,
|
||||
config,
|
||||
|
|
@ -136,8 +122,6 @@ def squeak_controller(
|
|||
squeak_store,
|
||||
squeak_core,
|
||||
payment_processor,
|
||||
network_manager,
|
||||
download_manager,
|
||||
twitter_forwarder,
|
||||
node_settings,
|
||||
config,
|
||||
|
|
@ -150,8 +134,6 @@ def regtest_squeak_controller(
|
|||
squeak_store,
|
||||
squeak_core,
|
||||
payment_processor,
|
||||
network_manager,
|
||||
download_manager,
|
||||
twitter_forwarder,
|
||||
node_settings,
|
||||
regtest_config,
|
||||
|
|
@ -161,8 +143,6 @@ def regtest_squeak_controller(
|
|||
squeak_store,
|
||||
squeak_core,
|
||||
payment_processor,
|
||||
network_manager,
|
||||
download_manager,
|
||||
twitter_forwarder,
|
||||
node_settings,
|
||||
regtest_config,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue