├── static ├── img │ ├── dash.png │ ├── world.png │ ├── bitcoin.png │ ├── bitcoin-sv.png │ ├── litecoin.png │ ├── bitcoin-cash.png │ └── network.svg ├── flags │ ├── blank.gif │ ├── flags.png │ ├── iso_countries.json │ └── flags.min.css ├── css │ ├── images │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ └── ui-bg_highlight-soft_100_eeeeee_1x100.png │ ├── dc.min.css │ ├── custom.css │ └── jquery-ui.min.css └── js │ └── crossfilter.min.js ├── requirements.txt ├── .dockerignore ├── geoip └── README.txt ├── .env.dist ├── Dockerfile ├── templates ├── footer.html ├── docs.html ├── about.html └── base.html ├── LICENSE ├── docker-compose.yml ├── README.md ├── config.py ├── crawler_config.yml ├── autodoc.py ├── models.py ├── server.py ├── crawler.py └── protocol.py /static/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/dash.png -------------------------------------------------------------------------------- /static/img/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/world.png -------------------------------------------------------------------------------- /static/flags/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/flags/blank.gif -------------------------------------------------------------------------------- /static/flags/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/flags/flags.png -------------------------------------------------------------------------------- /static/img/bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/bitcoin.png -------------------------------------------------------------------------------- /static/img/bitcoin-sv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/bitcoin-sv.png -------------------------------------------------------------------------------- /static/img/litecoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/litecoin.png -------------------------------------------------------------------------------- /static/img/bitcoin-cash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/img/bitcoin-cash.png -------------------------------------------------------------------------------- /static/css/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/css/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /static/css/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/css/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /static/css/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/css/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /static/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/opennodes/master/static/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | geoip2 3 | ipaddress 4 | PySocks 5 | requests 6 | sqlalchemy 7 | flask_sqlalchemy 8 | numpy 9 | pandas 10 | pyyaml 11 | waitress 12 | psycopg2-binary 13 | python-dotenv -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.db 2 | venv 3 | __pycache__ 4 | .idea 5 | .ipynb_checkpoints 6 | *.pyc 7 | .git 8 | dash 9 | geoip 10 | db_cache 11 | postgres 12 | static 13 | **/temp/ 14 | **/logs/ 15 | **/geoip/ 16 | node_modules 17 | -------------------------------------------------------------------------------- /geoip/README.txt: -------------------------------------------------------------------------------- 1 | Download the ASN, City, and Country .mmdb files from the geolite page on the maxmind site and 2 | place them in this directory, ensuring the following names: 3 | 4 | ``` 5 | GeoLite2-ASN.mmdb 6 | GeoLite2-City.mmdb 7 | GeoLite2-Country.mmdb 8 | ``` -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | DB_TYPE=postgresql 2 | DB_USER=postgres 3 | DB_PASS=password 4 | DB_HOST=opennodes_postgres 5 | DB_PORT=5432 6 | DB_NAME=opennodes 7 | 8 | SERVER_HOST=0.0.0.0 9 | SERVER_PORT=8888 10 | 11 | DASH_RPC_URI=http://127.0.0.1:9998 12 | DASH_RPC_USER=myrpcuser 13 | DASH_RPC_PASS=myrpcpassword -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base Image - can't use python:alpine-3.7 because MSSQL drivers don't support alpine linux 2 | FROM python:3.9-alpine 3 | ENV PYTHONUNBUFFERED=1 4 | 5 | RUN apk --no-cache add g++ postgresql-dev 6 | 7 | COPY requirements.txt requirements.txt 8 | RUN pip install --no-cache-dir -r requirements.txt 9 | 10 | COPY . . 11 | CMD ['python', 'daemon.py'] 12 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
{{ doc.docstring|urlize|nl2br }}
61 | 62 |9 | Open Nodes aims to map out and compare the status of several Satoshi-based coin networks. Despite 10 | the fact that these coins share the same protocol, there currently exists no easy way to directly 11 | compare these coins in a single location. Additionally, no data is currently available for Dash 12 | full nodes, only masternodes. These statistics are essential to evaluate the relative health of each 13 | network. 14 |
15 | 16 |18 | Open Nodes maps out the network by recursively connecting to nodes and issuing getaddr 19 | commands. Any connected nodes that have been online in the past 24 hours are then added to the 20 | crawl. Only nodes which are configured to allow remote connections and accept the handshake from the 21 | crawler are included in the presented dataset. Nodes with a version number below the minimum are 22 | also excluded. All nodes are designated as "Active" or "Inactive" based on the last reported block 23 | height. If the node's block height is more than an hour out of consensus with the network median 24 | then it is flagged as inactive. 25 |
26 | 27 |a;++a,++j)H[j]=m[a],t&&(Z[j]=E[a]),K[j]=w[a];for(;u>O;++O,++j)H[j]=Q[O],t&&(Z[j]=rt[O]),K[j]=V[O]+(t?x:n);p=at(H),lt=p[0],ht=p[1]}function n(r,t,e){ct.forEach(function(r){r(Q,V,t,e)}),Q=V=null}function d(r){if(t){for(var e=0,n=0;ee;e++)r[e]!==m&&(n!==e&&(X[n]=X[e]),n++);X.length=n}for(var o,i=H.length,u=0,f=0;i>u;++u)o=K[u],r[o]!==m&&(u!==f&&(H[f]=H[u]),K[f]=r[o],t&&(Z[f]=Z[u]),++f);for(H.length=f,t&&(Z.length=f);i>f;)K[f++]=0;var a=at(H);lt=a[0],ht=a[1]}function _(r){var e=r[0],n=r[1];if(tt)return tt=null,q(function(r,t){return t>=e&&n>t},0===r[0]&&r[1]===H.length),lt=e,ht=n,it;var o,i,u,f=[],a=[],c=[],s=[];if(lt>e)for(o=e,i=Math.min(lt,n);i>o;++o)f.push(K[o]),c.push(o);else if(e>lt)for(o=lt,i=Math.min(e,ht);i>o;++o)a.push(K[o]),s.push(o);if(n>ht)for(o=Math.max(e,ht),i=n;i>o;++o)f.push(K[o]),c.push(o);else if(ht>n)for(o=Math.max(lt,n),i=ht;i>o;++o)a.push(K[o]),s.push(o);if(t){var l=[],h=[];for(o=0;o n;++n)!(z[G][o=K[n]]&W)^!!(i=r(H[n],n))&&(i?u.push(o):f.push(o));if(t)for(n=0;s>n;++n)r(H[n],n)?(u.push(K[n]),a.push(n)):(f.push(K[n]),c.push(n));if(t){var l=[],h=[];for(n=0;n 0&&(u=e);--i>=lt&&r>0;)z.zero(n=K[i])&&(u>0?--u:(o.push(S[n]),--r));if(t)for(i=0;i 0;i++)z.zero(n=ut[i])&&(u>0?--u:(o.push(S[n]),--r));return o}function D(r,e){var n,o,i=[],u=0;if(e&&e>0&&(u=e),t)for(n=0;n 0;n++)z.zero(o=ut[n])&&(u>0?--u:(i.push(S[o]),--r));for(n=lt;ht>n&&r>0;)z.zero(o=K[n])&&(u>0?--u:(i.push(S[o]),--r)),n++;return i}function I(r){function e(e,n,c,l){function h(){return t?(B++,void 0):(++B===T&&(w=f.arrayWiden(w,P<<=1),q=f.arrayWiden(q,P),T=u(P)),void 0)}t&&(L=c,c=H.length-e.length,l=e.length);var p,y,g,b,_,x,m=C,w=t?[]:o(B,T),E=U,O=D,k=I,A=B,j=0,$=0;for(X&&(E=k=s),X&&(O=k=s),C=new Array(B),B=0,q=t?A?q:[]:A>1?f.arrayLengthen(q,M):o(M,T),A&&(g=(y=m[0]).key);l>$&&!((b=r(e[$]))>=b);)++$;for(;l>$;){for(y&&b>=g?(_=y,x=g,w[j]=B,y=m[++j],y&&(g=y.key)):(_={key:b,value:k()},x=b),C[B]=_;x>=b&&(p=n[$]+(t?L:c),t?q[p]?q[p].push(B):q[p]=[B]:q[p]=B,_.value=E(_.value,S[p],!0),z.zeroExcept(p,G,J)||(_.value=O(_.value,S[p],!1)),!(++$>=l));)b=r(e[$]);h()}for(;A>j;)C[w[j]=B]=m[j++],h();if(t)for(var N=0;M>N;N++)q[N]||(q[N]=[]);if(B>j)if(t)for(j=0;L>j;++j)for(N=0;N j;++j)q[j]=w[q[j]];p=F.indexOf(Q),B>1||t?(Q=i,V=v):(!B&&Y&&(B=1,C=[{key:null,value:k()}]),1===B?(Q=a,V=d):(Q=s,V=s),q=null),F[p]=Q}function n(r){if(B>1||t){var e,n,u,f=B,c=C,l=o(f,f);if(t){for(e=0,u=0;M>e;++e)if(r[e]!==m){for(q[u]=q[e],n=0;ne;++e)r[e]!==m&&(l[q[u]=q[e]]=1,++u);for(C=[],B=0,e=0;f>e;++e)l[e]&&(l[e]=B++,C.push(c[e]));if(B>1||t)if(t)for(e=0;u>e;++e)for(n=0;ne;++e)q[e]=l[q[e]];else q=null;F[F.indexOf(Q)]=B>1||t?(V=v,Q=i):1===B?(V=d,Q=a):V=Q=s}else if(1===B){if(Y)return;for(var h=0;M>h;++h)if(r[h]!==m)return;C=[],B=0,F[F.indexOf(Q)]=Q=V=s}}function i(r,e,n,o,i){if(!(r===W&&e===G||X)){var u,f,a,c,s;if(t){for(u=0,c=n.length;c>u;++u)if(z.zeroExcept(a=n[u],G,J))for(f=0;fu;++u)if(z.onlyExcept(a=o[u],G,J,e,r))for(f=0;fu;++u)z.zeroExcept(a=n[u],G,J)&&(s=C[q[a]],s.value=U(s.value,S[a],!1));for(u=0,c=o.length;c>u;++u)z.onlyExcept(a=o[u],G,J,e,r)&&(s=C[q[a]],s.value=D(s.value,S[a],i))}}}function a(r,t,e,n,o){if(!(r===W&&t===G||X)){var i,u,f,a=C[0];for(i=0,f=e.length;f>i;++i)z.zeroExcept(u=e[i],G,J)&&(a.value=U(a.value,S[u],!1));for(i=0,f=n.length;f>i;++i)z.onlyExcept(u=n[i],G,J,t,r)&&(a.value=D(a.value,S[u],o))}}function v(){var r,e,n;for(r=0;B>r;++r)C[r].value=I();if(t){for(r=0;M>r;++r)for(e=0;er;++r)if(!z.zeroExcept(r,G,J))for(e=0;er;++r)n=C[q[r]],n.value=U(n.value,S[r],!0);for(r=0;M>r;++r)z.zeroExcept(r,G,J)||(n=C[q[r]],n.value=D(n.value,S[r],!1))}}function d(){var r,t=C[0];for(t.value=I(),r=0;M>r;++r)t.value=U(t.value,S[r],!0);for(r=0;M>r;++r)z.zeroExcept(r,G,J)||(t.value=D(t.value,S[r],!1))}function y(){return X&&(V(),X=!1),C}function g(r){var t=N(y(),0,C.length,r);return R.sort(t,0,t.length)}function _(r,t,e){return U=r,D=t,I=e,X=!0,j}function x(){return _(b.reduceIncrement,b.reduceDecrement,l)}function w(r){return _(b.reduceAdd(r),b.reduceSubtract(r),l)}function E(r){function t(t){return r(t.value)}return N=h.by(t),R=p.by(t),j}function O(){return E(c)}function k(){return B}function A(){var r=F.indexOf(Q);return r>=0&&F.splice(r,1),r=ct.indexOf(e),r>=0&&ct.splice(r,1),r=$.indexOf(n),r>=0&&$.splice(r,1),r=st.indexOf(j),r>=0&&st.splice(r,1),j}var j={top:g,all:y,reduce:_,reduceCount:x,reduceSum:w,order:E,orderNatural:O,size:k,dispose:A,remove:A};st.push(j);var C,q,N,R,U,D,I,L,P=8,T=u(P),B=0,Q=s,V=s,X=!0,Y=r===s;return arguments.length<1&&(r=c),F.push(Q),ct.push(e),$.push(n),e(H,K,0,M),x().orderNatural()}function L(){var r=I(s),t=r.all;return delete r.all,delete r.top,delete r.order,delete r.orderNatural,delete r.size,r.value=function(){return t()[0].value},r}function P(){st.forEach(function(r){r.dispose()});var r=C.indexOf(e);return r>=0&&C.splice(r,1),r=C.indexOf(n),r>=0&&C.splice(r,1),r=$.indexOf(d),r>=0&&$.splice(r,1),z.masks[G]&=J,k()}if("string"==typeof r){var T=r;r=function(r){return x(r,T)}}var W,J,G,B,H,K,Q,V,X,Y,Z,rt,tt,et,nt,ot,it={filter:w,filterExact:E,filterRange:O,filterFunction:j,filterAll:k,currentFilter:N,hasCurrentFilter:R,top:U,bottom:D,group:I,groupAll:L,dispose:P,remove:P,accessor:r,id:function(){return B}},ut=[],ft=g.by(function(r){return Q[r]}),at=a.filterAll,ct=[],st=[],lt=0,ht=0,pt=0;C.unshift(e),C.push(n),$.push(d);var vt=z.add();return G=vt.offset,W=vt.one,J=~W,B=G<<7|Math.log(W)/Math.log(2),e(S,0,M),n(S,0,M),it}function _(){function r(r,t){var e;if(!v)for(e=t;M>e;++e)a=c(a,S[e],!0),z.zero(e)||(a=s(a,S[e],!1))}function t(r,t,e,n,o){var i,u,f;if(!v){for(i=0,f=e.length;f>i;++i)z.zero(u=e[i])&&(a=c(a,S[u],o));for(i=0,f=n.length;f>i;++i)z.only(u=n[i],t,r)&&(a=s(a,S[u],o))}}function e(){var r;for(a=h(),r=0;M>r;++r)a=c(a,S[r],!0),z.zero(r)||(a=s(a,S[r],!1))}function n(r,t,e){return c=r,s=t,h=e,v=!0,p}function o(){return n(b.reduceIncrement,b.reduceDecrement,l)}function i(r){return n(b.reduceAdd(r),b.reduceSubtract(r),l)}function u(){return v&&(e(),v=!1),a}function f(){var e=F.indexOf(t);return e>=0&&F.splice(e,1),e=C.indexOf(r),e>=0&&C.splice(e,1),p}var a,c,s,h,p={reduce:n,reduceCount:o,reduceSum:i,value:u,dispose:f,remove:f},v=!0;return F.push(t),C.push(r),r(S,0,M),o()}function w(){return M}function E(){return S}function O(r){var t=[],n=0,o=e(r||[]);for(n=0;M>n;n++)z.zeroExceptMask(n,o)&&t.push(S[n]);return t}function k(r){return"function"!=typeof r?(console.warn("onChange callback parameter must be a function!"),void 0):(q.push(r),function(){q.splice(q.indexOf(r),1)})}function A(r){for(var t=0;tt?f.array8:65537>t?f.array16:f.array32)(r)}function i(r){for(var t=o(r,r),e=-1;++e >>1)+1;--i>0;)n(r,i,o,t);return r}function e(r,t,e){for(var o,i=e-t;--i>0;)o=r[t],r[t]=r[t+i],r[t+i]=o,n(r,1,i,t);return r}function n(t,e,n,o){for(var i,u=t[--o+e],f=r(u);(i=e<<1)<=n&&(n>i&&r(t[o+i])>r(t[o+i+1])&&i++,!(f<=r(t[o+i])));)t[o+e]=t[o+i],e=i;t[o+e]=u}return t.sort=e,t}var n=r("./identity");t.exports=e(n),t.exports.by=e},{"./identity":10}],9:[function(r,t){"use strict";function e(r){function t(t,n,o,i){var u,f,a,c=new Array(i=Math.min(o-n,i));for(f=0;i>f;++f)c[f]=t[n++];if(e(c,0,i),o>n){u=r(c[0]);do r(a=t[n])>u&&(c[0]=a,u=r(e(c,0,i)[0]));while(++n o;++o){for(var i=o,u=t[o],f=r(u);i>e&&r(t[i-1])>f;--i)t[i]=t[i-1];t[i]=u}return t}return t}var n=r("./identity");t.exports=e(n),t.exports.by=e},{"./identity":10}],12:[function(r,t){"use strict";function e(){return null}t.exports=e},{}],13:[function(r,t){"use strict";function e(r,t,e){for(var n=0,o=t.length,i=e?JSON.parse(JSON.stringify(r)):new Array(o);o>n;++n)i[n]=r[t[n]];return i}t.exports=e},{}],14:[function(r,t){function e(r){function t(r,t,o){return(i>o-t?n:e)(r,t,o)}function e(e,n,o){var i,u=0|(o-n)/6,f=n+u,a=o-1-u,c=n+o-1>>1,s=c-u,l=c+u,h=e[f],p=r(h),v=e[s],d=r(v),y=e[c],g=r(y),b=e[l],_=r(b),x=e[a],m=r(x);p>d&&(i=h,h=v,v=i,i=p,p=d,d=i),_>m&&(i=b,b=x,x=i,i=_,_=m,m=i),p>g&&(i=h,h=y,y=i,i=p,p=g,g=i),d>g&&(i=v,v=y,y=i,i=d,d=g,g=i),p>_&&(i=h,h=b,b=i,i=p,p=_,_=i),g>_&&(i=y,y=b,b=i,i=g,g=_,_=i),d>m&&(i=v,v=x,x=i,i=d,d=m,m=i),d>g&&(i=v,v=y,y=i,i=d,d=g,g=i),_>m&&(i=b,b=x,x=i,i=_,_=m,m=i);var w=v,E=d,O=b,k=_;e[f]=h,e[s]=e[n],e[c]=y,e[l]=e[o-1],e[a]=x;var A=n+1,z=o-2,j=k>=E&&E>=k;if(j)for(var S=A;z>=S;++S){var M=e[S],F=r(M);if(E>F)S!==A&&(e[S]=e[A],e[A]=M),++A;else if(F>E)for(;;){var C=r(e[z]);{if(!(C>E)){if(E>C){e[S]=e[A],e[A++]=e[z],e[z--]=M;break}e[S]=e[z],e[z--]=M;break}z--}}}else!function(){for(var t=A;z>=t;t++){var n=e[t],o=r(n);if(E>o)t!==A&&(e[t]=e[A],e[A]=n),++A;else if(o>k)for(;;){var i=r(e[z]);{if(!(i>k)){E>i?(e[t]=e[A],e[A++]=e[z],e[z--]=n):(e[t]=e[z],e[z--]=n);break}if(z--,t>z)break}}}}();return e[n]=e[A-1],e[A-1]=w,e[o-1]=e[z+1],e[z+1]=O,t(e,n,A-1),t(e,z+2,o),j?e:(f>A&&z>a&&!function(){for(var t,n;(t=r(e[A]))<=E&&t>=E;)++A;for(;(n=r(e[z]))<=k&&n>=k;)--z;for(var o=A;z>=o;o++){var i=e[o],u=r(i);if(E>=u&&u>=E)o!==A&&(e[o]=e[A],e[A]=i),A++;else if(k>=u&&u>=k)for(;;){n=r(e[z]);{if(!(k>=n&&n>=k)){E>n?(e[o]=e[A],e[A++]=e[z],e[z--]=i):(e[o]=e[z],e[z--]=i);break}if(z--,o>z)break}}}}(),t(e,A,z+1))}var n=o.by(r);return t}var n=r("./identity"),o=r("./insertionsort"),i=32;t.exports=e(n),t.exports.by=e},{"./identity":10,"./insertionsort":11}],15:[function(r,t){"use strict";function e(r){return r+1}function n(r){return r-1}function o(r){return function(t,e){return t+ +r(e)}}function i(r){return function(t,e){return t-r(e)}}t.exports={reduceIncrement:e,reduceDecrement:n,reduceAdd:o,reduceSubtract:i}},{}],16:[function(r,t){"use strict";function e(){return 0}t.exports=e},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /static/css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.1 - 2014-08-13 2 | * http://jqueryui.com 3 | * Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px 5 | * Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} -------------------------------------------------------------------------------- /crawler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Open Nodes Crawler 3 | Copyright (c) 2018 Opennodes / Blake Bjorn Anderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | 25 | import datetime 26 | import json 27 | import logging 28 | import os 29 | import re 30 | import socket 31 | import sys 32 | import time 33 | from concurrent.futures import ThreadPoolExecutor 34 | 35 | import numpy as np 36 | import pandas as pd 37 | import requests 38 | from geoip2.database import Reader 39 | from geoip2.errors import AddressNotFoundError 40 | from sqlalchemy import and_, or_, func, not_, case 41 | 42 | from config import load_config 43 | from models import Node, NodeVisitation, CrawlSummary, UserAgent, session 44 | from protocol import ProtocolError, Connection, ConnectionError, Keepalive 45 | 46 | logging.basicConfig(level=logging.INFO) 47 | 48 | CONF = load_config() 49 | ASN = Reader("geoip/GeoLite2-ASN.mmdb") 50 | COUNTRY = Reader("geoip/GeoLite2-Country.mmdb") 51 | CITY = Reader("geoip/GeoLite2-City.mmdb") 52 | RENAMED_COUNTRIES = {"South Korea": "Republic of Korea"} 53 | USER_AGENTS = {} 54 | 55 | 56 | def get_user_agent_id(user_agent): 57 | user_agent = str(user_agent) 58 | if len(user_agent) > 60: 59 | user_agent = user_agent[:60] 60 | if user_agent not in USER_AGENTS: 61 | u = session.query(UserAgent).filter(UserAgent.user_agent == user_agent).first() 62 | if u is None: 63 | u = UserAgent(user_agent=user_agent) 64 | session.add(u) 65 | session.flush() 66 | logging.info(f"New User Agent > {u.id} {u.user_agent}") 67 | USER_AGENTS[str(user_agent)] = int(u.id) 68 | return USER_AGENTS[user_agent] 69 | 70 | 71 | def connect(network, address, port, to_services, network_data, user_agent=None, explicit_p2p=False, p2p_nodes=True, 72 | from_services=None, keepalive=False, attempt=1): 73 | results = {'network': network, 'address': address, 'port': port, 74 | 'timestamp': datetime.datetime.utcnow(), 'seen': 0, 'attempt': attempt} 75 | 76 | try: 77 | handshake_msgs = [] 78 | new_addrs = [] 79 | 80 | proxy = CONF['tor_proxy'] if address.endswith(".onion") else None 81 | 82 | conn = Connection((address, port), 83 | (CONF['source_address'], 0), 84 | magic_number=network_data['magic_number'], 85 | socket_timeout=CONF['socket_timeout'], 86 | proxy=proxy, 87 | protocol_version=int(network_data['protocol_version']), 88 | min_protocol_version=network_data['min_protocol_version'], 89 | to_services=int(to_services), 90 | from_services=int(from_services or network_data['services']), 91 | user_agent=user_agent or CONF['user_agent'], 92 | height=int(network_data['height']), 93 | relay=CONF['relay']) 94 | 95 | try: 96 | conn.open() 97 | except (ProtocolError, ConnectionError, socket.error) as err: 98 | results['error'] = str(err) 99 | logging.debug("connection failed %s %s", type(err), err) 100 | else: 101 | try: 102 | handshake_msgs = conn.handshake() 103 | assert handshake_msgs 104 | results['seen'] = 1 105 | results['height'] = int(handshake_msgs[0]['height']) 106 | results['version'] = int(handshake_msgs[0]['version']) 107 | results['user_agent'] = handshake_msgs[0]['user_agent'].decode() 108 | results['services'] = int(handshake_msgs[0]['services']) 109 | except (ProtocolError, ConnectionError, socket.error, AssertionError) as err: 110 | results['error'] = str(err) 111 | logging.debug("handshake failed %s", err) 112 | 113 | msgs = [] 114 | if len(handshake_msgs) > 0 and (p2p_nodes or explicit_p2p): 115 | getaddr = True 116 | chance = CONF['getaddr_prop'] 117 | if chance < 1.0 and p2p_nodes and not explicit_p2p and "--seed" not in sys.argv: 118 | if np.random.rand() > chance: 119 | getaddr = False 120 | 121 | if getaddr: 122 | try: 123 | conn.getaddr(block=False) 124 | msgs = msgs + conn.get_messages(commands=[b"addr"]) 125 | time.sleep(5) 126 | msgs = msgs + conn.get_messages(commands=[b"addr"]) 127 | except (ProtocolError, ConnectionError, socket.error) as err: 128 | logging.debug("getaddr failed %s", err) 129 | if keepalive: 130 | Keepalive(conn, 10).keepalive(addr=True if p2p_nodes else False) 131 | for msg in msgs: 132 | if msg['count'] > 1: 133 | ts = results['timestamp'].timestamp() 134 | for addr in msg['addr_list']: 135 | if ts - addr['timestamp'] < 12 * 60 * 60: # within 6 hours 136 | new_addrs.append(addr) 137 | conn.close() 138 | return results, new_addrs 139 | except Exception as err: 140 | logging.warning("unspecified connection error: %s", err) 141 | return {}, [] 142 | 143 | 144 | def get_seeds(port, dns_seeds, address_seeds, default_services=0): 145 | """ 146 | Initializes a list of reachable nodes from DNS seeders and hardcoded nodes to bootstrap the crawler. 147 | """ 148 | export_list = [] 149 | for seeder in dns_seeds: 150 | nodes = [] 151 | 152 | try: 153 | ipv4_nodes = socket.getaddrinfo(seeder, None, socket.AF_INET) 154 | except socket.gaierror: 155 | if CONF['ipv6']: 156 | try: 157 | ipv6_nodes = socket.getaddrinfo(seeder, None, socket.AF_INET6) 158 | except socket.gaierror as err: 159 | logging.warning("%s %s", seeder, err) 160 | else: 161 | nodes.extend(ipv6_nodes) 162 | else: 163 | nodes.extend(ipv4_nodes) 164 | 165 | for node in nodes: 166 | address = node[-1][0] 167 | export_list.append((address, port, default_services)) 168 | 169 | for address in address_seeds: 170 | export_list.append((address, port, default_services)) 171 | 172 | return export_list 173 | 174 | 175 | def init_crawler(networks): 176 | # Populates list of all known node addresses and block heights 177 | db_networks = [x[0] for x in session.query(Node.network).distinct().all()] 178 | node_addresses = {} 179 | recent_heights = {} 180 | for network in set(db_networks + networks): 181 | node_addresses[network] = {f"{y.address};{y.port}" for y in 182 | session.query(Node.address, Node.port).filter(Node.network == network).all()} 183 | count = session.query(Node).filter(and_(Node.network == network, Node.last_height != None)).count() 184 | if count > 0: 185 | median = session.query(Node.last_height).filter(and_(Node.network == network, Node.last_height != None)) \ 186 | .order_by(Node.last_height).limit(1).offset(count // 2).one()[0] 187 | if median: 188 | recent_heights[network] = [median] 189 | if network not in recent_heights: 190 | recent_heights[network] = [500000] 191 | return node_addresses, recent_heights 192 | 193 | 194 | def check_dns(network_data, node_addresses): 195 | nodes = [] 196 | for network in network_data: 197 | nc = network_data[network] 198 | dns_node_addrs = get_seeds(nc['port'], nc['dns_seeds'], nc['address_seeds'], default_services=nc['services']) 199 | for nodeAddr in dns_node_addrs: 200 | if nodeAddr[0] and nodeAddr[1]: 201 | if not f"{nodeAddr[0]};{nodeAddr[1]}" in node_addresses[network]: 202 | node_addresses[network].add(f"{nodeAddr[0]};{nodeAddr[1]}") 203 | new_node = Node(network=network, address=nodeAddr[0], 204 | port=int(nodeAddr[1]), services=int(nodeAddr[2])) 205 | nodes.append(new_node) 206 | return nodes 207 | 208 | 209 | def prune_nodes(): 210 | # prune old nodes that can't be reached 211 | pruned = session.query(Node) \ 212 | .filter( 213 | and_( 214 | Node.last_seen == None, Node.first_checked != None, 215 | Node.first_checked <= datetime.datetime.utcnow() - datetime.timedelta(days=CONF['min_pruning_age']) 216 | )).delete() 217 | 218 | if pruned > 0: 219 | logging.info(f"Pruned {pruned} nodes") 220 | 221 | # prune visitations that no longer have a parent node 222 | if CONF['prune_visitations']: 223 | deleted = session.query(NodeVisitation) \ 224 | .outerjoin(Node, Node.id == NodeVisitation.parent_id) \ 225 | .filter(Node.address == None).delete(synchronize_session=False) 226 | logging.info(f"{deleted} Visitations deleted") 227 | 228 | session.commit() 229 | 230 | 231 | def calculate_pending_nodes(start_time): 232 | now = datetime.datetime.utcnow() 233 | # Get a list of all never checked nodes, and nodes that have been checked recently: 234 | q = session.query(Node) 235 | q = q.filter(or_( 236 | Node.first_checked == None, 237 | Node.last_checked == None, 238 | # Assume 30m interval 239 | # If it hasn't been seen before, check every 6h 240 | and_(Node.last_seen == None, Node.last_checked != None, 241 | Node.last_checked < now - datetime.timedelta(minutes=CONF['crawl_interval'] * 12)), 242 | # If it has been seen in the last 6 hours, check it every 30 minutes 243 | and_(Node.last_seen != None, Node.last_seen > now - datetime.timedelta(hours=6), 244 | Node.last_checked < now - datetime.timedelta(minutes=CONF['crawl_interval'])), 245 | # If it has been seen in the last 2 weeks, check it every 12 hours 246 | and_(Node.last_seen != None, Node.last_seen > now - datetime.timedelta(hours=24 * 14), 247 | Node.last_checked < now - datetime.timedelta(minutes=CONF['crawl_interval'] * 24)), 248 | # Otherwise every day 249 | and_(Node.last_seen != None, 250 | Node.last_checked < now - datetime.timedelta(minutes=CONF['crawl_interval'] * 48)) 251 | )).filter(not_(and_(Node.last_checked != None, Node.last_checked > start_time))) 252 | 253 | if CONF['crawl_order']: 254 | case_order = [] 255 | for i in range(len(CONF['crawl_order'])): 256 | case_order.append((Node.network == CONF['crawl_order'][i], str(i))) 257 | q = q.order_by(case(case_order, else_=Node.network), Node.seen.desc(), Node.last_checked) 258 | else: 259 | q = q.order_by(Node.seen.desc(), Node.last_checked) 260 | 261 | if CONF['max_queue'] > 0: 262 | count = q.count() 263 | q = q.limit(CONF['max_queue']) 264 | else: 265 | count = q.count() 266 | if count > CONF['max_queue']: 267 | logging.info(f"{count} nodes pending") 268 | 269 | if CONF['database_concurrency']: 270 | nodes = q.with_for_update().all() 271 | session.bulk_update_mappings(Node, [ 272 | {'id': x.id, 'last_checked': now} for x in nodes]) 273 | session.commit() 274 | return nodes 275 | 276 | return q.all() 277 | 278 | 279 | def process_pending_nodes(node_addresses, node_processing_queue, recent_heights, thread_pool, mnodes=None): 280 | futures_dict = {} 281 | 282 | checked_nodes = 0 283 | seen_nodes = 0 284 | pending_nodes = 0 285 | skipped_nodes = 0 286 | retried_nodes = 0 287 | found_on_retry = 0 288 | new_nodes_to_add = [] 289 | 290 | for net in recent_heights: 291 | CONF['networks'][net]['height'] = max(set(recent_heights[net]), 292 | key=recent_heights[net].count) 293 | recent_heights[net] = [CONF['networks'][net]['height']] 294 | 295 | # Get list of seen IPs and Ports so we don't send a bitcoin magic number to a bitcoin-cash node 296 | q = session.query(Node.network, Node.address, Node.port, Node.last_seen) \ 297 | .filter(Node.last_seen > datetime.datetime.utcnow() - datetime.timedelta(days=3)) 298 | 299 | active_ips = {} 300 | for x in q.all(): 301 | key = x.address + "|" + str(x.port) 302 | if key not in active_ips: 303 | active_ips[key] = (x.network, x.last_seen) 304 | else: 305 | # Prioritize bitcoin cash nodes, as its the only client that bans when the wrong magic number is sent 306 | if x.network == "bitcoin-cash": 307 | active_ips[key] = (x.network, x.last_seen) 308 | elif x.last_seen > active_ips[key][1] and active_ips[key][0] != "bitcoin-cash": 309 | active_ips[key] = (x.network, x.last_seen) 310 | 311 | while node_processing_queue: 312 | node = node_processing_queue.pop(0) 313 | if f"{node.address}|{node.port}" in active_ips and \ 314 | active_ips[f"{node.address}|{node.port}"][0] != node.network: 315 | node.last_checked = datetime.datetime.utcnow() 316 | session.add(node) 317 | skipped_nodes += 1 318 | continue 319 | future = thread_pool.submit(connect, node.network, node.address, node.port, node.services, 320 | CONF['networks'][node.network]) 321 | futures_dict[f"{node.network}|{node.address}|{node.port}"] = node, future 322 | time.sleep(0.001) 323 | 324 | total_to_complete = len(futures_dict) 325 | 326 | while len(futures_dict) > 0: 327 | time.sleep(1) 328 | for i in list(futures_dict.keys()): 329 | if not futures_dict[i][1].done(): 330 | continue 331 | 332 | checked_nodes += 1 333 | if checked_nodes % 1000 == 0: 334 | logging.info(f" {round(checked_nodes / total_to_complete * 100.0, 1)}%") 335 | 336 | node, future = futures_dict.pop(i) 337 | result, new_addrs = future.result() 338 | if not result: 339 | continue 340 | 341 | if not result['seen']: 342 | if CONF['retry_threshold'] and CONF['retry_threshold'] > 0 and (not node.seen or ( 343 | node.last_seen and node.last_seen < datetime.datetime.utcnow() - 344 | datetime.timedelta(hours=CONF['retry_threshold']))): 345 | pass 346 | elif result['attempt'] < CONF['retries'] + 1: 347 | future = thread_pool.submit(connect, node.network, node.address, node.port, node.services, 348 | CONF['networks'][node.network], attempt=result['attempt'] + 1) 349 | futures_dict[f"{node.network}|{node.address}|{node.port}"] = node, future 350 | total_to_complete += 1 351 | retried_nodes += 1 352 | continue 353 | elif result['seen'] and result['attempt'] > 1: 354 | found_on_retry += 1 355 | 356 | x = result['timestamp'] 357 | timestamp = datetime.datetime(x.year, x.month, x.day, x.hour, x.minute, x.second, x.microsecond) 358 | 359 | node.last_checked = timestamp 360 | if node.first_checked is None: 361 | node.first_checked = timestamp 362 | if result["seen"] and not any((x.match(result['user_agent']) for x in CONF['excluded_user_agents'])): 363 | node.version = result['version'] 364 | node.last_seen = timestamp 365 | node.services = result['services'] 366 | node.user_agent = result['user_agent'] 367 | node.last_height = result['height'] 368 | if node.first_seen is None: 369 | node.first_seen = timestamp 370 | node.country, node.city, node.aso, node.asn = geocode_ip(node.address) 371 | node.seen = True 372 | 373 | seen_nodes += 1 374 | recent_heights[result['network']].append(result['height']) 375 | 376 | session.add(node) 377 | 378 | if node.seen: 379 | if not node.id: 380 | session.commit() 381 | vis = NodeVisitation(parent_id=node.id, 382 | user_agent_id=get_user_agent_id(result['user_agent']) 383 | if 'user_agent' in result else None, 384 | success=result["seen"], 385 | timestamp=timestamp, 386 | height=result['height'] if result["seen"] else None) 387 | 388 | if mnodes and node.network == "dash" and f"{node.address}:{node.port}" in mnodes: 389 | vis.is_masternode = True 390 | 391 | session.add(vis) 392 | 393 | if new_addrs: 394 | for n in new_addrs: 395 | addr = n['ipv4'] or n['ipv6'] or n['onion'] 396 | if not f"{addr};{n['port']}" in node_addresses[result['network']]: 397 | pending_nodes += 1 398 | node_addresses[result['network']].add(f"{addr};{n['port']}") 399 | new_node = Node(network=str(result['network']), address=addr, port=int(n['port']), 400 | services=int(n['services'])) 401 | if CONF['database_concurrency']: 402 | new_nodes_to_add.append(new_node) 403 | else: 404 | session.add(new_node) 405 | 406 | if CONF['database_concurrency']: 407 | # Get all unchecked nodes and nodes first seen in the past hour, 408 | # don't insert any new nodes that have already been inserted 409 | nn = session.query(Node.network, Node.address, Node.port) \ 410 | .filter(or_(Node.first_checked == None, 411 | Node.first_checked > datetime.datetime.utcnow() - datetime.timedelta(hours=1))) \ 412 | .with_for_update().all() 413 | new_set = {f"{n.network};{n.address};{n.port}" for n in nn} 414 | for i in reversed(range(len(new_nodes_to_add))): 415 | ni = f"{new_nodes_to_add[i].network};{new_nodes_to_add[i].address};{new_nodes_to_add[i].port}" 416 | if ni in new_set: 417 | del new_nodes_to_add[i] 418 | 419 | session.commit() 420 | logging.info(f"Checked {checked_nodes - retried_nodes} Nodes, {seen_nodes} Seen, {pending_nodes} More queued up. " 421 | f"({found_on_retry}/{retried_nodes} retry successes, {skipped_nodes} skipped x-network nodes)") 422 | return node_processing_queue, node_addresses 423 | 424 | 425 | def update_masternode_list(): 426 | if os.environ.get("DASH_RPC_URI"): 427 | resp = requests.post(os.environ.get("DASH_RPC_URI"), 428 | json={"jsonrpc": "2.0", "id": "jsonrpc", "method": "masternode", "params": ["list"]}, 429 | auth=(os.environ.get("DASH_RPC_USER"), os.environ.get("DASH_RPC_PASS"))) 430 | masternodes = resp.json()['result'] 431 | else: 432 | comm = "dash-cli" 433 | if os.path.isdir(CONF['dash_cli_path']): 434 | comm = os.path.join(CONF['dash_cli_path'], "dash-cli") 435 | masternodes = os.popen(f"{comm} masternode list full").read().strip() 436 | masternodes = json.loads(masternodes) 437 | 438 | m_nodes = set() 439 | if masternodes: 440 | for i, vals in masternodes.items(): 441 | if isinstance(vals, dict): 442 | address = vals['address'] 443 | else: 444 | address = vals[-1] 445 | m_nodes.add(address.strip()) 446 | 447 | if not masternodes and CONF['dash_masternodes_api']: 448 | try: 449 | m_nodes = set(requests.post(CONF['dash_masternodes_api']).json()) 450 | except: 451 | pass 452 | 453 | if m_nodes: 454 | with open("static/masternode_list.txt", 'w') as f: 455 | f.write("\n".join(m_nodes)) 456 | elif os.path.isfile("static/masternode_list.txt"): 457 | with open("static/masternode_list.txt", "r") as f: 458 | m_nodes = set(f.read().splitlines(keepends=False)) 459 | return m_nodes 460 | 461 | 462 | def set_master_nodes(m_nodes): 463 | if not m_nodes: 464 | return 465 | window_idx = 0 466 | window_size = 10000 467 | q = session.query(Node).filter(Node.seen == True) 468 | while True: 469 | start, stop = window_size * window_idx, window_size * (window_idx + 1) 470 | nodes = q.slice(start, stop).all() 471 | if nodes is None: 472 | break 473 | for n in nodes: 474 | if n.address + ":" + str(n.port) in m_nodes: 475 | n.is_masternode = True 476 | session.add(n) 477 | elif n.is_masternode: 478 | n.is_masternode = False 479 | session.add(n) 480 | session.commit() 481 | window_idx += 1 482 | if len(nodes) < window_size: 483 | break 484 | 485 | 486 | def code_ip_type(inp): 487 | if ".onion" in inp: 488 | return "Onion" 489 | elif "." in inp: 490 | return "IPv4" 491 | elif ":" in inp: 492 | return "IPv6" 493 | else: 494 | return "Unknown" 495 | 496 | 497 | def geocode_ip(address): 498 | aso = None 499 | asn = None 500 | country = None 501 | city = None 502 | if not address.endswith(".onion"): 503 | try: 504 | aso = ASN.asn(address).autonomous_system_organization 505 | asn = ASN.asn(address).autonomous_system_number 506 | except AddressNotFoundError: 507 | pass 508 | try: 509 | country = COUNTRY.country(address).country.name 510 | country = RENAMED_COUNTRIES.get(country, country) 511 | city = CITY.city(address).city.name 512 | except AddressNotFoundError: 513 | pass 514 | return country, city, aso, asn 515 | 516 | 517 | def check_active(height, deviation_config): 518 | return (deviation_config[1] - deviation_config[0]) <= height <= (deviation_config[1] + deviation_config[0]) 519 | 520 | 521 | def dump_summary(): 522 | # Set updated countries 523 | for n in session.query(Node).all(): 524 | n.country, n.city, n.aso, n.asn = geocode_ip(n.address, ) 525 | 526 | # Get and set dash masternodes 527 | if CONF['get_dash_masternodes']: 528 | mnodes = update_masternode_list() 529 | set_master_nodes(mnodes) 530 | logging.info("masternodes updated") 531 | 532 | q = session.query(Node.id, Node.network, Node.address, Node.port, Node.user_agent, Node.version, Node.asn, Node.aso, 533 | Node.country, Node.city, Node.last_seen, Node.last_height, Node.is_masternode) \ 534 | .filter(Node.seen == True) \ 535 | .filter(Node.last_seen >= datetime.datetime.utcnow() - datetime.timedelta(days=7)) 536 | 537 | nodes = pd.read_sql(q.statement, q.session.bind) 538 | nodes[['port', 'version', 'last_height']] = nodes[['port', 'version', 'last_height']].fillna(0) 539 | nodes = nodes.fillna("") 540 | 541 | if nodes.empty: 542 | logging.warning("Nodes table is empty, no results to dump") 543 | return 544 | 545 | # Exclude user agents 546 | if CONF['excluded_user_agents']: 547 | for agent_re in CONF['excluded_user_agents']: 548 | agent_re = re.compile(agent_re) 549 | nodes = nodes[~nodes['user_agent'].str.match(agent_re)].copy() 550 | 551 | now = datetime.datetime.utcnow() 552 | labels = [] 553 | for age, label in [(2, "2h"), (8, "8h"), (24, "24h"), (24 * 7, "7d"), (24 * 30, "30d")]: 554 | stt = time.time() 555 | q = session.query(Node.id, 556 | func.sum(case([(NodeVisitation.success, 1)], else_=0)).label("success"), 557 | func.count(NodeVisitation.parent_id).label("total")) \ 558 | .join(NodeVisitation, Node.id == NodeVisitation.parent_id) \ 559 | .group_by(Node.id) \ 560 | .filter(Node.last_seen > now - datetime.timedelta(hours=age)) \ 561 | .filter(NodeVisitation.timestamp >= now - datetime.timedelta(hours=age)) 562 | df = pd.read_sql(q.statement, q.session.bind) 563 | df[label] = (df['success'] / df['total']).fillna(0.0) 564 | nodes = nodes.merge(df[['id', label]], how="left") 565 | labels.append(label) 566 | logging.info(f"done {label} in {round(time.time() - stt, 3)}s") 567 | nodes = nodes.drop(['id'], 1) 568 | nodes[labels] = nodes[labels].fillna(0.0).round(3) 569 | nodes[['network', 'address']] = nodes[['network', 'address']].fillna("") 570 | nodes['address_type'] = nodes['address'].apply(code_ip_type) 571 | 572 | nodes['network'] = nodes[['network', 'user_agent']].apply( 573 | lambda x: "bitcoin-sv" if x['network'] == 'bitcoin-cash' and ' SV' in x['user_agent'] else x['network'], axis=1) 574 | 575 | networks = nodes['network'].unique() 576 | 577 | # Calculate summaries 578 | summaries = {} 579 | for network in networks: 580 | summary_df = nodes[(nodes['network'] == network) & 581 | (nodes['last_seen'] > datetime.datetime.utcnow() - datetime.timedelta( 582 | hours=8))] 583 | if summary_df.empty: 584 | continue 585 | 586 | summaries[network] = { 587 | "min": int(summary_df['last_height'].fillna(np.inf).min()), 588 | "max": int(summary_df['last_height'].fillna(0.0).max()), 589 | "mean": float(summary_df['last_height'].mean()), 590 | "stdev": float(summary_df['last_height'].std()), 591 | "med": float(summary_df['last_height'].median()), 592 | "1q": float(np.percentile(summary_df['last_height'], 25)), 593 | "3q": float(np.percentile(summary_df['last_height'], 75)), 594 | "2.5pct": float(np.percentile(summary_df['last_height'], 1)), 595 | "97.5pct": float(np.percentile(summary_df['last_height'], 99)), 596 | "age_min": nodes[nodes['network'] == network]['last_seen'].min().timestamp(), 597 | "age_max": summary_df['last_seen'].max().timestamp() 598 | } 599 | summaries[network]['iqr'] = summaries[network]['3q'] - summaries[network]['1q'] 600 | summaries[network]['95_range'] = summaries[network]['97.5pct'] - summaries[network]['2.5pct'] 601 | 602 | summaries["_timestamp"] = datetime.datetime.utcnow().isoformat() 603 | with open("static/network_summaries.json", 'w') as f: 604 | json.dump(summaries, f) 605 | 606 | if CONF['inactive_use_iqr']: 607 | deviations = {network: summaries[network]['iqr'] * ( 608 | CONF['inactive_threshold'][network] if network in CONF['inactive_threshold'] else 609 | CONF['inactive_threshold']['default']) for network in networks} 610 | else: 611 | deviations = {net: CONF['inactive_threshold'][net] if net in CONF['inactive_threshold'] else \ 612 | CONF['inactive_threshold']['default'] for net in networks} 613 | 614 | for i in deviations: 615 | deviations[i] = (deviations[i], summaries[i]['3q']) 616 | 617 | nodes['is_active'] = nodes[['network', 'last_height']] \ 618 | .apply(lambda x: check_active(x['last_height'], deviations[x['network']]), axis=1) 619 | 620 | if not CONF['export_inactive_nodes']: 621 | nodes = nodes[nodes['is_active']].copy() 622 | 623 | nodes['last_seen'] = nodes['last_seen'].values.astype(np.int64) // 10 ** 9 624 | nodes.to_csv("static/data.csv", index=False) 625 | 626 | with open("static/data.txt", "w") as f: 627 | f.write(space_sep_df(nodes)) 628 | 629 | for network in nodes['network'].unique(): 630 | net_df = nodes[nodes['network'] == network].copy() 631 | net_df = net_df.drop(['network'], 1) 632 | 633 | net_df.to_csv(f"static/data_{network}.csv", index=False) 634 | with open(os.path.join("static", f"data_{network}.json"), "w") as f: 635 | json.dump({'data': net_df.to_dict(orient="records")}, f) 636 | with open(os.path.join("static", f"data_{network}.txt"), "w") as f: 637 | f.write(space_sep_df(net_df)) 638 | 639 | nodes = nodes.drop(['user_agent', 'version', 'last_height'], 1) 640 | with open(os.path.join("static", "data.json"), "w") as f: 641 | json.dump({'data': nodes.to_dict(orient="records")}, f) 642 | 643 | # Write unique addresses only 644 | def group_nets(x): 645 | return ", ".join(sorted(set(x))) 646 | 647 | nodes = nodes.groupby(by=['address', 'asn', 'aso', 'country', 'city', 'address_type'], as_index=False).agg( 648 | {"network": group_nets, "2h": "mean", "8h": "mean", "24h": "mean", "7d": "mean", "30d": "mean"}) 649 | nodes.to_csv("static/data_unique.csv", index=False) 650 | 651 | with open(os.path.join("static", "data_unique.json"), "w") as f: 652 | json.dump({'data': nodes.to_dict(orient="records")}, f) 653 | with open(os.path.join("static", "data_unique.txt"), "w") as f: 654 | f.write(space_sep_df(nodes)) 655 | 656 | for network in networks: 657 | net_df = nodes[nodes['network'].str.contains(network)] 658 | net_df = net_df.drop(['network'], 1) 659 | net_df.to_csv(os.path.join("static", f"data_{network}_unique.csv"), index=False) 660 | with open(os.path.join("static", f"data_{network}_unique.json"), "w") as f: 661 | json.dump({'data': net_df.to_dict(orient="records")}, f) 662 | with open(os.path.join("static", f"data_{network}_unique.txt"), "w") as f: 663 | f.write(space_sep_df(net_df)) 664 | 665 | 666 | def space_sep_df(df, spacing=3): 667 | df = df.copy() 668 | df = pd.DataFrame([df.columns], columns=df.columns).append(df) 669 | for col in df.columns: 670 | df[col] = df[col].astype(str) 671 | max_len = df[col].str.len().max() + spacing 672 | df[col] = df[col].str.pad(max_len, side="right") 673 | out_str = "\n".join(("".join((str(row[x + 1]) for x in range(len(df.columns)))) for row in df.itertuples())) 674 | return out_str 675 | 676 | 677 | def main(seed=False): 678 | start_time = datetime.datetime.utcnow() 679 | thread_pool = ThreadPoolExecutor(max_workers=CONF['threads']) 680 | networks = list(CONF['networks'].keys()) 681 | prune_nodes() 682 | node_addresses, recent_heights = init_crawler(networks) 683 | 684 | if CONF['get_dash_masternodes']: 685 | mnodes = update_masternode_list() 686 | else: 687 | mnodes = None 688 | 689 | if seed: 690 | seed_nodes = check_dns(CONF['networks'], node_addresses) 691 | if seed_nodes: 692 | for n in seed_nodes: 693 | session.add(n) 694 | session.commit() 695 | 696 | node_processing_queue = calculate_pending_nodes(start_time) 697 | while node_processing_queue: 698 | node_processing_queue, node_addresses = process_pending_nodes(node_addresses, node_processing_queue, 699 | recent_heights, thread_pool, mnodes) 700 | node_processing_queue = calculate_pending_nodes(start_time) 701 | logging.info(f"Crawling complete in {round((datetime.datetime.utcnow() - start_time).seconds, 1)} seconds") 702 | 703 | 704 | def dump(): 705 | start_time = datetime.datetime.utcnow() 706 | dump_summary() 707 | generate_historic_data() 708 | logging.info(f"Results saved in {round((datetime.datetime.utcnow() - start_time).seconds, 1)} seconds") 709 | 710 | 711 | def generate_historic_data(): 712 | networks = [x[0] for x in session.query(Node.network).distinct()] 713 | sd = session.query(func.min(Node.first_seen)).one()[0] 714 | start_date = datetime.datetime(sd.year, sd.month, sd.day, 715 | sd.hour // CONF['historic_interval'] * CONF['historic_interval'], 0, 0) 716 | end_date = session.query(func.max(Node.last_seen)).one()[0] 717 | 718 | historic_interval = datetime.timedelta(hours=CONF['historic_interval']) 719 | 720 | last_date = start_date 721 | while last_date < end_date: 722 | last_date += historic_interval 723 | 724 | interval_end = start_date + historic_interval 725 | session.query(CrawlSummary).filter( 726 | CrawlSummary.timestamp >= (last_date - datetime.timedelta(hours=CONF['historic_interval'] * 1.5))).delete() 727 | session.commit() 728 | while interval_end < end_date: 729 | if session.query(CrawlSummary).filter(CrawlSummary.timestamp == interval_end).count() >= 1: 730 | interval_end += historic_interval 731 | continue 732 | logging.info(f"Summarizing period starting with {interval_end - historic_interval}") 733 | 734 | sv_sq = session.query(UserAgent.id).filter(UserAgent.user_agent.ilike("% SV%")).subquery() 735 | 736 | case_stmt = case([(sv_sq.c.id != None, 'bitcoin-sv')], else_=Node.network) 737 | 738 | q = session.query(NodeVisitation.parent_id.label("id"), 739 | case_stmt.label("network"), 740 | func.max(NodeVisitation.height).label("height"), 741 | func.max(case([(NodeVisitation.is_masternode, 1)], else_=0)).label("is_masternode")) \ 742 | .join(sv_sq, NodeVisitation.user_agent_id == sv_sq.c.id) \ 743 | .join(Node, Node.id == NodeVisitation.parent_id) \ 744 | .filter(NodeVisitation.timestamp >= interval_end - historic_interval) \ 745 | .filter(NodeVisitation.timestamp <= interval_end) \ 746 | .filter(NodeVisitation.success == True) \ 747 | .filter(Node.first_seen <= interval_end) \ 748 | .filter(Node.last_seen >= interval_end - historic_interval) \ 749 | .group_by(NodeVisitation.parent_id, case_stmt) 750 | df = pd.read_sql(q.statement, q.session.bind) 751 | 752 | df['height'] = df['height'].astype(int) 753 | if not df.empty: 754 | networks = df['network'].unique() 755 | 756 | medians = df.groupby(by=['network']).agg({"height": "median"}) 757 | deviations = {network: CONF['inactive_threshold'][network] if network in CONF['inactive_threshold'] else \ 758 | CONF['inactive_threshold']['default'] for network in networks} 759 | 760 | for i in list(deviations.keys()): 761 | if i in medians.index: 762 | deviations[i] = (deviations[i], medians.loc[i]['height']) 763 | else: 764 | deviations.pop(i) 765 | 766 | df['active'] = df[['network', 'height']].apply( 767 | lambda x: check_active(x['height'], deviations[x['network']]), axis=1) 768 | df = df[df['active']].drop(['active'], 1) 769 | 770 | for network in networks: 771 | net_df = df[df['network'] == network] 772 | cs = CrawlSummary(timestamp=interval_end, 773 | network=network, 774 | node_count=len(net_df), 775 | masternode_count=sum(net_df['is_masternode']), 776 | lookback_hours=CONF['historic_interval']) 777 | 778 | session.add(cs) 779 | session.commit() 780 | 781 | interval_end += datetime.timedelta(hours=CONF['historic_interval']) 782 | 783 | q = session.query(CrawlSummary).order_by(CrawlSummary.timestamp) 784 | df = pd.read_sql(q.statement, q.session.bind) 785 | df['timestamp'] = df['timestamp'].values.astype(np.int64) // 10 ** 9 786 | 787 | for network in networks: 788 | df[df['network'] == network][['timestamp', 'node_count', 'masternode_count']] \ 789 | .to_json(os.path.join("static", f"history_{network}.json"), orient='records') 790 | 791 | 792 | def prune_database(): 793 | if not os.path.isdir("db_cache"): 794 | os.mkdir("db_cache") 795 | 796 | q = session.query(Node) 797 | nodes = pd.read_sql(q.statement, q.session.bind) 798 | 799 | fv = session.query(func.min(NodeVisitation.timestamp)).first()[0] 800 | end_date = datetime.datetime.utcnow() - datetime.timedelta(hours=24 * CONF['max_pruning_age']) 801 | end_date = datetime.datetime(end_date.year, end_date.month, end_date.day, 0, 0, 0) 802 | 803 | current_date = datetime.datetime(fv.year, fv.month, fv.day, 0, 0, 0) 804 | current_end = current_date + datetime.timedelta(days=1) 805 | 806 | while current_end < end_date: 807 | vq = session.query(NodeVisitation) \ 808 | .filter(NodeVisitation.timestamp >= current_date) \ 809 | .filter(NodeVisitation.timestamp < current_end) 810 | 811 | f_name = f"visitations_{current_date.strftime('%Y-%m-%d')}.gz" 812 | f_name = os.path.join("db_cache", f_name) 813 | f_name_alt = f"nodes_{current_date.strftime('%Y-%m-%d')}.gz" 814 | f_name_alt = os.path.join("db_cache", f_name_alt) 815 | 816 | df = pd.read_sql(vq.statement, vq.session.bind) 817 | an = nodes.merge(df[['parent_id']].drop_duplicates(), left_on="id", right_on="parent_id") 818 | an = an[[x for x in an.columns if x != "parent_id"]] 819 | 820 | df['timestamp'] = pd.to_datetime(df['timestamp']) 821 | df['timestamp'] = df['timestamp'].astype(np.int64) / 1000000000 822 | df['height'] = df['height'].fillna(-1).apply(lambda x: int(x) if x > 0 else "") 823 | df['success'] = df['success'].fillna(-1).apply(lambda x: int(x) if x > 0 else "") 824 | df['user_agent_id'] = df['user_agent_id'].fillna(-1).apply(lambda x: int(x) if x > 0 else "") 825 | df['is_masternode'] = df['is_masternode'].fillna(-1).apply(lambda x: int(x) if x > 0 else "") 826 | df.to_csv(f_name, compression="gzip", index=False) 827 | 828 | for col in ("first_seen", "last_seen", "first_checked", "last_checked"): 829 | an[col] = pd.to_datetime(an[col]) 830 | an[col] = an[col].astype(np.int64) / 1000000000 831 | for col in ("seen", "last_height", "version", "services", "is_masternode"): 832 | an[col] = an[col].apply(lambda x: int(x) if x else "") 833 | an.to_csv(f_name_alt, compression="gzip", index=False) 834 | 835 | deleted = vq.delete() 836 | session.commit() 837 | 838 | current_date = current_date + datetime.timedelta(days=1) 839 | current_end = current_date + datetime.timedelta(days=1) 840 | logging.info(f"pruned up to {current_end} // {deleted} visitations removed") 841 | 842 | 843 | if __name__ == "__main__": 844 | 845 | if "--crawl" in sys.argv: 846 | main(seed=True if "--seed" in sys.argv else False) 847 | 848 | if "--dump" in sys.argv: 849 | dump() 850 | 851 | if "--prune" in sys.argv: 852 | prune_database() 853 | 854 | if "--daemon" in sys.argv: 855 | conf = CONF.get('daemon', {}) 856 | crawl_interval = int(conf.get('crawl_interval', 15)) 857 | dump_interval = int(conf.get('dump_interval', 60)) 858 | prune_interval = conf.get('prune_interval', None) 859 | current = int(time.time()) 860 | last_minutes = -1 861 | 862 | main(seed=True) 863 | dump() 864 | if prune_interval is not None: 865 | prune_database() 866 | while True: 867 | minutes = int(current / 60) 868 | if minutes != last_minutes: 869 | last_minutes = minutes 870 | if minutes % crawl_interval == 0: 871 | main(seed=False) 872 | if minutes % dump_interval == 0: 873 | dump() 874 | if prune_interval is not None and minutes % int(prune_interval) == 0: 875 | prune_database() 876 | 877 | current += 1 878 | while current > time.time(): 879 | time.sleep(0.1) 880 | -------------------------------------------------------------------------------- /protocol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # protocol.py - Bitcoin protocol access for Bitnodes. 5 | # 6 | # Copyright (c) Addy Yeow Chin Heng 7 | # 8 | # Modified by open-nodes project for python3 compatibility 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining 11 | # a copy of this software and associated documentation files (the 12 | # "Software"), to deal in the Software without restriction, including 13 | # without limitation the rights to use, copy, modify, merge, publish, 14 | # distribute, sublicense, and/or sell copies of the Software, and to 15 | # permit persons to whom the Software is furnished to do so, subject to 16 | # the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | """ 30 | Bitcoin protocol access for Bitnodes. 31 | Reference: https://en.bitcoin.it/wiki/Protocol_specification 32 | 33 | ------------------------------------------------------------------------------- 34 | PACKET STRUCTURE FOR BITCOIN PROTOCOL 35 | protocol version >= 70001 36 | ------------------------------------------------------------------------------- 37 | [---MESSAGE---] 38 | [ 4] MAGIC_NUMBER (\xF9\xBE\xB4\xD9) uint32_t 39 | [12] COMMAND char[12] 40 | [ 4] LENGTH H uint16_t 54 | [26] ADDR_FROM 55 | [ 8] SERVICES H uint16_t 60 | [ 8] NONCE= 70001) bool 64 | 65 | [---ADDR_PAYLOAD---] 66 | [..] COUNT variable integer 67 | [..] ADDR_LIST multiple of COUNT (max 1000) 68 | [ 4] TIMESTAMP H uint16_t 74 | 75 | [---PING_PAYLOAD---] 76 | [ 8] NONCELE (little-endian) 520 | msg['prev_block_hash'] = hexlify(data.read(32)[::-1]) 521 | 522 | # BE -> LE 523 | msg['merkle_root'] = hexlify(data.read(32)[::-1]) 524 | 525 | msg['timestamp'] = struct.unpack(" BE 544 | ] 545 | return b''.join(payload) 546 | 547 | def serialize_block_headers_payload(self, headers): 548 | payload = [ 549 | self.serialize_int(len(headers)), 550 | ] 551 | payload.extend( 552 | [self.serialize_block_header(header) for header in headers]) 553 | return b''.join(payload) 554 | 555 | def deserialize_block_headers_payload(self, data): 556 | msg = {} 557 | data = BytesIO(data) 558 | 559 | msg['count'] = self.deserialize_int(data) 560 | msg['headers'] = [] 561 | for _ in range(msg['count']): 562 | header = self.deserialize_block_header(data) 563 | msg['headers'].append(header) 564 | 565 | return msg 566 | 567 | def serialize_network_address(self, addr): 568 | network_address = [] 569 | if len(addr) == 4: 570 | (timestamp, services, ip_address, port) = addr 571 | network_address.append(struct.pack("H", port)) 589 | return b''.join(network_address) 590 | 591 | def deserialize_network_address(self, data, has_timestamp=False): 592 | timestamp = None 593 | if has_timestamp: 594 | timestamp = unpack("H", data.read(2)) 601 | _ipv6 += _ipv4 602 | 603 | ipv4 = "" 604 | ipv6 = "" 605 | onion = "" 606 | 607 | if _ipv6[:6] == ONION_PREFIX: 608 | onion = b32encode(_ipv6[6:]).lower().decode("utf8") + ".onion" # use .onion 609 | else: 610 | ipv6 = socket.inet_ntop(socket.AF_INET6, _ipv6) 611 | ipv4 = socket.inet_ntop(socket.AF_INET, _ipv4) 612 | if ipv4 in ipv6: 613 | ipv6 = "" # use ipv4 614 | else: 615 | ipv4 = "" # use ipv6 616 | 617 | return { 618 | 'timestamp': timestamp, 619 | 'services': services, 620 | 'ipv4': ipv4, 621 | 'ipv6': ipv6, 622 | 'onion': onion, 623 | 'port': port, 624 | } 625 | 626 | def serialize_inventory(self, item): 627 | (inv_type, inv_hash) = item 628 | payload = [ 629 | struct.pack(" BE 631 | ] 632 | return b''.join(payload) 633 | 634 | def deserialize_inventory(self, data): 635 | inv_type = unpack(" LE 637 | return { 638 | 'type': inv_type, 639 | 'hash': hexlify(inv_hash), 640 | } 641 | 642 | def serialize_tx_in(self, tx_in): 643 | payload = [ 644 | unhexlify(tx_in['prev_out_hash'])[::-1], # LE -> BE 645 | struct.pack(" LE 655 | prev_out_index = struct.unpack(" BE 689 | unhexlify(header['merkle_root'])[::-1], # LE -> BE 690 | struct.pack(" LE 700 | header = BytesIO(header) 701 | version = struct.unpack(" LE 703 | merkle_root = header.read(32)[::-1] # BE -> LE 704 | timestamp = struct.unpack(" 0: 806 | chunks = [] 807 | while length > 0: 808 | chunk = self.socket.recv(SOCKET_BUFSIZE) 809 | if not chunk: 810 | raise RemoteHostClosedConnection("{} closed connection".format(self.to_addr)) 811 | chunks.append(chunk) 812 | length -= len(chunk) 813 | data = b''.join(chunks) 814 | else: 815 | data = self.socket.recv(SOCKET_BUFSIZE) 816 | if not data: 817 | raise RemoteHostClosedConnection("{} closed connection".format(self.to_addr)) 818 | if len(data) > SOCKET_BUFSIZE: 819 | end_t = time.time() 820 | self.bps.append((len(data) * 8) / (end_t - start_t)) 821 | return data 822 | 823 | def get_messages(self, length=0, commands=None): 824 | msgs = [] 825 | data = self.recv(length=length) 826 | while len(data) > 0: 827 | time.sleep(0.0001) 828 | try: 829 | (msg, data) = self.serializer.deserialize_msg(data) 830 | except PayloadTooShortError: 831 | data += self.recv( 832 | length=self.serializer.required_len - len(data)) 833 | (msg, data) = self.serializer.deserialize_msg(data) 834 | if msg.get('command') == b"ping": 835 | self.pong(msg['nonce']) # respond to ping immediately 836 | elif msg.get('command') == b"version": 837 | self.verack() # respond to version immediately 838 | msgs.append(msg) 839 | if len(msgs) > 0 and commands: 840 | msgs[:] = [m for m in msgs if m.get('command') in commands] 841 | return msgs 842 | 843 | def set_min_version(self, version): 844 | self.serializer.protocol_version = min( 845 | self.serializer.protocol_version, 846 | version.get(b'version', self.serializer.protocol_version)) 847 | 848 | def handshake(self): 849 | # [version] >>> 850 | msg = self.serializer.serialize_msg( 851 | command=b"version", to_addr=self.to_addr, from_addr=self.from_addr) 852 | self.send(msg) 853 | 854 | # <<< [version 124 bytes] [verack 24 bytes] 855 | time.sleep(1) 856 | msgs = self.get_messages(length=148, commands=[b"version", b"verack"]) 857 | if len(msgs) > 0: 858 | msgs[:] = sorted(msgs, key=itemgetter('command'), reverse=True) 859 | self.set_min_version(msgs[0]) 860 | return msgs 861 | 862 | def verack(self): 863 | # [verack] >>> 864 | msg = self.serializer.serialize_msg(command=b"verack") 865 | self.send(msg) 866 | 867 | def getaddr(self, block=True): 868 | # [getaddr] >>> 869 | msg = self.serializer.serialize_msg(command=b"getaddr") 870 | self.send(msg) 871 | 872 | # Caller should call get_messages separately. 873 | if not block: 874 | return None 875 | 876 | # <<< [addr].. 877 | time.sleep(3) 878 | msgs = self.get_messages(commands=[b"addr"]) 879 | return msgs 880 | 881 | def getpeerinfo(self, block=True): 882 | # [getaddr] >>> 883 | msg = self.serializer.serialize_msg(command=b"getpeerinfo") 884 | self.send(msg) 885 | 886 | # Caller should call get_messages separately. 887 | if not block: 888 | return None 889 | 890 | # <<< [addr].. 891 | msgs = self.get_messages(commands=[b"getpeerinfo"]) 892 | return msgs 893 | 894 | def addr(self, addr_list): 895 | # addr_list = [(TIMESTAMP, SERVICES, "IP_ADDRESS", PORT),] 896 | # [addr] >>> 897 | msg = self.serializer.serialize_msg( 898 | command=b"addr", addr_list=addr_list) 899 | self.send(msg) 900 | 901 | def ping(self, nonce=None): 902 | if nonce is None: 903 | nonce = random.getrandbits(64) 904 | 905 | # [ping] >>> 906 | msg = self.serializer.serialize_msg(command=b"ping", nonce=nonce) 907 | self.send(msg) 908 | 909 | def pong(self, nonce): 910 | # [pong] >>> 911 | msg = self.serializer.serialize_msg(command=b"pong", nonce=nonce) 912 | self.send(msg) 913 | 914 | def inv(self, inventory): 915 | # inventory = [(INV_TYPE, "INV_HASH"),] 916 | # [inv] >>> 917 | msg = self.serializer.serialize_msg( 918 | command=b"inv", inventory=inventory) 919 | self.send(msg) 920 | 921 | def getdata(self, inventory): 922 | # inventory = [(INV_TYPE, "INV_HASH"),] 923 | # [getdata] >>> 924 | msg = self.serializer.serialize_msg( 925 | command=b"getdata", inventory=inventory) 926 | self.send(msg) 927 | 928 | # <<< [tx] [block].. 929 | time.sleep(1) 930 | msgs = self.get_messages(commands=[b"tx", b"block"]) 931 | return msgs 932 | 933 | def getblocks(self, block_hashes, last_block_hash=None): 934 | if last_block_hash is None: 935 | last_block_hash = "0" * 64 936 | 937 | # block_hashes = ["BLOCK_HASH",] 938 | # [getblocks] >>> 939 | msg = self.serializer.serialize_msg(command=b"getblocks", 940 | block_hashes=block_hashes, 941 | last_block_hash=last_block_hash) 942 | self.send(msg) 943 | 944 | # <<< [inv].. 945 | time.sleep(1) 946 | msgs = self.get_messages(commands=[b"inv"]) 947 | return msgs 948 | 949 | def getheaders(self, block_hashes, last_block_hash=None): 950 | if last_block_hash is None: 951 | last_block_hash = "0" * 64 952 | 953 | # block_hashes = ["BLOCK_HASH",] 954 | # [getheaders] >>> 955 | msg = self.serializer.serialize_msg(command=b"getheaders", 956 | block_hashes=block_hashes, 957 | last_block_hash=last_block_hash) 958 | self.send(msg) 959 | 960 | # <<< [headers].. 961 | time.sleep(1) 962 | msgs = self.get_messages(commands=[b"headers"]) 963 | return msgs 964 | 965 | def headers(self, headers): 966 | # headers = [{ 967 | # 'version': VERSION, 968 | # 'prev_block_hash': PREV_BLOCK_HASH, 969 | # 'merkle_root': MERKLE_ROOT, 970 | # 'timestamp': TIMESTAMP, 971 | # 'bits': BITS, 972 | # 'nonce': NONCE 973 | # },] 974 | # [headers] >>> 975 | msg = self.serializer.serialize_msg(command=b"headers", headers=headers) 976 | self.send(msg) 977 | 978 | class Keepalive(object): 979 | """ 980 | Implements keepalive mechanic to keep the specified connection with a node. 981 | """ 982 | def __init__(self, conn, keepalive_time): 983 | self.conn = conn 984 | self.keepalive_time = keepalive_time 985 | 986 | def keepalive(self, addr=False): 987 | st = time.time() 988 | last_ping = time.time() - 10 989 | addrs = [] 990 | while time.time() - st < self.keepalive_time: 991 | if time.time() - last_ping > 9: 992 | try: 993 | self.ping() 994 | last_ping = time.time() 995 | except socket.error as err: 996 | logging.debug("keepalive failed %s", err) 997 | break 998 | time.sleep(0.3) 999 | try: 1000 | if addr: 1001 | new = self.conn.get_messages(commands=[b'addr']) 1002 | addrs += new 1003 | else: 1004 | self.conn.get_messages() 1005 | except socket.timeout: 1006 | pass 1007 | except (ProtocolError, ConnectionError, socket.error) as err: 1008 | logging.debug("getmsg failed %s", err) 1009 | break 1010 | return addrs 1011 | 1012 | def ping(self): 1013 | """ 1014 | Sends a ping message. Ping time is stored in Redis for round-trip time 1015 | (RTT) calculation. 1016 | """ 1017 | nonce = random.getrandbits(64) 1018 | try: 1019 | self.conn.ping(nonce=nonce) 1020 | except socket.error: 1021 | raise 1022 | self.last_ping = time.time() 1023 | --------------------------------------------------------------------------------