From 951b2abb63299da7e1332220d50e1f59cf71aa0e Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Tue, 17 Dec 2024 22:38:26 -0600 Subject: [PATCH] More unit tests, for BlockVerificationSessionSync Signed-off-by: Alfredo Gutierrez --- .../AbstractBlockVerificationSession.java | 6 +- .../BlockVerificationSessionSyncTest.java | 227 ++++++++++++++++++ .../resources/test-blocks/hashing-01.blk.gz | Bin 0 -> 14820 bytes 3 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionSyncTest.java create mode 100644 server/src/test/resources/test-blocks/hashing-01.blk.gz diff --git a/server/src/main/java/com/hedera/block/server/verification/session/AbstractBlockVerificationSession.java b/server/src/main/java/com/hedera/block/server/verification/session/AbstractBlockVerificationSession.java index 1ab610a9..0f3adecb 100644 --- a/server/src/main/java/com/hedera/block/server/verification/session/AbstractBlockVerificationSession.java +++ b/server/src/main/java/com/hedera/block/server/verification/session/AbstractBlockVerificationSession.java @@ -34,7 +34,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; /** * An abstract base class providing common functionality for block verification sessions. @@ -94,11 +93,8 @@ public CompletableFuture getVerificationResult() { * * @param blockItems the block items to process * @throws ParseException if a parsing error occurs - * @throws ExecutionException if a concurrency error occurs - * @throws InterruptedException if the operation is interrupted */ - protected void processBlockItems(List blockItems) - throws ParseException, ExecutionException, InterruptedException { + protected void processBlockItems(List blockItems) throws ParseException { for (BlockItemUnparsed item : blockItems) { final BlockItemUnparsed.ItemOneOfType kind = item.item().kind(); diff --git a/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionSyncTest.java b/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionSyncTest.java new file mode 100644 index 00000000..a7d45e8a --- /dev/null +++ b/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionSyncTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.block.server.verification.session; + +import static com.hedera.block.common.utils.FileUtilities.readGzipFileUnsafe; +import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlockTime; +import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksError; +import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksFailed; +import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksVerified; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.hedera.block.server.metrics.MetricsService; +import com.hedera.block.server.verification.BlockVerificationStatus; +import com.hedera.block.server.verification.VerificationResult; +import com.hedera.block.server.verification.signature.SignatureVerifier; +import com.hedera.hapi.block.BlockItemUnparsed; +import com.hedera.hapi.block.BlockUnparsed; +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockProof; +import com.hedera.hapi.block.stream.input.EventHeader; +import com.hedera.hapi.block.stream.output.BlockHeader; +import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import com.swirlds.metrics.api.Counter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.verification.VerificationMode; + +class BlockVerificationSessionSyncTest { + + @Mock + private MetricsService metricsService; + + @Mock + private SignatureVerifier signatureVerifier; + + @Mock + private Counter verificationBlocksVerified; + + @Mock + private Counter verificationBlocksFailed; + + @Mock + private Counter verificationBlockTime; + + @Mock Counter verificationBlocksError; + + final Bytes hashing01BlockHash = Bytes.fromHex("006ae77f87ff57df598f4d6536dcb5c0a5c1f840c2fef817b2faebd554d32cfc9a4eaee1d873ed88de668b53b7839117"); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + // set metrics + when(metricsService.get(VerificationBlocksVerified)).thenReturn(verificationBlocksVerified); + when(metricsService.get(VerificationBlocksFailed)).thenReturn(verificationBlocksFailed); + when(metricsService.get(VerificationBlockTime)).thenReturn(verificationBlockTime); + when(metricsService.get(VerificationBlocksError)).thenReturn(verificationBlocksError); + } + + private List getTestBlock1Items() throws IOException, ParseException, URISyntaxException { + Path block01Path = Path.of(getClass().getResource("/test-blocks/hashing-01.blk.gz").toURI()); + Bytes block01Bytes = Bytes.wrap(readGzipFileUnsafe(block01Path)); + BlockUnparsed blockUnparsed = BlockUnparsed.PROTOBUF.parse(block01Bytes); + + return blockUnparsed.blockItems(); + } + + @Test + void testSuccessfulVerification() throws Exception { + // Given + List blockItems = getTestBlock1Items(); + BlockHeader blockHeader = BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader()); + BlockVerificationSessionSync session = + new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier); + when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class))).thenReturn(true); + + // When + session.appendBlockItems(blockItems); + CompletableFuture future = session.getVerificationResult(); + VerificationResult result = future.get(); + + // Then + assertEquals(BlockVerificationStatus.VERIFIED, result.status()); + assertEquals(1L, result.blockNumber()); + assertEquals(hashing01BlockHash, result.blockHash()); + assertFalse(session.isRunning()); + verify(verificationBlocksVerified, times(1)).increment(); + verify(verificationBlockTime, times(1)).add(any(Long.class)); + verifyNoMoreInteractions(verificationBlocksFailed); + } + + @Test + void testSuccessfulVerification_multipleAppends() throws Exception { + // Given + List blockItems = getTestBlock1Items(); + // Slice list into 2 parts of different sizes + List blockItems1 = blockItems.subList(0, 3); + List blockItems2 = blockItems.subList(3, blockItems.size()); + BlockHeader blockHeader = BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader()); + BlockVerificationSessionSync session = + new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier); + when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class))).thenReturn(true); + + // When + session.appendBlockItems(blockItems1); + session.appendBlockItems(blockItems2); + CompletableFuture future = session.getVerificationResult(); + VerificationResult result = future.get(); + + // Then + assertEquals(BlockVerificationStatus.VERIFIED, result.status()); + assertEquals(1L, result.blockNumber()); + assertEquals(hashing01BlockHash, result.blockHash()); + assertFalse(session.isRunning()); + verify(verificationBlocksVerified, times(1)).increment(); + verify(verificationBlockTime, times(1)).add(any(Long.class)); + verifyNoMoreInteractions(verificationBlocksFailed); + } + + @Test + void testVerificationFailure() throws Exception { + // Given + List blockItems = getTestBlock1Items(); + final Bytes hashing01BlockHash = Bytes.fromHex("006ae77f87ff57df598f4d6536dcb5c0a5c1f840c2fef817b2faebd554d32cfc9a4eaee1d873ed88de668b53b7839117"); + BlockHeader blockHeader = BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader()); + BlockVerificationSessionSync session = + new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier); + when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class))).thenReturn(false); + + // When + session.appendBlockItems(blockItems); + CompletableFuture future = session.getVerificationResult(); + VerificationResult result = future.get(); + + // Then + assertEquals(BlockVerificationStatus.SIGNATURE_INVALID, result.status()); + assertEquals(1L, result.blockNumber()); + assertEquals(hashing01BlockHash, result.blockHash()); + assertFalse(session.isRunning()); + verifyNoMoreInteractions(verificationBlocksVerified); + verify(verificationBlocksFailed, times(1)).increment(); + } + + @Test + void testAppendBlockItemsNotRunning() throws Exception { + // Given + List blockItems = getTestBlock1Items(); + BlockHeader blockHeader = BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader()); + BlockVerificationSessionSync session = + new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier); + when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class))).thenReturn(true); + // send a whole block and wait for the result, the session should be completed. + session.appendBlockItems(blockItems); + CompletableFuture future = session.getVerificationResult(); + VerificationResult result = future.get(); + + // metrics should be 1 + verify(verificationBlocksVerified, times(1)).increment(); + verify(verificationBlockTime, times(1)).add(any(Long.class)); + + // When + // Try to append more items after the session has completed + session.appendBlockItems(blockItems); + + // Then + // counters should still be 1 + verify(verificationBlocksVerified, times(1)).increment(); + verify(verificationBlockTime, times(1)).add(any(Long.class)); + } + + @Test + void testParseException() throws IOException, ParseException, URISyntaxException, ExecutionException, InterruptedException { + // Given + List blockItems = getTestBlock1Items(); + BlockHeader blockHeader = BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader()); + blockItems.set(blockItems.size()-1, BlockItemUnparsed.newBuilder().blockProof(Bytes.wrap("invalid")).build()); + BlockVerificationSessionSync session = + new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier); + + // When + session.appendBlockItems(blockItems); + CompletableFuture future = session.getVerificationResult(); + + // Then + assertTrue(future.isCompletedExceptionally()); + verify(verificationBlocksError, times(1)).increment(); + } + +} diff --git a/server/src/test/resources/test-blocks/hashing-01.blk.gz b/server/src/test/resources/test-blocks/hashing-01.blk.gz new file mode 100644 index 0000000000000000000000000000000000000000..750a050a27c9b3c84c775089aab2fe8eb858b565 GIT binary patch literal 14820 zcmeIWS2Wyj*!~;+t7eqwWe_ch=$)Angb<@eqDBj%6QY+fdW$Z|7(|R7y%W8+=skKT zdT0OMcdh?g`*0uagT3~1bg%2=bMjp4y6&4L5_t5#?_qzzQ)1$Z<-xz(E6k~n;)!P5 z(;1%En5Uy`wkj&QFSS$)Il_CAMavJRSsThN2b(Phn?Ekqn5b`_-T2524v1!MUp4t< zUfq|RpybImYNNN^2JfUCnNzZ)UU8@^s&NIf=Op}X{U>ZLaT$KUp{Upgi|T}Xb|t*3 z?M#3dxBI1*4Lr@`<&FOEo2iZBgz*PkF5Abhza9B6Tja}~{!Zt!)o(q2Vjt}{`Q++8 zKCSxLH!~=7I|@Tm_>{4uYHO;YeAq8y=jl|Zj;WtFPTp~}r_<=s(>RV_bX zL7M`F7scZ{;@hbyMZ`n>E5G&KYZ`?Jt5>EZbSTy!!8MN%#xD3@IwIPjPT_a73gK4V zrX){LSqQ=1e*;pq3ei>=QxZ~CR-oYS(SR4NLZp?oDG3uQD@gFvBZRr@?>`c|jqBSLwsVn36D{OoIgXJVKbd?AnFrK9CTgmIDMcJVGeD?7D^LOb5thDsZ~?I)tT7 zNoY{Z2*G)e5QeV3HsOX3B#%+cfr1T30}iz9kyak21I#iN1YQ4@>EviDXomNK1#dk< zSi1Jwg>ODYJwlNO2qHX`KwZEx9V;z+loir63P6zu3TAsKQFVna(+SeD$5?fkM&Y93 z0t9c42K;DU9}P1G2*!ITQFOgprsJVykGA?@8byMN3lt-u1&Oiv}9Szvf=0sTOm=4g%xZ-zVmgz)kb7HK< zO{4HpqXB|x9!iv5*2{GKv^mjMm8JvlWn8JdGTMbVKSYtCMgs*mj|Mzxb0V!in-1W~ zxMFn~b_mm$MlqvCg9KCl4dj}b);0@f=ve4H9l%PB8izSSK3&39J)@rU&apqV>T#p=c?vPAu9BJRX3?2akuLmB8aj zG$(jG3jGm$N(aphgcBq51K_*}{XjSaVm1ISjhGFD6Ck7l;9Ll)z!4f;Uo4mru!9X- z0Cwq1%MtK)(+6)!`K0OELaXej|Vd$ zKYap3vs#k?(d^cYfI8S38&GGnrU2Agt%(42c58ZI5NwSP46<2M1B0yAWWXT1H8a2u zw#EYZ*{sO{epYKjfS=u(4k!j&;{nBN)=z+9R%=q=J!q9RvPE$Hj=hHhnhfb7g1&|H z&_gF7TRaF9HY)?}%%dzTzzZK~M{YrkM6qv4g31-XGJ(pKzsiEV0?{;(9xNysq=y_D z1KHw07-O??;Ywn$TH#9Ku+rexJz~|vt;1#&$F0N4(gVEkkn`kgWJnp%8WvK9e2oy< z2wKBKHj=NAB7Hz>I7nmmEm6?1;#UFCvBFmtkXH~|9MZ!Ay@zl=f?`3qLC`=5Hvok~ zxM`uS5N=#3D}=*kCPzf*tdj1pP+DJ&?iMWFX)p3oZ+;g`{`Cgcgv}_#r=LmH{gN=D*;^aU=Dx_ z4y+V(N(9ug_b>sT;2s>nldXpm@MP^F20Yn&7=SBq4*}4mG-3;0iv*j1*FwSa;8T34 zW58E=#BCt_5du2^4n|-H!a)c`0Q?045eNqmumCtW0u~6TMYILLB@k_aa9jji0Q@O} zEf7wH$PR#uAhHADBnYzrI3L0+5YC8L34qHWRs!MJ2#NqW8-gMbPJxIIfD0kw14j&S zoA6*ffGQR&A5g`E*#N3IutU%(8Ia4)%>-D2xp4qXHf~D5l9ihnuw>_E0M^0W1i(5Q zHx01P%KaE!A?{D2WK0zYsBga`>3 zd4UKC903qY0VCW9rN9wATwgqxIY5sCTLtI|V3a`GBUl2Eh6~dM(y(D;KpH+w7)Zl{ zRRC#tFjpWA2X+Fa5x|H6{YS7!Kpz+O8qmjv^#l6&FkV0(3swl|1PD2LvCv1tGEsWPyh)BDWw#qS&^0K;_V{q#&;lv=yX>8d?q6l0=|zSfz1iuvoou zXK=D;052>gBWUdrl97B3KpL}cae!Q4UnxN@N?(ORF3_*UAQ#22ydW2auM8lsV6+~j zhY5NB>A`_ALwYEoC`b=6)ELsk09}Oi5J2T1TcQYK999$DD{NMI+$*dsali`)c}u?b z7>Ny{dW6I#rvi`&5EU*GK~6=2gn_8AkuY*9B4itg3Ln`?~G68a6oO$>bx;bwr&K)4B@k`QhhXdQ(6 zG1L>XC5u?Z$$AW2KSCapCq70Jfpi`riO6*TWCTbD7a2jW^Phj&w%9?>pm0LaGetNL z=$QhX4)jbJE(t1t!0|u@FX0@ZYb06&f+2$HLNN5u2?z!sDhk0+L#rSdGN>B_!wfxx zV6dR15DYmq8iFB&szWez&_T$SFhUL|iyF7;QPx}BD(oyV+$yXrcibwREHYfTM_F%h z-LSI+fNN}IJ!l*cSx-Jriu3}F;~>4r$BB{Gpm74^HTn2sBtA&w5fYzV1VDy>L~xNI zd!+1End9FoM#QM`S?ykP&Q<{>u?IkUngL0;I1rA_UThju3(L6-SGD9|TmxHM=70w)0Nyo7Ust`TSf2!&s7a$k{s4N6S18ss} z9z%U07#8Rq#QG5w2VxC^20^R=r~<^A7TON6#)Yy&tf`eQnFbw$0i!coQ%7B;;7~#cj!iF6HE(9=U;P?>?1svnTjDcfp*dlO@50e9q zv0$a-r}V%TTMrFz#oF^<{y;|Nc2;1Viei}d`b;<41^OQv;yGI5L$t7dc>cA5oz2e9N0FXN&uq+`X0f) z0e!fz_dp*uYzFAVhe-l`Sg<;v4-e)E^!*pEyvB5fFXc$-r ziDm=qM4`>Vr{qwUKsX^nH2}_oPz{9BA^HN~l8C-QI3D6z0GtEyEO3Mi*B2Y63lw0( zCV&Ebm?%(y1*-xI@L+C00S@d8C?J550-qkiqJdAiFm>P)Hf#|1gb&LkKcxezz!*HB ziVgDwsA9#C0#)o7Ccq7h!2#UZFqD8Vbc6;x9*lkt9*;&lfX9Q-%;51zv@v)*6fFlH zk45`|MFP-)CJ08AUpg|wDwweMVNS_U^WKJk>0fed)HTLJx5lsHxdaCGiN8?;D3ja2q(^mirr1&)|Fx0*Z)Uf8Aitc zuRiw;K2fHkQ=$2P->Jd#lMg}9+wAY;583`3PyQc{*M0_vMI6hRv&@<^;&*#WD4RXU znzNi~1BG{tg2IHv-{Ouq?tB@Qw_WU2-mOKQIxdMeyZH_~vdp&+U$7jWe4}TeKdk$J z@pl|PlfH1IrQH{#_$w?rwmK%rHA>0 z9ed8CU3d1t^n0Sk?*7vyR^m+p`e7LbL$v&@3 zpXWXKZKYC4Nq3;eB12+AHlZ^#%Qa%Q03*q z-K7X8=RHQHINHVIWBTDr<Thfh*e_WnKzPKef466Ex3DLJEVTz ztbe;xO<+>5kh$m~^5x!xsYP$}lJ?ETNj@lJm5G?}h@H$pRdJ-Wf6gGTWu9(hbI6n>Zz}KFsr%=H--p7@OT9l*qBB;(R?VpJ29H5&q=|^* z>w&%W>)D0p%w=l!efRgxS$Pr6h4cOv74~wJp~v-CrNnUf-HGNz06%N@h12*e&v&JzO|ybGI;O=QLD7q9ZztXs-;7np<(ofG zM-?5-w0i(5qcxbhp@37_)PsS7^_9ko=8P^2~%J)Tw`IPB^sFpLmz|eDcNl9J`?KcI-WH ztLcyHw-fuj^g{Ws<0;pUw|`snnblqF{f8REg@?cKpa0~x48{HySXcfla6(yyPRgUK zx2m#JthX>MZG3SFeLE#e*;;Ce5Ij4uKU__NePQg)F#5Dm<)hYAb~3LXG}bpPO~+e0 zVqbev;1ahJ{k^^SYPlk2H3cR?<<=!&A}Gh(RXnn{Hq~U->AfH6219#UrdJsS#&r~p^H+o~F(b`o!Hv1AN{^PFzqT1t z+xSpuR^y$_EbD(SQLLB?FsZCmqFeydl%-YlbL`Z^DX>Qvy|hRxZ&&puDNp(u6}!tdqt zqUNH(+Z0?Bpf4JY84}V(*%?xq0v>-JBo0S1H@fGIGL&OKGBlN~$M}Z0un;KG&)$NHhc&IEkX9$0`aoe7QU?B6d`1C55$sxSLTr#&^|V{3#_B?!HG!`g!IHs6~M)b=9y(lC2I zN1bP6_4;{9p5`(7`&>GNIloaVD<}JluT)p`byRDjn?q^0mM=HnHr@T_8p>_qL_I8$~ zJMnNN{b!DU@F*|;*vYS#iH){Y z{dTAa<%*olKBV35-ad%Q^DdnT$9DSHxRZF>;@<4XrGDE}_QQSPmPg31EB~81VF$df zRypd!7tt5{eI^&m|5C2mFH`Oa_Zh?*nqFJo>1wqwM}y2Vx9i3+(%RIU{M*L0vK#!L zcJh)I=F;nvTdM`Vx^dW}L(nIbhp19_O@N?g?_;aPFQdeHeb%*Kp`E75rmE2%ItJCq zH{Dh)Au2oV(L=(gG$;IQCYpPj_%y^<>FW-G6P@KA<)4cH<+Swp5 z)8bIDT{ZchD~WgQGS0wWzP;1CVD9pz%9Val=su|;onl0Wvf0Gn9_dA1+AN%#GygkHSOkA(XwQP{k zkdndJ$gRrHNsMC({z<&@-oq`mZHtJEG+uDuOxn@7m5FI9E0qlQQg*19TC$jT9(;AT zH>GY7g*Yu%IkWqhZjKRb8$%yd3iOd-en;5_iJPRkT$8cj-!b?l+B=Mz-IbM>TJUhhIgMrz#j zVt-la(st~Cj1B$Wz??lT>KHwC`)hnwQM(}2%&=5F@{8?|fhQTTz+%%UAP+Cq5KMgr zf0F)Ms%vi3lTVOmCvrs1+P`gG*+kgG8fG)R?QG~7l(V!sG%zI5WB;Y+?hb!~TGuob z)+IrIc%fYwM489`-qE()^E{GxYbeIKOJn0><*xBvW%FTWYKS}Ps6GL9GoZf~uTpkQMcuXOrHqJgZl4m5<)(SJ|S{%m;hc^(*k2 zZ?3{K5B}ZksLAb7En4m;wo6fdb$hq)r9Sr0$U`N^@b}|}n|xY|RJTL^sh0M}nbp>* zD&7mZ1n-xhymEgR9o5?z)hkw)7&4!e5ywpCE*n3#RN7lBJq^WOVu^RWV|^H9$&%lR z9nrWWzdpMM06v(eUnHabnY<1tdHOY(kqhU#ugpB!$IGD&sD1u|%T z3QSZ_L?%SivT}Q)BwgC4$IlWXNllWY=xI+9IojM!rsZYa15?G?_03-DZ_cpQeAl?& zOGDrld&<=;qE5Ss28K#e1#p@9Y!Bi5FtaONy6vlCKC>!SOXq0nQ=#SRRT9x&oQ0ju zv1t>`nBzdi613U4t;SL2^+Oj2z3$spF}&GmuQ=x5+#)%Z>W;YEbb-e*RdCZv`G#HL z=rUHisY!y|wiCI}saRg$JvgPAE=C}P+7)A^rMMMmTm)Z=ADcZfp&%N|bOQUO_dg}y z;w*qU3#r4}EH>%8s{01!>$Hwe3_H&U?LQuUNg?ZyO4_CW9#_cposZ2kEm?iMa>;Lt z+Gf{fDqwVzhDTS4(B$+vO@)2B>E5{B94qhMxID^o!@#4gwBt+YJa5^k`IL4rJDspF zPbW=Gli@LNKXyMaSUv)hHd?Sxk+1hdhxMz9|6X@6rHI=*rpCE|9My%fx87UWrOSso zQKb#)O3N}or>BkV>v!e+B$1oasE2-6^Ut>1dCrB9_#U4Sf2x8hK zblqV;b(w?v_sE~nFteP^T^eZ~ucKt^l4kkJu>1`Ab)9-BvG{=HP@LbIdTF9o_&T2> zQRE^!(qZzh*!DHmHtC)86{lLT*|jr$Uiwq!bfT&Jw~jvB7jFE|%aL#P^c-4V9o3lp zSh`j?%8_sLA7@$4yk@`My(15q_giTCfs*sNULX9g;r;us;a&dM@LVBJTi%OyY2UQI zVJ#oTV&b@|bKDQBE&Y=8Hj39G%;s+5%XgfcbmH>*#oFZN%~|{K_Sp;7VWEG?crLGW zI=_p{FVxMTI-1mVqRCusH!hj8_Sj!r7*uXr{o(4}*9sAF)QwGImNiBnl$48%v+em9 zrT50T3ALxaWS46+_>{`zI^u2LXvBtY~gu? z`@4$QltW{m+bc5G#e=iB@)cZW?2xF^lb)U{q5BM#+S|p#1Sw6}?P3U?nl65Wc8R6L z3tko(3@^(e*YH~nj!LZ(r_UnBt1(JtM&3c!N)P#6gSm>qL>Y&?T91*NsEwZud?Bm_ zV@^-yo|+qu)g|mErRP+NPfqlM74d`NbCy3rNIjEp?^((}F4D zcZ6jaT`jwN%d2Ns$!BiF+^p+7i=lh(kT z(&NQc^!;rkq6x8UtNo*Pi^1}vT>Zz-f6ngaZqKKJSygjZbmy%|*U zpkC`yx4m6FMCp&ecyC!vR%gsi`nvngLsW7*a~l0?rC1AcrrY-`GxtZz%uNz&pIz|Q zJds^a(_XYHnlW^BxY4uPlxDEKyAh2j(Th&ZNRDmbcAiC&Bg zeo>(sdg-b$@oS7Yaj~cNoO4r^W-8~4U7H5IK&Rx5-4geGm}qK3Rr?n8U}zBZro?-! zu-fM0mGmLQia%eSdCHa;_3DMv!r?QMj$ehB)^;ZXu8E>Nm*XC)`>vnc`rF#KD~Mye z?`#eZcZ?dQB|b7cO+`iKI97NN&fqcsx!Jd&rd+jMv7%hB zKyH7=E_QyGHr}$fx7@PcB(v8r7^mL;^5zAZL=882+~ zPI@c~;?HuY-Gxi!_ze#q%X%>HWCEQ1*tOD54I> zSi^QjS2`wIyE`gO^NnWAdA_oqSLO0&7QTKaN9rP?WYnT*4ZoBDQ{MLFpVm;pj!CuJ zN@ax)XWurTe@CkmsBTP)JDtD}BJS7*i&DEzJ9Xq`kP>05uR|=$^J4xsK=svwBV~5g zSVD0ZwAKZ6^v5`T{U>T!TiX^Vs!vr|O+3F|=qZznPwZNWPj(_TjbB)db9o)-C#DTY z3h#RP&9v3$v1C#H{(e2!idKjgn=ymTO^(d$-C`!&-XphwJl@-B-;m4!4=-oAf0Qab3+y(Vr;k zxb&LYBcgoEZu=Y+^fNd4QvWRQk8w+VMWvZE333Wu;T+6x$X_erWJ?Dn;eU3-r3rCA zurExKoJc7*vN|~Xz2*UqY0=?+!T%3qn7!*$KUPhWUy|Ic(o+1BuyM+2(rEhy&}{!J zlg~7#wr7hxA) z4iDT#2H}PyP75|kCM!9$-=%hMmuRwl+PA=G%Tyw;IHprZj;}+ zF~$hm^{4I!xjMWrUR(R-dvm1Ve{mK4epqezJXvxx-r33LYdHOwo~7SLTAyU-P8;ih z|(SMb0lBJL3j{Gj?KJ0#+Wz^&_mK!6VIbCYJ zyUzCbX7@i;%m3v=xqRHNNI0`siTYu-_V(oa2I-@Yr{LyYoON;&LDSbon#F}t#HESs z@ApshI8+3tqPvGU>>K;sr(WOfZVT0!8}3*(?vu4E^%3HS1bZ_wv>lG05iJe-eDYNs z5cMQ8FW|bXOYEP2d;d49cW;AL=s5PwOPLQ+WGY`5UH-N$3yr|D`mXZSEb}1Ebp};2 zZx~c4UeA!KcFX;8naXo^vY@Q2W6>h-Urll2cV&TJ^elBJ->gnf5)wbj39k@4@>VIH zig~VEO>KVsmNFV25*?iXpCFE%I?9I&=}_kzYd&!p*M)H6iZA%4Cg z^_UHh@tMGSbULF$kNlE!ojW38IA83$&G5C{XzGX3{MLx4yNW*@sK;ii?NwD$^Nzdq zP``xVPe#)?Q)-G|7jkms3NX9OcxmmF`Met{Lsf;u>Jf#C3+`ReGcbOgU3G+rz0vqQ z^l|j)w{o(&r&x&OvGJA)C|2%)IGq_hZs+Jiy~a#fGrGBepaV)&x<9? zP|;5s6M%E%4L#u`Z~6WGAp>!?eE%r~R zVxWPKYK|4^RP;r=@(jT@aWf09)EI%|6$5Qn`DDH^}7iz!t8 zV0>RmynGU$@8Egwp0Mg}zV5Nw9eO*}>GTt2n)FYuWjdGRy}x$z!F<@$D=oyjLLYDc zvI=d#^B2zu!DmOj8_^M0`n?4O^Q%TD)p>N*$;Y1~P`HL942O(rDVZ-SDfl94CebR~ z)zWG=Z`v*Xm92!Hh@GD|ZQ!3l?-VEm_LU-CaoY);#1L(UIF>Zpm9bB$wu;WH(#z<# z{<$x>q%O$1WS|_sM1N5-P~Udt#JM+KBG}x5F?#o^bJEiFlDIO}9QbmmGSzG{E9HcX z{A714EB0^N7!^1z@-Z=E>Kj*MYf9Kc?#8OEoyQ*eC6%x=>}~p9^y9@uSsH|@%k?j& z9DGjCoCE!f2n@DzRNa$ut?Bu-Q(P@~#6J?ZGvXrDPrF<7vgg$BpH<<^$_h7UyMfRQU ziRRd4{IJ1{Til?(Y|90k7Mzs!_h!ot&`uaM8+yFoT3sm*U(ZD;J4|#cbvl%qPqvmA z;}4M5vruI=KGUHpm)5nrH!Zug58nvtew?guM!GFj=Ju>R@ooFvTVrb@+r`mjkATne#68gJEQS)QA>(3+oL$m31+?CS@(lK{imp4 z!pb5xlN)iN>1WLX*hcc5AMaQbDs5OvUf)_&2Tv7#mS?0zS}AKP5qKmb#pj!51-C?& ziSV46-zd_bRTMV&i8YCTndxXymjb9|rGK~&o%~q|%7T&a#J!8ky&6{hh zh1mPB2TlJD|DD*&|Bm5|4$z(@Phx+Rb|6Ww4)2^hc^;9t!x_rqNH$+3|I|mBXG8mgu(^Lo z((0J4!1l4i#cRr?Li@`VbZzTf+4>wumM$* zzXUlqm&9~rDt$3k&!O^CMLejQ>4xJo-R+AaVUrq(IjhdARWk>s?Bg}`cysRGVb8$4 zI{TTG;7FU6M~5XrLqp7-lw=yTZRcBg!QX(fUNef4&L<^xBm5V67sVBX7t`FbIU`_< zSDnk%PfW8`f$&E&gGVZI8yYu3Hd~3Nv}Ieib7f!J>e@p!VX%s8(+}Gn!R+Eu?_RU4 z#QZZFloT0G_jp!vWOKt=^I37Kbo&gnxq-$Z$DQY%qRS4cDTq`?flhUx1}6H zo=B9t7|$p*_SjkHOKCoLT}A_WtfXtnTa=-Wk^PT>H3YCEc2G+ybW#yt9G#XqcwuMV z&`ZH)HL)k=;OJqk;@<7lRgk!E*!!V#HAiwm<7xJ~XSrkz&(q7W2D^q5g8|)|&I>%- z6R7tL{Mk<-ii)d~4L6tWpdpEyWC9Jf7h6UG;Z$LlJ7r5-Ubvk+7f;8y)HiseQa-0W z|CeVo=kfYijJhY&x%9I|rf+PFR#p{jo@18*_c-D3}`xo)FoLN1IWC`$I0e_Pq&^W-#;-)Gys zYGIeDlR=pz^s-=aUu@1<_xS0;%lcL~QWsl+S?Z9N)CCikbv)Z#{E?N|-pxQnq6Pme z4c7s=%OC6YBVMaV&d=k*$p>-GDq^ULH63yE`_Aw<&NmJ#t9T zqeb-_)Cs@h8L~gJ5Pf1IDf=~F;rm%Mrd!J?C?JYmc)u@nQyl~D_!y4KMC$1PM}RqH?P@U7Ez}% zoPrJR4lOReRF$qGOnMV{B8O$wc)W`_qLZf zA5ToJ#7DG(TBZC{?yRTPpC*FkeZr^WNwZ5SR#hqapI>_!M49agl&XGIc8=g?Qq_N= zelXYmq2lI;s^E#T=kb_$BY`lt;ut~M^3Eho*}Bv2h} zjs-#u0(z;5^_zZIN2_T<(>tQ|$gJ@ZRR`fm%3(7 z3o4jz8&?o|4scx3lo-Ro%O56@ylTq5f18hXcr8@g#K+>wkz?SAw``oTBjV)} zvyDWU^GgXbKH2WKF@rke??@JE%uM-L8WpUamzHww{Ff-(S0s=Yc$mwFgSQMdgbjf~ zfuC^PNsVY3$qC5CmHyt){O*C?&aqPx9OWOz4ls7;zpRxJRRsOD3@6zSzAqU zkGyx5*4Jpd$6JPbmr}JJRNertpV~6gUgX^?59Rx>yVBI6-rM`kKNJm+eujlTDN zbKfO&9ed~o>{_=;rTvPzS1!r%AAfn;9Z4y~s6^Z1{=ej%+y5bFo%|0u>N}yiRonCx zi=_L1%9$kY7)L!Ps2bN}|s=w8_&5CTrpr&MM*sDE3`JYu1#PMt4 zoXhIQ?f66-M7rB$LtVr>nbS#pLK;t~i~p63Y543rX6(Go(do=j2eBfGif` ztgWZ*7QMN7h3u1+%`CgGpp#UAuOX^OZjMKfMbDH*&ukC*J1n$4JRLrk%#b7$t6Vc2 zbED+pN{O|KH{*uy9KR$VF}cm=lUR~^a^ATr2LEK6GPD>wAncNV+C@|oLiSH}pVC2c zv?AR){dqL}HRRrFUw=nxjA$;QJJh1_R)=3hIk8;xlb5x|fU3^COcKr8tG{J#lLy)K zRW);FiVTr-&3tx(GSVBhPWB#=rk%d>H@4!Ru(|lXlZ*%X)tH^4zljcdmmBawf^DD8 z?0B``_lxIE?M%_l-O#VWBSt&9{QkBp*u zFFG}A=x@?iPWe`s zsr|6kyW9S}LUjJUl;w&Q7f49mO6?AQ@}iI5GcU^Z<8FP-r&%W7C$bEG{q(w&kj?Qe}F6Y|NIuw-6ps9 ztbwOllFxKUj!WE=;&r~1>$7NX7_QuxIQgYBWHNI@cBOcu^@ck4M?+lZ&$Y)_3$6wZ z|AZySTkG5&I|#=tNxrM#6+xCY+uj+KBCJbyrxJoVZb|QK-Z*^Np*!xN#uYjUn`Isl zv+qrc$E;Di(RG&&Y+UoS>O~FdUme4P3;*`u&-sn=v~GT`ujSQ-2?*M)?;o33=A&$7 zZ_`x!KgXVP$H>S3jE1FL97nT;k`8|_DQte?AtK`33VZ(S=(Ts@A$4hXm-w z4fEqqddHS8ejgn!>7|MtYq@Vbhm15}`ajy&W!yLXxG1MdlltzCzAGwA@GdJfzBEjq zU*&h3!aXiWgg_lYSO^#!{sIuDi09y7PS%sXeHapv+r#qDbd<%zki5pE8Y6K zy}>C{Tof`_Z>=l+Vl1r1!o@MLOz7&-#)x_m%Ju)!c=@n2w4DDloP#s$!PWFGn6`gl zRJCa0)_3Fl(AF@yqXIpYo|RnM)|-EEG`IIqh1ol;^xtnfof7k2nx7OgeQWQ_uhjSZ@F_Tof zBE`~FM45?C8~86>yr-?RYxKX;e&c&MuL)E&KM7Qo7|jr7nLN8lpD&U}ak%vhwp^g= z-9KKv>Cb7;STTa)&0)7~+0Z3*S%h0$4dQA4)TfpkGu>v{c3hBDeic(#(eOCF~) z;c!g$_&}NKEHuC2wYbSfFXd;c`u)n4mWMLbZ|l41LX)TUN-xjobGCx7WL!m#?C^>?R$IyQ4EeC68Q4=Ot`Fqha7J4Oa(c(fU#EHgP*ca-e9mLI SmA(3a1HZIex_8EU^nU>gXL!c| literal 0 HcmV?d00001