From b8f627d0927d73b76078a809743dfbae11ab48d8 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 7 Sep 2023 16:09:36 +0200 Subject: [PATCH] add docs --- docs/404.html | 456 ++ docs/assets/images/favicon.png | Bin 0 -> 1870 bytes .../assets/javascripts/bundle.b4d07000.min.js | 29 + .../javascripts/bundle.b4d07000.min.js.map | 8 + .../javascripts/lunr/min/lunr.ar.min.js | 1 + .../javascripts/lunr/min/lunr.da.min.js | 18 + .../javascripts/lunr/min/lunr.de.min.js | 18 + .../javascripts/lunr/min/lunr.du.min.js | 18 + .../javascripts/lunr/min/lunr.es.min.js | 18 + .../javascripts/lunr/min/lunr.fi.min.js | 18 + .../javascripts/lunr/min/lunr.fr.min.js | 18 + .../javascripts/lunr/min/lunr.hi.min.js | 1 + .../javascripts/lunr/min/lunr.hu.min.js | 18 + .../javascripts/lunr/min/lunr.hy.min.js | 1 + .../javascripts/lunr/min/lunr.it.min.js | 18 + .../javascripts/lunr/min/lunr.ja.min.js | 1 + .../javascripts/lunr/min/lunr.jp.min.js | 1 + .../javascripts/lunr/min/lunr.kn.min.js | 1 + .../javascripts/lunr/min/lunr.ko.min.js | 1 + .../javascripts/lunr/min/lunr.multi.min.js | 1 + .../javascripts/lunr/min/lunr.nl.min.js | 18 + .../javascripts/lunr/min/lunr.no.min.js | 18 + .../javascripts/lunr/min/lunr.pt.min.js | 18 + .../javascripts/lunr/min/lunr.ro.min.js | 18 + .../javascripts/lunr/min/lunr.ru.min.js | 18 + .../javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + .../javascripts/lunr/min/lunr.sv.min.js | 18 + .../javascripts/lunr/min/lunr.ta.min.js | 1 + .../javascripts/lunr/min/lunr.te.min.js | 1 + .../javascripts/lunr/min/lunr.th.min.js | 1 + .../javascripts/lunr/min/lunr.tr.min.js | 18 + .../javascripts/lunr/min/lunr.vi.min.js | 1 + .../javascripts/lunr/min/lunr.zh.min.js | 1 + docs/assets/javascripts/lunr/tinyseg.js | 206 + docs/assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.208ed371.min.js | 42 + .../workers/search.208ed371.min.js.map | 8 + docs/assets/stylesheets/main.26e3688c.min.css | 1 + .../stylesheets/main.26e3688c.min.css.map | 1 + .../stylesheets/palette.ecc896b0.min.css | 1 + .../stylesheets/palette.ecc896b0.min.css.map | 1 + docs/circuitrelay.nim | 83 + docs/circuitrelay/index.html | 567 ++ docs/directchat.nim | 198 + docs/go-daemon/bootstrap.nim | 55 + docs/go-daemon/chat.nim | 134 + docs/go-daemon/daemonapi/index.html | 514 ++ docs/go-daemon/node.nim | 47 + docs/helloworld.nim | 89 + docs/hexdump.nim | 92 + docs/index.html | 490 ++ docs/search/search_index.json | 1 + docs/sitemap.xml | 48 + docs/sitemap.xml.gz | Bin 0 -> 309 bytes docs/tutorial_1_connect.nim | 95 + docs/tutorial_1_connect/index.html | 629 ++ docs/tutorial_2_customproto.nim | 74 + docs/tutorial_2_customproto/index.html | 553 ++ docs/tutorial_3_protobuf.nim | 162 + docs/tutorial_3_protobuf/index.html | 714 ++ docs/tutorial_4_gossipsub.nim | 163 + docs/tutorial_4_gossipsub/index.html | 685 ++ docs/tutorial_5_discovery.nim | 132 + docs/tutorial_5_discovery/index.html | 663 ++ docs/tutorial_6_game.nim | 259 + docs/tutorial_6_game/index.html | 798 ++ 67 files changed, 14991 insertions(+) create mode 100644 docs/404.html create mode 100644 docs/assets/images/favicon.png create mode 100644 docs/assets/javascripts/bundle.b4d07000.min.js create mode 100644 docs/assets/javascripts/bundle.b4d07000.min.js.map create mode 100644 docs/assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 docs/assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 docs/assets/javascripts/lunr/tinyseg.js create mode 100644 docs/assets/javascripts/lunr/wordcut.js create mode 100644 docs/assets/javascripts/workers/search.208ed371.min.js create mode 100644 docs/assets/javascripts/workers/search.208ed371.min.js.map create mode 100644 docs/assets/stylesheets/main.26e3688c.min.css create mode 100644 docs/assets/stylesheets/main.26e3688c.min.css.map create mode 100644 docs/assets/stylesheets/palette.ecc896b0.min.css create mode 100644 docs/assets/stylesheets/palette.ecc896b0.min.css.map create mode 100644 docs/circuitrelay.nim create mode 100644 docs/circuitrelay/index.html create mode 100644 docs/directchat.nim create mode 100644 docs/go-daemon/bootstrap.nim create mode 100644 docs/go-daemon/chat.nim create mode 100644 docs/go-daemon/daemonapi/index.html create mode 100644 docs/go-daemon/node.nim create mode 100644 docs/helloworld.nim create mode 100644 docs/hexdump.nim create mode 100644 docs/index.html create mode 100644 docs/search/search_index.json create mode 100644 docs/sitemap.xml create mode 100644 docs/sitemap.xml.gz create mode 100644 docs/tutorial_1_connect.nim create mode 100644 docs/tutorial_1_connect/index.html create mode 100644 docs/tutorial_2_customproto.nim create mode 100644 docs/tutorial_2_customproto/index.html create mode 100644 docs/tutorial_3_protobuf.nim create mode 100644 docs/tutorial_3_protobuf/index.html create mode 100644 docs/tutorial_4_gossipsub.nim create mode 100644 docs/tutorial_4_gossipsub/index.html create mode 100644 docs/tutorial_5_discovery.nim create mode 100644 docs/tutorial_5_discovery/index.html create mode 100644 docs/tutorial_6_game.nim create mode 100644 docs/tutorial_6_game/index.html diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 000000000..6bab32594 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/assets/images/favicon.png b/docs/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/docs/assets/javascripts/bundle.b4d07000.min.js b/docs/assets/javascripts/bundle.b4d07000.min.js new file mode 100644 index 000000000..3c0bdad9a --- /dev/null +++ b/docs/assets/javascripts/bundle.b4d07000.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var _=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?_:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():_))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>_),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=M("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Circuit Relay example

+

Circuit Relay can be used when a node cannot reach another node +directly, but can reach it through a another node (the Relay).

+

That may happen because of NAT, Firewalls, or incompatible transports.

+

More informations here.

+
import chronos, stew/byteutils
+import libp2p,
+       libp2p/protocols/connectivity/relay/[relay, client]
+
+# Helper to create a circuit relay node
+proc createCircuitRelaySwitch(r: Relay): Switch =
+  SwitchBuilder.new()
+    .withRng(newRng())
+    .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ])
+    .withTcpTransport()
+    .withMplex()
+    .withNoise()
+    .withCircuitRelay(r)
+    .build()
+
+proc main() {.async.} =
+  # Create a custom protocol
+  let customProtoCodec = "/test"
+  var proto = new LPProtocol
+  proto.codec = customProtoCodec
+  proto.handler = proc(conn: Connection, proto: string) {.async.} =
+    var msg = string.fromBytes(await conn.readLp(1024))
+    echo "1 - Dst Received: ", msg
+    assert "test1" == msg
+    await conn.writeLp("test2")
+    msg = string.fromBytes(await conn.readLp(1024))
+    echo "2 - Dst Received: ", msg
+    assert "test3" == msg
+    await conn.writeLp("test4")
+
+  let
+    relay = Relay.new()
+    clSrc = RelayClient.new()
+    clDst = RelayClient.new()
+
+    # Create three hosts, enable relay client on two of them.
+    # The third one can relay connections for other peers.
+    # RelayClient can use a relay, Relay is a relay.
+    swRel = createCircuitRelaySwitch(relay)
+    swSrc = createCircuitRelaySwitch(clSrc)
+    swDst = createCircuitRelaySwitch(clDst)
+
+  swDst.mount(proto)
+
+  await swRel.start()
+  await swSrc.start()
+  await swDst.start()
+
+  let
+    # Create a relay address to swDst using swRel as the relay
+    addrs = MultiAddress.init($swRel.peerInfo.addrs[0] & "/p2p/" &
+                              $swRel.peerInfo.peerId & "/p2p-circuit").get()
+
+  # Connect Dst to the relay
+  await swDst.connect(swRel.peerInfo.peerId, swRel.peerInfo.addrs)
+
+  # Dst reserve a slot on the relay.
+  let rsvp = await clDst.reserve(swRel.peerInfo.peerId, swRel.peerInfo.addrs)
+
+  # Src dial Dst using the relay
+  let conn = await swSrc.dial(swDst.peerInfo.peerId, @[ addrs ], customProtoCodec)
+
+  await conn.writeLp("test1")
+  var msg = string.fromBytes(await conn.readLp(1024))
+  echo "1 - Src Received: ", msg
+  assert "test2" == msg
+  await conn.writeLp("test3")
+  msg = string.fromBytes(await conn.readLp(1024))
+  echo "2 - Src Received: ", msg
+  assert "test4" == msg
+
+  await relay.stop()
+  await allFutures(swSrc.stop(), swDst.stop(), swRel.stop())
+
+waitFor(main())
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/directchat.nim b/docs/directchat.nim new file mode 100644 index 000000000..b550d4805 --- /dev/null +++ b/docs/directchat.nim @@ -0,0 +1,198 @@ +when not(compileOption("threads")): + {.fatal: "Please, compile this program with the --threads:on option!".} + +import + strformat, strutils, + stew/byteutils, + chronos, + libp2p + +const DefaultAddr = "/ip4/127.0.0.1/tcp/0" + +const Help = """ + Commands: /[?|help|connect|disconnect|exit] + help: Prints this help + connect: dials a remote peer + disconnect: ends current session + exit: closes the chat +""" + +type + Chat = ref object + switch: Switch # a single entry point for dialing and listening to peer + stdinReader: StreamTransport # transport streams between read & write file descriptor + conn: Connection # connection to the other peer + connected: bool # if the node is connected to another peer + +## +# Stdout helpers, to write the prompt +## +proc writePrompt(c: Chat) = + if c.connected: + stdout.write '\r' & $c.switch.peerInfo.peerId & ": " + stdout.flushFile() + +proc writeStdout(c: Chat, str: string) = + echo '\r' & str + c.writePrompt() + +## +# Chat Protocol +## +const ChatCodec = "/nim-libp2p/chat/1.0.0" + +type + ChatProto = ref object of LPProtocol + +proc new(T: typedesc[ChatProto], c: Chat): T = + let chatproto = T() + + # create handler for incoming connection + proc handle(stream: Connection, proto: string) {.async.} = + if c.connected and not c.conn.closed: + c.writeStdout "a chat session is already in progress - refusing incoming peer!" + await stream.close() + else: + await c.handlePeer(stream) + await stream.close() + + # assign the new handler + chatproto.handler = handle + chatproto.codec = ChatCodec + return chatproto + +## +# Chat application +## +proc handlePeer(c: Chat, conn: Connection) {.async.} = + # Handle a peer (incoming or outgoing) + try: + c.conn = conn + c.connected = true + c.writeStdout $conn.peerId & " connected" + + # Read loop + while true: + let + strData = await conn.readLp(1024) + str = string.fromBytes(strData) + c.writeStdout $conn.peerId & ": " & $str + + except LPStreamEOFError: + defer: c.writeStdout $conn.peerId & " disconnected" + await c.conn.close() + c.connected = false + +proc dialPeer(c: Chat, address: string) {.async.} = + # Parse and dial address + let + multiAddr = MultiAddress.init(address).tryGet() + # split the peerId part /p2p/... + peerIdBytes = multiAddr[multiCodec("p2p")] + .tryGet() + .protoAddress() + .tryGet() + remotePeer = PeerId.init(peerIdBytes).tryGet() + # split the wire address + ip4Addr = multiAddr[multiCodec("ip4")].tryGet() + tcpAddr = multiAddr[multiCodec("tcp")].tryGet() + wireAddr = ip4Addr & tcpAddr + + echo &"dialing peer: {multiAddr}" + asyncSpawn c.handlePeer(await c.switch.dial(remotePeer, @[wireAddr], ChatCodec)) + +proc readLoop(c: Chat) {.async.} = + while true: + if not c.connected: + echo "type an address or wait for a connection:" + echo "type /[help|?] for help" + + c.writePrompt() + + let line = await c.stdinReader.readLine() + if line.startsWith("/help") or line.startsWith("/?"): + echo Help + continue + + if line.startsWith("/disconnect"): + c.writeStdout "Ending current session" + if c.connected and c.conn.closed.not: + await c.conn.close() + c.connected = false + elif line.startsWith("/connect"): + c.writeStdout "enter address of remote peer" + let address = await c.stdinReader.readLine() + if address.len > 0: + await c.dialPeer(address) + + elif line.startsWith("/exit"): + if c.connected and c.conn.closed.not: + await c.conn.close() + c.connected = false + + await c.switch.stop() + c.writeStdout "quitting..." + return + else: + if c.connected: + await c.conn.writeLp(line) + else: + try: + if line.startsWith("/") and "p2p" in line: + await c.dialPeer(line) + except CatchableError as exc: + echo &"unable to dial remote peer {line}" + echo exc.msg + +proc readInput(wfd: AsyncFD) {.thread.} = + ## This thread performs reading from `stdin` and sends data over + ## pipe to main thread. + let transp = fromPipe(wfd) + + while true: + let line = stdin.readLine() + discard waitFor transp.write(line & "\r\n") + +proc main() {.async.} = + let + rng = newRng() # Single random number source for the whole application + + # Pipe to read stdin from main thread + (rfd, wfd) = createAsyncPipe() + stdinReader = fromPipe(rfd) + + var thread: Thread[AsyncFD] + try: + thread.createThread(readInput, wfd) + except Exception as exc: + quit("Failed to create thread: " & exc.msg) + + var localAddress = MultiAddress.init(DefaultAddr).tryGet() + + var switch = SwitchBuilder + .new() + .withRng(rng) # Give the application RNG + .withAddress(localAddress) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager + .build() + + let chat = Chat( + switch: switch, + stdinReader: stdinReader) + + switch.mount(ChatProto.new(chat)) + + await switch.start() + + let id = $switch.peerInfo.peerId + echo "PeerId: " & id + echo "listening on: " + for a in switch.peerInfo.addrs: + echo &"{a}/p2p/{id}" + + await chat.readLoop() + quit(0) + +waitFor(main()) diff --git a/docs/go-daemon/bootstrap.nim b/docs/go-daemon/bootstrap.nim new file mode 100644 index 000000000..69523edd2 --- /dev/null +++ b/docs/go-daemon/bootstrap.nim @@ -0,0 +1,55 @@ +import chronos, nimcrypto, strutils +import ../../libp2p/daemon/daemonapi +import ../hexdump + +const + PubSubTopic = "test-net" + +proc dumpSubscribedPeers(api: DaemonAPI) {.async.} = + var peers = await api.pubsubListPeers(PubSubTopic) + echo "= List of connected and subscribed peers:" + for item in peers: + echo item.pretty() + +proc dumpAllPeers(api: DaemonAPI) {.async.} = + var peers = await api.listPeers() + echo "Current connected peers count = ", len(peers) + for item in peers: + echo item.peer.pretty() + +proc monitor(api: DaemonAPI) {.async.} = + while true: + echo "Dumping all peers" + await dumpAllPeers(api) + await sleepAsync(5000) + +proc main() {.async.} = + echo "= Starting P2P bootnode" + var api = await newDaemonApi({DHTFull, PSGossipSub}) + var id = await api.identity() + echo "= P2P bootnode ", id.peer.pretty(), " started." + let mcip4 = multiCodec("ip4") + let mcip6 = multiCodec("ip6") + echo "= You can use one of this addresses to bootstrap your nodes:" + for item in id.addresses: + if item.protoCode() == mcip4 or item.protoCode() == mcip6: + echo $item & "/ipfs/" & id.peer.pretty() + + asyncSpawn monitor(api) + + proc pubsubLogger(api: DaemonAPI, + ticket: PubsubTicket, + message: PubSubMessage): Future[bool] {.async.} = + let msglen = len(message.data) + echo "= Recieved pubsub message with length ", msglen, + " bytes from peer ", message.peer.pretty() + echo dumpHex(message.data) + await api.dumpSubscribedPeers() + result = true + + var ticket = await api.pubsubSubscribe(PubSubTopic, pubsubLogger) + +when isMainModule: + waitFor(main()) + while true: + poll() diff --git a/docs/go-daemon/chat.nim b/docs/go-daemon/chat.nim new file mode 100644 index 000000000..f15667d20 --- /dev/null +++ b/docs/go-daemon/chat.nim @@ -0,0 +1,134 @@ +import chronos, nimcrypto, strutils +import ../../libp2p/daemon/daemonapi + +## nim c -r --threads:on chat.nim +when not(compileOption("threads")): + {.fatal: "Please, compile this program with the --threads:on option!".} + +const + ServerProtocols = @["/test-chat-stream"] + +type + CustomData = ref object + api: DaemonAPI + remotes: seq[StreamTransport] + consoleFd: AsyncFD + serveFut: Future[void] + +proc threadMain(wfd: AsyncFD) {.thread.} = + ## This procedure performs reading from `stdin` and sends data over + ## pipe to main thread. + var transp = fromPipe(wfd) + + while true: + var line = stdin.readLine() + let res = waitFor transp.write(line & "\r\n") + +proc serveThread(udata: CustomData) {.async.} = + ## This procedure perform reading on pipe and sends data to remote clients. + var transp = fromPipe(udata.consoleFd) + + proc remoteReader(transp: StreamTransport) {.async.} = + while true: + var line = await transp.readLine() + if len(line) == 0: + break + echo ">> ", line + + while true: + try: + var line = await transp.readLine() + if line.startsWith("/connect"): + var parts = line.split(" ") + if len(parts) == 2: + var peerId = PeerId.init(parts[1]) + var address = MultiAddress.init(multiCodec("p2p-circuit")) + address &= MultiAddress.init(multiCodec("p2p"), peerId) + echo "= Searching for peer ", peerId.pretty() + var id = await udata.api.dhtFindPeer(peerId) + echo "= Peer " & parts[1] & " found at addresses:" + for item in id.addresses: + echo $item + echo "= Connecting to peer ", $address + await udata.api.connect(peerId, @[address], 30) + echo "= Opening stream to peer chat ", parts[1] + var stream = await udata.api.openStream(peerId, ServerProtocols) + udata.remotes.add(stream.transp) + echo "= Connected to peer chat ", parts[1] + asyncSpawn remoteReader(stream.transp) + elif line.startsWith("/search"): + var parts = line.split(" ") + if len(parts) == 2: + var peerId = PeerId.init(parts[1]) + echo "= Searching for peer ", peerId.pretty() + var id = await udata.api.dhtFindPeer(peerId) + echo "= Peer " & parts[1] & " found at addresses:" + for item in id.addresses: + echo $item + elif line.startsWith("/consearch"): + var parts = line.split(" ") + if len(parts) == 2: + var peerId = PeerId.init(parts[1]) + echo "= Searching for peers connected to peer ", parts[1] + var peers = await udata.api.dhtFindPeersConnectedToPeer(peerId) + echo "= Found ", len(peers), " connected to peer ", parts[1] + for item in peers: + var peer = item.peer + var addresses = newSeq[string]() + var relay = false + for a in item.addresses: + addresses.add($a) + if a.protoName() == "/p2p-circuit": + relay = true + break + if relay: + echo peer.pretty(), " * ", " [", addresses.join(", "), "]" + else: + echo peer.pretty(), " [", addresses.join(", "), "]" + elif line.startsWith("/exit"): + break + else: + var msg = line & "\r\n" + echo "<< ", line + var pending = newSeq[Future[int]]() + for item in udata.remotes: + pending.add(item.write(msg)) + if len(pending) > 0: + var results = await all(pending) + except: + echo getCurrentException().msg + +proc main() {.async.} = + var data = new CustomData + data.remotes = newSeq[StreamTransport]() + + var (rfd, wfd) = createAsyncPipe() + if rfd == asyncInvalidPipe or wfd == asyncInvalidPipe: + raise newException(ValueError, "Could not initialize pipe!") + + data.consoleFd = rfd + + data.serveFut = serveThread(data) + var thread: Thread[AsyncFD] + thread.createThread(threadMain, wfd) + + echo "= Starting P2P node" + data.api = await newDaemonApi({DHTFull, Bootstrap}) + await sleepAsync(3000) + var id = await data.api.identity() + + proc streamHandler(api: DaemonAPI, stream: P2PStream) {.async.} = + echo "= Peer ", stream.peer.pretty(), " joined chat" + data.remotes.add(stream.transp) + while true: + var line = await stream.transp.readLine() + if len(line) == 0: + break + echo ">> ", line + + await data.api.addHandler(ServerProtocols, streamHandler) + echo "= Your PeerId is ", id.peer.pretty() + await data.serveFut + +when isMainModule: + waitFor(main()) diff --git a/docs/go-daemon/daemonapi/index.html b/docs/go-daemon/daemonapi/index.html new file mode 100644 index 000000000..fc5045eaa --- /dev/null +++ b/docs/go-daemon/daemonapi/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + Table of Contents - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Table of Contents

+ +

Introduction

+

This is a libp2p-backed daemon wrapping the functionalities of go-libp2p for use in Nim.
+For more information about the go daemon, check out this repository.

+

Installation

+
# clone and install dependencies
+git clone https://github.com/status-im/nim-libp2p
+cd nim-libp2p
+nimble install
+
+# perform unit tests
+nimble test
+
+# update the git submodule to install the go daemon 
+git submodule update --init --recursive
+go version
+git clone https://github.com/libp2p/go-libp2p-daemon
+cd go-libp2p-daemon
+git checkout v0.0.1
+go install ./...
+cd ..
+
+

Usage

+

Example

+

Examples can be found in the examples folder

+

Getting Started

+

Try out the chat example. Full code can be found here:

+
nim c -r --threads:on examples/directchat.nim
+
+

This will output a peer ID such as QmbmHfVvouKammmQDJck4hz33WvVktNEe7pasxz2HgseRu which you can use in another instance to connect to it.

+
./examples/directchat
+/connect QmbmHfVvouKammmQDJck4hz33WvVktNEe7pasxz2HgseRu
+
+

You can now chat between the instances!

+

Chat example

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/go-daemon/node.nim b/docs/go-daemon/node.nim new file mode 100644 index 000000000..0f3495d80 --- /dev/null +++ b/docs/go-daemon/node.nim @@ -0,0 +1,47 @@ +import chronos, nimcrypto, strutils, os +import ../../libp2p/daemon/daemonapi + +const + PubSubTopic = "test-net" + +proc main(bn: string) {.async.} = + echo "= Starting P2P node" + var bootnodes = bn.split(",") + var api = await newDaemonApi({DHTFull, PSGossipSub, WaitBootstrap}, + bootstrapNodes = bootnodes, + peersRequired = 1) + var id = await api.identity() + echo "= P2P node ", id.peer.pretty(), " started:" + for item in id.addresses: + echo item + + proc pubsubLogger(api: DaemonAPI, + ticket: PubsubTicket, + message: PubSubMessage): Future[bool] {.async.} = + let msglen = len(message.data) + echo "= Recieved pubsub message with length ", msglen, + " bytes from peer ", message.peer.pretty(), ": " + var strdata = cast[string](message.data) + echo strdata + result = true + + var ticket = await api.pubsubSubscribe(PubSubTopic, pubsubLogger) + + # Waiting for gossipsub interval + while true: + var peers = await api.pubsubListPeers(PubSubTopic) + if len(peers) > 0: + break + await sleepAsync(1000) + + var data = "HELLO\r\n" + var msgData = cast[seq[byte]](data) + await api.pubsubPublish(PubSubTopic, msgData) + +when isMainModule: + if paramCount() != 1: + echo "Please supply bootnodes!" + else: + waitFor(main(paramStr(1))) + while true: + poll() diff --git a/docs/helloworld.nim b/docs/helloworld.nim new file mode 100644 index 000000000..7f144aa76 --- /dev/null +++ b/docs/helloworld.nim @@ -0,0 +1,89 @@ +import chronos # an efficient library for async +import stew/byteutils # various utils +import libp2p + +## +# Create our custom protocol +## +const TestCodec = "/test/proto/1.0.0" # custom protocol string identifier + +type + TestProto = ref object of LPProtocol # declare a custom protocol + +proc new(T: typedesc[TestProto]): T = + + # every incoming connections will be in handled in this closure + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + echo "Got from remote - ", string.fromBytes(await conn.readLp(1024)) + await conn.writeLp("Roger p2p!") + + # We must close the connections ourselves when we're done with it + await conn.close() + + return T(codecs: @[TestCodec], handler: handle) + +## +# Helper to create a switch/node +## +proc createSwitch(ma: MultiAddress, rng: ref HmacDrbgContext): Switch = + var switch = SwitchBuilder + .new() + .withRng(rng) # Give the application RNG + .withAddress(ma) # Our local address(es) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager + .build() + + result = switch + +## +# The actual application +## +proc main() {.async, gcsafe.} = + let + rng = newRng() # Single random number source for the whole application + # port 0 will take a random available port + # `tryGet` will throw an exception if the MultiAddress failed + # (for instance, if the address is not well formatted) + ma1 = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + ma2 = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + + # setup the custom proto + let testProto = TestProto.new() + + # setup the two nodes + let + switch1 = createSwitch(ma1, rng) #Create the two switches + switch2 = createSwitch(ma2, rng) + + # mount the proto on switch1 + # the node will now listen for this proto + # and call the handler everytime a client request it + switch1.mount(testProto) + + # Start the nodes. This will start the transports + # and listen on each local addresses + await switch1.start() + await switch2.start() + + # the node addrs is populated with it's + # actual port during the start + + # use the second node to dial the first node + # using the first node peerid and address + # and specify our custom protocol codec + let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec) + + # conn is now a fully setup connection, we talk directly to the node1 custom protocol handler + await conn.writeLp("Hello p2p!") # writeLp send a length prefixed buffer over the wire + + # readLp reads length prefixed bytes and returns a buffer without the prefix + echo "Remote responded with - ", string.fromBytes(await conn.readLp(1024)) + + # We must close the connection ourselves when we're done with it + await conn.close() + + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + +waitFor(main()) diff --git a/docs/hexdump.nim b/docs/hexdump.nim new file mode 100644 index 000000000..2fb8d766f --- /dev/null +++ b/docs/hexdump.nim @@ -0,0 +1,92 @@ +# +# Copyright (c) 2016 Eugene Kabanov +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from strutils import toHex, repeat + +proc dumpHex*(pbytes: pointer, nbytes: int, items = 1, ascii = true): string = + ## Return hexadecimal memory dump representation pointed by ``p``. + ## ``nbytes`` - number of bytes to show + ## ``items`` - number of bytes in group (supported ``items`` count is + ## 1, 2, 4, 8) + ## ``ascii`` - if ``true`` show ASCII representation of memory dump. + result = "" + let hexSize = items * 2 + var i = 0 + var slider = pbytes + var asciiText = "" + while i < nbytes: + if i %% 16 == 0: + result = result & toHex(cast[BiggestInt](slider), + sizeof(BiggestInt) * 2) & ": " + var k = 0 + while k < items: + var ch = cast[ptr char](cast[uint](slider) + k.uint)[] + if ord(ch) > 31 and ord(ch) < 127: asciiText &= ch else: asciiText &= "." + inc(k) + case items: + of 1: + result = result & toHex(cast[BiggestInt](cast[ptr uint8](slider)[]), + hexSize) + of 2: + result = result & toHex(cast[BiggestInt](cast[ptr uint16](slider)[]), + hexSize) + of 4: + result = result & toHex(cast[BiggestInt](cast[ptr uint32](slider)[]), + hexSize) + of 8: + result = result & toHex(cast[BiggestInt](cast[ptr uint64](slider)[]), + hexSize) + else: + raise newException(ValueError, "Wrong items size!") + result = result & " " + slider = cast[pointer](cast[uint](slider) + items.uint) + i = i + items + if i %% 16 == 0: + result = result & " " & asciiText + asciiText.setLen(0) + result = result & "\n" + + if i %% 16 != 0: + var spacesCount = ((16 - (i %% 16)) div items) * (hexSize + 1) + 1 + result = result & repeat(' ', spacesCount) + result = result & asciiText + result = result & "\n" + +proc dumpHex*[T](v: openArray[T], items: int = 0, ascii = true): string = + ## Return hexadecimal memory dump representation of openArray[T] ``v``. + ## ``items`` - number of bytes in group (supported ``items`` count is + ## 0, 1, 2, 4, 8). If ``items`` is ``0`` group size will depend on + ## ``sizeof(T)``. + ## ``ascii`` - if ``true`` show ASCII representation of memory dump. + var i = 0 + if items == 0: + when sizeof(T) == 2: + i = 2 + elif sizeof(T) == 4: + i = 4 + elif sizeof(T) == 8: + i = 8 + else: + i = 1 + else: + i = items + result = dumpHex(unsafeAddr v[0], sizeof(T) * len(v), i, ascii) diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..abdb1593e --- /dev/null +++ b/docs/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + Introduction - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

nim-libp2p documentation

+

Welcome to the nim-libp2p documentation!

+

Here, you'll find tutorials to help you get started, as well as +the full reference.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/search/search_index.json b/docs/search/search_index.json new file mode 100644 index 000000000..d523f38be --- /dev/null +++ b/docs/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"nim-libp2p documentation","text":"

Welcome to the nim-libp2p documentation!

Here, you'll find tutorials to help you get started, as well as the full reference.

"},{"location":"circuitrelay/","title":"Circuit Relay example","text":"

Circuit Relay can be used when a node cannot reach another node directly, but can reach it through a another node (the Relay).

That may happen because of NAT, Firewalls, or incompatible transports.

More informations here.

import chronos, stew/byteutils\nimport libp2p,\nlibp2p/protocols/connectivity/relay/[relay, client]\n# Helper to create a circuit relay node\nproc createCircuitRelaySwitch(r: Relay): Switch =\nSwitchBuilder.new()\n.withRng(newRng())\n.withAddresses(@[ MultiAddress.init(\"/ip4/0.0.0.0/tcp/0\").tryGet() ])\n.withTcpTransport()\n.withMplex()\n.withNoise()\n.withCircuitRelay(r)\n.build()\nproc main() {.async.} =\n# Create a custom protocol\nlet customProtoCodec = \"/test\"\nvar proto = new LPProtocol\nproto.codec = customProtoCodec\nproto.handler = proc(conn: Connection, proto: string) {.async.} =\nvar msg = string.fromBytes(await conn.readLp(1024))\necho \"1 - Dst Received: \", msg\nassert \"test1\" == msg\nawait conn.writeLp(\"test2\")\nmsg = string.fromBytes(await conn.readLp(1024))\necho \"2 - Dst Received: \", msg\nassert \"test3\" == msg\nawait conn.writeLp(\"test4\")\nlet\nrelay = Relay.new()\nclSrc = RelayClient.new()\nclDst = RelayClient.new()\n# Create three hosts, enable relay client on two of them.\n# The third one can relay connections for other peers.\n# RelayClient can use a relay, Relay is a relay.\nswRel = createCircuitRelaySwitch(relay)\nswSrc = createCircuitRelaySwitch(clSrc)\nswDst = createCircuitRelaySwitch(clDst)\nswDst.mount(proto)\nawait swRel.start()\nawait swSrc.start()\nawait swDst.start()\nlet\n# Create a relay address to swDst using swRel as the relay\naddrs = MultiAddress.init($swRel.peerInfo.addrs[0] & \"/p2p/\" &\n$swRel.peerInfo.peerId & \"/p2p-circuit\").get()\n# Connect Dst to the relay\nawait swDst.connect(swRel.peerInfo.peerId, swRel.peerInfo.addrs)\n# Dst reserve a slot on the relay.\nlet rsvp = await clDst.reserve(swRel.peerInfo.peerId, swRel.peerInfo.addrs)\n# Src dial Dst using the relay\nlet conn = await swSrc.dial(swDst.peerInfo.peerId, @[ addrs ], customProtoCodec)\nawait conn.writeLp(\"test1\")\nvar msg = string.fromBytes(await conn.readLp(1024))\necho \"1 - Src Received: \", msg\nassert \"test2\" == msg\nawait conn.writeLp(\"test3\")\nmsg = string.fromBytes(await conn.readLp(1024))\necho \"2 - Src Received: \", msg\nassert \"test4\" == msg\nawait relay.stop()\nawait allFutures(swSrc.stop(), swDst.stop(), swRel.stop())\nwaitFor(main())\n
"},{"location":"tutorial_1_connect/","title":"Simple ping tutorial","text":"

Hi all, welcome to the first nim-libp2p tutorial!

This tutorial is for everyone who is interested in building peer-to-peer applications. No Nim programming experience is needed.

To give you a quick overview, Nim is the programming language we are using and nim-libp2p is the Nim implementation of libp2p, a modular library that enables the development of peer-to-peer network applications.

Hope you'll find it helpful in your journey of learning. Happy coding! ;)

"},{"location":"tutorial_1_connect/#before-you-start","title":"Before you start","text":"

The only prerequisite here is Nim, the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found here.

Install Nim via their official website. Check Nim's installation via nim --version and its package manager Nimble via nimble --version.

You can now install the latest version of nim-libp2p:

nimble install libp2p@#master\n

"},{"location":"tutorial_1_connect/#a-simple-ping-application","title":"A simple ping application","text":"

We'll start by creating a simple application, which is starting two libp2p switch, and pinging each other using the Ping protocol.

You can find the source of this tutorial (and other tutorials) in the libp2p/examples folder!

Let's create a part1.nim, and import our dependencies:

import chronos\nimport libp2p\nimport libp2p/protocols/ping\n
chronos the asynchronous framework used by nim-libp2p

Next, we'll create an helper procedure to create our switches. A switch needs a bit of configuration, and it will be easier to do this configuration only once:

proc createSwitch(ma: MultiAddress, rng: ref HmacDrbgContext): Switch =\nvar switch = SwitchBuilder\n.new()\n.withRng(rng)       # Give the application RNG\n.withAddress(ma)    # Our local address(es)\n.withTcpTransport() # Use TCP as transport\n.withMplex()        # Use Mplex as muxer\n.withNoise()        # Use Noise as secure manager\n.build()\nreturn switch\n
This will create a switch using Mplex as a multiplexer, Noise to secure the communication, and TCP as an underlying transport.

You can of course tweak this, to use a different or multiple transport, or tweak the configuration of Mplex and Noise, but this is some sane defaults that we'll use going forward.

Let's now start to create our main procedure:

proc main() {.async, gcsafe.} =\nlet\nrng = newRng()\nlocalAddress = MultiAddress.init(\"/ip4/0.0.0.0/tcp/0\").tryGet()\npingProtocol = Ping.new(rng=rng)\n
We created some variables that we'll need for the rest of the application: the global rng instance, our localAddress, and an instance of the Ping protocol. The address is in the MultiAddress format. The port 0 means \"take any port available\".

tryGet is procedure which is part of nim-result, that will throw an exception if the supplied MultiAddress is invalid.

We can now create our two switches:

  let\nswitch1 = createSwitch(localAddress, rng)\nswitch2 = createSwitch(localAddress, rng)\nswitch1.mount(pingProtocol)\nawait switch1.start()\nawait switch2.start()\n
We've mounted the pingProtocol on our first switch. This means that the first switch will actually listen for any ping requests coming in, and handle them accordingly.

Now that we've started the nodes, they are listening for incoming peers. We can find out which port was attributed, and the resulting local addresses, by using switch1.peerInfo.addrs.

We'll dial the first switch from the second one, by specifying it's Peer ID, it's MultiAddress and the Ping protocol codec:

  let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, PingCodec)\n
We now have a Ping connection setup between the second and the first switch, we can use it to actually ping the node:
  # ping the other node and echo the ping duration\necho \"ping: \", await pingProtocol.ping(conn)\n# We must close the connection ourselves when we're done with it\nawait conn.close()\n
And that's it! Just a little bit of cleanup: shutting down the switches, waiting for them to stop, and we'll call our main procedure:
  await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports\nwaitFor(main())\n
You can now run this program using nim c -r part1.nim, and you should see the dialing sequence, ending with a ping output.

In the next tutorial, we'll look at how to create our own custom protocol.

"},{"location":"tutorial_2_customproto/","title":"Custom protocol in libp2p","text":"

In the previous tutorial, we've looked at how to create a simple ping program using the nim-libp2p.

We'll now look at how to create a custom protocol inside the libp2p

Let's create a part2.nim, and import our dependencies:

import chronos\nimport stew/byteutils\nimport libp2p\n
This is similar to the first tutorial, except we don't need to import the Ping protocol.

Next, we'll declare our custom protocol

const TestCodec = \"/test/proto/1.0.0\"\ntype TestProto = ref object of LPProtocol\n
We've set a protocol ID, and created a custom LPProtocol. In a more complex protocol, we could use this structure to store interesting variables.

A protocol generally has two part: and handling/server part, and a dialing/client part. Theses two parts can be identical, but in our trivial protocol, the server will wait for a message from the client, and the client will send a message, so we have to handle the two cases separately.

Let's start with the server part:

proc new(T: typedesc[TestProto]): T =\n# every incoming connections will in be handled in this closure\nproc handle(conn: Connection, proto: string) {.async, gcsafe.} =\n# Read up to 1024 bytes from this connection, and transform them into\n# a string\necho \"Got from remote - \", string.fromBytes(await conn.readLp(1024))\n# We must close the connections ourselves when we're done with it\nawait conn.close()\nreturn T.new(codecs = @[TestCodec], handler = handle)\n
This is a constructor for our TestProto, that will specify our codecs and a handler, which will be called for each incoming peer asking for this protocol. In our handle, we simply read a message from the connection and echo it.

We can now create our client part:

proc hello(p: TestProto, conn: Connection) {.async.} =\nawait conn.writeLp(\"Hello p2p!\")\n
Again, pretty straight-forward, we just send a message on the connection.

We can now create our main procedure:

proc main() {.async, gcsafe.} =\nlet\nrng = newRng()\ntestProto = TestProto.new()\nswitch1 = newStandardSwitch(rng=rng)\nswitch2 = newStandardSwitch(rng=rng)\nswitch1.mount(testProto)\nawait switch1.start()\nawait switch2.start()\nlet conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec)\nawait testProto.hello(conn)\n# We must close the connection ourselves when we're done with it\nawait conn.close()\nawait allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports\n
This is very similar to the first tutorial's main, the only noteworthy difference is that we use newStandardSwitch, which is similar to the createSwitch of the first tutorial, but is bundled directly in libp2p

We can now wrap our program by calling our main proc:

waitFor(main())\n
And that's it! In the next tutorial, we'll create a more complex protocol using Protobuf.

"},{"location":"tutorial_3_protobuf/","title":"Protobuf usage","text":"

In the previous tutorial, we created a simple \"ping\" protocol. Most real protocol want their messages to be structured and extensible, which is why most real protocols use protobuf to define their message structures.

Here, we'll create a slightly more complex protocol, which parses & generate protobuf messages. Let's start by importing our dependencies, as usual:

import chronos\nimport stew/results # for Opt[T]\nimport libp2p\n

"},{"location":"tutorial_3_protobuf/#protobuf-encoding-decoding","title":"Protobuf encoding & decoding","text":"

This will be the structure of our messages:

message MetricList {\nmessage Metric {\nstring name = 1;\nfloat value = 2;\n}\nrepeated Metric metrics = 2;\n}\n
We'll create our protobuf types, encoders & decoders, according to this format. To create the encoders & decoders, we are going to use minprotobuf (included in libp2p).

While more modern technics (such as nim-protobuf-serialization) exists, minprotobuf is currently the recommended method to handle protobuf, since it has been used in production extensively, and audited.

type\nMetric = object\nname: string\nvalue: float\nMetricList = object\nmetrics: seq[Metric]\n{.push raises: [].}\nproc encode(m: Metric): ProtoBuffer =\n  result = initProtoBuffer()\n  result.write(1, m.name)\n  result.write(2, m.value)\n  result.finish()\nproc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] =\n  var res: Metric\nlet pb = initProtoBuffer(buf)\n  # \"getField\" will return a Result[bool, ProtoError].\n  # The Result will hold an error if the protobuf is invalid.\n  # The Result will hold \"false\" if the field is missing\n#\n  # We are just checking the error, and ignoring whether the value\n# is present or not (default values are valid).\n  discard ? pb.getField(1, res.name)\n  discard ? pb.getField(2, res.value)\n  ok(res)\nproc encode(m: MetricList): ProtoBuffer =\n  result = initProtoBuffer()\n  for metric in m.metrics:\n    result.write(1, metric.encode())\n  result.finish()\nproc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] =\n  var\nres: MetricList\nmetrics: seq[seq[byte]]\n  let pb = initProtoBuffer(buf)\n  discard ? pb.getRepeatedField(1, metrics)\n  for metric in metrics:\n    res.metrics &= ? Metric.decode(metric)\n  ok(res)\n

"},{"location":"tutorial_3_protobuf/#results-instead-of-exceptions","title":"Results instead of exceptions","text":"

As you can see, this part of the program also uses Results instead of exceptions for error handling. We start by {.push raises: [].}, which will prevent every non-async function from raising exceptions.

Then, we use nim-result to convey errors to function callers. A Result[T, E] will either hold a valid result of type T, or an error of type E.

You can check if the call succeeded by using res.isOk, and then get the value using res.value or the error by using res.error.

Another useful tool is ?, which will unpack a Result if it succeeded, or if it failed, exit the current procedure returning the error.

nim-result is packed with other functionalities that you'll find in the nim-result repository.

Results and exception are generally interchangeable, but have different semantics that you may or may not prefer.

"},{"location":"tutorial_3_protobuf/#creating-the-protocol","title":"Creating the protocol","text":"

We'll next create a protocol, like in the last tutorial, to request these metrics from our host

type\nMetricCallback = proc: Future[MetricList] {.raises: [], gcsafe.}\nMetricProto = ref object of LPProtocol\nmetricGetter: MetricCallback\nproc new(_: typedesc[MetricProto], cb: MetricCallback): MetricProto =\nvar res: MetricProto\nproc handle(conn: Connection, proto: string) {.async, gcsafe.} =\nlet\nmetrics = await res.metricGetter()\nasProtobuf = metrics.encode()\nawait conn.writeLp(asProtobuf.buffer)\nawait conn.close()\nres = MetricProto.new(@[\"/metric-getter/1.0.0\"], handle)\nres.metricGetter = cb\nreturn res\nproc fetch(p: MetricProto, conn: Connection): Future[MetricList] {.async.} =\nlet protobuf = await conn.readLp(2048)\n# tryGet will raise an exception if the Result contains an error.\n# It's useful to bridge between exception-world and result-world\nreturn MetricList.decode(protobuf).tryGet()\n
We can now create our main procedure:
proc main() {.async, gcsafe.} =\nlet rng = newRng()\nproc randomMetricGenerator: Future[MetricList] {.async.} =\nlet metricCount = rng[].generate(uint32) mod 16\nfor i in 0 ..< metricCount + 1:\nresult.metrics.add(Metric(\nname: \"metric_\" & $i,\nvalue: float(rng[].generate(uint16)) / 1000.0\n))\nreturn result\nlet\nmetricProto1 = MetricProto.new(randomMetricGenerator)\nmetricProto2 = MetricProto.new(randomMetricGenerator)\nswitch1 = newStandardSwitch(rng=rng)\nswitch2 = newStandardSwitch(rng=rng)\nswitch1.mount(metricProto1)\nawait switch1.start()\nawait switch2.start()\nlet\nconn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, metricProto2.codecs)\nmetrics = await metricProto2.fetch(conn)\nawait conn.close()\nfor metric in metrics.metrics:\necho metric.name, \" = \", metric.value\nawait allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports\nwaitFor(main())\n
If you run this program, you should see random metrics being sent from the switch1 to the switch2.

"},{"location":"tutorial_4_gossipsub/","title":"GossipSub","text":"

In this tutorial, we'll build a simple GossipSub network to broadcast the metrics we built in the previous tutorial.

GossipSub is used to broadcast some messages in a network, and allows to balance between latency, bandwidth usage, privacy and attack resistance.

You'll find a good explanation on how GossipSub works here. There are a lot of parameters you can tweak to adjust how GossipSub behaves but here we'll use the sane defaults shipped with libp2p.

We'll start by creating our metric structure like previously

import chronos\nimport stew/results\nimport libp2p\nimport libp2p/protocols/pubsub/rpc/messages\ntype\nMetric = object\nname: string\nvalue: float\nMetricList = object\nhostname: string\nmetrics: seq[Metric]\n{.push raises: [].}\nproc encode(m: Metric): ProtoBuffer =\n  result = initProtoBuffer()\n  result.write(1, m.name)\n  result.write(2, m.value)\n  result.finish()\nproc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] =\n  var res: Metric\nlet pb = initProtoBuffer(buf)\n  discard ? pb.getField(1, res.name)\n  discard ? pb.getField(2, res.value)\n  ok(res)\nproc encode(m: MetricList): ProtoBuffer =\n  result = initProtoBuffer()\n  for metric in m.metrics:\n    result.write(1, metric.encode())\n  result.write(2, m.hostname)\n  result.finish()\nproc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] =\n  var\nres: MetricList\nmetrics: seq[seq[byte]]\n  let pb = initProtoBuffer(buf)\n  discard ? pb.getRepeatedField(1, metrics)\n  for metric in metrics:\n    res.metrics &= ? Metric.decode(metric)\n  ? pb.getRequiredField(2, res.hostname)\n  ok(res)\n
This is exactly like the previous structure, except that we added a hostname to distinguish where the metric is coming from.

Now we'll create a small GossipSub network to broadcast the metrics, and collect them on one of the node.

type Node = tuple[switch: Switch, gossip: GossipSub, hostname: string]\nproc oneNode(node: Node, rng: ref HmacDrbgContext) {.async.} =\n# This procedure will handle one of the node of the network\nnode.gossip.addValidator([\"metrics\"],\nproc(topic: string, message: Message): Future[ValidationResult] {.async.} =\nlet decoded = MetricList.decode(message.data)\nif decoded.isErr: return ValidationResult.Reject\nreturn ValidationResult.Accept\n)\n# This \"validator\" will attach to the `metrics` topic and make sure\n# that every message in this topic is valid. This allows us to stop\n# propagation of invalid messages quickly in the network, and punish\n# peers sending them.\n# `John` will be responsible to log the metrics, the rest of the nodes\n# will just forward them in the network\nif node.hostname == \"John\":\nnode.gossip.subscribe(\"metrics\",\nproc (topic: string, data: seq[byte]) {.async.} =\necho MetricList.decode(data).tryGet()\n)\nelse:\nnode.gossip.subscribe(\"metrics\", nil)\n# Create random metrics 10 times and broadcast them\nfor _ in 0..<10:\nawait sleepAsync(500.milliseconds)\nvar metricList = MetricList(hostname: node.hostname)\nlet metricCount = rng[].generate(uint32) mod 4\nfor i in 0 ..< metricCount + 1:\nmetricList.metrics.add(Metric(\nname: \"metric_\" & $i,\nvalue: float(rng[].generate(uint16)) / 1000.0\n))\ndiscard await node.gossip.publish(\"metrics\", encode(metricList).buffer)\nawait node.switch.stop()\n
For our main procedure, we'll create a few nodes, and connect them together. Note that they are not all interconnected, but GossipSub will take care of broadcasting to the full network nonetheless.
proc main {.async.} =\nlet rng = newRng()\nvar nodes: seq[Node]\nfor hostname in [\"John\", \"Walter\", \"David\", \"Thuy\", \"Amy\"]:\nlet\nswitch = newStandardSwitch(rng=rng)\ngossip = GossipSub.init(switch = switch, triggerSelf = true)\nswitch.mount(gossip)\nawait switch.start()\nnodes.add((switch, gossip, hostname))\nfor index, node in nodes:\n# Connect to a few neighbors\nfor otherNodeIdx in index - 1 .. index + 2:\nif otherNodeIdx notin 0 ..< nodes.len or otherNodeIdx == index: continue\nlet otherNode = nodes[otherNodeIdx]\nawait node.switch.connect(\notherNode.switch.peerInfo.peerId,\notherNode.switch.peerInfo.addrs)\nvar allFuts: seq[Future[void]]\nfor node in nodes:\nallFuts.add(oneNode(node, rng))\nawait allFutures(allFuts)\nwaitFor(main())\n
If you run this program, you should see something like:
(hostname: \"John\", metrics: @[(name: \"metric_0\", value: 42.097), (name: \"metric_1\", value: 50.99), (name: \"metric_2\", value: 47.86), (name: \"metric_3\", value: 5.368)])\n(hostname: \"Walter\", metrics: @[(name: \"metric_0\", value: 39.452), (name: \"metric_1\", value: 15.606), (name: \"metric_2\", value: 14.059), (name: \"metric_3\", value: 6.68)])\n(hostname: \"David\", metrics: @[(name: \"metric_0\", value: 9.82), (name: \"metric_1\", value: 2.862), (name: \"metric_2\", value: 15.514)])\n(hostname: \"Thuy\", metrics: @[(name: \"metric_0\", value: 59.038)])\n(hostname: \"Amy\", metrics: @[(name: \"metric_0\", value: 55.616), (name: \"metric_1\", value: 23.52), (name: \"metric_2\", value: 59.081), (name: \"metric_3\", value: 2.516)])\n

This is John receiving & logging everyone's metrics.

"},{"location":"tutorial_4_gossipsub/#going-further","title":"Going further","text":"

Building efficient & safe GossipSub networks is a tricky subject. By tweaking the gossip params and topic params, you can achieve very different properties.

Also see reports for GossipSub v1.1

If you are interested in broadcasting for your application, you may want to use Waku, which builds on top of GossipSub, and adds features such as history, spam protection, and light node friendliness.

"},{"location":"tutorial_5_discovery/","title":"Discovery Manager","text":"

In the previous tutorial, we built a custom protocol using protobuf and spread informations (some metrics) on the network using gossipsub. For this tutorial, on the other hand, we'll go back on a simple example we'll try to discover a specific peers to greet on the network.

First, as usual, we import the dependencies:

import sequtils\nimport chronos\nimport stew/byteutils\nimport libp2p\nimport libp2p/protocols/rendezvous\nimport libp2p/discovery/rendezvousinterface\nimport libp2p/discovery/discoverymngr\n
We'll not use newStandardSwitch this time as we need the discovery protocol RendezVous to be mounted on the switch using withRendezVous.

Note that other discovery methods such as Kademlia or discv5 exist.

proc createSwitch(rdv: RendezVous = RendezVous.new()): Switch =\nSwitchBuilder.new()\n.withRng(newRng())\n.withAddresses(@[ MultiAddress.init(\"/ip4/0.0.0.0/tcp/0\").tryGet() ])\n.withTcpTransport()\n.withYamux()\n.withNoise()\n.withRendezVous(rdv)\n.build()\n# Create a really simple protocol to log one message received then close the stream\nconst DumbCodec = \"/dumb/proto/1.0.0\"\ntype DumbProto = ref object of LPProtocol\nproc new(T: typedesc[DumbProto], nodeNumber: int): T =\nproc handle(conn: Connection, proto: string) {.async, gcsafe.} =\necho \"Node\", nodeNumber, \" received: \", string.fromBytes(await conn.readLp(1024))\nawait conn.close()\nreturn T.new(codecs = @[DumbCodec], handler = handle)\n

"},{"location":"tutorial_5_discovery/#bootnodes","title":"Bootnodes","text":"

The first time a p2p program is ran, he needs to know how to join its network. This is generally done by hard-coding a list of stable nodes in the binary, called \"bootnodes\". These bootnodes are a critical part of a p2p network, since they are used by every new user to onboard the network.

By using libp2p, we can use any node supporting our discovery protocol (rendezvous in this case) as a bootnode. For this example, we'll create a bootnode, and then every peer will advertise itself on the bootnode, and use it to find other peers

proc main() {.async, gcsafe.} =\nlet bootNode = createSwitch()\nawait bootNode.start()\n# Create 5 nodes in the network\nvar\nswitches: seq[Switch] = @[]\ndiscManagers: seq[DiscoveryManager] = @[]\nfor i in 0..5:\nlet rdv = RendezVous.new()\n# Create a remote future to await at the end of the program\nlet switch = createSwitch(rdv)\nswitch.mount(DumbProto.new(i))\nswitches.add(switch)\n# A discovery manager is a simple tool, you can set it up by adding discovery\n# interfaces (such as RendezVousInterface) then you can use it to advertise\n# something on the network or to request something from it.\nlet dm = DiscoveryManager()\n# A RendezVousInterface is a RendezVous protocol wrapped to be usable by the\n# DiscoveryManager.\ndm.add(RendezVousInterface.new(rdv))\ndiscManagers.add(dm)\n# We can now start the switch and connect to the bootnode\nawait switch.start()\nawait switch.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs)\n# Each nodes of the network will advertise on some topics (EvenGang or OddClub)\ndm.advertise(RdvNamespace(if i mod 2 == 0: \"EvenGang\" else: \"OddClub\"))\n
We can now create the newcomer. This peer will connect to the boot node, and use it to discover peers & greet them.

  let\nrdv = RendezVous.new()\nnewcomer = createSwitch(rdv)\ndm = DiscoveryManager()\nawait newcomer.start()\nawait newcomer.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs)\ndm.add(RendezVousInterface.new(rdv, ttr = 250.milliseconds))\n# Use the discovery manager to find peers on the OddClub topic to greet them\nlet queryOddClub = dm.request(RdvNamespace(\"OddClub\"))\nfor _ in 0..2:\nlet\n# getPeer give you a PeerAttribute containing informations about the peer.\nres = await queryOddClub.getPeer()\n# Here we will use the PeerId and the MultiAddress to greet him\nconn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec)\nawait conn.writeLp(\"Odd Club suuuucks! Even Gang is better!\")\n# Uh-oh!\nawait conn.close()\n# Wait for the peer to close the stream\nawait conn.join()\n# Queries will run in a loop, so we must stop them when we are done\nqueryOddClub.stop()\n# Maybe it was because he wanted to join the EvenGang\nlet queryEvenGang = dm.request(RdvNamespace(\"EvenGang\"))\nfor _ in 0..2:\nlet\nres = await queryEvenGang.getPeer()\nconn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec)\nawait conn.writeLp(\"Even Gang is sooo laaame! Odd Club rocks!\")\n# Or maybe not...\nawait conn.close()\nawait conn.join()\nqueryEvenGang.stop()\n# What can I say, some people just want to watch the world burn... Anyway\n# Stop all the discovery managers\nfor d in discManagers:\nd.stop()\ndm.stop()\n# Stop all the switches\nawait allFutures(switches.mapIt(it.stop()))\nawait allFutures(bootNode.stop(), newcomer.stop())\nwaitFor(main())\n
"},{"location":"tutorial_6_game/","title":"Tron example","text":"

In this tutorial, we will create a video game based on libp2p, using all of the features we talked about in the last tutorials.

We will: - Discover peers using the Discovery Manager - Use GossipSub to find a play mate - Create a custom protocol to play with him

While this may look like a daunting project, it's less than 150 lines of code.

The game will be a simple Tron. We will use nico as a game engine. (you need to run nimble install nico to have it available)

We will start by importing our dependencies and creating our types

import os\nimport nico, chronos, stew/byteutils, stew/endians2\nimport libp2p\nimport libp2p/protocols/rendezvous\nimport libp2p/discovery/rendezvousinterface\nimport libp2p/discovery/discoverymngr\nconst\ndirections = @[(K_UP, 0, -1), (K_LEFT, -1, 0), (K_DOWN, 0, 1), (K_RIGHT, 1, 0)]\nmapSize = 32\ntickPeriod = 0.2\ntype\nPlayer = ref object\nx, y: int\ncurrentDir, nextDir: int\nlost: bool\ncolor: int\nGame = ref object\ngameMap: array[mapSize * mapSize, int]\ntickTime: float\nlocalPlayer, remotePlayer: Player\npeerFound: Future[Connection]\nhasCandidate: bool\ntickFinished: Future[int]\nGameProto = ref object of LPProtocol\nproc new(_: type[Game]): Game =\n# Default state of a game\nresult = Game(\ntickTime: -3.0, # 3 seconds of \"warm-up\" time\nlocalPlayer: Player(x: 4, y: 16, currentDir: 3, nextDir: 3, color: 8),\nremotePlayer: Player(x: 27, y: 16, currentDir: 1, nextDir: 1, color: 12),\npeerFound: newFuture[Connection]()\n)\nfor pos in 0 .. result.gameMap.high:\nif pos mod mapSize in [0, mapSize - 1] or pos div mapSize in [0, mapSize - 1]:\nresult.gameMap[pos] = 7\n

"},{"location":"tutorial_6_game/#game-logic","title":"Game Logic","text":"

The networking during the game will work like this:

  • Each player will have tickPeriod (0.1) seconds to choose a direction that he wants to go to (default to current direction)
  • After tickPeriod, we will send our choosen direction to the peer, and wait for his direction
  • Once we have both direction, we will \"tick\" the game, and restart the loop, as long as both player are alive.

This is a very simplistic scheme, but creating proper networking for video games is an art

The main drawback of this scheme is that the more ping you have with the peer, the slower the game will run. Or invertedly, the less ping you have, the faster it runs!

proc update(g: Game, dt: float32) =\n# Will be called at each frame of the game.\n#\n# Because both Nico and Chronos have a main loop,\n# they must share the control of the main thread.\n# This is a hacky way to make this happen\nwaitFor(sleepAsync(1.milliseconds))\n# Don't do anything if we are still waiting for an opponent\nif not(g.peerFound.finished()) or isNil(g.tickFinished): return\ng.tickTime += dt\n# Update the wanted direction, making sure we can't go backward\nfor i in 0 .. directions.high:\nif i != (g.localPlayer.currentDir + 2 mod 4) and keyp(directions[i][0]):\ng.localPlayer.nextDir = i\nif g.tickTime > tickPeriod and not g.tickFinished.finished():\n# We choosen our next direction, let the networking know\ng.localPlayer.currentDir = g.localPlayer.nextDir\ng.tickFinished.complete(g.localPlayer.currentDir)\nproc tick(g: Game, p: Player) =\n# Move player and check if he lost\np.x += directions[p.currentDir][1]\np.y += directions[p.currentDir][2]\nif g.gameMap[p.y * mapSize + p.x] != 0: p.lost = true\ng.gameMap[p.y * mapSize + p.x] = p.color\nproc mainLoop(g: Game, peer: Connection) {.async.} =\nwhile not (g.localPlayer.lost or g.remotePlayer.lost):\nif g.tickTime > 0.0:\ng.tickTime = 0\ng.tickFinished = newFuture[int]()\n# Wait for a choosen direction\nlet dir = await g.tickFinished\n# Send it\nawait peer.writeLp(toBytes(uint32(dir)))\n# Get the one from the peer\ng.remotePlayer.currentDir = int uint32.fromBytes(await peer.readLp(8))\n# Tick the players & restart\ng.tick(g.remotePlayer)\ng.tick(g.localPlayer)\n
We'll draw the map & put some texts when necessary:
proc draw(g: Game) =\nfor pos, color in g.gameMap:\nsetColor(color)\nboxFill(pos mod 32 * 4, pos div 32 * 4, 4, 4)\nlet text = if not(g.peerFound.finished()): \"Matchmaking..\"\nelif g.tickTime < -1.5: \"Welcome to Etron\"\nelif g.tickTime < 0.0: \"- \" & $(int(abs(g.tickTime) / 0.5) + 1) & \" -\"\nelif g.remotePlayer.lost and g.localPlayer.lost: \"DEUCE\"\nelif g.localPlayer.lost: \"YOU LOOSE\"\nelif g.remotePlayer.lost: \"YOU WON\"\nelse: \"\"\nprintc(text, screenWidth div 2, screenHeight div 2)\n

"},{"location":"tutorial_6_game/#matchmaking","title":"Matchmaking","text":"

To find an opponent, we will broadcast our address on a GossipSub topic, and wait for someone to connect to us. We will also listen to that topic, and connect to anyone broadcasting his address.

If we are looking for a game, we'll send ok to let the peer know that we are available, check that he is also available, and launch the game.

proc new(T: typedesc[GameProto], g: Game): T =\nproc handle(conn: Connection, proto: string) {.async, gcsafe.} =\ndefer: await conn.closeWithEof()\nif g.peerFound.finished or g.hasCandidate:\nawait conn.close()\nreturn\ng.hasCandidate = true\nawait conn.writeLp(\"ok\")\nif \"ok\" != string.fromBytes(await conn.readLp(1024)):\ng.hasCandidate = false\nreturn\ng.peerFound.complete(conn)\n# The handler of a protocol must wait for the stream to\n# be finished before returning\nawait conn.join()\nreturn T.new(codecs = @[\"/tron/1.0.0\"], handler = handle)\nproc networking(g: Game) {.async.} =\n# Create our switch, similar to the GossipSub example and\n# the Discovery examples combined\nlet\nrdv = RendezVous.new()\nswitch = SwitchBuilder.new()\n.withRng(newRng())\n.withAddresses(@[ MultiAddress.init(\"/ip4/0.0.0.0/tcp/0\").tryGet() ])\n.withTcpTransport()\n.withYamux()\n.withNoise()\n.withRendezVous(rdv)\n.build()\ndm = DiscoveryManager()\ngameProto = GameProto.new(g)\ngossip = GossipSub.init(\nswitch = switch,\ntriggerSelf = false)\ndm.add(RendezVousInterface.new(rdv))\nswitch.mount(gossip)\nswitch.mount(gameProto)\ngossip.subscribe(\n\"/tron/matchmaking\",\nproc (topic: string, data: seq[byte]) {.async.} =\n# If we are still looking for an opponent,\n# try to match anyone broadcasting it's address\nif g.peerFound.finished or g.hasCandidate: return\ng.hasCandidate = true\ntry:\nlet\n(peerId, multiAddress) = parseFullAddress(data).tryGet()\nstream = await switch.dial(peerId, @[multiAddress], gameProto.codec)\nawait stream.writeLp(\"ok\")\nif (await stream.readLp(10)) != \"ok\".toBytes:\ng.hasCandidate = false\nreturn\ng.peerFound.complete(stream)\n# We are \"player 2\"\nswap(g.localPlayer, g.remotePlayer)\nexcept CatchableError as exc:\ndiscard\n)\nawait switch.start()\ndefer: await switch.stop()\n# As explained in the last tutorial, we need a bootnode to be able\n# to find peers. We could use any libp2p running rendezvous (or any\n# node running tron). We will take it's MultiAddress from the command\n# line parameters\nif paramCount() > 0:\nlet (peerId, multiAddress) = paramStr(1).parseFullAddress().tryGet()\nawait switch.connect(peerId, @[multiAddress])\nelse:\necho \"No bootnode provided, listening on: \", switch.peerInfo.fullAddrs.tryGet()\n# Discover peers from the bootnode, and connect to them\ndm.advertise(RdvNamespace(\"tron\"))\nlet discoveryQuery = dm.request(RdvNamespace(\"tron\"))\ndiscoveryQuery.forEach:\ntry:\nawait switch.connect(peer[PeerId], peer.getAll(MultiAddress))\nexcept CatchableError as exc:\necho \"Failed to dial a peer: \", exc.msg\n# We will try to publish our address multiple times, in case\n# it takes time to establish connections with other GossipSub peers\nvar published = false\nwhile not published:\nawait sleepAsync(500.milliseconds)\nfor fullAddr in switch.peerInfo.fullAddrs.tryGet():\nif (await gossip.publish(\"/tron/matchmaking\", fullAddr.bytes)) == 0:\npublished = false\nbreak\npublished = true\ndiscoveryQuery.stop()\n# We now wait for someone to connect to us (or for us to connect to someone)\nlet peerConn = await g.peerFound\ndefer: await peerConn.closeWithEof()\nawait g.mainLoop(peerConn)\nlet\ngame = Game.new()\nnetFut = networking(game)\nnico.init(\"Status\", \"Tron\")\nnico.createWindow(\"Tron\", mapSize * 4, mapSize * 4, 4, false)\nnico.run(proc = discard, proc(dt: float32) = game.update(dt), proc = game.draw())\nwaitFor(netFut.cancelAndWait())\n
And that's it! If you want to run this code locally, the simplest way is to use the first node as a boot node for the second one. But you can also use any rendezvous node

"},{"location":"go-daemon/daemonapi/","title":"Table of Contents","text":"
  • Introduction
  • Installation
  • Usage
  • Example
  • Getting Started
"},{"location":"go-daemon/daemonapi/#introduction","title":"Introduction","text":"

This is a libp2p-backed daemon wrapping the functionalities of go-libp2p for use in Nim. For more information about the go daemon, check out this repository.

"},{"location":"go-daemon/daemonapi/#installation","title":"Installation","text":"
# clone and install dependencies\ngit clone https://github.com/status-im/nim-libp2p\ncd nim-libp2p\nnimble install\n\n# perform unit tests\nnimble test\n# update the git submodule to install the go daemon \ngit submodule update --init --recursive\ngo version\ngit clone https://github.com/libp2p/go-libp2p-daemon\ncd go-libp2p-daemon\ngit checkout v0.0.1\ngo install ./...\ncd ..\n
"},{"location":"go-daemon/daemonapi/#usage","title":"Usage","text":""},{"location":"go-daemon/daemonapi/#example","title":"Example","text":"

Examples can be found in the examples folder

"},{"location":"go-daemon/daemonapi/#getting-started","title":"Getting Started","text":"

Try out the chat example. Full code can be found here:

nim c -r --threads:on examples/directchat.nim\n

This will output a peer ID such as QmbmHfVvouKammmQDJck4hz33WvVktNEe7pasxz2HgseRu which you can use in another instance to connect to it.

./examples/directchat\n/connect QmbmHfVvouKammmQDJck4hz33WvVktNEe7pasxz2HgseRu\n

You can now chat between the instances!

"}]} \ No newline at end of file diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 000000000..4312ed9dd --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,48 @@ + + + + https://status-im.github.io/nim-libp2p/docs/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/circuitrelay/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_1_connect/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_2_customproto/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_3_protobuf/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_4_gossipsub/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_5_discovery/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/tutorial_6_game/ + 2023-06-07 + daily + + + https://status-im.github.io/nim-libp2p/docs/go-daemon/daemonapi/ + 2023-06-07 + daily + + \ No newline at end of file diff --git a/docs/sitemap.xml.gz b/docs/sitemap.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..c1d75f628ea5e6f50fc0271bcd86b7e8f00cf78b GIT binary patch literal 309 zcmV-50m}X#iwFoMPJmRM*@a2&@_Kv@=%q1)djN|xVl`O4Ku_3+nVZ21p;)tdfj$)rJTeWoZi(R?{Cd> zy&qRY3Wijyvf0tDUSjmGj2(~1mLMuCJhd1!Be~=Yv75-c_N5+IRjzZu^>nGhpd8OB zlw&B0n5Y5UYKTjkT0rK&HU=i&dp4saHn8)H`v5`F;ym_UziGN{(>-O&O}n$nML4Yv zL4A(10CO7HopL2(zi5Ajln`Ws7?csGGKgb}7(f`l<`SJ#G8V?&bD1cj^#LOm#=hYf z>M0!x1ApKeiNI5u3L}5yGmykQ?@IfiTdsvwWk73eW}z%Pc5TAL|ASvz-cjQZ|3~4K H(*^(lA?uOm literal 0 HcmV?d00001 diff --git a/docs/tutorial_1_connect.nim b/docs/tutorial_1_connect.nim new file mode 100644 index 000000000..d8d8c2b5a --- /dev/null +++ b/docs/tutorial_1_connect.nim @@ -0,0 +1,95 @@ +## # Simple ping tutorial +## +## Hi all, welcome to the first nim-libp2p tutorial! +## +## !!! tips "" +## This tutorial is for everyone who is interested in building peer-to-peer applications. No Nim programming experience is needed. +## +## To give you a quick overview, **Nim** is the programming language we are using and **nim-libp2p** is the Nim implementation of [libp2p](https://libp2p.io/), a modular library that enables the development of peer-to-peer network applications. +## +## Hope you'll find it helpful in your journey of learning. Happy coding! ;) +## +## ## Before you start +## The only prerequisite here is [Nim](https://nim-lang.org/), the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found [here](https://nim-lang.org/docs/tut1.html). +## +## Install Nim via their [official website](https://nim-lang.org/install.html). +## Check Nim's installation via `nim --version` and its package manager Nimble via `nimble --version`. +## +## You can now install the latest version of `nim-libp2p`: +## ```bash +## nimble install libp2p@#master +## ``` +## +## ## A simple ping application +## We'll start by creating a simple application, which is starting two libp2p [switch](https://docs.libp2p.io/concepts/stream-multiplexing/#switch-swarm), and pinging each other using the [Ping](https://docs.libp2p.io/concepts/protocols/#ping) protocol. +## +## !!! tips "" +## You can find the source of this tutorial (and other tutorials) in the [libp2p/examples](https://github.com/status-im/nim-libp2p/tree/master/examples) folder! +## +## Let's create a `part1.nim`, and import our dependencies: +import chronos + +import libp2p +import libp2p/protocols/ping + +## [chronos](https://github.com/status-im/nim-chronos) the asynchronous framework used by `nim-libp2p` +## +## Next, we'll create an helper procedure to create our switches. A switch needs a bit of configuration, and it will be easier to do this configuration only once: +proc createSwitch(ma: MultiAddress, rng: ref HmacDrbgContext): Switch = + var switch = SwitchBuilder + .new() + .withRng(rng) # Give the application RNG + .withAddress(ma) # Our local address(es) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager + .build() + + return switch + +## This will create a switch using [Mplex](https://docs.libp2p.io/concepts/stream-multiplexing/) as a multiplexer, Noise to secure the communication, and TCP as an underlying transport. +## +## You can of course tweak this, to use a different or multiple transport, or tweak the configuration of Mplex and Noise, but this is some sane defaults that we'll use going forward. +## +## +## Let's now start to create our main procedure: +proc main() {.async, gcsafe.} = + let + rng = newRng() + localAddress = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + pingProtocol = Ping.new(rng=rng) + ## We created some variables that we'll need for the rest of the application: the global `rng` instance, our `localAddress`, and an instance of the `Ping` protocol. + ## The address is in the [MultiAddress](https://github.com/multiformats/multiaddr) format. The port `0` means "take any port available". + ## + ## `tryGet` is procedure which is part of [nim-result](https://github.com/arnetheduck/nim-result/), that will throw an exception if the supplied MultiAddress is invalid. + ## + ## We can now create our two switches: + let + switch1 = createSwitch(localAddress, rng) + switch2 = createSwitch(localAddress, rng) + + switch1.mount(pingProtocol) + + await switch1.start() + await switch2.start() + ## We've **mounted** the `pingProtocol` on our first switch. This means that the first switch will actually listen for any ping requests coming in, and handle them accordingly. + ## + ## Now that we've started the nodes, they are listening for incoming peers. + ## We can find out which port was attributed, and the resulting local addresses, by using `switch1.peerInfo.addrs`. + ## + ## We'll **dial** the first switch from the second one, by specifying it's **Peer ID**, it's **MultiAddress** and the **`Ping` protocol codec**: + let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, PingCodec) + ## We now have a `Ping` connection setup between the second and the first switch, we can use it to actually ping the node: + # ping the other node and echo the ping duration + echo "ping: ", await pingProtocol.ping(conn) + + # We must close the connection ourselves when we're done with it + await conn.close() + ## And that's it! Just a little bit of cleanup: shutting down the switches, waiting for them to stop, and we'll call our `main` procedure: + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + +waitFor(main()) + +## You can now run this program using `nim c -r part1.nim`, and you should see the dialing sequence, ending with a ping output. +## +## In the [next tutorial](tutorial_2_customproto.md), we'll look at how to create our own custom protocol. diff --git a/docs/tutorial_1_connect/index.html b/docs/tutorial_1_connect/index.html new file mode 100644 index 000000000..66887873b --- /dev/null +++ b/docs/tutorial_1_connect/index.html @@ -0,0 +1,629 @@ + + + + + + + + + + + + + + + + + + + + + + + + Simple connection - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Simple ping tutorial

+

Hi all, welcome to the first nim-libp2p tutorial!

+
+

This tutorial is for everyone who is interested in building peer-to-peer applications. No Nim programming experience is needed.

+
+

To give you a quick overview, Nim is the programming language we are using and nim-libp2p is the Nim implementation of libp2p, a modular library that enables the development of peer-to-peer network applications.

+

Hope you'll find it helpful in your journey of learning. Happy coding! ;)

+

Before you start

+

The only prerequisite here is Nim, the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found here.

+

Install Nim via their official website. +Check Nim's installation via nim --version and its package manager Nimble via nimble --version.

+

You can now install the latest version of nim-libp2p: +

nimble install libp2p@#master
+

+

A simple ping application

+

We'll start by creating a simple application, which is starting two libp2p switch, and pinging each other using the Ping protocol.

+
+

You can find the source of this tutorial (and other tutorials) in the libp2p/examples folder!

+
+

Let's create a part1.nim, and import our dependencies: +

import chronos
+
+import libp2p
+import libp2p/protocols/ping
+
+chronos the asynchronous framework used by nim-libp2p

+

Next, we'll create an helper procedure to create our switches. A switch needs a bit of configuration, and it will be easier to do this configuration only once: +

proc createSwitch(ma: MultiAddress, rng: ref HmacDrbgContext): Switch =
+  var switch = SwitchBuilder
+    .new()
+    .withRng(rng)       # Give the application RNG
+    .withAddress(ma)    # Our local address(es)
+    .withTcpTransport() # Use TCP as transport
+    .withMplex()        # Use Mplex as muxer
+    .withNoise()        # Use Noise as secure manager
+    .build()
+
+  return switch
+
+This will create a switch using Mplex as a multiplexer, Noise to secure the communication, and TCP as an underlying transport.

+

You can of course tweak this, to use a different or multiple transport, or tweak the configuration of Mplex and Noise, but this is some sane defaults that we'll use going forward.

+

Let's now start to create our main procedure: +

proc main() {.async, gcsafe.} =
+  let
+    rng = newRng()
+    localAddress = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()
+    pingProtocol = Ping.new(rng=rng)
+
+We created some variables that we'll need for the rest of the application: the global rng instance, our localAddress, and an instance of the Ping protocol. +The address is in the MultiAddress format. The port 0 means "take any port available".

+

tryGet is procedure which is part of nim-result, that will throw an exception if the supplied MultiAddress is invalid.

+

We can now create our two switches: +

  let
+    switch1 = createSwitch(localAddress, rng)
+    switch2 = createSwitch(localAddress, rng)
+
+  switch1.mount(pingProtocol)
+
+  await switch1.start()
+  await switch2.start()
+
+We've mounted the pingProtocol on our first switch. This means that the first switch will actually listen for any ping requests coming in, and handle them accordingly.

+

Now that we've started the nodes, they are listening for incoming peers. +We can find out which port was attributed, and the resulting local addresses, by using switch1.peerInfo.addrs.

+

We'll dial the first switch from the second one, by specifying it's Peer ID, it's MultiAddress and the Ping protocol codec: +

  let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, PingCodec)
+
+We now have a Ping connection setup between the second and the first switch, we can use it to actually ping the node: +
  # ping the other node and echo the ping duration
+  echo "ping: ", await pingProtocol.ping(conn)
+
+  # We must close the connection ourselves when we're done with it
+  await conn.close()
+
+And that's it! Just a little bit of cleanup: shutting down the switches, waiting for them to stop, and we'll call our main procedure: +
  await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
+
+waitFor(main())
+
+You can now run this program using nim c -r part1.nim, and you should see the dialing sequence, ending with a ping output.

+

In the next tutorial, we'll look at how to create our own custom protocol.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_2_customproto.nim b/docs/tutorial_2_customproto.nim new file mode 100644 index 000000000..0bf6c197a --- /dev/null +++ b/docs/tutorial_2_customproto.nim @@ -0,0 +1,74 @@ +## # Custom protocol in libp2p +## +## In the [previous tutorial](tutorial_1_connect.md), we've looked at how to create a simple ping program using the `nim-libp2p`. +## +## We'll now look at how to create a custom protocol inside the libp2p +## +## Let's create a `part2.nim`, and import our dependencies: +import chronos +import stew/byteutils + +import libp2p +## This is similar to the first tutorial, except we don't need to import the `Ping` protocol. +## +## Next, we'll declare our custom protocol +const TestCodec = "/test/proto/1.0.0" + +type TestProto = ref object of LPProtocol + +## We've set a [protocol ID](https://docs.libp2p.io/concepts/protocols/#protocol-ids), and created a custom `LPProtocol`. In a more complex protocol, we could use this structure to store interesting variables. +## +## A protocol generally has two part: and handling/server part, and a dialing/client part. +## Theses two parts can be identical, but in our trivial protocol, the server will wait for a message from the client, and the client will send a message, so we have to handle the two cases separately. +## +## Let's start with the server part: + +proc new(T: typedesc[TestProto]): T = + # every incoming connections will in be handled in this closure + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + # Read up to 1024 bytes from this connection, and transform them into + # a string + echo "Got from remote - ", string.fromBytes(await conn.readLp(1024)) + # We must close the connections ourselves when we're done with it + await conn.close() + + return T.new(codecs = @[TestCodec], handler = handle) + +## This is a constructor for our `TestProto`, that will specify our `codecs` and a `handler`, which will be called for each incoming peer asking for this protocol. +## In our handle, we simply read a message from the connection and `echo` it. +## +## We can now create our client part: +proc hello(p: TestProto, conn: Connection) {.async.} = + await conn.writeLp("Hello p2p!") + +## Again, pretty straight-forward, we just send a message on the connection. +## +## We can now create our main procedure: +proc main() {.async, gcsafe.} = + let + rng = newRng() + testProto = TestProto.new() + switch1 = newStandardSwitch(rng=rng) + switch2 = newStandardSwitch(rng=rng) + + switch1.mount(testProto) + + await switch1.start() + await switch2.start() + + let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec) + + await testProto.hello(conn) + + # We must close the connection ourselves when we're done with it + await conn.close() + + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + +## This is very similar to the first tutorial's `main`, the only noteworthy difference is that we use `newStandardSwitch`, which is similar to the `createSwitch` of the first tutorial, but is bundled directly in libp2p +## +## We can now wrap our program by calling our main proc: +waitFor(main()) + +## And that's it! +## In the [next tutorial](tutorial_3_protobuf.md), we'll create a more complex protocol using Protobuf. diff --git a/docs/tutorial_2_customproto/index.html b/docs/tutorial_2_customproto/index.html new file mode 100644 index 000000000..59c2430cd --- /dev/null +++ b/docs/tutorial_2_customproto/index.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + Create a custom protocol - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Custom protocol in libp2p

+

In the previous tutorial, we've looked at how to create a simple ping program using the nim-libp2p.

+

We'll now look at how to create a custom protocol inside the libp2p

+

Let's create a part2.nim, and import our dependencies: +

import chronos
+import stew/byteutils
+
+import libp2p
+
+This is similar to the first tutorial, except we don't need to import the Ping protocol.

+

Next, we'll declare our custom protocol +

const TestCodec = "/test/proto/1.0.0"
+
+type TestProto = ref object of LPProtocol
+
+We've set a protocol ID, and created a custom LPProtocol. In a more complex protocol, we could use this structure to store interesting variables.

+

A protocol generally has two part: and handling/server part, and a dialing/client part. +Theses two parts can be identical, but in our trivial protocol, the server will wait for a message from the client, and the client will send a message, so we have to handle the two cases separately.

+

Let's start with the server part: +

proc new(T: typedesc[TestProto]): T =
+  # every incoming connections will in be handled in this closure
+  proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
+    # Read up to 1024 bytes from this connection, and transform them into
+    # a string
+    echo "Got from remote - ", string.fromBytes(await conn.readLp(1024))
+    # We must close the connections ourselves when we're done with it
+    await conn.close()
+
+  return T.new(codecs = @[TestCodec], handler = handle)
+
+This is a constructor for our TestProto, that will specify our codecs and a handler, which will be called for each incoming peer asking for this protocol. +In our handle, we simply read a message from the connection and echo it.

+

We can now create our client part: +

proc hello(p: TestProto, conn: Connection) {.async.} =
+  await conn.writeLp("Hello p2p!")
+
+Again, pretty straight-forward, we just send a message on the connection.

+

We can now create our main procedure: +

proc main() {.async, gcsafe.} =
+  let
+    rng = newRng()
+    testProto = TestProto.new()
+    switch1 = newStandardSwitch(rng=rng)
+    switch2 = newStandardSwitch(rng=rng)
+
+  switch1.mount(testProto)
+
+  await switch1.start()
+  await switch2.start()
+
+  let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec)
+
+  await testProto.hello(conn)
+
+  # We must close the connection ourselves when we're done with it
+  await conn.close()
+
+  await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
+
+This is very similar to the first tutorial's main, the only noteworthy difference is that we use newStandardSwitch, which is similar to the createSwitch of the first tutorial, but is bundled directly in libp2p

+

We can now wrap our program by calling our main proc: +

waitFor(main())
+
+And that's it! +In the next tutorial, we'll create a more complex protocol using Protobuf.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_3_protobuf.nim b/docs/tutorial_3_protobuf.nim new file mode 100644 index 000000000..4ba7ac98f --- /dev/null +++ b/docs/tutorial_3_protobuf.nim @@ -0,0 +1,162 @@ +## # Protobuf usage +## +## In the [previous tutorial](tutorial_2_customproto.md), we created a simple "ping" protocol. +## Most real protocol want their messages to be structured and extensible, which is why +## most real protocols use [protobuf](https://developers.google.com/protocol-buffers) to +## define their message structures. +## +## Here, we'll create a slightly more complex protocol, which parses & generate protobuf +## messages. Let's start by importing our dependencies, as usual: +import chronos +import stew/results # for Opt[T] + +import libp2p + +## ## Protobuf encoding & decoding +## This will be the structure of our messages: +## ```protobuf +## message MetricList { +## message Metric { +## string name = 1; +## float value = 2; +## } +## +## repeated Metric metrics = 2; +## } +## ``` +## We'll create our protobuf types, encoders & decoders, according to this format. +## To create the encoders & decoders, we are going to use minprotobuf +## (included in libp2p). +## +## While more modern technics +## (such as [nim-protobuf-serialization](https://github.com/status-im/nim-protobuf-serialization)) +## exists, minprotobuf is currently the recommended method to handle protobuf, since it has +## been used in production extensively, and audited. +type + Metric = object + name: string + value: float + + MetricList = object + metrics: seq[Metric] + +{.push raises: [].} + +proc encode(m: Metric): ProtoBuffer = + result = initProtoBuffer() + result.write(1, m.name) + result.write(2, m.value) + result.finish() + +proc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] = + var res: Metric + let pb = initProtoBuffer(buf) + # "getField" will return a Result[bool, ProtoError]. + # The Result will hold an error if the protobuf is invalid. + # The Result will hold "false" if the field is missing + # + # We are just checking the error, and ignoring whether the value + # is present or not (default values are valid). + discard ? pb.getField(1, res.name) + discard ? pb.getField(2, res.value) + ok(res) + +proc encode(m: MetricList): ProtoBuffer = + result = initProtoBuffer() + for metric in m.metrics: + result.write(1, metric.encode()) + result.finish() + +proc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] = + var + res: MetricList + metrics: seq[seq[byte]] + let pb = initProtoBuffer(buf) + discard ? pb.getRepeatedField(1, metrics) + + for metric in metrics: + res.metrics &= ? Metric.decode(metric) + ok(res) + +## ## Results instead of exceptions +## As you can see, this part of the program also uses Results instead of exceptions for error handling. +## We start by `{.push raises: [].}`, which will prevent every non-async function from raising +## exceptions. +## +## Then, we use [nim-result](https://github.com/arnetheduck/nim-result) to convey +## errors to function callers. A `Result[T, E]` will either hold a valid result of type +## T, or an error of type E. +## +## You can check if the call succeeded by using `res.isOk`, and then get the +## value using `res.value` or the error by using `res.error`. +## +## Another useful tool is `?`, which will unpack a Result if it succeeded, +## or if it failed, exit the current procedure returning the error. +## +## nim-result is packed with other functionalities that you'll find in the +## nim-result repository. +## +## Results and exception are generally interchangeable, but have different semantics +## that you may or may not prefer. +## +## ## Creating the protocol +## We'll next create a protocol, like in the last tutorial, to request these metrics from our host +type + MetricCallback = proc: Future[MetricList] {.raises: [], gcsafe.} + MetricProto = ref object of LPProtocol + metricGetter: MetricCallback + +proc new(_: typedesc[MetricProto], cb: MetricCallback): MetricProto = + var res: MetricProto + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + let + metrics = await res.metricGetter() + asProtobuf = metrics.encode() + await conn.writeLp(asProtobuf.buffer) + await conn.close() + + res = MetricProto.new(@["/metric-getter/1.0.0"], handle) + res.metricGetter = cb + return res + +proc fetch(p: MetricProto, conn: Connection): Future[MetricList] {.async.} = + let protobuf = await conn.readLp(2048) + # tryGet will raise an exception if the Result contains an error. + # It's useful to bridge between exception-world and result-world + return MetricList.decode(protobuf).tryGet() + +## We can now create our main procedure: +proc main() {.async, gcsafe.} = + let rng = newRng() + proc randomMetricGenerator: Future[MetricList] {.async.} = + let metricCount = rng[].generate(uint32) mod 16 + for i in 0 ..< metricCount + 1: + result.metrics.add(Metric( + name: "metric_" & $i, + value: float(rng[].generate(uint16)) / 1000.0 + )) + return result + let + metricProto1 = MetricProto.new(randomMetricGenerator) + metricProto2 = MetricProto.new(randomMetricGenerator) + switch1 = newStandardSwitch(rng=rng) + switch2 = newStandardSwitch(rng=rng) + + switch1.mount(metricProto1) + + await switch1.start() + await switch2.start() + + let + conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, metricProto2.codecs) + metrics = await metricProto2.fetch(conn) + await conn.close() + + for metric in metrics.metrics: + echo metric.name, " = ", metric.value + + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + +waitFor(main()) + +## If you run this program, you should see random metrics being sent from the switch1 to the switch2. diff --git a/docs/tutorial_3_protobuf/index.html b/docs/tutorial_3_protobuf/index.html new file mode 100644 index 000000000..5940f1834 --- /dev/null +++ b/docs/tutorial_3_protobuf/index.html @@ -0,0 +1,714 @@ + + + + + + + + + + + + + + + + + + + + + + + + Protobuf - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Protobuf usage

+

In the previous tutorial, we created a simple "ping" protocol. +Most real protocol want their messages to be structured and extensible, which is why +most real protocols use protobuf to +define their message structures.

+

Here, we'll create a slightly more complex protocol, which parses & generate protobuf +messages. Let's start by importing our dependencies, as usual: +

import chronos
+import stew/results # for Opt[T]
+
+import libp2p
+

+

Protobuf encoding & decoding

+

This will be the structure of our messages: +

message MetricList {
+  message Metric {
+    string name = 1;
+    float value = 2;
+  }
+
+  repeated Metric metrics = 2;
+}
+
+We'll create our protobuf types, encoders & decoders, according to this format. +To create the encoders & decoders, we are going to use minprotobuf +(included in libp2p).

+

While more modern technics +(such as nim-protobuf-serialization) +exists, minprotobuf is currently the recommended method to handle protobuf, since it has +been used in production extensively, and audited. +

type
+  Metric = object
+    name: string
+    value: float
+
+  MetricList = object
+    metrics: seq[Metric]
+
+{.push raises: [].}
+
+proc encode(m: Metric): ProtoBuffer =
+  result = initProtoBuffer()
+  result.write(1, m.name)
+  result.write(2, m.value)
+  result.finish()
+
+proc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] =
+  var res: Metric
+  let pb = initProtoBuffer(buf)
+  # "getField" will return a Result[bool, ProtoError].
+  # The Result will hold an error if the protobuf is invalid.
+  # The Result will hold "false" if the field is missing
+  #
+  # We are just checking the error, and ignoring whether the value
+  # is present or not (default values are valid).
+  discard ? pb.getField(1, res.name)
+  discard ? pb.getField(2, res.value)
+  ok(res)
+
+proc encode(m: MetricList): ProtoBuffer =
+  result = initProtoBuffer()
+  for metric in m.metrics:
+    result.write(1, metric.encode())
+  result.finish()
+
+proc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] =
+  var
+    res: MetricList
+    metrics: seq[seq[byte]]
+  let pb = initProtoBuffer(buf)
+  discard ? pb.getRepeatedField(1, metrics)
+
+  for metric in metrics:
+    res.metrics &= ? Metric.decode(metric)
+  ok(res)
+

+

Results instead of exceptions

+

As you can see, this part of the program also uses Results instead of exceptions for error handling. +We start by {.push raises: [].}, which will prevent every non-async function from raising +exceptions.

+

Then, we use nim-result to convey +errors to function callers. A Result[T, E] will either hold a valid result of type +T, or an error of type E.

+

You can check if the call succeeded by using res.isOk, and then get the +value using res.value or the error by using res.error.

+

Another useful tool is ?, which will unpack a Result if it succeeded, +or if it failed, exit the current procedure returning the error.

+

nim-result is packed with other functionalities that you'll find in the +nim-result repository.

+

Results and exception are generally interchangeable, but have different semantics +that you may or may not prefer.

+

Creating the protocol

+

We'll next create a protocol, like in the last tutorial, to request these metrics from our host +

type
+  MetricCallback = proc: Future[MetricList] {.raises: [], gcsafe.}
+  MetricProto = ref object of LPProtocol
+    metricGetter: MetricCallback
+
+proc new(_: typedesc[MetricProto], cb: MetricCallback): MetricProto =
+  var res: MetricProto
+  proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
+    let
+      metrics = await res.metricGetter()
+      asProtobuf = metrics.encode()
+    await conn.writeLp(asProtobuf.buffer)
+    await conn.close()
+
+  res = MetricProto.new(@["/metric-getter/1.0.0"], handle)
+  res.metricGetter = cb
+  return res
+
+proc fetch(p: MetricProto, conn: Connection): Future[MetricList] {.async.} =
+  let protobuf = await conn.readLp(2048)
+  # tryGet will raise an exception if the Result contains an error.
+  # It's useful to bridge between exception-world and result-world
+  return MetricList.decode(protobuf).tryGet()
+
+We can now create our main procedure: +
proc main() {.async, gcsafe.} =
+  let rng = newRng()
+  proc randomMetricGenerator: Future[MetricList] {.async.} =
+    let metricCount = rng[].generate(uint32) mod 16
+    for i in 0 ..< metricCount + 1:
+      result.metrics.add(Metric(
+        name: "metric_" & $i,
+        value: float(rng[].generate(uint16)) / 1000.0
+      ))
+    return result
+  let
+    metricProto1 = MetricProto.new(randomMetricGenerator)
+    metricProto2 = MetricProto.new(randomMetricGenerator)
+    switch1 = newStandardSwitch(rng=rng)
+    switch2 = newStandardSwitch(rng=rng)
+
+  switch1.mount(metricProto1)
+
+  await switch1.start()
+  await switch2.start()
+
+  let
+    conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, metricProto2.codecs)
+    metrics = await metricProto2.fetch(conn)
+  await conn.close()
+
+  for metric in metrics.metrics:
+    echo metric.name, " = ", metric.value
+
+  await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
+
+waitFor(main())
+
+If you run this program, you should see random metrics being sent from the switch1 to the switch2.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_4_gossipsub.nim b/docs/tutorial_4_gossipsub.nim new file mode 100644 index 000000000..0e8274caf --- /dev/null +++ b/docs/tutorial_4_gossipsub.nim @@ -0,0 +1,163 @@ +## # GossipSub +## +## In this tutorial, we'll build a simple GossipSub network +## to broadcast the metrics we built in the previous tutorial. +## +## GossipSub is used to broadcast some messages in a network, +## and allows to balance between latency, bandwidth usage, +## privacy and attack resistance. +## +## You'll find a good explanation on how GossipSub works +## [here.](https://docs.libp2p.io/concepts/publish-subscribe/) There are a lot +## of parameters you can tweak to adjust how GossipSub behaves but here we'll +## use the sane defaults shipped with libp2p. +## +## We'll start by creating our metric structure like previously + +import chronos +import stew/results + +import libp2p +import libp2p/protocols/pubsub/rpc/messages + +type + Metric = object + name: string + value: float + + MetricList = object + hostname: string + metrics: seq[Metric] + +{.push raises: [].} + +proc encode(m: Metric): ProtoBuffer = + result = initProtoBuffer() + result.write(1, m.name) + result.write(2, m.value) + result.finish() + +proc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] = + var res: Metric + let pb = initProtoBuffer(buf) + discard ? pb.getField(1, res.name) + discard ? pb.getField(2, res.value) + ok(res) + +proc encode(m: MetricList): ProtoBuffer = + result = initProtoBuffer() + for metric in m.metrics: + result.write(1, metric.encode()) + result.write(2, m.hostname) + result.finish() + +proc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] = + var + res: MetricList + metrics: seq[seq[byte]] + let pb = initProtoBuffer(buf) + discard ? pb.getRepeatedField(1, metrics) + + for metric in metrics: + res.metrics &= ? Metric.decode(metric) + ? pb.getRequiredField(2, res.hostname) + ok(res) + +## This is exactly like the previous structure, except that we added +## a `hostname` to distinguish where the metric is coming from. +## +## Now we'll create a small GossipSub network to broadcast the metrics, +## and collect them on one of the node. + +type Node = tuple[switch: Switch, gossip: GossipSub, hostname: string] + +proc oneNode(node: Node, rng: ref HmacDrbgContext) {.async.} = + # This procedure will handle one of the node of the network + node.gossip.addValidator(["metrics"], + proc(topic: string, message: Message): Future[ValidationResult] {.async.} = + let decoded = MetricList.decode(message.data) + if decoded.isErr: return ValidationResult.Reject + return ValidationResult.Accept + ) + # This "validator" will attach to the `metrics` topic and make sure + # that every message in this topic is valid. This allows us to stop + # propagation of invalid messages quickly in the network, and punish + # peers sending them. + + # `John` will be responsible to log the metrics, the rest of the nodes + # will just forward them in the network + if node.hostname == "John": + node.gossip.subscribe("metrics", + proc (topic: string, data: seq[byte]) {.async.} = + echo MetricList.decode(data).tryGet() + ) + else: + node.gossip.subscribe("metrics", nil) + + # Create random metrics 10 times and broadcast them + for _ in 0..<10: + await sleepAsync(500.milliseconds) + var metricList = MetricList(hostname: node.hostname) + let metricCount = rng[].generate(uint32) mod 4 + for i in 0 ..< metricCount + 1: + metricList.metrics.add(Metric( + name: "metric_" & $i, + value: float(rng[].generate(uint16)) / 1000.0 + )) + + discard await node.gossip.publish("metrics", encode(metricList).buffer) + await node.switch.stop() + +## For our main procedure, we'll create a few nodes, and connect them together. +## Note that they are not all interconnected, but GossipSub will take care of +## broadcasting to the full network nonetheless. +proc main {.async.} = + let rng = newRng() + var nodes: seq[Node] + + for hostname in ["John", "Walter", "David", "Thuy", "Amy"]: + let + switch = newStandardSwitch(rng=rng) + gossip = GossipSub.init(switch = switch, triggerSelf = true) + switch.mount(gossip) + await switch.start() + + nodes.add((switch, gossip, hostname)) + + for index, node in nodes: + # Connect to a few neighbors + for otherNodeIdx in index - 1 .. index + 2: + if otherNodeIdx notin 0 ..< nodes.len or otherNodeIdx == index: continue + let otherNode = nodes[otherNodeIdx] + await node.switch.connect( + otherNode.switch.peerInfo.peerId, + otherNode.switch.peerInfo.addrs) + + var allFuts: seq[Future[void]] + for node in nodes: + allFuts.add(oneNode(node, rng)) + + await allFutures(allFuts) + +waitFor(main()) + +## If you run this program, you should see something like: +## ``` +## (hostname: "John", metrics: @[(name: "metric_0", value: 42.097), (name: "metric_1", value: 50.99), (name: "metric_2", value: 47.86), (name: "metric_3", value: 5.368)]) +## (hostname: "Walter", metrics: @[(name: "metric_0", value: 39.452), (name: "metric_1", value: 15.606), (name: "metric_2", value: 14.059), (name: "metric_3", value: 6.68)]) +## (hostname: "David", metrics: @[(name: "metric_0", value: 9.82), (name: "metric_1", value: 2.862), (name: "metric_2", value: 15.514)]) +## (hostname: "Thuy", metrics: @[(name: "metric_0", value: 59.038)]) +## (hostname: "Amy", metrics: @[(name: "metric_0", value: 55.616), (name: "metric_1", value: 23.52), (name: "metric_2", value: 59.081), (name: "metric_3", value: 2.516)]) +## ``` +## +## This is John receiving & logging everyone's metrics. +## +## ## Going further +## Building efficient & safe GossipSub networks is a tricky subject. By tweaking the [gossip params](https://status-im.github.io/nim-libp2p/master/libp2p/protocols/pubsub/gossipsub/types.html#GossipSubParams) +## and [topic params](https://status-im.github.io/nim-libp2p/master/libp2p/protocols/pubsub/gossipsub/types.html#TopicParams), +## you can achieve very different properties. +## +## Also see reports for [GossipSub v1.1](https://gateway.ipfs.io/ipfs/QmRAFP5DBnvNjdYSbWhEhVRJJDFCLpPyvew5GwCCB4VxM4) +## +## If you are interested in broadcasting for your application, you may want to use [Waku](https://waku.org/), which builds on top of GossipSub, +## and adds features such as history, spam protection, and light node friendliness. diff --git a/docs/tutorial_4_gossipsub/index.html b/docs/tutorial_4_gossipsub/index.html new file mode 100644 index 000000000..aa2be32ba --- /dev/null +++ b/docs/tutorial_4_gossipsub/index.html @@ -0,0 +1,685 @@ + + + + + + + + + + + + + + + + + + + + + + + + GossipSub - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

GossipSub

+

In this tutorial, we'll build a simple GossipSub network +to broadcast the metrics we built in the previous tutorial.

+

GossipSub is used to broadcast some messages in a network, +and allows to balance between latency, bandwidth usage, +privacy and attack resistance.

+

You'll find a good explanation on how GossipSub works +here. There are a lot +of parameters you can tweak to adjust how GossipSub behaves but here we'll +use the sane defaults shipped with libp2p.

+

We'll start by creating our metric structure like previously +

import chronos
+import stew/results
+
+import libp2p
+import libp2p/protocols/pubsub/rpc/messages
+
+type
+  Metric = object
+    name: string
+    value: float
+
+  MetricList = object
+    hostname: string
+    metrics: seq[Metric]
+
+{.push raises: [].}
+
+proc encode(m: Metric): ProtoBuffer =
+  result = initProtoBuffer()
+  result.write(1, m.name)
+  result.write(2, m.value)
+  result.finish()
+
+proc decode(_: type Metric, buf: seq[byte]): Result[Metric, ProtoError] =
+  var res: Metric
+  let pb = initProtoBuffer(buf)
+  discard ? pb.getField(1, res.name)
+  discard ? pb.getField(2, res.value)
+  ok(res)
+
+proc encode(m: MetricList): ProtoBuffer =
+  result = initProtoBuffer()
+  for metric in m.metrics:
+    result.write(1, metric.encode())
+  result.write(2, m.hostname)
+  result.finish()
+
+proc decode(_: type MetricList, buf: seq[byte]): Result[MetricList, ProtoError] =
+  var
+    res: MetricList
+    metrics: seq[seq[byte]]
+  let pb = initProtoBuffer(buf)
+  discard ? pb.getRepeatedField(1, metrics)
+
+  for metric in metrics:
+    res.metrics &= ? Metric.decode(metric)
+  ? pb.getRequiredField(2, res.hostname)
+  ok(res)
+
+This is exactly like the previous structure, except that we added +a hostname to distinguish where the metric is coming from.

+

Now we'll create a small GossipSub network to broadcast the metrics, +and collect them on one of the node. +

type Node = tuple[switch: Switch, gossip: GossipSub, hostname: string]
+
+proc oneNode(node: Node, rng: ref HmacDrbgContext) {.async.} =
+  # This procedure will handle one of the node of the network
+  node.gossip.addValidator(["metrics"],
+    proc(topic: string, message: Message): Future[ValidationResult] {.async.} =
+      let decoded = MetricList.decode(message.data)
+      if decoded.isErr: return ValidationResult.Reject
+      return ValidationResult.Accept
+  )
+  # This "validator" will attach to the `metrics` topic and make sure
+  # that every message in this topic is valid. This allows us to stop
+  # propagation of invalid messages quickly in the network, and punish
+  # peers sending them.
+
+  # `John` will be responsible to log the metrics, the rest of the nodes
+  # will just forward them in the network
+  if node.hostname == "John":
+    node.gossip.subscribe("metrics",
+      proc (topic: string, data: seq[byte]) {.async.} =
+        echo MetricList.decode(data).tryGet()
+    )
+  else:
+    node.gossip.subscribe("metrics", nil)
+
+  # Create random metrics 10 times and broadcast them
+  for _ in 0..<10:
+    await sleepAsync(500.milliseconds)
+    var metricList = MetricList(hostname: node.hostname)
+    let metricCount = rng[].generate(uint32) mod 4
+    for i in 0 ..< metricCount + 1:
+      metricList.metrics.add(Metric(
+        name: "metric_" & $i,
+        value: float(rng[].generate(uint16)) / 1000.0
+      ))
+
+    discard await node.gossip.publish("metrics", encode(metricList).buffer)
+  await node.switch.stop()
+
+For our main procedure, we'll create a few nodes, and connect them together. +Note that they are not all interconnected, but GossipSub will take care of +broadcasting to the full network nonetheless. +
proc main {.async.} =
+  let rng = newRng()
+  var nodes: seq[Node]
+
+  for hostname in ["John", "Walter", "David", "Thuy", "Amy"]:
+    let
+      switch = newStandardSwitch(rng=rng)
+      gossip = GossipSub.init(switch = switch, triggerSelf = true)
+    switch.mount(gossip)
+    await switch.start()
+
+    nodes.add((switch, gossip, hostname))
+
+  for index, node in nodes:
+    # Connect to a few neighbors
+    for otherNodeIdx in index - 1 .. index + 2:
+      if otherNodeIdx notin 0 ..< nodes.len or otherNodeIdx == index: continue
+      let otherNode = nodes[otherNodeIdx]
+      await node.switch.connect(
+        otherNode.switch.peerInfo.peerId,
+        otherNode.switch.peerInfo.addrs)
+
+  var allFuts: seq[Future[void]]
+  for node in nodes:
+    allFuts.add(oneNode(node, rng))
+
+  await allFutures(allFuts)
+
+waitFor(main())
+
+If you run this program, you should see something like: +
(hostname: "John", metrics: @[(name: "metric_0", value: 42.097), (name: "metric_1", value: 50.99), (name: "metric_2", value: 47.86), (name: "metric_3", value: 5.368)])
+(hostname: "Walter", metrics: @[(name: "metric_0", value: 39.452), (name: "metric_1", value: 15.606), (name: "metric_2", value: 14.059), (name: "metric_3", value: 6.68)])
+(hostname: "David", metrics: @[(name: "metric_0", value: 9.82), (name: "metric_1", value: 2.862), (name: "metric_2", value: 15.514)])
+(hostname: "Thuy", metrics: @[(name: "metric_0", value: 59.038)])
+(hostname: "Amy", metrics: @[(name: "metric_0", value: 55.616), (name: "metric_1", value: 23.52), (name: "metric_2", value: 59.081), (name: "metric_3", value: 2.516)])
+

+

This is John receiving & logging everyone's metrics.

+

Going further

+

Building efficient & safe GossipSub networks is a tricky subject. By tweaking the gossip params +and topic params, +you can achieve very different properties.

+

Also see reports for GossipSub v1.1

+

If you are interested in broadcasting for your application, you may want to use Waku, which builds on top of GossipSub, +and adds features such as history, spam protection, and light node friendliness.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_5_discovery.nim b/docs/tutorial_5_discovery.nim new file mode 100644 index 000000000..889087736 --- /dev/null +++ b/docs/tutorial_5_discovery.nim @@ -0,0 +1,132 @@ +## # Discovery Manager +## +## In the [previous tutorial](tutorial_4_gossipsub.md), we built a custom protocol using [protobuf](https://developers.google.com/protocol-buffers) and +## spread informations (some metrics) on the network using gossipsub. +## For this tutorial, on the other hand, we'll go back on a simple example +## we'll try to discover a specific peers to greet on the network. +## +## First, as usual, we import the dependencies: +import sequtils +import chronos +import stew/byteutils + +import libp2p +import libp2p/protocols/rendezvous +import libp2p/discovery/rendezvousinterface +import libp2p/discovery/discoverymngr + +## We'll not use newStandardSwitch this time as we need the discovery protocol +## [RendezVous](https://github.com/libp2p/specs/blob/master/rendezvous/README.md) to be mounted on the switch using withRendezVous. +## +## Note that other discovery methods such as [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) or [discv5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) exist. +proc createSwitch(rdv: RendezVous = RendezVous.new()): Switch = + SwitchBuilder.new() + .withRng(newRng()) + .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ]) + .withTcpTransport() + .withYamux() + .withNoise() + .withRendezVous(rdv) + .build() + +# Create a really simple protocol to log one message received then close the stream +const DumbCodec = "/dumb/proto/1.0.0" +type DumbProto = ref object of LPProtocol +proc new(T: typedesc[DumbProto], nodeNumber: int): T = + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + echo "Node", nodeNumber, " received: ", string.fromBytes(await conn.readLp(1024)) + await conn.close() + return T.new(codecs = @[DumbCodec], handler = handle) + +## ## Bootnodes +## The first time a p2p program is ran, he needs to know how to join +## its network. This is generally done by hard-coding a list of stable +## nodes in the binary, called "bootnodes". These bootnodes are a +## critical part of a p2p network, since they are used by every new +## user to onboard the network. +## +## By using libp2p, we can use any node supporting our discovery protocol +## (rendezvous in this case) as a bootnode. For this example, we'll +## create a bootnode, and then every peer will advertise itself on the +## bootnode, and use it to find other peers +proc main() {.async, gcsafe.} = + let bootNode = createSwitch() + await bootNode.start() + + # Create 5 nodes in the network + var + switches: seq[Switch] = @[] + discManagers: seq[DiscoveryManager] = @[] + + for i in 0..5: + let rdv = RendezVous.new() + # Create a remote future to await at the end of the program + let switch = createSwitch(rdv) + switch.mount(DumbProto.new(i)) + switches.add(switch) + + # A discovery manager is a simple tool, you can set it up by adding discovery + # interfaces (such as RendezVousInterface) then you can use it to advertise + # something on the network or to request something from it. + let dm = DiscoveryManager() + # A RendezVousInterface is a RendezVous protocol wrapped to be usable by the + # DiscoveryManager. + dm.add(RendezVousInterface.new(rdv)) + discManagers.add(dm) + + # We can now start the switch and connect to the bootnode + await switch.start() + await switch.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs) + + # Each nodes of the network will advertise on some topics (EvenGang or OddClub) + dm.advertise(RdvNamespace(if i mod 2 == 0: "EvenGang" else: "OddClub")) + + ## We can now create the newcomer. This peer will connect to the boot node, and use + ## it to discover peers & greet them. + let + rdv = RendezVous.new() + newcomer = createSwitch(rdv) + dm = DiscoveryManager() + await newcomer.start() + await newcomer.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs) + dm.add(RendezVousInterface.new(rdv, ttr = 250.milliseconds)) + + # Use the discovery manager to find peers on the OddClub topic to greet them + let queryOddClub = dm.request(RdvNamespace("OddClub")) + for _ in 0..2: + let + # getPeer give you a PeerAttribute containing informations about the peer. + res = await queryOddClub.getPeer() + # Here we will use the PeerId and the MultiAddress to greet him + conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec) + await conn.writeLp("Odd Club suuuucks! Even Gang is better!") + # Uh-oh! + await conn.close() + # Wait for the peer to close the stream + await conn.join() + # Queries will run in a loop, so we must stop them when we are done + queryOddClub.stop() + + # Maybe it was because he wanted to join the EvenGang + let queryEvenGang = dm.request(RdvNamespace("EvenGang")) + for _ in 0..2: + let + res = await queryEvenGang.getPeer() + conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec) + await conn.writeLp("Even Gang is sooo laaame! Odd Club rocks!") + # Or maybe not... + await conn.close() + await conn.join() + queryEvenGang.stop() + # What can I say, some people just want to watch the world burn... Anyway + + # Stop all the discovery managers + for d in discManagers: + d.stop() + dm.stop() + + # Stop all the switches + await allFutures(switches.mapIt(it.stop())) + await allFutures(bootNode.stop(), newcomer.stop()) + +waitFor(main()) diff --git a/docs/tutorial_5_discovery/index.html b/docs/tutorial_5_discovery/index.html new file mode 100644 index 000000000..1c9952c78 --- /dev/null +++ b/docs/tutorial_5_discovery/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + Discovery Manager - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Discovery Manager

+

In the previous tutorial, we built a custom protocol using protobuf and +spread informations (some metrics) on the network using gossipsub. +For this tutorial, on the other hand, we'll go back on a simple example +we'll try to discover a specific peers to greet on the network.

+

First, as usual, we import the dependencies: +

import sequtils
+import chronos
+import stew/byteutils
+
+import libp2p
+import libp2p/protocols/rendezvous
+import libp2p/discovery/rendezvousinterface
+import libp2p/discovery/discoverymngr
+
+We'll not use newStandardSwitch this time as we need the discovery protocol +RendezVous to be mounted on the switch using withRendezVous.

+

Note that other discovery methods such as Kademlia or discv5 exist. +

proc createSwitch(rdv: RendezVous = RendezVous.new()): Switch =
+  SwitchBuilder.new()
+    .withRng(newRng())
+    .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ])
+    .withTcpTransport()
+    .withYamux()
+    .withNoise()
+    .withRendezVous(rdv)
+    .build()
+
+# Create a really simple protocol to log one message received then close the stream
+const DumbCodec = "/dumb/proto/1.0.0"
+type DumbProto = ref object of LPProtocol
+proc new(T: typedesc[DumbProto], nodeNumber: int): T =
+  proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
+    echo "Node", nodeNumber, " received: ", string.fromBytes(await conn.readLp(1024))
+    await conn.close()
+  return T.new(codecs = @[DumbCodec], handler = handle)
+

+

Bootnodes

+

The first time a p2p program is ran, he needs to know how to join +its network. This is generally done by hard-coding a list of stable +nodes in the binary, called "bootnodes". These bootnodes are a +critical part of a p2p network, since they are used by every new +user to onboard the network.

+

By using libp2p, we can use any node supporting our discovery protocol +(rendezvous in this case) as a bootnode. For this example, we'll +create a bootnode, and then every peer will advertise itself on the +bootnode, and use it to find other peers +

proc main() {.async, gcsafe.} =
+  let bootNode = createSwitch()
+  await bootNode.start()
+
+  # Create 5 nodes in the network
+  var
+    switches: seq[Switch] = @[]
+    discManagers: seq[DiscoveryManager] = @[]
+
+  for i in 0..5:
+    let rdv = RendezVous.new()
+    # Create a remote future to await at the end of the program
+    let switch = createSwitch(rdv)
+    switch.mount(DumbProto.new(i))
+    switches.add(switch)
+
+    # A discovery manager is a simple tool, you can set it up by adding discovery
+    # interfaces (such as RendezVousInterface) then you can use it to advertise
+    # something on the network or to request something from it.
+    let dm = DiscoveryManager()
+    # A RendezVousInterface is a RendezVous protocol wrapped to be usable by the
+    # DiscoveryManager.
+    dm.add(RendezVousInterface.new(rdv))
+    discManagers.add(dm)
+
+    # We can now start the switch and connect to the bootnode
+    await switch.start()
+    await switch.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs)
+
+    # Each nodes of the network will advertise on some topics (EvenGang or OddClub)
+    dm.advertise(RdvNamespace(if i mod 2 == 0: "EvenGang" else: "OddClub"))
+
+We can now create the newcomer. This peer will connect to the boot node, and use +it to discover peers & greet them.

+
  let
+    rdv = RendezVous.new()
+    newcomer = createSwitch(rdv)
+    dm = DiscoveryManager()
+  await newcomer.start()
+  await newcomer.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs)
+  dm.add(RendezVousInterface.new(rdv, ttr = 250.milliseconds))
+
+  # Use the discovery manager to find peers on the OddClub topic to greet them
+  let queryOddClub = dm.request(RdvNamespace("OddClub"))
+  for _ in 0..2:
+    let
+      # getPeer give you a PeerAttribute containing informations about the peer.
+      res = await queryOddClub.getPeer()
+      # Here we will use the PeerId and the MultiAddress to greet him
+      conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec)
+    await conn.writeLp("Odd Club suuuucks! Even Gang is better!")
+    # Uh-oh!
+    await conn.close()
+    # Wait for the peer to close the stream
+    await conn.join()
+  # Queries will run in a loop, so we must stop them when we are done
+  queryOddClub.stop()
+
+  # Maybe it was because he wanted to join the EvenGang
+  let queryEvenGang = dm.request(RdvNamespace("EvenGang"))
+  for _ in 0..2:
+    let
+      res = await queryEvenGang.getPeer()
+      conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec)
+    await conn.writeLp("Even Gang is sooo laaame! Odd Club rocks!")
+    # Or maybe not...
+    await conn.close()
+    await conn.join()
+  queryEvenGang.stop()
+  # What can I say, some people just want to watch the world burn... Anyway
+
+  # Stop all the discovery managers
+  for d in discManagers:
+    d.stop()
+  dm.stop()
+
+  # Stop all the switches
+  await allFutures(switches.mapIt(it.stop()))
+  await allFutures(bootNode.stop(), newcomer.stop())
+
+waitFor(main())
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_6_game.nim b/docs/tutorial_6_game.nim new file mode 100644 index 000000000..f3be6d372 --- /dev/null +++ b/docs/tutorial_6_game.nim @@ -0,0 +1,259 @@ +## # Tron example +## +## In this tutorial, we will create a video game based on libp2p, using +## all of the features we talked about in the last tutorials. +## +## We will: +## - Discover peers using the Discovery Manager +## - Use GossipSub to find a play mate +## - Create a custom protocol to play with him +## +## While this may look like a daunting project, it's less than 150 lines of code. +## +## The game will be a simple Tron. We will use [nico](https://github.com/ftsf/nico) +## as a game engine. (you need to run `nimble install nico` to have it available) +## +## ![multiplay](https://user-images.githubusercontent.com/13471753/198852714-b55048e3-f233-4723-900d-2193ad259fe1.gif) +## +## We will start by importing our dependencies and creating our types +import os +import nico, chronos, stew/byteutils, stew/endians2 +import libp2p +import libp2p/protocols/rendezvous +import libp2p/discovery/rendezvousinterface +import libp2p/discovery/discoverymngr + +const + directions = @[(K_UP, 0, -1), (K_LEFT, -1, 0), (K_DOWN, 0, 1), (K_RIGHT, 1, 0)] + mapSize = 32 + tickPeriod = 0.2 + +type + Player = ref object + x, y: int + currentDir, nextDir: int + lost: bool + color: int + + Game = ref object + gameMap: array[mapSize * mapSize, int] + tickTime: float + localPlayer, remotePlayer: Player + peerFound: Future[Connection] + hasCandidate: bool + tickFinished: Future[int] + + GameProto = ref object of LPProtocol + +proc new(_: type[Game]): Game = + # Default state of a game + result = Game( + tickTime: -3.0, # 3 seconds of "warm-up" time + localPlayer: Player(x: 4, y: 16, currentDir: 3, nextDir: 3, color: 8), + remotePlayer: Player(x: 27, y: 16, currentDir: 1, nextDir: 1, color: 12), + peerFound: newFuture[Connection]() + ) + for pos in 0 .. result.gameMap.high: + if pos mod mapSize in [0, mapSize - 1] or pos div mapSize in [0, mapSize - 1]: + result.gameMap[pos] = 7 + +## ## Game Logic +## The networking during the game will work like this: +## +## * Each player will have `tickPeriod` (0.1) seconds to choose +## a direction that he wants to go to (default to current direction) +## * After `tickPeriod`, we will send our choosen direction to the peer, +## and wait for his direction +## * Once we have both direction, we will "tick" the game, and restart the +## loop, as long as both player are alive. +## +## This is a very simplistic scheme, but creating proper networking for +## video games is an [art](https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization) +## +## The main drawback of this scheme is that the more ping you have with +## the peer, the slower the game will run. Or invertedly, the less ping you +## have, the faster it runs! +proc update(g: Game, dt: float32) = + # Will be called at each frame of the game. + # + # Because both Nico and Chronos have a main loop, + # they must share the control of the main thread. + # This is a hacky way to make this happen + waitFor(sleepAsync(1.milliseconds)) + # Don't do anything if we are still waiting for an opponent + if not(g.peerFound.finished()) or isNil(g.tickFinished): return + g.tickTime += dt + + # Update the wanted direction, making sure we can't go backward + for i in 0 .. directions.high: + if i != (g.localPlayer.currentDir + 2 mod 4) and keyp(directions[i][0]): + g.localPlayer.nextDir = i + + if g.tickTime > tickPeriod and not g.tickFinished.finished(): + # We choosen our next direction, let the networking know + g.localPlayer.currentDir = g.localPlayer.nextDir + g.tickFinished.complete(g.localPlayer.currentDir) + +proc tick(g: Game, p: Player) = + # Move player and check if he lost + p.x += directions[p.currentDir][1] + p.y += directions[p.currentDir][2] + if g.gameMap[p.y * mapSize + p.x] != 0: p.lost = true + g.gameMap[p.y * mapSize + p.x] = p.color + +proc mainLoop(g: Game, peer: Connection) {.async.} = + while not (g.localPlayer.lost or g.remotePlayer.lost): + if g.tickTime > 0.0: + g.tickTime = 0 + g.tickFinished = newFuture[int]() + + # Wait for a choosen direction + let dir = await g.tickFinished + # Send it + await peer.writeLp(toBytes(uint32(dir))) + + # Get the one from the peer + g.remotePlayer.currentDir = int uint32.fromBytes(await peer.readLp(8)) + # Tick the players & restart + g.tick(g.remotePlayer) + g.tick(g.localPlayer) + +## We'll draw the map & put some texts when necessary: +proc draw(g: Game) = + for pos, color in g.gameMap: + setColor(color) + boxFill(pos mod 32 * 4, pos div 32 * 4, 4, 4) + let text = if not(g.peerFound.finished()): "Matchmaking.." + elif g.tickTime < -1.5: "Welcome to Etron" + elif g.tickTime < 0.0: "- " & $(int(abs(g.tickTime) / 0.5) + 1) & " -" + elif g.remotePlayer.lost and g.localPlayer.lost: "DEUCE" + elif g.localPlayer.lost: "YOU LOOSE" + elif g.remotePlayer.lost: "YOU WON" + else: "" + printc(text, screenWidth div 2, screenHeight div 2) + + +## ## Matchmaking +## To find an opponent, we will broadcast our address on a +## GossipSub topic, and wait for someone to connect to us. +## We will also listen to that topic, and connect to anyone +## broadcasting his address. +## +## If we are looking for a game, we'll send `ok` to let the +## peer know that we are available, check that he is also available, +## and launch the game. +proc new(T: typedesc[GameProto], g: Game): T = + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + defer: await conn.closeWithEof() + if g.peerFound.finished or g.hasCandidate: + await conn.close() + return + g.hasCandidate = true + await conn.writeLp("ok") + if "ok" != string.fromBytes(await conn.readLp(1024)): + g.hasCandidate = false + return + g.peerFound.complete(conn) + # The handler of a protocol must wait for the stream to + # be finished before returning + await conn.join() + return T.new(codecs = @["/tron/1.0.0"], handler = handle) + +proc networking(g: Game) {.async.} = + # Create our switch, similar to the GossipSub example and + # the Discovery examples combined + let + rdv = RendezVous.new() + switch = SwitchBuilder.new() + .withRng(newRng()) + .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ]) + .withTcpTransport() + .withYamux() + .withNoise() + .withRendezVous(rdv) + .build() + dm = DiscoveryManager() + gameProto = GameProto.new(g) + gossip = GossipSub.init( + switch = switch, + triggerSelf = false) + dm.add(RendezVousInterface.new(rdv)) + + switch.mount(gossip) + switch.mount(gameProto) + + gossip.subscribe( + "/tron/matchmaking", + proc (topic: string, data: seq[byte]) {.async.} = + # If we are still looking for an opponent, + # try to match anyone broadcasting it's address + if g.peerFound.finished or g.hasCandidate: return + g.hasCandidate = true + + try: + let + (peerId, multiAddress) = parseFullAddress(data).tryGet() + stream = await switch.dial(peerId, @[multiAddress], gameProto.codec) + + await stream.writeLp("ok") + if (await stream.readLp(10)) != "ok".toBytes: + g.hasCandidate = false + return + g.peerFound.complete(stream) + # We are "player 2" + swap(g.localPlayer, g.remotePlayer) + except CatchableError as exc: + discard + ) + + await switch.start() + defer: await switch.stop() + + # As explained in the last tutorial, we need a bootnode to be able + # to find peers. We could use any libp2p running rendezvous (or any + # node running tron). We will take it's MultiAddress from the command + # line parameters + if paramCount() > 0: + let (peerId, multiAddress) = paramStr(1).parseFullAddress().tryGet() + await switch.connect(peerId, @[multiAddress]) + else: + echo "No bootnode provided, listening on: ", switch.peerInfo.fullAddrs.tryGet() + + # Discover peers from the bootnode, and connect to them + dm.advertise(RdvNamespace("tron")) + let discoveryQuery = dm.request(RdvNamespace("tron")) + discoveryQuery.forEach: + try: + await switch.connect(peer[PeerId], peer.getAll(MultiAddress)) + except CatchableError as exc: + echo "Failed to dial a peer: ", exc.msg + + # We will try to publish our address multiple times, in case + # it takes time to establish connections with other GossipSub peers + var published = false + while not published: + await sleepAsync(500.milliseconds) + for fullAddr in switch.peerInfo.fullAddrs.tryGet(): + if (await gossip.publish("/tron/matchmaking", fullAddr.bytes)) == 0: + published = false + break + published = true + + discoveryQuery.stop() + + # We now wait for someone to connect to us (or for us to connect to someone) + let peerConn = await g.peerFound + defer: await peerConn.closeWithEof() + + await g.mainLoop(peerConn) + +let + game = Game.new() + netFut = networking(game) +nico.init("Status", "Tron") +nico.createWindow("Tron", mapSize * 4, mapSize * 4, 4, false) +nico.run(proc = discard, proc(dt: float32) = game.update(dt), proc = game.draw()) +waitFor(netFut.cancelAndWait()) + +## And that's it! If you want to run this code locally, the simplest way is to use the +## first node as a boot node for the second one. But you can also use any rendezvous node diff --git a/docs/tutorial_6_game/index.html b/docs/tutorial_6_game/index.html new file mode 100644 index 000000000..99a3145dc --- /dev/null +++ b/docs/tutorial_6_game/index.html @@ -0,0 +1,798 @@ + + + + + + + + + + + + + + + + + + + + + + + + Game - nim-libp2p + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Tron example

+

In this tutorial, we will create a video game based on libp2p, using +all of the features we talked about in the last tutorials.

+

We will: +- Discover peers using the Discovery Manager +- Use GossipSub to find a play mate +- Create a custom protocol to play with him

+

While this may look like a daunting project, it's less than 150 lines of code.

+

The game will be a simple Tron. We will use nico +as a game engine. (you need to run nimble install nico to have it available)

+

multiplay

+

We will start by importing our dependencies and creating our types +

import os
+import nico, chronos, stew/byteutils, stew/endians2
+import libp2p
+import libp2p/protocols/rendezvous
+import libp2p/discovery/rendezvousinterface
+import libp2p/discovery/discoverymngr
+
+const
+  directions = @[(K_UP, 0, -1), (K_LEFT, -1, 0), (K_DOWN, 0, 1), (K_RIGHT, 1, 0)]
+  mapSize = 32
+  tickPeriod = 0.2
+
+type
+  Player = ref object
+    x, y: int
+    currentDir, nextDir: int
+    lost: bool
+    color: int
+
+  Game = ref object
+    gameMap: array[mapSize * mapSize, int]
+    tickTime: float
+    localPlayer, remotePlayer: Player
+    peerFound: Future[Connection]
+    hasCandidate: bool
+    tickFinished: Future[int]
+
+  GameProto = ref object of LPProtocol
+
+proc new(_: type[Game]): Game =
+  # Default state of a game
+  result = Game(
+    tickTime: -3.0, # 3 seconds of "warm-up" time
+    localPlayer: Player(x: 4, y: 16, currentDir: 3, nextDir: 3, color: 8),
+    remotePlayer: Player(x: 27, y: 16, currentDir: 1, nextDir: 1, color: 12),
+    peerFound: newFuture[Connection]()
+  )
+  for pos in 0 .. result.gameMap.high:
+    if pos mod mapSize in [0, mapSize - 1] or pos div mapSize in [0, mapSize - 1]:
+      result.gameMap[pos] = 7
+

+

Game Logic

+

The networking during the game will work like this:

+
    +
  • Each player will have tickPeriod (0.1) seconds to choose + a direction that he wants to go to (default to current direction)
  • +
  • After tickPeriod, we will send our choosen direction to the peer, + and wait for his direction
  • +
  • Once we have both direction, we will "tick" the game, and restart the + loop, as long as both player are alive.
  • +
+

This is a very simplistic scheme, but creating proper networking for +video games is an art

+

The main drawback of this scheme is that the more ping you have with +the peer, the slower the game will run. Or invertedly, the less ping you +have, the faster it runs! +

proc update(g: Game, dt: float32) =
+  # Will be called at each frame of the game.
+  #
+  # Because both Nico and Chronos have a main loop,
+  # they must share the control of the main thread.
+  # This is a hacky way to make this happen
+  waitFor(sleepAsync(1.milliseconds))
+  # Don't do anything if we are still waiting for an opponent
+  if not(g.peerFound.finished()) or isNil(g.tickFinished): return
+  g.tickTime += dt
+
+  # Update the wanted direction, making sure we can't go backward
+  for i in 0 .. directions.high:
+    if i != (g.localPlayer.currentDir + 2 mod 4) and keyp(directions[i][0]):
+      g.localPlayer.nextDir = i
+
+  if g.tickTime > tickPeriod and not g.tickFinished.finished():
+    # We choosen our next direction, let the networking know
+    g.localPlayer.currentDir = g.localPlayer.nextDir
+    g.tickFinished.complete(g.localPlayer.currentDir)
+
+proc tick(g: Game, p: Player) =
+  # Move player and check if he lost
+  p.x += directions[p.currentDir][1]
+  p.y += directions[p.currentDir][2]
+  if g.gameMap[p.y * mapSize + p.x] != 0: p.lost = true
+  g.gameMap[p.y * mapSize + p.x] = p.color
+
+proc mainLoop(g: Game, peer: Connection) {.async.} =
+  while not (g.localPlayer.lost or g.remotePlayer.lost):
+    if g.tickTime > 0.0:
+      g.tickTime = 0
+    g.tickFinished = newFuture[int]()
+
+    # Wait for a choosen direction
+    let dir = await g.tickFinished
+    # Send it
+    await peer.writeLp(toBytes(uint32(dir)))
+
+    # Get the one from the peer
+    g.remotePlayer.currentDir = int uint32.fromBytes(await peer.readLp(8))
+    # Tick the players & restart
+    g.tick(g.remotePlayer)
+    g.tick(g.localPlayer)
+
+We'll draw the map & put some texts when necessary: +
proc draw(g: Game) =
+  for pos, color in g.gameMap:
+    setColor(color)
+    boxFill(pos mod 32 * 4, pos div 32 * 4, 4, 4)
+  let text = if not(g.peerFound.finished()): "Matchmaking.."
+             elif g.tickTime < -1.5: "Welcome to Etron"
+             elif g.tickTime < 0.0: "- " & $(int(abs(g.tickTime) / 0.5) + 1) & " -"
+             elif g.remotePlayer.lost and g.localPlayer.lost: "DEUCE"
+             elif g.localPlayer.lost: "YOU LOOSE"
+             elif g.remotePlayer.lost: "YOU WON"
+             else: ""
+  printc(text, screenWidth div 2, screenHeight div 2)
+

+

Matchmaking

+

To find an opponent, we will broadcast our address on a +GossipSub topic, and wait for someone to connect to us. +We will also listen to that topic, and connect to anyone +broadcasting his address.

+

If we are looking for a game, we'll send ok to let the +peer know that we are available, check that he is also available, +and launch the game. +

proc new(T: typedesc[GameProto], g: Game): T =
+  proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
+    defer: await conn.closeWithEof()
+    if g.peerFound.finished or g.hasCandidate:
+      await conn.close()
+      return
+    g.hasCandidate = true
+    await conn.writeLp("ok")
+    if "ok" != string.fromBytes(await conn.readLp(1024)):
+      g.hasCandidate = false
+      return
+    g.peerFound.complete(conn)
+    # The handler of a protocol must wait for the stream to
+    # be finished before returning
+    await conn.join()
+  return T.new(codecs = @["/tron/1.0.0"], handler = handle)
+
+proc networking(g: Game) {.async.} =
+  # Create our switch, similar to the GossipSub example and
+  # the Discovery examples combined
+  let
+    rdv = RendezVous.new()
+    switch = SwitchBuilder.new()
+      .withRng(newRng())
+      .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ])
+      .withTcpTransport()
+      .withYamux()
+      .withNoise()
+      .withRendezVous(rdv)
+      .build()
+    dm = DiscoveryManager()
+    gameProto = GameProto.new(g)
+    gossip = GossipSub.init(
+      switch = switch,
+      triggerSelf = false)
+  dm.add(RendezVousInterface.new(rdv))
+
+  switch.mount(gossip)
+  switch.mount(gameProto)
+
+  gossip.subscribe(
+    "/tron/matchmaking",
+    proc (topic: string, data: seq[byte]) {.async.} =
+      # If we are still looking for an opponent,
+      # try to match anyone broadcasting it's address
+      if g.peerFound.finished or g.hasCandidate: return
+      g.hasCandidate = true
+
+      try:
+        let
+          (peerId, multiAddress) = parseFullAddress(data).tryGet()
+          stream = await switch.dial(peerId, @[multiAddress], gameProto.codec)
+
+        await stream.writeLp("ok")
+        if (await stream.readLp(10)) != "ok".toBytes:
+          g.hasCandidate = false
+          return
+        g.peerFound.complete(stream)
+        # We are "player 2"
+        swap(g.localPlayer, g.remotePlayer)
+      except CatchableError as exc:
+        discard
+  )
+
+  await switch.start()
+  defer: await switch.stop()
+
+  # As explained in the last tutorial, we need a bootnode to be able
+  # to find peers. We could use any libp2p running rendezvous (or any
+  # node running tron). We will take it's MultiAddress from the command
+  # line parameters
+  if paramCount() > 0:
+    let (peerId, multiAddress) = paramStr(1).parseFullAddress().tryGet()
+    await switch.connect(peerId, @[multiAddress])
+  else:
+    echo "No bootnode provided, listening on: ", switch.peerInfo.fullAddrs.tryGet()
+
+  # Discover peers from the bootnode, and connect to them
+  dm.advertise(RdvNamespace("tron"))
+  let discoveryQuery = dm.request(RdvNamespace("tron"))
+  discoveryQuery.forEach:
+    try:
+      await switch.connect(peer[PeerId], peer.getAll(MultiAddress))
+    except CatchableError as exc:
+      echo "Failed to dial a peer: ", exc.msg
+
+  # We will try to publish our address multiple times, in case
+  # it takes time to establish connections with other GossipSub peers
+  var published = false
+  while not published:
+    await sleepAsync(500.milliseconds)
+    for fullAddr in switch.peerInfo.fullAddrs.tryGet():
+      if (await gossip.publish("/tron/matchmaking", fullAddr.bytes)) == 0:
+        published = false
+        break
+      published = true
+
+  discoveryQuery.stop()
+
+  # We now wait for someone to connect to us (or for us to connect to someone)
+  let peerConn = await g.peerFound
+  defer: await peerConn.closeWithEof()
+
+  await g.mainLoop(peerConn)
+
+let
+  game = Game.new()
+  netFut = networking(game)
+nico.init("Status", "Tron")
+nico.createWindow("Tron", mapSize * 4, mapSize * 4, 4, false)
+nico.run(proc = discard, proc(dt: float32) = game.update(dt), proc = game.draw())
+waitFor(netFut.cancelAndWait())
+
+And that's it! If you want to run this code locally, the simplest way is to use the +first node as a boot node for the second one. But you can also use any rendezvous node

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file