.author{grid-column:1;grid-row:3;align-content:flex-start}.Metadata_root__oCstk>.description,.Metadata_root__oCstk>.pills-list,.Metadata_root__oCstk>.stats,.Metadata_root__oCstk>.title{grid-column:1/span 2}.Metadata_root__oCstk>.title{margin-bottom:8px}.Metadata_root__oCstk>.stats{margin-bottom:12px}.Metadata_root__oCstk>.description{margin-top:12px}.Metadata_root__oCstk>.pills-list{margin-block-start:20px;margin-block-end:18px}.Metadata_root__oCstk>.actions{display:none}@media screen and (max-width:928px){.Metadata_root__oCstk{margin-block:8px 20px}.Metadata_root__oCstk>.stats{margin-bottom:16px}.Metadata_root__oCstk>.description{margin-top:0}.Metadata_root__oCstk>.author{grid-column:2;grid-row:5;margin-block-start:12px;margin-block-end:6px;justify-content:end}.Metadata_root__oCstk>.actions{display:flex;margin-top:16px;grid-column:1;grid-row:5}.Metadata_root__oCstk>.pills-list{margin-block-start:16px;margin-block-end:0}}.Metadata_root__oCstk .metadata-recs{grid-column:1/-1}.Heading_heading__3MAvZ{color:var(--blue-gray-900)}.Heading_h1__3k7S2{font-size:32px;font-weight:700}.Heading_h2__f9yvs{font-size:28px;font-weight:600}.Heading_h3__f1djd{font-size:24px}.Heading_h4__7tfLE{font-size:20px}.Heading_h5__jVM0l{font-size:16px;font-weight:400}.Heading_h6__uUTrd{font-size:14px;font-weight:400}.Title_root__svkHQ{color:var(--blue-gray-900);font-size:clamp(26px,1vw + 1rem,28px);font-weight:600;line-height:1.25;min-width:0;word-break:break-word}.Stats_root__p_BoZ{flex-wrap:wrap;display:flex;align-items:center;-moz-column-gap:6px;column-gap:6px;color:var(--blue-gray-600);white-space:nowrap;font-size:16px}.Stats_leftContent__588PR,.Stats_rightContent__8d0AF{display:flex;gap:6px}.Stats_root__p_BoZ span{font-size:16px}.Stats_root__p_BoZ .Stats_aiTag__zTzW8{margin-left:10px}@media screen and (max-width:928px){.Stats_root__p_BoZ span{line-height:1.5}.Stats_root__p_BoZ.Stats_extendedMetadata__wb62p .Stats_leftContent__588PR{width:100%}.Stats_root__p_BoZ .Stats_aiTag__zTzW8{margin:8px 0 0}.Stats_root__p_BoZ.Stats_extendedMetadata__wb62p .Stats_formatTypesBullet__xDv0L{display:none}}.Likes_root__WVQ1_{cursor:pointer;transition:color .2s ease-in-out;border-radius:4px}.Likes_root__WVQ1_:hover{color:var(--blue-gray-700)}.Tooltip_root__7FS0Y{background:var(--midnight-green-dark);border-radius:4px;box-shadow:0 .5px 5px rgba(0,0,0,.04),0 4px 11px rgba(0,0,0,.2);color:var(--white);font-weight:400;font-size:12px;line-height:15px;padding:6px 8px;opacity:0;visibility:hidden;animation:Tooltip_show__qVG5k .2s ease-in-out forwards;z-index:var(--popup-index)}.Tooltip_triggerWrapper___S2HG{flex-shrink:0;position:relative;align-items:center;justify-content:center}@keyframes Tooltip_show__qVG5k{to{opacity:1;visibility:visible}}.Tooltip_large__J4Fvl{padding:16px;display:flex;flex-direction:column;background:#fff;color:var(--black)}.AITag_tag__Xx37c{padding:0 12px;height:25px;border-radius:16px;background-color:#f0f2f9;color:#16171b;display:inline-flex;gap:8px;font-size:14px;line-height:1.5;font-weight:600}.AITag_tooltipContent__7JZR_{width:332px}.Author_root___6Bx5{--link-color:var(--blue-gray-800);position:relative;display:flex;align-items:center;gap:8px}.Author_link___lVxw{z-index:1;color:var(--link-color);font-weight:600;display:block}.Author_link___lVxw:before{content:"";position:absolute;inset:0}.Author_follow__Lw4TS{z-index:1}@media screen and (max-width:928px){.Author_link___lVxw:hover{color:var(--blue-gray-800)}}.Avatar_root__GNWHY{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;background-color:var(--white);color:var(--blue-gray-300);border-radius:50%;font-size:16px;font-weight:600;text-align:center;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow:hidden}.Avatar_initials__EJfVt{color:var(--white);transition:background-color .2s ease-in-out}.Avatar_initials__EJfVt,.Avatar_initials__EJfVt:hover{background-color:var(--blue-gray-600)}.Avatar_image__Bbtll{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.FollowButton_root__FxpBi{display:inline-flex;background-color:transparent;border:1px solid transparent;border-radius:4px;font-size:12px;padding:1px 6px;transition:background-color .2s ease-in-out,border-color .2s ease-in-out;cursor:pointer}.FollowButton_following__xKCww{border-color:#bf5905;color:#bf5905}.FollowButton_following__xKCww:hover{background-color:#ffead7;border-color:rgba(191,89,5,.5)}.FollowButton_follow__d_6u5{border-color:var(--celadon-blue-dark);color:var(--celadon-blue-dark)}.FollowButton_follow__d_6u5:hover{background-color:#eaf7ff;border-color:rgba(2,126,176,.5)}@media screen and (max-width:928px){.FollowButton_root__FxpBi{display:none}}.Description_root__kt4uq{--line-height:26px;position:relative}.Description_root__kt4uq.Description_clamped__PaV_1{padding-bottom:25px}.Description_root__kt4uq.Description_clamped__PaV_1 .Description_wrapper__hYE9_{mask-image:linear-gradient(to bottom,var(--white),transparent);-webkit-mask-image:linear-gradient(to bottom,var(--white),transparent)}.Description_wrapper__hYE9_{min-height:var(--line-height);display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.Description_noClamp__1z7c5,.Description_wrapper__hYE9_.Description_expanded__lRamt{-webkit-line-clamp:unset;-webkit-mask-image:none;mask-image:none}.Description_wrapper__hYE9_.Description_expanded__lRamt{height:auto}.Description_wrapper__hYE9_ p{color:var(--blue-gray-600);font-size:18px;line-height:var(--line-height);white-space:pre-wrap;word-break:break-word}.Description_more__ChrRK{position:absolute;padding:0;bottom:0;height:26px}.Description_less__BvWbY{padding:0}.Description_hidden__a9QZJ{display:none}@media screen and (max-width:928px){.Description_more__ChrRK{right:0;background-color:#fff}.Description_root__kt4uq.Description_clamped__PaV_1{padding-bottom:0}.Description_less__BvWbY,.Description_more__ChrRK{height:var(--line-height)}}.PillsList_root__2EydN{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.Pill_root__IqOYH{--bg:color-mix(in srgb,var(--celadon-blue-dark),90% transparent);--color:var(--blue-gray-800);height:40px;display:inline-flex;align-items:center;gap:6px;background-color:var(--bg);border-radius:100vmax;color:var(--color);font-size:16px;font-weight:600;padding-inline:16px;transition:color .2s ease-in-out,background-color .2s ease-in-out,filter .2s ease-in-out;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;text-decoration:none;white-space:nowrap}.Pill_root__IqOYH:not(.Pill_selected__VPtHm):hover{filter:brightness(.6)}.Pill_root__IqOYH.Pill_selected__VPtHm{--bg:var(--blue-gray-900);--color:var(--white)}@media screen and (max-width:520px){.Pill_root__IqOYH{height:28px;padding-inline:12px;gap:4px;font-size:12px}.Pill_icon__xE_Cg{--size:18px!important}}.Actions_root__00yIC{display:flex;align-items:baseline;gap:6px}.ReadingModeTooltip_root__laf5h{width:277px}.ToggleButtonGroup_root__vtGE_{display:inline-flex;gap:4px;padding:5px 8px;border-radius:4px;background-color:var(--white);border:1px solid var(--blue-gray-200)}.ToggleButton_root__jGx6U{border-radius:4px;background-color:var(--white);color:var(--blue-gray-700);border:none;width:44px;height:34px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}.ToggleButton_selected__51g6d{background-color:var(--blue-gray-200)}.SaveLoggedIn_icon__lk74r{color:var(--blue-gray-700)}.PopoverMenuContent_root__MsRtR{background:var(--white);box-shadow:0 .5px 5px rgba(0,0,0,.04),0 4px 11px rgba(0,0,0,.2);border-radius:4px;opacity:0;visibility:hidden;transition:opacity .2s ease-in-out,visibility .2s ease-in-out;z-index:1000;overflow-y:auto}.PopoverMenuContent_root__MsRtR.PopoverMenuContent_visible__O86I_{opacity:1;visibility:visible;transition-delay:0s}.PopoverMenuItem_item__iazpP{width:100%;display:flex;align-items:center;background-color:transparent;color:inherit;cursor:pointer;font-size:inherit;line-height:24px;padding:12px 16px;white-space:nowrap}.PopoverMenuItem_item__iazpP.PopoverMenuItem_highlight__inbqK,.PopoverMenuItem_item__iazpP:hover{background-color:var(--blue-gray-100)}.PopoverMenuSeparator_separator__UpSGw{width:calc(100% - 32px);height:1px;background-color:var(--blue-gray-300);margin:8px 16px}.SavePopover_popover__mZhIY{width:290px;max-height:220px;color:var(--blue-gray-800);font-weight:400;padding:6px 8px;font-size:12px;line-height:15px;overflow:hidden}.SavePopover_saveToNewList__MVBSu{display:flex;align-items:center;gap:6px;color:var(--midnight-green-dark);font-weight:600;font-size:18px;line-height:24px;padding:10px;width:100%}.SavePopover_addIcon__aJLJ3{color:var(--blue-gray-800)}.SavePopover_horizontalSeparator__bsI6Y{border:none;border-bottom:1px solid var(--blue-gray-300);margin:8px 0}.SavePopover_listsContainer__tGGtp{height:100%;max-height:144px;display:flex;flex-direction:column;align-items:center;overflow-y:auto}.SavePopover_saveToList__Fsfl9{color:var(--blue-gray-800);font-weight:400;font-size:16px;justify-content:space-between;line-height:24px;padding:12px 14px 12px 16px;width:100%}.SavePopover_saveToList__Fsfl9 span:not(.SavePopover_clampLines__Blxtm){display:flex;flex-direction:row;gap:35px;justify-content:space-between}.SavePopover_saveToList__Fsfl9 span.SavePopover_clampLines__Blxtm{display:inline-block;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap;width:180px}.SavePopover_listIcon__OVsXv{color:var(--blue-gray-900)}.SavePopover_noSavedLists__0Mh_H{color:var(--blue-gray-600);font-weight:400;font-size:16px;line-height:24px;padding:12px 16px;text-align:center}.SaveOptionsDrawer_drawerTrigger__Gb7nK{box-shadow:none;border:none;border-radius:4px;position:relative;z-index:7}.SaveOptionsDrawer_drawerTrigger__Gb7nK:active,.SaveOptionsDrawer_drawerTrigger__Gb7nK:active:focus,.SaveOptionsDrawer_drawerTrigger__Gb7nK:hover{border:none;box-shadow:none;background-color:transparent}.SaveOptionsDrawer_drawerTrigger__Gb7nK:focus{background:transparent}.SaveOptionsDrawerContent_drawerContent__J5JTL{margin:16px}.SaveOptionsDrawerContent_separator__UW5Rz{margin:8px 0}.SaveOptionsDrawerContent_itemsContainer__tCXw5{display:flex;flex-direction:column;margin-top:0;max-height:144px;overflow-y:auto}.SaveOptionsDrawerContent_drawerActionSecondary__7cq8j{font-weight:400;line-height:24px;padding:0 8px;margin:8px 0;width:100%}.SaveOptionsDrawerContent_drawerActionSecondary__7cq8j span{color:var(--blue-gray-800);justify-content:space-between;gap:30px}.SaveOptionsDrawerContent_drawerActionPrimary____QuR span{gap:14px}.SaveOptionsDrawerContent_drawerActionPrimary____QuR{color:var(--blue-gray-800);display:flex;font-weight:400;line-height:24px;align-items:center;justify-content:flex-start;padding:8px 4px;margin:14px 0}.SaveOptionsDrawerContent_drawerActionPrimary____QuR.SaveOptionsDrawerContent_newListButton__VzhfD{font-size:16px;font-weight:600}.SaveOptionsDrawerContent_addIcon__Wb2cq{color:var(--blue-gray-800)}.SaveOptionsDrawerContent_drawerActionSecondary__7cq8j span.SaveOptionsDrawerContent_clampLines__zfkfI{display:inline-block;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap;width:calc(100vw - 115px)}.SaveOptionsDrawerContent_listIcon__5dcfC{color:var(--blue-gray-900)}.SaveOptionsDrawerContent_noSavedLists__cpUBY{color:var(--blue-gray-600);font-weight:400;font-size:16px;line-height:24px;padding:12px 16px;text-align:center}.Separator_root__70Ime{--orientationMargin:0;background-color:var(--blue-gray-200);flex-shrink:0}.Separator_horizontal__czVEa{width:calc(100% - var(--orientationMargin) * 2);height:1px}.Separator_vertical__JYCCK{width:1px;height:calc(100% - var(--orientationMargin) * 2)}.SaveLoggedOut_icon__ny9X2{color:var(--blue-gray-700)}.Dropdown_root__Z78h8{display:inline-block;position:relative;color:inherit}.DropdownTrigger_trigger__SzsBj{display:flex;align-items:center;justify-content:center;background:transparent;border:none;font-size:inherit;padding:0;margin:0;cursor:pointer}.DropdownTrigger_trigger__SzsBj:active,.DropdownTrigger_trigger__SzsBj:focus,.DropdownTrigger_trigger__SzsBj:hover{background:transparent}.DropdownContent_content__3daFs{position:absolute;display:flex;flex-direction:column;align-items:flex-start;padding:8px 0;background-color:#fff;box-shadow:0 .5px 5px rgba(0,0,0,.039),0 3.75px 11px rgba(0,0,0,.19);border-radius:4px;color:var(--blue-gray-800);opacity:0;visibility:hidden;transition:transform .15s,opacity .15s,visibility 0s linear .15s;transform:scale(.95);z-index:var(--dropdown-index)}.DropdownContent_bottom-left__gioqM{top:calc(100% + 6px);left:0;transform-origin:top left}.DropdownContent_bottom-right__QJ94h{top:calc(100% + 6px);right:0;transform-origin:top right}.DropdownContent_top-left__O3Ryp{bottom:calc(100% + 6px);left:0;transform-origin:bottom left}.DropdownContent_top-right___Qe45{bottom:calc(100% + 6px);right:0;transform-origin:bottom right}.DropdownContent_content__3daFs.DropdownContent_open__6VoiP{visibility:visible;opacity:1;transform:none;transition-delay:0s}.DropdownItem_item__Sv0GT{width:100%;display:flex;align-items:center;background-color:transparent;color:inherit;cursor:pointer;font-size:inherit;line-height:24px;padding:12px 16px;white-space:nowrap}.DropdownItem_highlight__jO3zg,.DropdownItem_item__Sv0GT:hover{background-color:var(--blue-gray-100)}.DropdownSeparator_separator__EN82n{width:calc(100% - 32px);height:1px;background-color:var(--blue-gray-300);margin:8px 16px}.MoreDropdownButton_moreOptionsDropdown__GK_Uw{display:flex;align-items:center;justify-content:center;width:32px;height:32px}.MoreDropdownButton_item__t4HmI{gap:12px}.MoreDropdownButton_moreOptionsIcon__TpJLA{color:var(--blue-gray-700)}.MoreDropdownButton_icon__DxfY4{color:var(--blue-gray-800)}.MoreDropdownButton_tooltip__az4od{white-space:nowrap}.MetadataToolbar_root__c03ao{--shadow-opacity:0;display:grid;height:var(--metadata-toolbar-height);background-color:var(--white);z-index:var(--header-index)}.MetadataToolbar_wrapper__r7XEc{position:relative;display:grid;grid-template-columns:minmax(0,1fr) max-content 1fr;align-items:center;padding-inline-end:20px;padding-block:16px}.MetadataToolbar_underline__QQn0C{grid-column:1/-1;grid-row:2;height:1px;position:absolute;bottom:0;right:-20px;left:-30vw;background-color:var(--blue-gray-200);box-shadow:0 2px 4px 0 rgba(0 0 0/var(--shadow-opacity));animation:MetadataToolbar_reveal-shadow__5yBxP linear both;animation-timeline:scroll(block);animation-range:150px 450px}.MetadataToolbar_verticalMode__Bh759{position:sticky}.MetadataToolbar_title__jfTWv{font-size:18px;font-weight:600}.MetadataToolbar_actions__FB33C{width:-moz-max-content;width:max-content;justify-self:flex-end;display:flex;grid-column-end:-1}.MetadataToolbar_pageNumber__i6Bhj{display:inline-flex;align-items:center;margin-inline:1em;height:44px}.MetadataToolbar_isInReadingModeVariant__XMDcr{align-items:flex-start}.MetadataToolbar_isInReadingModeVariant__XMDcr .MetadataToolbar_downloadButton__ncS7o>button{padding:0 15px}@media screen and (max-width:928px){.MetadataToolbar_root__c03ao{display:none}}@media screen and (min-width:929px) and (max-width:1249px){.MetadataToolbar_isInReadingModeVariant__XMDcr .MetadataToolbar_pageNumber__i6Bhj{display:none}}@keyframes MetadataToolbar_reveal-shadow__5yBxP{to{--shadow-opacity:0.122}}.DownloadButton_root__adY00{margin-left:auto;display:inline-grid;gap:6px;justify-items:center;flex-shrink:0}.DownloadButton_savedStyling__k18od{font-weight:600;font-size:18px}.DownloadMultipleFormatDrawer_root__CWFxX{width:100%;padding:0 24px}.DownloadMultipleFormatDrawer_drawerHeading__8LnFw{margin:16px 0}.DownloadMultipleFormatDrawer_drawerContent__y815X{width:100%;padding:24px 0}.DownloadMultipleFormatDrawer_drawerRadioButtons__I_lQ4 label{margin-bottom:20px}.DownloadMultipleFormatDrawer_drawerRadioButtons__I_lQ4{margin-bottom:4px}.Fieldset_root__L2NQU{display:grid;padding:0;border:0}.Fieldset_root__L2NQU legend{display:none}.DownloadMultipleFormatPopover_popoverContent__IJudF{min-width:185px}.FadeInOut_root__v7Efq{position:relative;min-width:0;background-color:var(--snow-gray);padding:20px}.FadeInOut_root__v7Efq.FadeInOut_isInfographic__PdX2K{background-color:unset;padding-inline-start:0;padding-inline-end:20px}@media (max-width:928px){.FadeInOut_root__v7Efq,.FadeInOut_root__v7Efq.FadeInOut_isInfographic__PdX2K{background-color:unset;padding-inline:2px}}.FadeInOut_in__9dYWz{animation:FadeInOut_fadeIn__14JD0 .5s}.FadeInOut_out___eDZk{animation:FadeInOut_fadeOut__ILQaD .4s}@keyframes FadeInOut_fadeIn__14JD0{0%{opacity:0}to{opacity:1}}@keyframes FadeInOut_fadeOut__ILQaD{0%{opacity:1}to{opacity:0}}.VerticalSlideOverlayed_root__9Thd4{position:relative}.VerticalSlideOverlayed_root__9Thd4 .vertical-slide-image{position:absolute;inset:0}.VerticalSlideOverlayed_active___p5f2 .vertical-slide-image{opacity:.4}.VerticalSlideOverlayed_active___p5f2{background-color:#000}.VerticalSlideImage_image__VtE4p{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;box-shadow:0 0 0 1px var(--blue-gray-200);border-radius:8px;transition:opacity .3s cubic-bezier(.2,0,0,1);opacity:.9;overflow:clip;background-color:var(--blue-gray-600);animation-name:VerticalSlideImage_pulse__OPBSn;animation-direction:alternate;animation-duration:1s;animation-iteration-count:infinite;animation-timing-function:ease-in-out}.VerticalSlideImage_image__VtE4p.VerticalSlideImage_isLoaded__r4xAa{opacity:1;animation:none;background-color:unset}@keyframes VerticalSlideImage_pulse__OPBSn{0%{opacity:.09}to{opacity:.15}}.SlideScrollDetector_root__AIK38{position:relative;height:100%;width:100%;pointer-events:none}.SlideScrollDetector_detector__rhkyk{position:absolute;top:0;left:0;height:1px;width:100%}.SlideScrollDetector_detector1__NmF8s{top:25%}.SlideScrollDetector_detector2__32XWr{top:75%}.SlideActions_root__fB9Q4{position:absolute;right:0;top:0;display:grid;grid-template-columns:40px;grid-template-rows:40px 40px;grid-column-gap:12px;-moz-column-gap:12px;column-gap:12px;grid-row-gap:12px;row-gap:12px;padding:12px;justify-content:flex-end;align-items:center;text-align:end}.SlideActions_active__aD_e1{grid-template-columns:max-content 40px}.SlideActions_button__o8UXk{background-color:var(--blue-gray-100);color:var(--blue-gray-700)}.VerticalSlide_infographic__ij1FA,.VerticalSlide_root__jU_9r{position:relative}.VerticalSlide_root__jU_9r .vertical-slide-image{position:absolute;inset:0}.VerticalSlide_infographic__ij1FA .vertical-slide-image{position:unset}.VerticalPlayer_root__K8_YS{position:relative;display:grid;grid-template-columns:minmax(0,1fr);grid-gap:24px;gap:24px}@media screen and (max-width:928px){.VerticalPlayer_root__K8_YS{gap:8px}}.FreestarVideoAd_root__KDWgl{min-width:0;flex-shrink:0;aspect-ratio:16/9}.VerticalInterstitialAdWrapper_root__LxQh8{container-type:inline-size;position:relative;display:grid;min-height:280px;overflow-x:clip}.VerticalInterstitialAdWrapper_root__LxQh8:has(.interstitial-ad-container.has-fetched):not(:has(.interstitial-ad-container.has-fetched .freestar-ad-container:not(.unfilled))){display:none}.VerticalInterstitialAdWrapper_root__LxQh8:has(div.interstitial-ad-container.has-fetched div.freestar-ad-container.filled div),.VerticalInterstitialAdWrapper_root__LxQh8:has(div.interstitial-ad-container.has-fetched div.freestar-ad-container.unfilled iframe){display:block!important}.VerticalInterstitial_root__Dunl7{display:none}@media (max-width:520px){.VerticalInterstitial_root__Dunl7:not(.VerticalInterstitial_inVariant__xB9lL){display:block}}@container (max-width: 480px){.VerticalInterstitial_root__Dunl7.VerticalInterstitial_inVariant__xB9lL{display:block}}.FreestarAdContainer_root__qPPC_{position:relative;display:grid;place-content:center}.FreestarAdContainer_root__qPPC_.FreestarAdContainer_withFallback__A4lgm{aspect-ratio:var(--fallback-aspect-ratio)}.FreestarAdContainer_fallback__WreT9{position:absolute;inset:0;grid-template-columns:unset;place-content:center}.AdFallback_root__uAXsl{display:grid;justify-items:center;grid-template-columns:1fr;z-index:0}.MultipleIncontentSmall_root__x58Hs{display:none;grid-template-columns:repeat(auto-fit,minmax(max-content,300px));place-content:center;gap:16px}@media (min-width:521px){.MultipleIncontentSmall_root__x58Hs:not(.MultipleIncontentSmall_inVariant__uf1S8){display:grid}}@container (max-width: 616px){.MultipleIncontentSmall_root__x58Hs:not(.MultipleIncontentSmall_inVariant__uf1S8) .freestar-ad-container:nth-of-type(2){display:none}}@container (min-width: 616px) and (max-width: 688px){.MultipleIncontentSmall_root__x58Hs.MultipleIncontentSmall_inVariant__uf1S8{display:grid}}.MultipleIncontentWide_root__4WD8U{display:none;place-content:center}@container (min-width: 480px) and (max-width: 616px){.MultipleIncontentWide_root__4WD8U{display:grid}}.MultipleIncontentLarge_root__pGIAn{display:none;grid-template-columns:repeat(auto-fit,minmax(max-content,336px));place-content:center;gap:16px}@container (min-width: 688px){.MultipleIncontentLarge_root__pGIAn{display:grid}}.SlideRecs_root__likA5{--card-hover-background:var(--blue-gray-200);display:flex;flex-direction:column;gap:20px}.SlideRecs_root__likA5 h2{font-size:24px;font-weight:600}@media (max-width:1050px){.SlideRecs_root__likA5{display:none}}.SlideRecs_root__likA5 .SlideRecs_cards__Lbxtt{display:grid;grid-gap:16px;gap:16px}.SlideRecs_root__likA5 .SlideRecs_card__txc2D{position:relative;grid-template-columns:180px 1fr;grid-template-rows:auto;gap:16px}.SlideRecs_root__likA5 .slideshow-thumbnail{box-shadow:0 0 0 1px var(--blue-gray-200)}.SlideRecs_root__likA5 .slideshow-card-content{padding-block:0;padding-inline-end:36px;gap:8px}.SlideRecs_root__likA5 .slideshow-title{line-height:1;margin-block-end:0}.SlideRecs_root__likA5 .SlideRecs_wrapper__21j_w{display:flex;gap:8px;align-items:center}.SlideRecs_root__likA5 .SlideRecs_wrapper__21j_w a,.SlideRecs_root__likA5 .SlideRecs_wrapper__21j_w span{font-size:12px}.SlideRecs_root__likA5 .SlideRecs_save__RR8dD{position:absolute;top:8px;right:8px}.SlideRecs_root__likA5 .SlideRecs_author__zlhWO{color:var(--blue-gray-700);font-weight:600;text-decoration:none;z-index:2}.SlideRecs_root__likA5 .SlideRecs_author__zlhWO:first-letter{text-transform:uppercase}.SlideRecs_root__likA5 .SlideRecs_card__txc2D .slideshow-title{font-size:16px}.SlideRecs_root__likA5 .SlideRecs_description__0bGsx{color:var(--blue-gray-700);font-size:14px;display:-webkit-box;overflow:hidden;line-clamp:1;-webkit-line-clamp:1;-webkit-box-orient:vertical}.SlideRecs_root__likA5 .SlideRecs_dot__Db7HR{font-size:16px}.SlideRecs_root__likA5 .SlideRecs_tags__RCA1q{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.SlideRecs_root__likA5 .SlideRecs_tags__RCA1q span{display:grid;place-content:center;height:20px;background-color:var(--alice-blue-600);border-radius:100vmax;color:var(--blue-gray-700);font-size:11px;font-weight:600;padding-inline:12px;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap;text-transform:capitalize;z-index:2}.RecWithPopover_root__iNXfW{width:330px;display:flex;flex-direction:column;gap:8px}.RecWithPopover_root__iNXfW>*{line-height:18px}.RecWithPopover_root__iNXfW h3{font-size:16px;font-weight:600}.RecWithPopover_root__iNXfW p{flex:1 1;color:var(--blue-gray-600);font-size:14px;max-width:100%;display:-webkit-box;line-clamp:5;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow-y:hidden}.RecWithPopover_hasSiblingsWithPopover__dlaxN{border-radius:4px;padding:12px}.RecWithPopover_hasPopover__G8_rr:hover:not(:has(:is(.save:hover,.author:hover))){background-color:var(--card-hover-background)}.SlideshowCard_root__pD8t4{position:relative;display:grid;grid-template-rows:max-content minmax(0,1fr);grid-template-columns:minmax(0,1fr);align-content:flex-start;color:var(--blue-gray-600)}.SlideshowCard_root__pD8t4:hover .SlideshowCard_thumb__86aJk{scale:1.02}.SlideshowCard_content__xh7kV{display:flex;flex-direction:column;-moz-column-gap:8px;column-gap:8px;padding:16px 0}.SlideshowCardLink_root__p8KI7{position:absolute;inset:0;z-index:1;margin:4px}.Thumbnail_root__qLW0K{--ease:cubic-bezier(0.2,0,0,1);position:relative;background-color:var(--blue-gray-100);border:1px solid var(--blue-gray-100);border-radius:8px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);scale:1;transition:scale .2s var(--ease);overflow:hidden}.Thumbnail_thumb__UXO3a{--reveal-delay:calc(30ms * var(--index));position:absolute;inset:0;width:100%;height:100%;opacity:0;transition:opacity .3s var(--ease);transition-delay:var(--reveal-delay)}.Thumbnail_loaded__XOJ5p{opacity:1}.Thumbnail_blur__opK6A{filter:blur(8px)}.Thumbnail_cover__1zsIi{-o-object-fit:cover;object-fit:cover}.Thumbnail_contain__K6M0d{-o-object-fit:contain;object-fit:contain}.SlideshowTitle_root__2VccW{display:-webkit-box;color:var(--blue-gray-900);font-size:18px;font-weight:600;line-height:1.2;margin-bottom:8px;white-space:break-spaces;word-break:break-word;-webkit-box-orient:vertical}.RecSaveButton_root__0CS9m{grid-area:d}.RecSaveButton_icon__btwCp{fill:currentColor;z-index:2}@media (max-width:928px){.RecSaveButton_root__0CS9m{display:none}}.SlideshowAuthor_root__IkT1_{color:var(--celadon-blue);font-weight:600;text-decoration:underline;-webkit-text-decoration-color:transparent;text-decoration-color:transparent;text-decoration-thickness:1.5px;text-underline-offset:2px;transition:-webkit-text-decoration-color .2s ease-out;transition:text-decoration-color .2s ease-out;transition:text-decoration-color .2s ease-out,-webkit-text-decoration-color .2s ease-out;z-index:2}.SlideshowAuthor_root__IkT1_:hover{-webkit-text-decoration-color:var(--celadon-blue);text-decoration-color:var(--celadon-blue)}.CountTag_root__y1hE1,.SplitDot_root__lTZDc{color:var(--blue-gray-600);font-weight:400}.SlideshowStats_root__EQOR1{display:flex;align-items:center;gap:6px}.SlideshowStats_text___WD7l{color:var(--blue-gray-600)}.BelowReaderAd_root__NKeGg{margin-top:16px;margin-bottom:60px}.BelowReaderAd_root__NKeGg.BelowReaderAd_desktop__7_JN7{display:block;justify-items:flex-start}.BelowReaderAd_root__NKeGg.BelowReaderAd_mobile__08T3d{display:none}.BelowReaderAd_root__NKeGg .fallback-ad{justify-self:flex-start}@media screen and (max-width:928px){.BelowReaderAd_root__NKeGg.BelowReaderAd_desktop__7_JN7{display:none}.BelowReaderAd_root__NKeGg.BelowReaderAd_mobile__08T3d{display:block;justify-items:center}}.Sidebar_root__1BbNu{width:var(--sidebar-size);max-height:100dvh;overflow:clip scroll;position:sticky;top:var(--metadata-toolbar-offset,0);display:flex;flex-direction:column;padding-inline-start:28px;padding-block-start:32px;padding-block-end:430px}.Sidebar_root__1BbNu.Sidebar_withSidebarAds__0w0dT{max-height:unset;overflow:unset;position:static;top:unset;display:grid;grid-template-rows:repeat(var(--slots),1fr);padding-inline:28px}@media (max-width:1050px){.Sidebar_root__1BbNu,.Sidebar_root__1BbNu.Sidebar_withSidebarAds__0w0dT{display:none}}.AboveRecsAd_root__iTmTR{min-height:280px;margin-block-end:32px}.AboveRecsAd_root__iTmTR .freestar-ad-container{place-content:flex-start}@media (max-width:1050px){.AboveRecsAd_root__iTmTR .freestar-ad-container{place-content:center}}.AboveRecsAd_mobileAd__LYgqf{display:none}.AboveRecsAd_desktopAd__ymykj{display:block}@media (max-width:1050px){.AboveRecsAd_mobileAd__LYgqf{display:block}.AboveRecsAd_desktopAd__ymykj{display:none}}.RailRecommendations_root__zqtZQ{display:flex;flex-direction:column;gap:8px;padding-block-end:24px}.RailRecommendations_title__kt1D2{font-size:24px;color:var(--blue-gray-900);font-weight:600;margin-block-end:20px}@media only screen and (min-width:929px){.RailRecommendations_hidden__7Ct2B{display:none}}.RailCard_root__rZUGY{--card-hover-background:var(--blue-gray-100);width:100%;position:relative;border-radius:8px;padding-block:12px;padding-inline:8px}.RailCard_root__rZUGY.slideshow-card{grid-template-columns:160px minmax(0,1fr);grid-template-rows:auto;gap:12px}.RailCard_root__rZUGY .slideshow-card-content{padding:0}.RailCard_actionContainer__eXUHT{display:grid;grid-template-columns:minmax(0,1fr) 28px;grid-column-gap:8px;-moz-column-gap:8px;column-gap:8px}.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-author,.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-stats,.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-title{grid-area:unset}.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-author,.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-stats{grid-column:1/-1}.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-author{grid-row:2}.RailCard_root__rZUGY .RailCard_actionContainer__eXUHT .slideshow-stats{grid-row:3}.RailCard_link__d3BBm{z-index:1}.RailCard_info__Oqm6G{min-width:0;display:flex;flex-direction:column;justify-content:center;word-break:break-word}.RailCard_root__rZUGY .RailCard_title__Tvfiv{font-size:16px;margin-bottom:0;grid-area:a;word-break:break-word}.RailCard_root__rZUGY .RailCard_stats__ZvZms{margin-top:12px}.RailCard_stats__ZvZms .text{font-size:12px}.RailCard_root__rZUGY .RailCard_author__JYeYZ{color:var(--blue-gray-700);margin-top:8px;text-decoration:none}@media screen and (min-width:929px){.RailCard_root__rZUGY:hover{background-color:var(--blue-gray-100)}}.RelatedContent_root__29Np1{background-color:var(--blue-gray-100);border-top:1px solid var(--blue-gray-200);border-bottom:1px solid var(--blue-gray-200);padding-block:32px;position:relative}.RelatedContent_wrapper__riU7l{display:grid;grid-template-columns:minmax(0,1fr);grid-gap:32px;gap:32px;max-width:var(--max-content-width);margin-inline:auto}.RelatedContent_title__QUhpL{text-align:center;font-size:32px;font-weight:700}.RelatedContent_root__29Np1 .bottom-recs{display:grid}.RelatedContent_root__29Np1 .rail-recs{display:none}@media screen and (max-width:520px){.RelatedContent_root__29Np1 .bottom-recs{display:none}.RelatedContent_root__29Np1 .rail-recs{display:flex}.RelatedContent_wrapper__riU7l{padding-inline:16px}}.BottomRecommendation_root__7aU9w{display:grid;grid-gap:4px;gap:4px;padding-inline:24px}.BottomRecommendation_title__SRj68{font-size:22px;font-weight:600}.BottomRecommendation_count__4HpLo{color:var(--blue-gray-600);font-size:16px;font-weight:400}.Slider_root__c0Jo8{position:relative;display:grid}.Slider_scroller__KHjw4{display:flex;gap:20px;overflow:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;max-inline-size:100%;min-block-size:100%;touch-action:pan-x;-ms-overflow-style:none;scrollbar-width:none}.Slider_scroller__KHjw4::-webkit-scrollbar{display:none}.Slider_scroller__KHjw4>*{flex-grow:1;flex-shrink:0;scroll-snap-align:start}.Slider_scroller__KHjw4>:last-child{scroll-snap-align:end}.Slider_arrow__8LCca{display:grid;place-content:center;width:36px;height:36px;position:absolute;top:50%;background:var(--white);border:1px solid var(--blue-gray-200);border-radius:100vmax;box-shadow:0 2px 4px 0 rgba(0,0,0,.25);color:var(--blue-gray-800);padding:0;opacity:1;visibility:visible;pointer-events:all;transition:opacity .2s ease-in-out,visibility .2s ease-in-out;transition-delay:0s;cursor:pointer;z-index:1}.Slider_prev__YMssa{left:0;translate:-50% -50%}.Slider_next__fa9IO{right:0;translate:50% -50%}.Slider_hidden__rs7nK{opacity:0;visibility:hidden;pointer-events:none}.BottomRecommendationCard_root__gffTk{inline-size:clamp(220px,12.63rem + 3.45vw,260px);position:relative;border-radius:8px;background-color:#fff;border:1px solid var(--blue-gray-200)}.BottomRecommendationCard_root__gffTk .slideshow-thumbnail{border-radius:0;border-top-left-radius:8px;border-top-right-radius:8px}.BottomRecommendationCard_root__gffTk .slideshow-card-content{padding:16px}.BottomRecommendationCard_root__gffTk .slideshow-author{max-width:50%;word-break:break-all;white-space:break-spaces;-webkit-line-clamp:1;display:-webkit-box;-webkit-box-orient:vertical}.BottomRecommendationCard_root__gffTk .slideshow-stats{margin-block-start:auto}.BottomRecommendationCard_root__gffTk:focus,.BottomRecommendationCard_root__gffTk:focus-visible{outline:none;border-color:var(--celadon-blue);border-width:1px}.BottomRecommendationCard_root__gffTk .BottomRecommendationCard_text__5jKNE{display:-webkit-box;-webkit-box-orient:vertical;white-space:break-spaces;word-break:break-word;-webkit-line-clamp:1}.BottomRecommendationCard_link__pHORq:before{content:"";position:absolute;inset:0;z-index:1}.BottomRecommendationCard_metaLine__shwPk{display:flex;gap:8px;align-items:center;white-space:nowrap;grid-area:c}.ScribdRecommendation_root__t3ezS{display:grid;grid-gap:16px;gap:16px;padding:0 24px}.ScribdRecommendation_header__Jw_M1{display:grid;grid-template-columns:1fr max-content}.ScribdRecommendation_title__JZ5p7{font-size:22px;font-weight:600}.ScribdRecommendation_link__4DVQz{align-self:end;grid-column:2;grid-row:1/span 2;color:var(--celadon-blue-dark)}.ScribdRecommendation_link__4DVQz:hover{color:var(--celadon-blue)}.ScribdRecommendationCard_root__ef2Y_{--ease:cubic-bezier(0.2,0,0,1);--rec-bg-1:#f1e3e3;--rec-bg-2:#f6f4e3;--rec-bg-3:#e1eaec;--rec-bg-4:#efebef;--rec-bg-5:#f1f0f0;--rec-bg-6:#f4eadb;--rec-bg-7:#eaeee7;--rec-bg-8:#e3e8ef;--rec-bg-9:#f1eee6;max-width:172px;position:relative;height:100%;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:max-content minmax(0,1fr);align-content:flex-start;background-color:var(--white);border:1px solid var(--blue-gray-200);box-shadow:none;transition:box-shadow .2s ease-in-out;text-decoration:none}.ScribdRecommendationCard_root__ef2Y_:hover{box-shadow:0 2px 10px rgba(0,0,0,.1)}.ScribdRecommendationCard_thumb__5VVNh{position:relative;display:flex;background-color:var(--blue-gray-200);margin-block-start:24px;margin-inline:20px}.ScribdRecommendationCard_root__ef2Y_:before{position:absolute;inset:0;aspect-ratio:19/16;content:"";background-color:var(--block-color)}.ScribdRecommendationCard_thumb__5VVNh img{--reveal-delay:calc(30ms * var(--card-index));aspect-ratio:inherit;box-shadow:0 4px 6px rgba(0,0,0,.2);-o-object-fit:cover;object-fit:cover;opacity:0;visibility:hidden;transition:opacity .3s var(--ease);transition-delay:var(--reveal-delay)}.ScribdRecommendationCard_loaded__FTN_f img{opacity:1;visibility:visible}.ScribdRecommendationCard_content__ObcvL{display:flex;flex-direction:column;padding-block:20px;padding-inline:16px}.ScribdRecommendationCard_content__ObcvL .rating{padding-block-start:8px;margin-block-start:auto}@media (max-width:928px){.ScribdRecommendationCard_content__ObcvL .rating{flex-direction:column;align-items:flex-start}}.Rating_root__fgZQJ{display:flex;align-items:center;-moz-column-gap:6px;column-gap:6px;white-space:nowrap}.Rating_root__fgZQJ :first-child{color:#e47b01;font-size:16px;line-height:1}.Rating_root__fgZQJ :last-child{color:var(--blue-gray-600);font-size:14px;letter-spacing:-.25px}.Transcript_root__Vrf6Q{width:100%;max-width:var(--max-content-width);display:grid;grid-gap:8px;gap:8px;padding:32px 16px;margin-inline:auto;position:relative;background:#fff}.Transcript_title__YgAka{display:flex;align-items:center;gap:4px;font-weight:300;word-break:break-word}.Transcript_list__faItj{list-style-type:none;padding-inline-start:0;word-break:break-word}.Transcript_link__MLbGS{color:var(--celadon-blue);font-weight:700;line-height:22px;text-decoration:none;cursor:pointer}.EditorsNotes_root__3PcDF{padding:32px 16px;margin:0 auto}.EditorsNotes_heading__XR9E6{font-weight:700;font-size:22px}.EditorsNotes_list__NcG5Y{padding-left:30px;font-size:18px;font-style:italic;color:var(--blue-gray-600)}.EditorsNotes_item__ebBbj{word-break:break-word}@media screen and (min-width:1696px){.EditorsNotes_root__3PcDF{max-width:1688px}}.FixedDownloadButton_root__14xtQ{display:none}@media screen and (max-width:928px){.FixedDownloadButton_root__14xtQ{position:sticky;bottom:0;display:flex;justify-content:right;z-index:3;padding:16px}}.Modal_root__TYkzh[open]{opacity:1;animation:Modal_slide-in__GHXut .3s ease-out}.Modal_root__TYkzh{--max-height:calc(100dvb - var(--header-height));--slide-from:calc(-50% + 8px);--slide-to:-50%;--title-size:80px;max-width:100%;max-height:var(--max-height);top:50%;left:50%;translate:-50% -50%;box-shadow:0 0 0 1px rgba(9,30,66,.08),0 2px 1px rgba(9,30,66,.08),0 0 20px -6px rgba(9,30,66,.3);border:0;border-radius:var(--border-radius);padding:0;opacity:0;animation:Modal_slide-out__m_Ov2 .2s ease-in;transition:display allow-discrete .3s,overlay allow-discrete .3s;overflow:clip}@starting-style{.Modal_root__TYkzh[open]{opacity:0}}.Modal_root__TYkzh.Modal_small__hupRE{width:400px}.Modal_root__TYkzh.Modal_medium__j8NOV{width:600px}.Modal_root__TYkzh.Modal_large__ygVmr{width:800px}.Modal_root__TYkzh.Modal_xlarge__HeXWk{width:960px}.Modal_wrapper__4UTGq{position:relative;display:flex;flex-direction:column}.Modal_wrapper__4UTGq>h1+*{flex:1 1;max-height:calc(var(--max-height) - var(--title-size));overflow:clip auto}@media screen and (max-width:520px){.Modal_root__TYkzh,.Modal_root__TYkzh.Modal_large__ygVmr,.Modal_root__TYkzh.Modal_medium__j8NOV,.Modal_root__TYkzh.Modal_small__hupRE,.Modal_root__TYkzh.Modal_xlarge__HeXWk{width:100vw}}@media screen and (max-width:928px){.Modal_root__TYkzh.Modal_bottomPlacement__BUbfp{--slide-from:8px;--slide-to:0;width:100vw;top:unset;bottom:0;translate:-50% 0;border-bottom-left-radius:0;border-bottom-right-radius:0}}@keyframes Modal_slide-in__GHXut{0%{translate:-50% var(--slide-from);opacity:0}to{translate:-50% var(--slide-to);opacity:1}}@keyframes Modal_slide-out__m_Ov2{0%{translate:-50% var(--slide-to);opacity:1}to{translate:-50% var(--slide-from);opacity:0}}.Modal_root__TYkzh::backdrop{background-color:transparent;transition:display allow-discrete .3s,overlay allow-discrete .3s,background-color .3s}.Modal_root__TYkzh[open]::backdrop{background-color:rgba(0,0,0,.6)}@starting-style{.Modal_root__TYkzh[open]::backdrop{background-color:transparent}}.Modal_title__xhSfl{height:var(--title-size);display:flex;align-items:center;color:var(--blue-gray-900);font-size:20px;font-weight:600;border-bottom:1px solid var(--blue-gray-200);padding-inline-start:20px;padding-inline-end:60px}.Modal_title__xhSfl:first-letter{text-transform:capitalize}.Modal_content__R1F4d{padding-inline:20px;padding-block:24px}.Modal_actions__t63hZ{display:flex;align-items:center;justify-content:flex-end;gap:24px;padding-inline:20px;padding-block:16px}.CloseButton_root__JCTRm{position:absolute;right:16px;top:16px;width:40px;height:40px;display:grid;place-content:center;color:var(--blue-gray-600);border-radius:100vmax;background-color:transparent;border:0;padding:0;margin:0;transition:background-color .2s ease-in-out;cursor:pointer}.CloseButton_root__JCTRm:hover{background-color:rgba(var(--blue-gray-600-rgb),.05)}.ReportForm_root__RNqAc{display:grid;grid-gap:10px;gap:10px}.ReportForm_selectField__Kyyaj{max-width:unset!important}.LikeModal_more__R9uAk{justify-self:center;color:var(--celadon-blue);font-weight:500;opacity:0;visibility:hidden;transition:opacity .2s ease-in-out,visibility .2s ease-in-out}.LikeModal_more__R9uAk.LikeModal_visible__t1vr4{opacity:1;visibility:visible;transition-delay:0s}.LikesUserList_root__RMFUk{list-style:none;padding:0 0 16px;margin:0}.LikesUserList_root__RMFUk>li{display:grid}.LikesUserList_root__RMFUk>li:last-child .LikesUserList_link__NeMA0{border-bottom:none}.LikesUserList_link__NeMA0{display:grid;grid-template-columns:repeat(2,max-content) 1fr;grid-template-rows:repeat(2,min-content);grid-template-areas:"avatar username summary" "avatar title title";grid-column-gap:12px;-moz-column-gap:12px;column-gap:12px;grid-row-gap:0;row-gap:0;border-bottom:1px solid var(--blue-gray-200);padding:8px 0;text-decoration:none}.LikesUserList_avatar__VRXz2{grid-area:avatar}.LikesUserList_username__c84om{grid-area:username}.LikesUserList_summary___gbSG{grid-area:summary}.LikesUserList_title__UF0V6{grid-area:title}.ViewModal_content__GPKXy{padding-block:0}.ViewModal_row__xLxnz{display:flex;justify-content:space-between;border-bottom:1px dashed var(--blue-gray-300);color:var(--blue-gray-800);padding:16px 0}.ViewModal_row__xLxnz:last-child{border-bottom:none}.ConfirmRemoveSavedModal_description__2EAEu{color:var(--blue-gray-800);font-weight:400;font-size:16px;line-height:20px;padding:24px 0}div.SaveToNewListModal_input__Fi90k{max-width:unset;margin-bottom:24px}.SaveToNewListModal_checkboxWrapper__y_w0m .SaveToNewListModal_checkboxLabel__lMiU9:hover,.SaveToNewListModal_checkboxWrapper__y_w0m:hover .SaveToNewListModal_checkboxLabel__lMiU9,.SaveToNewListModal_checkboxWrapper__y_w0m:hover .SaveToNewListModal_input__Fi90k{color:var(--blue-gray-800)}.SaveToNewListModal_checkboxWrapper__y_w0m .SaveToNewListModal_checkboxLabel__lMiU9{color:var(--blue-gray-800);font-weight:400;font-size:14px;flex-direction:column;display:flex;line-height:18px}.SaveToNewListModal_imageContainer__Dx4nD{display:flex;flex-direction:column;justify-content:center;align-items:center;width:178px;margin:0 auto 24px}.SaveToNewListModal_imageContainer__Dx4nD img{height:100px}.SaveToNewListModal_errorContainer__FBZPH{margin:16px 0}
Svoboda | Graniru | BBC Russia | Golosameriki | Facebook
SlideShare a Scribd company logo
Hyperledger Besu 빨리 따라하기
Privat Networks
Hyperledger Besu
Hyperledger BESU = Hyperledger Ethereum
Public & Permissioned Chain
source : https://besu.hyperledger.org/private-networks
Hello World (Quorum Developer Quickstart)
Docker를 활용한 Private Network 구성
- 필요 도구 : Docker Compose, Node.js, Hardhat, MetaMask
- 실습 목적 : Private Network 구동(5초 마다 블록 자동 생성)하여
이더리움 스마트 컨트랙트 배포 및 토큰 전송 실습
- 실습 환경 : Four Besu IBFT 2.0 validator nodes and a non-validator node are
created to simulate a base network.
프로젝트 생성 및 기동
위 웹사이트의 가이드에 따라서 npx quorum-dev-quickstart 로 프로젝트 생성
- 프로젝트 생성 단계에서 ELK Stack은 N 으로 설정(설정시 docker image pull 및 기동에 상당 시간 소요)
- 프로젝트 디렉토리 이름은 자유롭게 설정
- 생성된 프로젝트 디렉토리에서 ./run.sh 로 기동
- 로그는 프로젝트 디렉토리에서 docker-compose logs -f docker-compose.yml 를 통해 확인 가능
- 초기에 노드간 연결이 완성되지 못해 로그에 에러 메시지가 많이 보이나, 10분 정도 기다리면 자동 정상화됨
- block explorer, Grafana 등 서비스 모니터링을 위한 주소 확인은 ./list.sh
- 필요 메모리 : 8GB
https://besu.hyperledger.org/private-networks/tutorials/quickstart
hre_1559_public_tx.js
- 스마트 컨트랙트 배포
- increment 함수 호출, 특정 변수 값 +1
- decrement 함수 호출, 특정 변수 값 -1
누가 호출하던 상관 없이 전역 count의
값이 변경되게 예제가 구현되어 있음
async function main(){
const provider = new ethers.JsonRpcProvider(host);
const wallet = new ethers.Wallet(accountPrivateKey, provider);
createContract(provider, wallet, contractAbi, contractBytecode)
.then(async function(contract){
console.log(contract);
contractAddress = await contract.getAddress();
console.log("Use the smart contracts 'get' function to read the contract's initialized value .. " )
await getValueAtAddress(provider, contractAbi, contractAddress);
console.log("Use the smart contracts 'increment' function to update that value .. " );
await incrementValueAtAddress(provider, wallet, contractAbi, contractAddress );
console.log("Verify the updated value that was set .. " )
await getValueAtAddress(provider, contractAbi, contractAddress);
console.log("Use the smart contracts 'decrement' function to update that value .. " );
await decrementValueAtAddress(provider, wallet, contractAbi, contractAddress );
console.log("Verify the updated value that was set .. " )
await getValueAtAddress(provider, contractAbi, contractAddress);
})
.catch(console.error);
}
source : https://besu.hyperledger.org/private-networks
hre_1559_public_tx.js
node scripts/public/hre_1559_public_tx.js
BaseContract {
target: '0xBca0fDc68d9b21b5bfB16D784389807017B2bbbc',
interface: Interface {
fragments: [ [FunctionFragment], [FunctionFragment], [FunctionFragment] ],
deploy: ConstructorFragment {
type: 'constructor',
inputs: [],
payable: false,
gas: null
},
fallback: null,
receive: false
},
runner: Wallet {
provider: JsonRpcProvider {},
address: '0xC9C913c8c3C1Cd416d80A0abF475db2062F161f6'
},
filters: {},
fallback: null,
[Symbol(_ethersInternal_contract)]: {}
}
Use the smart contracts 'get' function to read the contract's initialized value ..
Obtained value at deployed contract is: 0
Use the smart contracts 'increment' function to update that value ..
Verify the updated value that was set ..
Obtained value at deployed contract is: 1
Use the smart contracts 'decrement' function to update that value ..
Verify the updated value that was set ..
Obtained value at deployed contract is: 0
source : https://besu.hyperledger.org/private-networks
node scripts/public/hre_1559_public_tx.js
BaseContract {
target: '0xBca0fDc68d9b21b5bfB16D784389807017B2bbbc',
interface: Interface {
fragments: [ [FunctionFragment], [FunctionFragment], [FunctionFragment] ],
deploy: ConstructorFragment {
type: 'constructor',
inputs: [],
payable: false,
gas: null
},
fallback: null,
receive: false
},
runner: Wallet {
provider: JsonRpcProvider {},
address: '0xC9C913c8c3C1Cd416d80A0abF475db2062F161f6'
},
filters: {},
fallback: null,
[Symbol(_ethersInternal_contract)]: {}
}
Use the smart contracts 'get' function to read the contract's initialized value ..
Obtained value at deployed contract is: 0
Use the smart contracts 'increment' function to update that value ..
Verify the updated value that was set ..
Obtained value at deployed contract is: 1
Use the smart contracts 'decrement' function to update that value ..
Verify the updated value that was set ..
Obtained value at deployed contract is: 0
hre_1559_public_tx.js
source : https://besu.hyperledger.org/private-networks
hre_public_tx.js
- 컨트랙트 배포 (배포와 동시에 배포를 요청하는 지갑주소에 47을 설정
- 위 값을 123으로 업데이트
async function main(){
const provider = new ethers.JsonRpcProvider(host);
const wallet = new ethers.Wallet(accountPrivateKey, provider);
createContract(provider, wallet, contractAbi, contractBytecode, 47)
.then(async function(contract){
contractAddress = await contract.getAddress();
console.log("Contract deployed at address: " + contractAddress);
console.log("Use the smart contracts 'get' function to read the contract's constructor initialized value .. " )
await getValueAtAddress(provider, contractAbi, contractAddress);
console.log("Use the smart contracts 'set' function to update that value to 123 .. " );
await setValueAtAddress(provider, wallet, contractAbi, contractAddress, 123 );
console.log("Verify the updated value that was set .. " )
await getValueAtAddress(provider, contractAbi, contractAddress);
// await getAllPastEvents(host, contractAbi, tx.contractAddress);
})
source : https://besu.hyperledger.org/private-networks
hre_eth_tx.js
- A 계정의 잔고(90000)에서 B 계정(잔고 0)으로 16 wei 이체
Account A has balance of: 90000000000000000000000
Account B has balance of: 0
create and sign the txn
tx transactionHash: 0x2338ce8a2249de93b59dfa0e6e29eb7e5050c74880cfc20e8b6aaa0ebbc995b1
Account A has balance of: 89999999999999999999984
Account B has balance of: 16 source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
Add a Test Network
source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
Add account (신규 계정 생성 - 이더를 수신 받기 위한 테스트용 계정)
계정 생성 결과
source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
Add account (네트워크 상 등록된 계정 Import - 이더 송신용 계정)
Private Key Copy & Paste
계정 Import 결과
source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
In the Block Explorer, search for the new test account
by selecting the 🔍 and pasting the test account
address into the search box.
The new test account displays with a zero balance.
source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
9페이지에서 Import한 계정에서
8페이지에서 신규 생성한 계정으로
1 이더 전송
source : https://besu.hyperledger.org/private-networks
Create a transaction using MetaMask
결과 확인
source : https://besu.hyperledger.org/private-networks
Smart contract and dapp usage
# install dependencies
npm i
# compile the contract
npm run compile
npm run test
# deploy the contract to the quickstart network
npm run deploy-quorumtoken
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"
contract QuorumToken is ERC20 {
constructor(uint256 initialSupply)
ERC20("QuorumToken", "QT")
{
_mint(msg.sender, initialSupply);
}
}
Contract deploy at: 0x9a3DBCa554e9f6b9257aAa24010DA8377C57c17e
Smart contract and dapp usage
Create a QBFT network
block header validator selection method를 적용하여 validator 노드를 관리(추가/삭제)하는 예제
(  스마트 컨트랙트를 별도로 배포하여 관리하는 방법도 있음(contract validator selection method)
Create a private network using QBFT
디렉토리 생성
[root@master qbft-network]# pwd
/root/qbft-network
[root@master qbft-network]# tree
.
├── Node-1
│ └── data
├── Node-2
│ └── data
├── Node-3
│ └── data
└── Node-4
└── data
8 directories, 0 files
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
configuration file 생성
[root@master qbft-network]# pwd
/root/qbft-network
[root@master qbft-network]# ll
total 4
drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-1
drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-2
drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-3
drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-4
-rw-r--r--. 1 root root 1513 Jun 20 02:54 qbftConfigFile.json
{
"genesis": {
"config": {
"chainId": 1337,
"berlinBlock": 0,
"qbft": {
"blockperiodseconds": 2,
"epochlength": 30000,
"requesttimeoutseconds": 4
}
},
"nonce": "0x0",
"timestamp": "0x58ee40ba",
"gasLimit": "0x47b760",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
"privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "0xad78ebc5ac6200000"
},
"627306090abaB3A6e1400e9345bC60c78a8BEf57": {
"privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
},
"f17f52151EbEF6C7334FAD080c5704D77216b732": {
"privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
}
}
},
"blockchain": {
"nodes": {
"generate": true,
"count": 4
}
}
}
genesis property
(참고로, extraData 섹션은 Besu에서 자동 생성)
blockchain property
: 생성할 key pair 수
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
노드 키(공개키/비밀키) 및 genesis.json 생성
# ../besu/bin/besu operator generate-blockchain-config --config-file=qbftConfigFile.json --to=networkFiles --private-key-file-name=key
03:15:38.052-04:00 | main | INFO | GenerateBlockchainConfig | Generating 4 nodes keys.
03:15:38.062-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 0.
03:15:38.377-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 1.
03:15:38.409-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 2.
03:15:38.431-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 3.
03:15:38.461-04:00 | main | INFO | GenerateBlockchainConfig | Generating QBFT extra data.
03:15:38.500-04:00 | main | INFO | GenerateBlockchainConfig | Writing genesis file.
[root@master qbft-network]# tree
.
├── networkFiles
│ ├── genesis.json
│ └── keys
│ ├── 0x258ed3d4c40fa5cfecb67597c6dba6248804014c
│ │ ├── key
│ │ └── key.pub
│ ├── 0x4914ca1ac7afb825cc1a416817597144bf878180
│ │ ├── key
│ │ └── key.pub
│ ├── 0x898c92dc5fff31af05de0e1a465c0ff812120d15
│ │ ├── key
│ │ └── key.pub
│ └── 0x984769eacf61592cf42695a7147c9116c8ed738a
│ ├── key
│ └── key.pub
├── Node-1
│ └── data
├── Node-2
│ └── data
├── Node-3
│ └── data
├── Node-4
│ └── data
└── qbftConfigFile.json
생성 결과
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
genesis.json 파일 copy
# cp networkFiles/genesis.json ./
# ll
total 8
-rw-r--r--. 1 root root 1639 Jun 20 03:23 genesis.json
drwxr-xr-x. 3 root root 38 Jun 20 03:15 networkFiles
drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-1
drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-2
drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-3
drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-4
-rw-r--r--. 1 root root 1513 Jun 20 02:54 qbftConfigFile.json
키 파일들을 각 노드의 data 디렉토리로 copy
# cp networkFiles/keys/0x258ed3d4c40fa5cfecb67597c6dba6248804014c/* Node-1/data/
# cp networkFiles/keys/0x4914ca1ac7afb825cc1a416817597144bf878180/* Node-2/data/
# cp networkFiles/keys/0x898c92dc5fff31af05de0e1a465c0ff812120d15/* Node-3/data/
# cp networkFiles/keys/0x984769eacf61592cf42695a7147c9116c8ed738a/* Node-4/data/
# tree
├── genesis.json
├── networkFiles
│ ├── genesis.json
│ └── keys
│ ├── 0x258ed3d4c40fa5cfecb67597c6dba6248804014c
│ │ ├── key
│ │ └── key.pub
│ ├── 0x4914ca1ac7afb825cc1a416817597144bf878180
│ │ ├── key
│ │ └── key.pub
│ ├── 0x898c92dc5fff31af05de0e1a465c0ff812120d15
│ │ ├── key
│ │ └── key.pub
│ └── 0x984769eacf61592cf42695a7147c9116c8ed738a
│ ├── key
│ └── key.pub
├── Node-1
│ └── data
│ ├── key
│ └── key.pub
├── Node-2
│ └── data
│ ├── key
│ └── key.pub
├── Node-3
│ └── data
│ ├── key
│ └── key.pub
├── Node-4
│ └── data
│ ├── key
│ └── key.pub
└── qbftConfigFile.json
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
1번 노드 기동(bootnode)
# pwd
/root/qbft-network/Node-1
# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-http-cors-origins="all"
2024-06-20 03:42:54.457-04:00 | main | INFO | DefaultP2PNetwork | Enode URL
enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.
0.1:30303
2024-06-20 03:42:54.458-04:00 | main | INFO | DefaultP2PNetwork | Node address 0x258ed3d4c40fa5cfecb67597c6dba6248804014c
Copy the enode URL to specify Node-1 as the bootnode in the following steps.
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
2번 노드 기동(bootnode와 연결)
# pwd
/root/qbft-network/Node-2
[root@master Node-2]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b
acfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30304 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-
http-cors-origins="all" --rpc-http-port=8546
3번 노드 기동(bootnode와 연결)
# pwd
/root/qbft-network/Node-3
[root@master Node-3]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b
acfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30305 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-
http-cors-origins="all" --rpc-http-port=8547
이전 장에서 복사한 bootnode의 enode url
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
4번 노드 기동(bootnode와 연결)
# pwd
/root/qbft-network/Node-4
[root@master Node-4]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b
acfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30306 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-
http-cors-origins="all" --rpc-http-port=8548
이전 장에서 복사한 bootnode의 enode url
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
정상 기동 확인
curl -X POST --data '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"], "id":1}' localhost:8545
{"jsonrpc":"2.0","id":1,"result":["0x258ed3d4c40fa5cfecb67597c6dba6248804014c","0x4914ca1
ac7afb825cc1a416817597144bf878180","0x898c92dc5fff31af05de0e1a465c0ff812120d15","0
x984769eacf61592cf42695a7147c9116c8ed738a"]}
4개 노드의 주소 확인
- 이전 실습에서 노드키 자동 생성 결과로 얻었던 주소와 일치 여부 확인
at page 20
source : https://besu.hyperledger.org/private-networks
Create a private network using QBFT
추가 학습
- 검증 노드 추가/삭제
- 스마트컨트랙트 배포 및 토큰 전송 등(이전 챕터 Quorum Developer Quickstart 참고)
https://besu.hyperledger.org/private-networks/how-to/configure/consensus/qbft#add-and-remove-validators-using-block-headers
You can switch from the block header validator selection
method configured here, to the contract validator selection
method by updating the genesis file and configuring a
transition.
Create a privacy-enabled network
- Privacy transacion 기능 사용을 위해 Tessera 적용
- tx data를 암호화, 참여자 간에만 공유, 기밀성
- 각 노드마다 Tessera 실행
이전 챕터의 Developer Quickstart에서
초기 프로젝트 설정에서
privacy transaction 을 y 로 설정할 경우
tessera 가 자동 적용된다.
이번 장은 tessera를 step by step으로
적용하는 실습 챕터임
Create a privacy-enabled network
- 이전 챕터에서 실습한 QBFT 합의 알고리즘에 기반하여 실습 진행
- 각 Node-<N>/data 디렉토리에서 아래 명령 수행(각 노드의 data 디렉토리에서 기 실습한 db 삭제)
rm -rf caches/ database/ DATABASE_METADATA.json uploads
☞ bonsai는 privacy mode 지원하지 않음  forest로 설정하여 실습하기 위함
- Tessera 설치 (https://docs.tessera.consensys.io/HowTo/Get-started/Install/Distribution)
① https://github.com/ConsenSys/tessera/releases/latest 에서 최신 버전 다운로드 및 서버 업로드
② tar xvf tessera-[version].tar
③ vi /etc/profile
④ source /etc/profile
⑤ tessera help 명령어로 정상동작 확인
환경변수 추가
Create a privacy-enabled network
[root@master qbft-network]# pwd
/root/qbft-network
디렉토리 구조 생성
mkdir -p Node-1/Tessera Node-2/Tessera Node-3/Tessera Node-4/Tessera
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
각 노드 Tessera 디렉토리에서 Tessera 통신용 공개키/개인키 생성
cd qbft-network/Node-<N>/Tessera/
[root@master Tessera]# tessera -keygen -filename nodeKey
# ls
nodeKey.key nodeKey.pub
생성 결과
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
각 노드 Tessera 디렉토리에서 config 생성
- 각 노드 config 설정 값은 아래 링크 참조
https://besu.hyperledger.org/private-networks/tutorials/privacy#3-create-tessera-configuration-files
cd qbft-network/
# vi Node-<N>/Tessera/tessera.conf
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
각 Tessera 노드 실행
각 Node-<N>/Tessera 에서
tessera -configfile tessera.conf
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
Besu 노드 1 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화)
Node-1 에서 실행
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST --rpc-
http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors-origins="all" --
privacy-enabled --privacy-url=http://127.0.0.1:9102 --privacy-public-key-file=Tessera/nodeKey.pub --min-
gas-price=0
Copy the enode URL to specify Node-1 as the bootnode in the following steps.
Enode URL
enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@
127.0.0.1:30303
2024-06-21 09:21:11.319-04:00 | main | INFO | DefaultP2PNetwork | Node address 0x258ed3d4c40fa5cfecb67597c6dba6248804014c
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
Besu 노드 2 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화)
Node-2 에서 실행
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf
15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30304
--rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors-
origins="all" --rpc-http-port=8546 --privacy-enabled --privacy-url=http://127.0.0.1:9202 --privacy-
public-key-file=Tessera/nodeKey.pub --min-gas-price=0
이전 장에서 복사한 bootnode의 enode url
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf
15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30305
--rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors-
origins="all" --rpc-http-port=8547 --privacy-enabled --privacy-url=http://127.0.0.1:9302 --privacy-
public-key-file=Tessera/nodeKey.pub --min-gas-price=0
이전 장에서 복사한 bootnode의 enode url
Besu 노드 3 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화)
Node-3 에서 실행
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST --
bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf
15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30306
--rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors-
origins="all" --rpc-http-port=8548 --privacy-enabled --privacy-url=http://127.0.0.1:9402 --privacy-
public-key-file=Tessera/nodeKey.pub --min-gas-price=0
이전 장에서 복사한 bootnode의 enode url
Besu 노드 4 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화)
Node-4 에서 실행
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
결과
Tessera 노드
Besu 노드
source : https://besu.hyperledger.org/private-networks
Create a privacy-enabled network
Configure a multi-tenant node
- https://besu.hyperledger.org/private-networks/tutorials/privacy/multi-tenancy
① 특정 노드의 인스턴스를 복수로 실행할 수 있음
② 특정 노드에 jwt 토큰을 발행하고, 해당 토큰으로 클라이언트 요청으로 전달되는 jwt를 파싱하여
허가된 API에 대한 JSON-RPC 요청을 처리할 수 있음 (<- Privacy-enabled network 사용 이유)
- keyData가 여러 개 설정된 경우 각 Tessera 인스턴스는 자신의 키를 사용하여 트랜잭션을 처리
Create a permissioned network
account-allowlist와 nodes-allowlist로 계정과 노드의 permission을 관리
- perm_addNodesToAllowlist, removeAccountsFromAllowList, getAccountAllowlist 등의 API로 권한관리
- 이전 챕터에서 실습한 QBFT 합의 알고리즘에 기반하여 실습 진행
Create a permissioned network
Permission 설정 파일 생성
각 노드 Node-<N>/data 디렉토리에 아래 파일 생성
accounts-allowlist=["0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "0x627306090abaB3A6e1400e9345bC60c78a8BEf57"]
nodes-allowlist=[]
permissions_config.toml
Permission에 genesis파일에서 명시한 첫 2개의 account만 allowlist에 추가하여 실습하기
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
Node 1 기동
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled
--permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http-
api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*"
<기존 Privacy enabled network 실습과 차이>
--rpc-http-host="0.0.0.0" 은 외부 네트워크(메타마스크)등에서 접속을 허용하기 위함
--rpc-http-api에서 EEA, PRIV 제거 ( Private 기능을 사용하지 않는 실습)
--privacy-enabled --privacy-url=http://127.0.0.1:9202 --privacy-public-key-file=Tessera/nodeKey.pub 제거
--bootnode 제거 (Permission을 추가하여 노드간 연결을 뒤에서 진행(1번 노드를 bootnode 로 설정예정)
결과 로그에서 enode 복사
enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacf
e7a0d3fc082540af7cc56@127.0.0.1:30303
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
Node 2 기동
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled
--permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http-
api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*"
--p2p-port=30304 --rpc-http-port=8546
결과 로그에서 enode 복사
enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e
9e3a8a03e5f53819eebf518@127.0.0.1:30304
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
Node 3 기동
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled
--permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http-
api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*" -
-p2p-port=30305 --rpc-http-port=8547
결과 로그에서 enode 복사
enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf80c94233db35e99f9a82af2d07e1
35a38ce27e6c411ba569e@127.0.0.1:30305
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
Node 4 기동
../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled
--permissions-accounts-config-file-enabled --rpc-http-enabled --data-storage-format=FOREST --rpc-http-
api=ADMIN,ETH,NET,PERM,QBFT --host-allowlist="*" --rpc-http-host="0.0.0.0" --rpc-http-cors-origins="*"
--p2p-port=30306 --rpc-http-port=8548
결과 로그에서 enode 복사
enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2f9beae0375e7da5bb16628980dcfe2
bc2d91ecbcb2da8b1189cd@127.0.0.1:30306
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
Permission이 적용될 node 등록
- 각 노드의 rpc endpoint에 각 노드 구동시 출력된 enode (4개)를 등록함
- 이는 permissions_config.toml에 nodes-allowlist에 노드를 추가하는 동작과 동일
curl -X POST --data
'{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c
402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe
4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf
80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2
f9beae0375e7da5bb16628980dcfe2bc2d91ecbcb2da8b1189cd@127.0.0.1:30306"]], "id":1}' http://127.0.0.1:8545
curl -X POST --data
'{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c
402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe
4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf
80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2
f9beae0375e7da5bb16628980dcfe2bc2d91ecbcb2da8b1189cd@127.0.0.1:30306"]], "id":1}' http://127.0.0.1:8546
curl -X POST --data
'{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c
402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe
4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf
80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2
f9beae0375e7da5bb16628980dcfe2bc2d91ecbcb2da8b1189cd@127.0.0.1:30306"]], "id":1}' http://127.0.0.1:8547
curl -X POST --data
'{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c
402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe
4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf
80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2
f9beae0375e7da5bb16628980dcfe2bc2d91ecbcb2da8b1189cd@127.0.0.1:30306"]], "id":1}' http://127.0.0.1:8548
Create a permissioned network
Peer 노드 등록
- 노드 1 을 노드 2,3,4에 peer 노드로 등록
- 노드 2를 노드 3,4에 peer 노드로 등록
- 노드 3을 노드 4에 peer 노드로 등록
 노드 1이 bootnode로 인식됨
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7
36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30
303"],"id":1}' http://127.0.0.1:8546
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7
36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30
303"],"id":1}' http://127.0.0.1:8547
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7
36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30
303"],"id":1}' http://127.0.0.1:8548
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e
799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:
30304"],"id":1}' http://127.0.0.1:8547
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e
799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1:
30304"],"id":1}' http://127.0.0.1:8548
curl -X POST --data
'{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://00e334d5195139102afb717e3eede9c5c68d86e7cf
c4e66e56f879c19ec0dfdd56ac244c03ef6cf80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:3
0305"],"id":1}' http://127.0.0.1:8548
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
정상 동작 확인
○ 참여 peer 노드 갯수 확인
curl -X POST --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' localhost:8545
- 결과 : {"jsonrpc":"2.0","id":1,"result":"0x3"}
○ allowlist 확인
curl -X POST --data '{"jsonrpc":"2.0","method":"perm_getAccountsAllowlist","params":[],"id":1}' -H
"Content-Type: application/json" http://localhost:8545
- 결과 :
{"jsonrpc":"2.0","id":1,"result":["0xfe3b557e8fb62b89f4916b721be55ceb828dbd73","0x627306090abab3a6e140
0e9345bc60c78a8bef57"]}
○ bootnode 확인
curl -X POST --data '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' -H "Content-Type:
application/json" http://localhost:8545
- 결과 :
{"jsonrpc":"2.0","id":1,"result":{"enode":"enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@
127.0.0.1:30303","listenAddr":"127.0.0.1:30303","ip":"127.0.0.1","name":"besu/v24.5.2/linux-x86_64/oracle-java-
17","id":"952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56","ports":{"discovery":30303,"listener":303
03},"protocols":{"eth":{"config":{"chainId":1337,"berlinBlock":0,"qbft":{"epochLength":30000,"blockPeriodSeconds":2,"requestTimeoutSeconds":4}},"difficulty":5288,"genesis":"0x0bfff602eb27908c5021217
6f59c5a40e634c9a9da745f9d32f4bf5f2acbbd07","head":"0x93ff7d6663c8a449cb3f0f0c3930ba26ff2cd65a5a9c8d89abda9a0c4f79c70e","network":1337}}}}
Create a permissioned network
테스트
- allowlist 에 있는 1번 계정에서 allowlist에 없는 3번 계정으로 transfer 시도  Pending 으로 실패
0xfe3b557e8fb62b89f4916b721be55ceb828dbd73
0xf17f52151EbEF6C7334FAD080c5704D77216b732
Create a permissioned network
besu 로그에 pending transaction 확인 가능
① peding transaction을 삭제하는 기능을 besu에서 제공하지 않음
② pending tx를 삭제하기 위해서는 transaction pool size를 작게
조절해서 oldest tx부터 자동 삭제되게 하거나,
③ besu 를 전체 재기동한다. 다만, 이때에도 메타마스크에서
pending 중인 tx가 있기 때문에, 재기동하고 정상 tx를
메타마스크에서 요청하여도 이전 pending된 내역을 다시
메타마스크에서 처리를 시도하기 때문에 정상 tx도 처리가
안된다.
④ 이를 위해서 아래와 같이 besu 재기동 이후에 메타마스크에서
우측과 같은 처리가 필요하다.
메타마스크  Advanced 
Clear activity and nonce data 섹션의 버튼 클릭
Create a permissioned network
테스트
- allowlist 에 있는 1번 계정에서 allowlist에 존재하는 2번 계정으로 transfer 시도  성공
0xfe3b557e8fb62b89f4916b721be55ceb828dbd73
0x627306090abaB3A6e1400e9345bC60c78a8BEf
source : https://besu.hyperledger.org/private-networks
Create a permissioned network
노드 allowlist에 없는 임의의 besu 노드5 를 기동
(기동시 bootnode를 1번으로 지정)
 노드5에서 peer 노드의 갯수를 조회하면 0으로 출력
 perm_addNodesToAllowlist를 통해 등록하지 않았기 때문에 예상된 결과임
source : https://besu.hyperledger.org/private-networks
앞으로 다룰 내용
Kubernetes 배포 및 관리
Public Networks 실습
프로젝트 과제

More Related Content

Hyperledger Besu 빨리 따라하기 (Private Networks)

  • 1. Hyperledger Besu 빨리 따라하기 Privat Networks
  • 2. Hyperledger Besu Hyperledger BESU = Hyperledger Ethereum Public & Permissioned Chain source : https://besu.hyperledger.org/private-networks
  • 3. Hello World (Quorum Developer Quickstart) Docker를 활용한 Private Network 구성 - 필요 도구 : Docker Compose, Node.js, Hardhat, MetaMask - 실습 목적 : Private Network 구동(5초 마다 블록 자동 생성)하여 이더리움 스마트 컨트랙트 배포 및 토큰 전송 실습 - 실습 환경 : Four Besu IBFT 2.0 validator nodes and a non-validator node are created to simulate a base network.
  • 4. 프로젝트 생성 및 기동 위 웹사이트의 가이드에 따라서 npx quorum-dev-quickstart 로 프로젝트 생성 - 프로젝트 생성 단계에서 ELK Stack은 N 으로 설정(설정시 docker image pull 및 기동에 상당 시간 소요) - 프로젝트 디렉토리 이름은 자유롭게 설정 - 생성된 프로젝트 디렉토리에서 ./run.sh 로 기동 - 로그는 프로젝트 디렉토리에서 docker-compose logs -f docker-compose.yml 를 통해 확인 가능 - 초기에 노드간 연결이 완성되지 못해 로그에 에러 메시지가 많이 보이나, 10분 정도 기다리면 자동 정상화됨 - block explorer, Grafana 등 서비스 모니터링을 위한 주소 확인은 ./list.sh - 필요 메모리 : 8GB https://besu.hyperledger.org/private-networks/tutorials/quickstart
  • 5. hre_1559_public_tx.js - 스마트 컨트랙트 배포 - increment 함수 호출, 특정 변수 값 +1 - decrement 함수 호출, 특정 변수 값 -1 누가 호출하던 상관 없이 전역 count의 값이 변경되게 예제가 구현되어 있음 async function main(){ const provider = new ethers.JsonRpcProvider(host); const wallet = new ethers.Wallet(accountPrivateKey, provider); createContract(provider, wallet, contractAbi, contractBytecode) .then(async function(contract){ console.log(contract); contractAddress = await contract.getAddress(); console.log("Use the smart contracts 'get' function to read the contract's initialized value .. " ) await getValueAtAddress(provider, contractAbi, contractAddress); console.log("Use the smart contracts 'increment' function to update that value .. " ); await incrementValueAtAddress(provider, wallet, contractAbi, contractAddress ); console.log("Verify the updated value that was set .. " ) await getValueAtAddress(provider, contractAbi, contractAddress); console.log("Use the smart contracts 'decrement' function to update that value .. " ); await decrementValueAtAddress(provider, wallet, contractAbi, contractAddress ); console.log("Verify the updated value that was set .. " ) await getValueAtAddress(provider, contractAbi, contractAddress); }) .catch(console.error); } source : https://besu.hyperledger.org/private-networks
  • 6. hre_1559_public_tx.js node scripts/public/hre_1559_public_tx.js BaseContract { target: '0xBca0fDc68d9b21b5bfB16D784389807017B2bbbc', interface: Interface { fragments: [ [FunctionFragment], [FunctionFragment], [FunctionFragment] ], deploy: ConstructorFragment { type: 'constructor', inputs: [], payable: false, gas: null }, fallback: null, receive: false }, runner: Wallet { provider: JsonRpcProvider {}, address: '0xC9C913c8c3C1Cd416d80A0abF475db2062F161f6' }, filters: {}, fallback: null, [Symbol(_ethersInternal_contract)]: {} } Use the smart contracts 'get' function to read the contract's initialized value .. Obtained value at deployed contract is: 0 Use the smart contracts 'increment' function to update that value .. Verify the updated value that was set .. Obtained value at deployed contract is: 1 Use the smart contracts 'decrement' function to update that value .. Verify the updated value that was set .. Obtained value at deployed contract is: 0 source : https://besu.hyperledger.org/private-networks
  • 7. node scripts/public/hre_1559_public_tx.js BaseContract { target: '0xBca0fDc68d9b21b5bfB16D784389807017B2bbbc', interface: Interface { fragments: [ [FunctionFragment], [FunctionFragment], [FunctionFragment] ], deploy: ConstructorFragment { type: 'constructor', inputs: [], payable: false, gas: null }, fallback: null, receive: false }, runner: Wallet { provider: JsonRpcProvider {}, address: '0xC9C913c8c3C1Cd416d80A0abF475db2062F161f6' }, filters: {}, fallback: null, [Symbol(_ethersInternal_contract)]: {} } Use the smart contracts 'get' function to read the contract's initialized value .. Obtained value at deployed contract is: 0 Use the smart contracts 'increment' function to update that value .. Verify the updated value that was set .. Obtained value at deployed contract is: 1 Use the smart contracts 'decrement' function to update that value .. Verify the updated value that was set .. Obtained value at deployed contract is: 0 hre_1559_public_tx.js source : https://besu.hyperledger.org/private-networks
  • 8. hre_public_tx.js - 컨트랙트 배포 (배포와 동시에 배포를 요청하는 지갑주소에 47을 설정 - 위 값을 123으로 업데이트 async function main(){ const provider = new ethers.JsonRpcProvider(host); const wallet = new ethers.Wallet(accountPrivateKey, provider); createContract(provider, wallet, contractAbi, contractBytecode, 47) .then(async function(contract){ contractAddress = await contract.getAddress(); console.log("Contract deployed at address: " + contractAddress); console.log("Use the smart contracts 'get' function to read the contract's constructor initialized value .. " ) await getValueAtAddress(provider, contractAbi, contractAddress); console.log("Use the smart contracts 'set' function to update that value to 123 .. " ); await setValueAtAddress(provider, wallet, contractAbi, contractAddress, 123 ); console.log("Verify the updated value that was set .. " ) await getValueAtAddress(provider, contractAbi, contractAddress); // await getAllPastEvents(host, contractAbi, tx.contractAddress); }) source : https://besu.hyperledger.org/private-networks
  • 9. hre_eth_tx.js - A 계정의 잔고(90000)에서 B 계정(잔고 0)으로 16 wei 이체 Account A has balance of: 90000000000000000000000 Account B has balance of: 0 create and sign the txn tx transactionHash: 0x2338ce8a2249de93b59dfa0e6e29eb7e5050c74880cfc20e8b6aaa0ebbc995b1 Account A has balance of: 89999999999999999999984 Account B has balance of: 16 source : https://besu.hyperledger.org/private-networks
  • 10. Create a transaction using MetaMask Add a Test Network source : https://besu.hyperledger.org/private-networks
  • 11. Create a transaction using MetaMask Add account (신규 계정 생성 - 이더를 수신 받기 위한 테스트용 계정) 계정 생성 결과 source : https://besu.hyperledger.org/private-networks
  • 12. Create a transaction using MetaMask Add account (네트워크 상 등록된 계정 Import - 이더 송신용 계정) Private Key Copy & Paste 계정 Import 결과 source : https://besu.hyperledger.org/private-networks
  • 13. Create a transaction using MetaMask In the Block Explorer, search for the new test account by selecting the 🔍 and pasting the test account address into the search box. The new test account displays with a zero balance. source : https://besu.hyperledger.org/private-networks
  • 14. Create a transaction using MetaMask 9페이지에서 Import한 계정에서 8페이지에서 신규 생성한 계정으로 1 이더 전송 source : https://besu.hyperledger.org/private-networks
  • 15. Create a transaction using MetaMask 결과 확인 source : https://besu.hyperledger.org/private-networks
  • 16. Smart contract and dapp usage # install dependencies npm i # compile the contract npm run compile npm run test # deploy the contract to the quickstart network npm run deploy-quorumtoken import "@openzeppelin/contracts/token/ERC20/ERC20.sol" contract QuorumToken is ERC20 { constructor(uint256 initialSupply) ERC20("QuorumToken", "QT") { _mint(msg.sender, initialSupply); } } Contract deploy at: 0x9a3DBCa554e9f6b9257aAa24010DA8377C57c17e
  • 17. Smart contract and dapp usage
  • 18. Create a QBFT network block header validator selection method를 적용하여 validator 노드를 관리(추가/삭제)하는 예제 (  스마트 컨트랙트를 별도로 배포하여 관리하는 방법도 있음(contract validator selection method)
  • 19. Create a private network using QBFT 디렉토리 생성 [root@master qbft-network]# pwd /root/qbft-network [root@master qbft-network]# tree . ├── Node-1 │ └── data ├── Node-2 │ └── data ├── Node-3 │ └── data └── Node-4 └── data 8 directories, 0 files source : https://besu.hyperledger.org/private-networks
  • 20. Create a private network using QBFT configuration file 생성 [root@master qbft-network]# pwd /root/qbft-network [root@master qbft-network]# ll total 4 drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-1 drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-2 drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-3 drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-4 -rw-r--r--. 1 root root 1513 Jun 20 02:54 qbftConfigFile.json { "genesis": { "config": { "chainId": 1337, "berlinBlock": 0, "qbft": { "blockperiodseconds": 2, "epochlength": 30000, "requesttimeoutseconds": 4 } }, "nonce": "0x0", "timestamp": "0x58ee40ba", "gasLimit": "0x47b760", "difficulty": "0x1", "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", "coinbase": "0x0000000000000000000000000000000000000000", "alloc": { "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", "balance": "0xad78ebc5ac6200000" }, "627306090abaB3A6e1400e9345bC60c78a8BEf57": { "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", "balance": "90000000000000000000000" }, "f17f52151EbEF6C7334FAD080c5704D77216b732": { "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", "balance": "90000000000000000000000" } } }, "blockchain": { "nodes": { "generate": true, "count": 4 } } } genesis property (참고로, extraData 섹션은 Besu에서 자동 생성) blockchain property : 생성할 key pair 수 source : https://besu.hyperledger.org/private-networks
  • 21. Create a private network using QBFT 노드 키(공개키/비밀키) 및 genesis.json 생성 # ../besu/bin/besu operator generate-blockchain-config --config-file=qbftConfigFile.json --to=networkFiles --private-key-file-name=key 03:15:38.052-04:00 | main | INFO | GenerateBlockchainConfig | Generating 4 nodes keys. 03:15:38.062-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 0. 03:15:38.377-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 1. 03:15:38.409-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 2. 03:15:38.431-04:00 | main | INFO | GenerateBlockchainConfig | Generating keypair for node 3. 03:15:38.461-04:00 | main | INFO | GenerateBlockchainConfig | Generating QBFT extra data. 03:15:38.500-04:00 | main | INFO | GenerateBlockchainConfig | Writing genesis file. [root@master qbft-network]# tree . ├── networkFiles │ ├── genesis.json │ └── keys │ ├── 0x258ed3d4c40fa5cfecb67597c6dba6248804014c │ │ ├── key │ │ └── key.pub │ ├── 0x4914ca1ac7afb825cc1a416817597144bf878180 │ │ ├── key │ │ └── key.pub │ ├── 0x898c92dc5fff31af05de0e1a465c0ff812120d15 │ │ ├── key │ │ └── key.pub │ └── 0x984769eacf61592cf42695a7147c9116c8ed738a │ ├── key │ └── key.pub ├── Node-1 │ └── data ├── Node-2 │ └── data ├── Node-3 │ └── data ├── Node-4 │ └── data └── qbftConfigFile.json 생성 결과 source : https://besu.hyperledger.org/private-networks
  • 22. Create a private network using QBFT genesis.json 파일 copy # cp networkFiles/genesis.json ./ # ll total 8 -rw-r--r--. 1 root root 1639 Jun 20 03:23 genesis.json drwxr-xr-x. 3 root root 38 Jun 20 03:15 networkFiles drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-1 drwxr-xr-x. 3 root root 18 Jun 20 02:46 Node-2 drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-3 drwxr-xr-x. 3 root root 18 Jun 20 02:47 Node-4 -rw-r--r--. 1 root root 1513 Jun 20 02:54 qbftConfigFile.json 키 파일들을 각 노드의 data 디렉토리로 copy # cp networkFiles/keys/0x258ed3d4c40fa5cfecb67597c6dba6248804014c/* Node-1/data/ # cp networkFiles/keys/0x4914ca1ac7afb825cc1a416817597144bf878180/* Node-2/data/ # cp networkFiles/keys/0x898c92dc5fff31af05de0e1a465c0ff812120d15/* Node-3/data/ # cp networkFiles/keys/0x984769eacf61592cf42695a7147c9116c8ed738a/* Node-4/data/ # tree ├── genesis.json ├── networkFiles │ ├── genesis.json │ └── keys │ ├── 0x258ed3d4c40fa5cfecb67597c6dba6248804014c │ │ ├── key │ │ └── key.pub │ ├── 0x4914ca1ac7afb825cc1a416817597144bf878180 │ │ ├── key │ │ └── key.pub │ ├── 0x898c92dc5fff31af05de0e1a465c0ff812120d15 │ │ ├── key │ │ └── key.pub │ └── 0x984769eacf61592cf42695a7147c9116c8ed738a │ ├── key │ └── key.pub ├── Node-1 │ └── data │ ├── key │ └── key.pub ├── Node-2 │ └── data │ ├── key │ └── key.pub ├── Node-3 │ └── data │ ├── key │ └── key.pub ├── Node-4 │ └── data │ ├── key │ └── key.pub └── qbftConfigFile.json source : https://besu.hyperledger.org/private-networks
  • 23. Create a private network using QBFT 1번 노드 기동(bootnode) # pwd /root/qbft-network/Node-1 # ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-http-cors-origins="all" 2024-06-20 03:42:54.457-04:00 | main | INFO | DefaultP2PNetwork | Enode URL enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0. 0.1:30303 2024-06-20 03:42:54.458-04:00 | main | INFO | DefaultP2PNetwork | Node address 0x258ed3d4c40fa5cfecb67597c6dba6248804014c Copy the enode URL to specify Node-1 as the bootnode in the following steps. source : https://besu.hyperledger.org/private-networks
  • 24. Create a private network using QBFT 2번 노드 기동(bootnode와 연결) # pwd /root/qbft-network/Node-2 [root@master Node-2]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b [email protected]:30303 --p2p-port=30304 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc- http-cors-origins="all" --rpc-http-port=8546 3번 노드 기동(bootnode와 연결) # pwd /root/qbft-network/Node-3 [root@master Node-3]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b [email protected]:30303 --p2p-port=30305 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc- http-cors-origins="all" --rpc-http-port=8547 이전 장에서 복사한 bootnode의 enode url source : https://besu.hyperledger.org/private-networks
  • 25. Create a private network using QBFT 4번 노드 기동(bootnode와 연결) # pwd /root/qbft-network/Node-4 [root@master Node-4]# ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0b [email protected]:30303 --p2p-port=30306 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc- http-cors-origins="all" --rpc-http-port=8548 이전 장에서 복사한 bootnode의 enode url source : https://besu.hyperledger.org/private-networks
  • 26. Create a private network using QBFT 정상 기동 확인 curl -X POST --data '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"], "id":1}' localhost:8545 {"jsonrpc":"2.0","id":1,"result":["0x258ed3d4c40fa5cfecb67597c6dba6248804014c","0x4914ca1 ac7afb825cc1a416817597144bf878180","0x898c92dc5fff31af05de0e1a465c0ff812120d15","0 x984769eacf61592cf42695a7147c9116c8ed738a"]} 4개 노드의 주소 확인 - 이전 실습에서 노드키 자동 생성 결과로 얻었던 주소와 일치 여부 확인 at page 20 source : https://besu.hyperledger.org/private-networks
  • 27. Create a private network using QBFT 추가 학습 - 검증 노드 추가/삭제 - 스마트컨트랙트 배포 및 토큰 전송 등(이전 챕터 Quorum Developer Quickstart 참고) https://besu.hyperledger.org/private-networks/how-to/configure/consensus/qbft#add-and-remove-validators-using-block-headers You can switch from the block header validator selection method configured here, to the contract validator selection method by updating the genesis file and configuring a transition.
  • 28. Create a privacy-enabled network - Privacy transacion 기능 사용을 위해 Tessera 적용 - tx data를 암호화, 참여자 간에만 공유, 기밀성 - 각 노드마다 Tessera 실행 이전 챕터의 Developer Quickstart에서 초기 프로젝트 설정에서 privacy transaction 을 y 로 설정할 경우 tessera 가 자동 적용된다. 이번 장은 tessera를 step by step으로 적용하는 실습 챕터임
  • 29. Create a privacy-enabled network - 이전 챕터에서 실습한 QBFT 합의 알고리즘에 기반하여 실습 진행 - 각 Node-<N>/data 디렉토리에서 아래 명령 수행(각 노드의 data 디렉토리에서 기 실습한 db 삭제) rm -rf caches/ database/ DATABASE_METADATA.json uploads ☞ bonsai는 privacy mode 지원하지 않음  forest로 설정하여 실습하기 위함 - Tessera 설치 (https://docs.tessera.consensys.io/HowTo/Get-started/Install/Distribution) ① https://github.com/ConsenSys/tessera/releases/latest 에서 최신 버전 다운로드 및 서버 업로드 ② tar xvf tessera-[version].tar ③ vi /etc/profile ④ source /etc/profile ⑤ tessera help 명령어로 정상동작 확인 환경변수 추가
  • 30. Create a privacy-enabled network [root@master qbft-network]# pwd /root/qbft-network 디렉토리 구조 생성 mkdir -p Node-1/Tessera Node-2/Tessera Node-3/Tessera Node-4/Tessera source : https://besu.hyperledger.org/private-networks
  • 31. Create a privacy-enabled network 각 노드 Tessera 디렉토리에서 Tessera 통신용 공개키/개인키 생성 cd qbft-network/Node-<N>/Tessera/ [root@master Tessera]# tessera -keygen -filename nodeKey # ls nodeKey.key nodeKey.pub 생성 결과 source : https://besu.hyperledger.org/private-networks
  • 32. Create a privacy-enabled network 각 노드 Tessera 디렉토리에서 config 생성 - 각 노드 config 설정 값은 아래 링크 참조 https://besu.hyperledger.org/private-networks/tutorials/privacy#3-create-tessera-configuration-files cd qbft-network/ # vi Node-<N>/Tessera/tessera.conf source : https://besu.hyperledger.org/private-networks
  • 33. Create a privacy-enabled network 각 Tessera 노드 실행 각 Node-<N>/Tessera 에서 tessera -configfile tessera.conf source : https://besu.hyperledger.org/private-networks
  • 34. Create a privacy-enabled network Besu 노드 1 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화) Node-1 에서 실행 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST --rpc- http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors-origins="all" -- privacy-enabled --privacy-url=http://127.0.0.1:9102 --privacy-public-key-file=Tessera/nodeKey.pub --min- gas-price=0 Copy the enode URL to specify Node-1 as the bootnode in the following steps. Enode URL enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@ 127.0.0.1:30303 2024-06-21 09:21:11.319-04:00 | main | INFO | DefaultP2PNetwork | Node address 0x258ed3d4c40fa5cfecb67597c6dba6248804014c source : https://besu.hyperledger.org/private-networks
  • 35. Create a privacy-enabled network Besu 노드 2 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화) Node-2 에서 실행 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf 15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30304 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors- origins="all" --rpc-http-port=8546 --privacy-enabled --privacy-url=http://127.0.0.1:9202 --privacy- public-key-file=Tessera/nodeKey.pub --min-gas-price=0 이전 장에서 복사한 bootnode의 enode url source : https://besu.hyperledger.org/private-networks
  • 36. Create a privacy-enabled network ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf 15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30305 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors- origins="all" --rpc-http-port=8547 --privacy-enabled --privacy-url=http://127.0.0.1:9302 --privacy- public-key-file=Tessera/nodeKey.pub --min-gas-price=0 이전 장에서 복사한 bootnode의 enode url Besu 노드 3 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화) Node-3 에서 실행 source : https://besu.hyperledger.org/private-networks
  • 37. Create a privacy-enabled network ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --data-storage-format=FOREST -- bootnodes=enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf 15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303 --p2p-port=30306 --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT,EEA,PRIV --host-allowlist="*" --rpc-http-cors- origins="all" --rpc-http-port=8548 --privacy-enabled --privacy-url=http://127.0.0.1:9402 --privacy- public-key-file=Tessera/nodeKey.pub --min-gas-price=0 이전 장에서 복사한 bootnode의 enode url Besu 노드 4 실행 (실행 옵션으로 Tessera 연결 및 Privacy 지원 활성화) Node-4 에서 실행 source : https://besu.hyperledger.org/private-networks
  • 38. Create a privacy-enabled network 결과 Tessera 노드 Besu 노드 source : https://besu.hyperledger.org/private-networks
  • 39. Create a privacy-enabled network Configure a multi-tenant node - https://besu.hyperledger.org/private-networks/tutorials/privacy/multi-tenancy ① 특정 노드의 인스턴스를 복수로 실행할 수 있음 ② 특정 노드에 jwt 토큰을 발행하고, 해당 토큰으로 클라이언트 요청으로 전달되는 jwt를 파싱하여 허가된 API에 대한 JSON-RPC 요청을 처리할 수 있음 (<- Privacy-enabled network 사용 이유) - keyData가 여러 개 설정된 경우 각 Tessera 인스턴스는 자신의 키를 사용하여 트랜잭션을 처리
  • 40. Create a permissioned network account-allowlist와 nodes-allowlist로 계정과 노드의 permission을 관리 - perm_addNodesToAllowlist, removeAccountsFromAllowList, getAccountAllowlist 등의 API로 권한관리 - 이전 챕터에서 실습한 QBFT 합의 알고리즘에 기반하여 실습 진행
  • 41. Create a permissioned network Permission 설정 파일 생성 각 노드 Node-<N>/data 디렉토리에 아래 파일 생성 accounts-allowlist=["0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "0x627306090abaB3A6e1400e9345bC60c78a8BEf57"] nodes-allowlist=[] permissions_config.toml Permission에 genesis파일에서 명시한 첫 2개의 account만 allowlist에 추가하여 실습하기 source : https://besu.hyperledger.org/private-networks
  • 42. Create a permissioned network Node 1 기동 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled --permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http- api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*" <기존 Privacy enabled network 실습과 차이> --rpc-http-host="0.0.0.0" 은 외부 네트워크(메타마스크)등에서 접속을 허용하기 위함 --rpc-http-api에서 EEA, PRIV 제거 ( Private 기능을 사용하지 않는 실습) --privacy-enabled --privacy-url=http://127.0.0.1:9202 --privacy-public-key-file=Tessera/nodeKey.pub 제거 --bootnode 제거 (Permission을 추가하여 노드간 연결을 뒤에서 진행(1번 노드를 bootnode 로 설정예정) 결과 로그에서 enode 복사 enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacf [email protected]:30303 source : https://besu.hyperledger.org/private-networks
  • 43. Create a permissioned network Node 2 기동 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled --permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http- api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*" --p2p-port=30304 --rpc-http-port=8546 결과 로그에서 enode 복사 enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e [email protected]:30304 source : https://besu.hyperledger.org/private-networks
  • 44. Create a permissioned network Node 3 기동 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled --permissions-accounts-config-file-enabled --data-storage-format=FOREST --rpc-http-enabled --rpc-http- api=ADMIN,ETH,NET,PERM,QBFT --rpc-http-host="0.0.0.0" --host-allowlist="*" --rpc-http-cors-origins="*" - -p2p-port=30305 --rpc-http-port=8547 결과 로그에서 enode 복사 enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf80c94233db35e99f9a82af2d07e1 [email protected]:30305 source : https://besu.hyperledger.org/private-networks
  • 45. Create a permissioned network Node 4 기동 ../../besu/bin/besu --data-path=data --genesis-file=../genesis.json --permissions-nodes-config-file-enabled --permissions-accounts-config-file-enabled --rpc-http-enabled --data-storage-format=FOREST --rpc-http- api=ADMIN,ETH,NET,PERM,QBFT --host-allowlist="*" --rpc-http-host="0.0.0.0" --rpc-http-cors-origins="*" --p2p-port=30306 --rpc-http-port=8548 결과 로그에서 enode 복사 enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2f9beae0375e7da5bb16628980dcfe2 [email protected]:30306 source : https://besu.hyperledger.org/private-networks
  • 46. Create a permissioned network Permission이 적용될 node 등록 - 각 노드의 rpc endpoint에 각 노드 구동시 출력된 enode (4개)를 등록함 - 이는 permissions_config.toml에 nodes-allowlist에 노드를 추가하는 동작과 동일 curl -X POST --data '{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c [email protected]:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe [email protected]:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf [email protected]:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2 [email protected]:30306"]], "id":1}' http://127.0.0.1:8545 curl -X POST --data '{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c [email protected]:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe [email protected]:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf [email protected]:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2 [email protected]:30306"]], "id":1}' http://127.0.0.1:8546 curl -X POST --data '{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c [email protected]:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe [email protected]:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf [email protected]:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2 [email protected]:30306"]], "id":1}' http://127.0.0.1:8547 curl -X POST --data '{"jsonrpc":"2.0","method":"perm_addNodesToAllowlist","params":[["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c [email protected]:30303","enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e799a7d81d0422eae400db96903dc09dbe1626fe [email protected]:30304","enode://00e334d5195139102afb717e3eede9c5c68d86e7cfc4e66e56f879c19ec0dfdd56ac244c03ef6cf [email protected]:30305","enode://1da03cac51d4fc896950ca9399fa98923ac10a0b18135f198a905cf6ad563e95396d634f0dd2 [email protected]:30306"]], "id":1}' http://127.0.0.1:8548
  • 47. Create a permissioned network Peer 노드 등록 - 노드 1 을 노드 2,3,4에 peer 노드로 등록 - 노드 2를 노드 3,4에 peer 노드로 등록 - 노드 3을 노드 4에 peer 노드로 등록  노드 1이 bootnode로 인식됨 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7 36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30 303"],"id":1}' http://127.0.0.1:8546 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7 36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30 303"],"id":1}' http://127.0.0.1:8547 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d7 36b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30 303"],"id":1}' http://127.0.0.1:8548 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e 799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1: 30304"],"id":1}' http://127.0.0.1:8547 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://f1e0e6ee986f7c189bc75b3a2f0ff4256b9d5a0ce5e 799a7d81d0422eae400db96903dc09dbe1626fe4a4c7963cba5de1bae83d5e9e3a8a03e5f53819eebf518@127.0.0.1: 30304"],"id":1}' http://127.0.0.1:8548 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_addPeer","params":["enode://00e334d5195139102afb717e3eede9c5c68d86e7cf c4e66e56f879c19ec0dfdd56ac244c03ef6cf80c94233db35e99f9a82af2d07e135a38ce27e6c411ba569e@127.0.0.1:3 0305"],"id":1}' http://127.0.0.1:8548 source : https://besu.hyperledger.org/private-networks
  • 48. Create a permissioned network 정상 동작 확인 ○ 참여 peer 노드 갯수 확인 curl -X POST --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' localhost:8545 - 결과 : {"jsonrpc":"2.0","id":1,"result":"0x3"} ○ allowlist 확인 curl -X POST --data '{"jsonrpc":"2.0","method":"perm_getAccountsAllowlist","params":[],"id":1}' -H "Content-Type: application/json" http://localhost:8545 - 결과 : {"jsonrpc":"2.0","id":1,"result":["0xfe3b557e8fb62b89f4916b721be55ceb828dbd73","0x627306090abab3a6e140 0e9345bc60c78a8bef57"]} ○ bootnode 확인 curl -X POST --data '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' -H "Content-Type: application/json" http://localhost:8545 - 결과 : {"jsonrpc":"2.0","id":1,"result":{"enode":"enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@ 127.0.0.1:30303","listenAddr":"127.0.0.1:30303","ip":"127.0.0.1","name":"besu/v24.5.2/linux-x86_64/oracle-java- 17","id":"952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56","ports":{"discovery":30303,"listener":303 03},"protocols":{"eth":{"config":{"chainId":1337,"berlinBlock":0,"qbft":{"epochLength":30000,"blockPeriodSeconds":2,"requestTimeoutSeconds":4}},"difficulty":5288,"genesis":"0x0bfff602eb27908c5021217 6f59c5a40e634c9a9da745f9d32f4bf5f2acbbd07","head":"0x93ff7d6663c8a449cb3f0f0c3930ba26ff2cd65a5a9c8d89abda9a0c4f79c70e","network":1337}}}}
  • 49. Create a permissioned network 테스트 - allowlist 에 있는 1번 계정에서 allowlist에 없는 3번 계정으로 transfer 시도  Pending 으로 실패 0xfe3b557e8fb62b89f4916b721be55ceb828dbd73 0xf17f52151EbEF6C7334FAD080c5704D77216b732
  • 50. Create a permissioned network besu 로그에 pending transaction 확인 가능 ① peding transaction을 삭제하는 기능을 besu에서 제공하지 않음 ② pending tx를 삭제하기 위해서는 transaction pool size를 작게 조절해서 oldest tx부터 자동 삭제되게 하거나, ③ besu 를 전체 재기동한다. 다만, 이때에도 메타마스크에서 pending 중인 tx가 있기 때문에, 재기동하고 정상 tx를 메타마스크에서 요청하여도 이전 pending된 내역을 다시 메타마스크에서 처리를 시도하기 때문에 정상 tx도 처리가 안된다. ④ 이를 위해서 아래와 같이 besu 재기동 이후에 메타마스크에서 우측과 같은 처리가 필요하다. 메타마스크  Advanced  Clear activity and nonce data 섹션의 버튼 클릭
  • 51. Create a permissioned network 테스트 - allowlist 에 있는 1번 계정에서 allowlist에 존재하는 2번 계정으로 transfer 시도  성공 0xfe3b557e8fb62b89f4916b721be55ceb828dbd73 0x627306090abaB3A6e1400e9345bC60c78a8BEf source : https://besu.hyperledger.org/private-networks
  • 52. Create a permissioned network 노드 allowlist에 없는 임의의 besu 노드5 를 기동 (기동시 bootnode를 1번으로 지정)  노드5에서 peer 노드의 갯수를 조회하면 0으로 출력  perm_addNodesToAllowlist를 통해 등록하지 않았기 때문에 예상된 결과임 source : https://besu.hyperledger.org/private-networks
  • 53. 앞으로 다룰 내용 Kubernetes 배포 및 관리 Public Networks 실습 프로젝트 과제

Editor's Notes

  1. - block explorer 는 브라우저에서 접속 확인시 호스트명을 localhost로 접속
  2. node scripts/public/hre_public_tx.js Contract deployed at address: 0x9393486896D3ae612B4939afAF2C367Df17CC39B Use the smart contracts 'get' function to read the contract's constructor initialized value .. Obtained value at deployed contract is: 47 Use the smart contracts 'set' function to update that value to 123 .. Verify the updated value that was set .. Obtained value at deployed contract is: 123
  3. contract validator selection method : https://github.com/ConsenSys/validator-smart-contracts
  4. enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303
  5. enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303
  6. enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303
  7. enode://952bfa93e63eecb6adc4c07688df3af41c9d71c4d736b108102758835bb7672ec6abf15f0afc7ae19d9c402a7e48fc92d350fe0bacfe7a0d3fc082540af7cc56@127.0.0.1:30303
  8. [root@master data]# more DATABASE_METADATA.json { "v2" : { "format" : "BONSAI", "version" : 2 } [root@master data]# rm -rf caches/ database/ DATABASE_METADATA.json uploads
  9. BftProcessorExecutor-QBFT-0 | INFO | QbftBesuControllerBuilder | Imported #2,723 / 0 tx / 1 pending / 0 (0.0%) gas / (0xca591a196bcda39e188779ca8c040ba77f83e963f7a742de29ba7544d94555e1)