Index for BIP 157 block filters #14121

pull jimpo wants to merge 12 commits into bitcoin:master from jimpo:bip157-index changing 18 files +1240 −22
  1. jimpo commented at 9:28 pm on August 31, 2018: contributor

    This introduces a new BlockFilterIndex class, which is required for BIP 157 support.

    The index is uses the asynchronous BaseIndex infrastructure driven by the ValidationInterface callbacks. Filters are stored sequentially in flat files and the disk location of each filter is indexed in LevelDB along with the filter hash and header. The index is designed to ensure persistence of filters reorganized out of the main chain to simplify the BIP 157 net implementation.

    Stats (block height = 565500):

    • Syncing the index from scratch takes 45m
    • Total index size is 3.8 GiB
  2. jimpo force-pushed on Aug 31, 2018
  3. jimpo force-pushed on Aug 31, 2018
  4. DrahtBot commented at 10:47 pm on August 31, 2018: member

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Conflicts

    No conflicts as of last run.

  5. in src/index/blockfilter.cpp:77 in 1c2079125a outdated
    72+                std::make_pair(pindex->GetBlockHash(), filter.GetEncodedFilter()));
    73+    batch.Write(std::make_pair(DB_FILTER_HASH, height_key),
    74+                std::make_pair(pindex->GetBlockHash(), filter.GetHash()));
    75+    batch.Write(std::make_pair(DB_FILTER_HEADER, height_key),
    76+                std::make_pair(pindex->GetBlockHash(), filter.ComputeHeader(prev_header)));
    77+    return m_db->WriteBatch(batch);
    


    leishman commented at 11:31 pm on August 31, 2018:
    Out of scope for this PR, but is there a reason we can’t write a batch of entries for more than a single block? Could we write 100 - 1000 blocks worth of entries in each batch write to speed up the migrations? This introduces some complexity, but perhaps it’s worth it?

    jimpo commented at 6:10 pm on September 2, 2018:
    Could be worth looking into during ThreadSync. As you note though, it’s a fairly independent change.
  6. in src/index/blockfilter.h:15 in 1c2079125a outdated
    10+#include <index/base.h>
    11+
    12+/**
    13+ * BlockFilterIndex is used to store and retrieve block filters, hashes, and headers for a range of
    14+ * blocks by height. An index is constructed for each supported filter type with its own database
    15+ * (ie. filter data for different types are stored in differente databases).
    


    leishman commented at 11:33 pm on August 31, 2018:
    typo differente
  7. in src/index/blockfilter.cpp:244 in 1c2079125a outdated
    239+    auto it = filters_out.rbegin();
    240+    auto encoded_filter_it = encoded_filters.rbegin();
    241+    const CBlockIndex* pindex = stop_index;
    242+
    243+    while (it != filters_out.rend()) {
    244+        *it = BlockFilter(m_filter_type, pindex->GetBlockHash(), std::move(*encoded_filter_it));
    


    leishman commented at 11:50 pm on August 31, 2018:
    Is assigning to the dereferenced pointer here instead of using a vector function an optimization? I can’t see an obvious reason there is anything wrong with this, but just double checking.

    jimpo commented at 6:11 pm on September 2, 2018:
    What do you mean by a vector function? This just seemed to be the most immediate way to do the assignment to me.

    leishman commented at 7:40 pm on September 2, 2018:
    I meant using something like insert instead of direct assignment. It’s been a while since I’ve written a lot of c++.
  8. jimpo force-pushed on Aug 31, 2018
  9. in src/index/blockfilter.cpp:12 in b384c3e86c outdated
     7+#include <dbwrapper.h>
     8+#include <index/blockfilter.h>
     9+#include <util.h>
    10+#include <validation.h>
    11+
    12+/* The index database stores three items for each block: the encoded filter, its D256 hash, and the
    


    jimpo commented at 0:02 am on September 1, 2018:
    s/D256/DSHA256/
  10. gmaxwell commented at 0:17 am on September 1, 2018: contributor
    Storing large records in leveldb is generally a bad idea. Is there a particular reason this doesn’t work like the undo data?
  11. jimpo commented at 0:47 am on September 1, 2018: contributor
    @gmaxwell If by that you mean writing the filters sequentially in flat files then indexing the disk positions in LevelDB, I hadn’t considered that, but it may be worthwhile. The downside of course is additional complexity. What are you mostly concerned about, read or write performance? I’d want to benchmark reads and writes to determine if the DB value sizes are problematic before making the change. With filters on average being 2% of block size and a LevelDB file size limit of 2 MiB, each file could still store ~200 filters (ignoring keys and overhead and such).
  12. laanwj added the label UTXO Db and Indexes on Sep 1, 2018
  13. jimpo commented at 1:45 am on September 3, 2018: contributor

    @gmaxwell I put together a (not-production-ready) branch to test your suggestion of writing filters to flat files. In sample size n=1 experiments, I measured that the time to write the entire block index was <1% faster using flat files, and reading 5,000 sequential filters (starting at height 500,000) was 11% slower. The total storage space consumed is nearly the same (3.4 GiB total). Happy to provide the log files/iPython notebooks I used if you’d like.

    Given the additional complexity and absence of significantly improved performance, I think writing filters directly into LevelDB is the way to go.

  14. jimpo force-pushed on Sep 3, 2018
  15. jimpo force-pushed on Sep 3, 2018
  16. jimpo force-pushed on Sep 3, 2018
  17. in src/blockfilter.h:57 in 9b1e7a6cf2 outdated
    53@@ -44,19 +54,16 @@ class GCSFilter
    54 public:
    55 
    56     /** Constructs an empty filter. */
    57-    GCSFilter(uint64_t siphash_k0 = 0, uint64_t siphash_k1 = 0, uint8_t P = 0, uint32_t M = 0);
    58+    GCSFilter(const Params& params = Params());
    


    practicalswift commented at 7:35 am on September 5, 2018:
    Please make explicit :-)
  18. in src/index/blockfilter.cpp:112 in 9b1e7a6cf2 outdated
    107+    return true;
    108+}
    109+
    110+bool BlockFilterIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
    111+{
    112+    assert(current_tip->GetAncestor(new_tip->nHeight) == new_tip);
    


    practicalswift commented at 7:37 am on September 5, 2018:
    Assertions should not have side effects. Please move GetAncestor outside of assertion :-)

    jimpo commented at 10:53 pm on September 5, 2018:
    What is the side effect? GetAncestor is a const method.

    practicalswift commented at 5:42 pm on September 10, 2018:
    @jimpo You’re right! Forget my comment :-)
  19. in src/blockfilter.h:67 in 9b1e7a6cf2 outdated
    68+    GCSFilter(const Params& params, const ElementSet& elements);
    69 
    70-    uint8_t GetP() const { return m_P; }
    71     uint32_t GetN() const { return m_N; }
    72-    uint32_t GetM() const { return m_M; }
    73+    const Params& GetParams() const { return m_params; }
    


    practicalswift commented at 7:38 am on September 5, 2018:
    Remove GetParams()? Not used?

    jimpo commented at 10:53 pm on September 5, 2018:
    No, it’s not used, but it feels like there should be a getter.

    practicalswift commented at 7:40 am on September 6, 2018:
    Unused code is untested code, so I suggest removing it or adding a test for it :-)
  20. in test/functional/rpc_getblockfilter.py:21 in 9b1e7a6cf2 outdated
    16+    def set_test_params(self):
    17+        self.setup_clean_chain = True
    18+        self.num_nodes = 2
    19+        self.extra_args = [["-blockfilterindex"], []]
    20+
    21+    def run_test (self):
    


    practicalswift commented at 7:43 am on September 7, 2018:
    0./test/functional/rpc_getblockfilter.py:21:17: E211 whitespace before '('
    
  21. in src/index/blockfilter.cpp:16 in 9b1e7a6cf2 outdated
    11+
    12+/* The index database stores three items for each block: the encoded filter, its dSHA256 hash, and
    13+ * the header. Those belonging to blocks on the active chain are indexed by height, and those
    14+ * belonging to blocks that have been reorganized out of the active chain are indexed by block hash.
    15+ * This ensures that filter data for any block that becomes part of the active chain can always be
    16+ * retrieved, alleviating timing concerns.
    


    Sjors commented at 3:45 pm on September 7, 2018:
    Can you explain this? What would be wrong with always indexing by block hash? Especially given that getblockfilter takes a block hash argument.

    jimpo commented at 6:51 pm on September 8, 2018:
    Indexing by hash is less efficient when fetching a range of filters or filter hashes by height, which is a common access pattern in BIP 157.
  22. Sjors commented at 3:47 pm on September 7, 2018: member

    Concept ACK.

    Can you mention -blockfilterindex and getblockfilter in the PR description, as well as perhaps add a release note?

    It’s nice to be able to quickly delete an index, so having a separate file for each type makes sense to me (as is the case now: indexes/blockindex/basic/...).

    Lightly tested on macOS. I get a few mismatching headers compared to the test vectors:

     0src/bitcoin-cli getblockfilter 00000000fd3ceb2404ff07a785c7fdcc76619edc8ed61bd25134eaa22084366a "basic"
     1{
     2  "filter": "0db414c859a07e8205876354a210a75042d0463404913d61a8e068e58a3ae2aa080026",
     3  "header": "c582d51c0ca365e3fcf36c51cb646d7f83a67e867cb4743fd2128e3e022b700c"
     4}
     5
     6000000000000015d6077a411a8f5cc95caf775ccf11c54e27df75ce58d187313 
     7-> "header": "546c574a0472144bcaf9b6aeabf26372ad87c7af7d1ee0dbfae5e099abeae49c"
     8
     90000000000000c00901f2049055e2a437c819d79a3d54fd63e6af796cd7b8a79
    10-> "header": "0965a544743bbfa36f254446e75630c09404b3d164a261892372977538928ed5
    

    The filters do match.

    We should probably include those test vectors. In addition, it would be nice to have script to compare every single block with the btcd RPC, testnet and mainnet.

  23. unknown approved
  24. unknown commented at 4:52 pm on September 8, 2018: none
    good idea
  25. DrahtBot added the label Needs rebase on Sep 10, 2018
  26. jimpo force-pushed on Sep 10, 2018
  27. jimpo commented at 7:36 pm on September 10, 2018: contributor
    @Sjors Thanks for testing and finding that incompatibility! It has been fixed with 775c160ee266bc61d1dcb6f35265354e3f9f5dbc, and roasbeef or I will update the BIP to clarify this point.
  28. DrahtBot removed the label Needs rebase on Sep 10, 2018
  29. Sjors commented at 1:37 pm on September 11, 2018: member
    @jimpo ok, those three examples now match. Is there an up to date Btcd branch that can be used to compare other blocks? cc @Roasbeef
  30. jimpo commented at 4:22 pm on September 11, 2018: contributor
    @Sjors There is nothing that checks block by block, but the filter headers commit to all previous filters in the chain, so comparing the headers at the chain tip on both change is equivalent to comparing blocks individually. btcd also has an RPC for fetching filter headers getcfheader.
  31. jimpo force-pushed on Sep 11, 2018
  32. in src/test/blockfilter_index_tests.cpp:130 in d4d3ba7ceb outdated
    125+        std::vector<uint256> filter_hashes;
    126+
    127+        for (const CBlockIndex* block_index = chainActive.Genesis();
    128+             block_index != nullptr;
    129+             block_index = chainActive.Next(block_index)) {
    130+
    


    practicalswift commented at 7:58 am on September 23, 2018:
    02018-09-22 21:14:22 cpplint(pr=14121): src/test/blockfilter_index_tests.cpp:130:  Redundant blank line at the start of a code block should be deleted.  [whitespace/blank_line] [2]
    
  33. in src/index/blockfilterindex.cpp:182 in d4d3ba7ceb outdated
    177+
    178+        if (!db_it->Valid() || !db_it->GetKey(key) || key != expected_key) {
    179+            return false;
    180+        }
    181+
    182+        size_t i = height - start_height;
    


    practicalswift commented at 7:35 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): index/blockfilterindex.cpp:182:27: warning: implicit conversion changes signedness: 'int' to 'size_t' (aka 'unsigned long') [-Wsign-conversion]
    
  34. in src/index/blockfilterindex.cpp:197 in d4d3ba7ceb outdated
    192+    for (const CBlockIndex* block_index = stop_index;
    193+         block_index && block_index->nHeight >= start_height;
    194+         block_index = block_index->pprev) {
    195+        uint256 block_hash = block_index->GetBlockHash();
    196+
    197+        size_t i = block_index->nHeight - start_height;
    


    practicalswift commented at 7:35 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): index/blockfilterindex.cpp:197:41: warning: implicit conversion changes signedness: 'int' to 'size_t' (aka 'unsigned long') [-Wsign-conversion]
    
  35. in src/index/blockfilterindex.cpp:237 in d4d3ba7ceb outdated
    232+    std::vector<std::vector<unsigned char>> encoded_filters;
    233+    if (!LookupRange(*m_db, m_name, DB_FILTER, start_height, stop_index, encoded_filters)) {
    234+        return false;
    235+    }
    236+
    237+    filters_out.resize(stop_index->nHeight - start_height + 1);
    


    practicalswift commented at 7:35 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): index/blockfilterindex.cpp:237:59: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  36. in src/index/blockfilterindex.cpp:170 in d4d3ba7ceb outdated
    165+    if (start_height > stop_index->nHeight) {
    166+        return error("%s: start height (%d) is greater than stop height (%d)",
    167+                     __func__, start_height, stop_index->nHeight);
    168+    }
    169+
    170+    std::vector<std::pair<uint256, T>> values(stop_index->nHeight - start_height + 1);
    


    practicalswift commented at 7:36 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): index/blockfilterindex.cpp:170:82: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  37. in src/index/blockfilterindex.cpp:191 in d4d3ba7ceb outdated
    186+        }
    187+
    188+        db_it->Next();
    189+    }
    190+
    191+    results.resize(stop_index->nHeight - start_height + 1);
    


    practicalswift commented at 7:36 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): index/blockfilterindex.cpp:191:55: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  38. in src/test/blockfilter_index_tests.cpp:209 in d4d3ba7ceb outdated
    204+    }
    205+
    206+    // Check that filters for stale blocks on A can be retrieved.
    207+    chainA_last_header = last_header;
    208+    for (int i = 0; i < 2; i++) {
    209+        const auto& block = chainA[i];
    


    practicalswift commented at 7:38 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): test/blockfilter_index_tests.cpp:209:36: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  39. in src/test/blockfilter_index_tests.cpp:222 in d4d3ba7ceb outdated
    217+        CheckFilterLookups(filter_index, block_index, chainA_last_header);
    218+    }
    219+
    220+    // Reorg back to chain A.
    221+     for (int i = 2; i < 4; i++) {
    222+         const auto& block = chainA[i];
    


    practicalswift commented at 7:38 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): test/blockfilter_index_tests.cpp:222:37: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  40. in src/test/blockfilter_index_tests.cpp:234 in d4d3ba7ceb outdated
    229+     for (int i = 0; i < 3; i++) {
    230+         const CBlockIndex* block_index;
    231+
    232+         {
    233+             LOCK(cs_main);
    234+             block_index = LookupBlockIndex(chainA[i]->GetHash());
    


    practicalswift commented at 7:38 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): test/blockfilter_index_tests.cpp:234:52: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  41. in src/test/blockfilter_index_tests.cpp:241 in d4d3ba7ceb outdated
    236+         BOOST_CHECK(filter_index.BlockUntilSyncedToCurrentChain());
    237+         CheckFilterLookups(filter_index, block_index, chainA_last_header);
    238+
    239+         {
    240+             LOCK(cs_main);
    241+             block_index = LookupBlockIndex(chainB[i]->GetHash());
    


    practicalswift commented at 7:39 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): test/blockfilter_index_tests.cpp:241:52: warning: implicit conversion changes signedness: 'int' to 'std::vector::size_type' (aka 'unsigned long') [-Wsign-conversion]
    
  42. in src/test/blockfilter_index_tests.cpp:287 in d4d3ba7ceb outdated
    282+
    283+    // Initialize returns false if index already exists.
    284+    BOOST_CHECK(!InitBlockFilterIndex(BlockFilterType::BASIC, 1 << 20, true, false));
    285+
    286+    int iter_count = 0;
    287+    ForEachBlockFilterIndex([&iter_count](BlockFilterIndex& index) { iter_count++; });
    


    practicalswift commented at 7:39 pm on September 25, 2018:
    02018-09-25 20:53:15 clang(pr=14121): test/blockfilter_index_tests.cpp:287:61: warning: unused parameter 'index' [-Wunused-parameter]
    
  43. in src/index/base.cpp:155 in d4d3ba7ceb outdated
    151@@ -149,6 +152,17 @@ bool BaseIndex::WriteBestBlock(const CBlockIndex* block_index)
    152     return true;
    153 }
    154 
    155+bool BaseIndex::Rewind(const CBlockIndex* _current_tip, const CBlockIndex* new_tip)
    


    practicalswift commented at 3:35 pm on September 30, 2018:
    _current_tip is unused?

    jimpo commented at 8:09 pm on September 30, 2018:
    It’s used in a subclass implementation.
  44. DrahtBot added the label Needs rebase on Nov 6, 2018
  45. jimpo force-pushed on Nov 8, 2018
  46. DrahtBot removed the label Needs rebase on Nov 8, 2018
  47. jimpo force-pushed on Nov 8, 2018
  48. DrahtBot added the label Needs rebase on Nov 9, 2018
  49. MarcoFalke referenced this in commit a4564b9b07 on Dec 22, 2018
  50. jimpo force-pushed on Dec 27, 2018
  51. in src/index/blockfilterindex.h:48 in e6024f3810 outdated
    32+
    33+    const char* GetName() const override { return m_name.c_str(); }
    34+
    35+public:
    36+    /** Constructs the index, which becomes available to be queried. */
    37+    explicit BlockFilterIndex(BlockFilterType filter_type,
    


    practicalswift commented at 10:47 am on December 28, 2018:
    Isn’t explicit redundant here?

    Empact commented at 5:47 pm on March 10, 2019:
    Not as of C++11, because of list initialization https://en.cppreference.com/w/cpp/language/explicit
  52. DrahtBot removed the label Needs rebase on Dec 28, 2018
  53. gmaxwell commented at 7:14 pm on January 3, 2019: contributor

    NAK. Storing large variable size blobs is leveldb is entirely unlike our other usage, imposes differet loads, memory behaviors, and would make it infeasible to drop leveldb in the future for e.g. an open hash table. Creating an imaginary performance concern and then measuring that it isn’t small doesn’t change any of this.

    [I apologize for missing the prior reply until now– September was a bad and busy month for the project.]

  54. jamesob commented at 7:21 pm on January 3, 2019: member

    @gmaxwell what’s your preferred alternative? Flatfiles?

    It’s worth noting that each index lives in its own ldb database at the moment, so UTXO storage can be migrated independently of any given index. As @jimpo notes above, reading out of flatfiles is 11% slower than ldb per his measurements. Given that block filters are obviously a read-heavy part of the system, I think that’s a significant enough difference to justify use of ldb.

  55. gmaxwell commented at 7:34 pm on January 3, 2019: contributor

    what’s your preferred alternative? Flatfiles?

    Storing the like block and undo data: files with the data in them and file_no,offset in the in memory index.

    As @jimpo notes above, reading out of flatfiles is 11% slower than ldb per his measurements

    A microbenchmark is probably not particularly informative there. As leveldb is adding another layer of caching and memory use. Implemented correctly and compariably it shouldn’t be possible for anything else to be faster, since it’s the most direct way of storing the data.

  56. jimpo commented at 6:50 am on January 7, 2019: contributor
    @gmaxwell To be clear, you are suggesting adding block filter header, hash, and filter disk location to the CBlockIndex entries? This is an additional 72 bytes per block. I think this has significant disadvantages compared to the the approach of creating an optional index which is built asynchronously and can be deleted/rebuilt independently, mostly in terms of modularity. I’d be OK with a separate index (like in this PR), that stores references to filters saved in flat files, but putting them in the block index seems way too tightly coupled to me. Especially if new filter types are added in the future.
  57. TheBlueMatt commented at 9:22 pm on January 7, 2019: member
    I’m really not a fan of the idea of shoving more data into CBlockIndex entries/existing leveldbs, however I do agree with Greg’s suggestion that we store the actual data in flat files. That would imply adding a new leveldb which just stores mappings to offsets in flat files, then loads the data from there.
  58. DrahtBot added the label Needs rebase on Jan 9, 2019
  59. in src/blockfilter.cpp:252 in e6024f3810 outdated
    247@@ -248,7 +248,8 @@ static GCSFilter::ElementSet BasicFilterElements(const CBlock& block,
    248     for (const CTransactionRef& tx : block.vtx) {
    249         for (const CTxOut& txout : tx->vout) {
    250             const CScript& script = txout.scriptPubKey;
    251-            if (script.empty() || script[0] == OP_RETURN) continue;
    252+            if (script.empty()) continue;
    253+            if (script[0] == OP_RETURN && script.IsPushOnly(script.begin() + 1)) continue;
    


    gmaxwell commented at 8:11 pm on January 10, 2019:
    This change doesn’t appear consistent with BIP158. (“The scriptPubKey of each output, aside from all OP_RETURN output scripts.”, “We exclude all OP_RETURN outputs in order to allow filters to easily be committed to in the future via a soft-fork.”) Also I don’t think it’s a good idea? They’re no less unspendable even if they’re not push only.

    jimpo commented at 10:59 pm on January 10, 2019:
    Yeah, the BIP should probably clarify exactly what that means. I made this change because 1) btcd does it and 2) the standard TX_NULL_DATA script type is defined this way. In other words, scripts beginning with OP_RETURN and not followed by pushdata are non-standard. I think it makes sense to stick with this definition of “OP_RETURN output”, but don’t feel too strongly.

    TheBlueMatt commented at 1:02 am on January 23, 2019:
    We should probably match IsUnspendable (ie that it just starts with OP_RETURN). TX_NULL_DATA is just an internal thing, not something to mirror this based on.

    sipa commented at 0:17 am on January 24, 2019:
    I agree with @TheBlueMatt that there is no need to leak our internal TX_NULL_DATA template into the BIP. Would it make sense to use CScript::IsUnspendable instead (or at least equivalent to its current definition), which triggers on ((length > 0 and first_byte = OP_RETURN) or length > 10000)?

    jimpo commented at 6:28 pm on March 3, 2019:
    This has been clarified in the BIP and updated here.
  60. laanwj referenced this in commit 2d46f1be0c on Mar 2, 2019
  61. jimpo force-pushed on Mar 3, 2019
  62. jimpo force-pushed on Mar 3, 2019
  63. DrahtBot removed the label Needs rebase on Mar 3, 2019
  64. jimpo force-pushed on Mar 3, 2019
  65. jimpo force-pushed on Mar 3, 2019
  66. jimpo commented at 10:14 pm on March 3, 2019: contributor
    This has been rebased and modified to store filter data in flat files as discussed.
  67. in src/rpc/blockchain.cpp:2304 in 9ab0b7a32f outdated
    2298@@ -2297,6 +2299,82 @@ UniValue scantxoutset(const JSONRPCRequest& request)
    2299     return result;
    2300 }
    2301 
    2302+static UniValue getblockfilter(const JSONRPCRequest& request)
    2303+{
    2304+    if (request.fHelp || request.params.size() != 2) {
    


    Sjors commented at 4:30 pm on March 5, 2019:
    || request.params.empty() || request.params.size() > 2
  68. in src/rpc/blockchain.cpp:2326 in 1a52982bd8 outdated
    2321+            }.ToString()
    2322+        );
    2323+    }
    2324+
    2325+    uint256 block_hash = uint256S(request.params[0].get_str());
    2326+    std::string filtertype_name = request.params[1].get_str();
    


    Sjors commented at 4:46 pm on March 5, 2019:
    0    std::string filtertype_name = "basic";
    1    if(!request.params[1].isNull()) {
    2        filtertype_name = request.params[1].get_str();
    3    }
    
  69. Sjors commented at 4:47 pm on March 5, 2019: member

    I tested on macOS 10.14.3 on a testnet node that was previously pruned and needed a reindex: bitcoind -reindex -blockfilterindex=1. That seemed to work well, because it created about 8 fltr0000?.dat files. But then I restarted the node a while later and it generated another 8 such files.

    I did getblockfilter for testnet block 0000000000000014ef5083802666af5ed6966131154556cf09f9db0ede1fca8a at height 1,483,520:

    0{
    1  "filter": "8dc5594cd8195fdd5be5393cf7aee01e4dec7c155aed480f9f910659a6c65d60d71eabcb521310fc0799aac0451596534ed4265247f30e76015ac886b3946dd8143da6171cd9f146932adc9f755207ecf7354b63972b4e4bcd1467f96f0455fdaed2e4fdc2a427eb577771854de488878e8eb861d0d818ab9200bf3ef0734e4dad964e4aa3771f81db3997fb13ecfc4c2283a4723f41787dbab24dfe006bfcc85e227d17fb967f3577b89b0242d16fe43a858d31d1fa5396868d809d84822e71409228c73c41124d520580fd18524c8cecf152ccf274377d3897397edf0ba47e46336d7b8b43f1436c94d8172f2d8a06b2c23b90b9e9793d04aa3d756b0231af99b8ff21044666e813d3dc40ef7a2c1bb7a737a9246b9fb24c309374a705ca0526b7949f7df898702d0dac76bac30e64770ab79068c0ac83d24bf5876df74e81dda9b537e42bb2e8211e6e87f0580357216d9d7b0c9607d15517cb590bb86a7d984e4e34de1a40c76d4ca33657a4882d62717932",
    2  "header": "eaf0306f5c3856bdf202898fbabc915c855bb69eaa9df3d788b5f2794e6b2699"
    3}
    

    Someone should compare that to btcd (or with a p2p network Python script from a known node).

    Code looks well organised but a bit over my head to review atm.

  70. Sjors commented at 12:14 pm on March 6, 2019: member

    Mainnet result for block 0000000000000000001cd1a6b5d0d226d2af982c87d7c82dc033936af84c7d88 at height 565,900:

    0{
    1  "filter": "fd5927fd10419d25d44e3608af3c634ad45a6c6d7604a8ec4f587e7f0db41509cfdbffd588d25d4a8d5eb55155700285e8cbd266887ba7dbf357d1f6ac7aaa8748c945adaeb78ee6c4f1cf467b765a2d593352e65bc2e31bd3f7cf41fb6fbd10b6312b13b4831dac52f185403f62d0e4c92deeeefb23bc9565893fdc48c818e0ee5868febf07e21d2de51c6c7433de5430f1d0e2db0a9365966d8e8a420b4254756cccfad4a74b0d43dbe06d454c11c3da6523963dee995cdac584902929eee4db379d1061bede0ec204a2d27c6e58bc209b59cc3a8662588fd48635900bc48fadcc9e7efa500ebbde03ff22d0316824afc0b354d427da6014c47758ad2f7d198c98cd2c46e0134a58faaf0cd49d08ea2c8bcc485f92e61298321075868b6f12656d1f976c5779a9b45f169cb39e384cf010e444375331a2b8e2fe14c07e9eadf2c429ba30fc6e56eb05e4c209a09a1b3989fa84440be20ff5af2815c4e73cecbc6f171ba91829dfecbc4cc886b8c8fb59dba69b84740f3cc07ed8aa99a194effa2c8687fa8e71fcc2421014f92df79c761cd144dfb4840a24d87f9881fa5f0065684f8e0d553f7a9a002a15a38ab749591b112a0d75da19906948b9272b3727c6110d6dcf1af9b60d65f459f0e1f17bf74249bbd389b98736e37b0aad8d8a0534baff3cb9fe36a02d5cdfa494c8ac58ee73d71347f0e0fdbc8d22c2d942b3dead231c39dd78957dc2797bccf3d40c99601c6953812424e91cf94312e64d85dfef7dd0ee5aaa90d1ae03f8469860a42f822fc83e60ff2ae834eb53192287528f5d89d6f294239a29d5dd50c69a01ea37723dda5e231f7922851df5dc32133290202b0db6efd725e7a025cc9550d274e7a614eb8f1060dc6f263ccb96536b75575966f17b27c5002691a0a315c63feeb7df971276b25f9b6f202c41755e368f2a8c1e726a460a0e4fb100786920cfee01087cebc1a3fe045ffba16495e817b86802fd7bfce37dd07db0d3b0f36cbb1d531c2a571bc0399af10c8c2c4e5d2eba362bebf0cb4ef71f1a11d71e5a8a0c4d56d19cf13a87c354a62e69db909d38a8647b7e6273332f0eb6a9ca12afda5c885eba40bc0fa2206e929ff84457a4bd9375ad8f0784d6b7fa451d05e467ebf36407a30acb383c55ab9017292ee304bd3d3a74c55c59a7b552b5a5f73388641f1f64203d2ea27c6bf6081e16cea29c067594cb6573dccd3a842b67804fd1b59643887abf287aec4627ca85a9c708b07655ef18058c43c09adeb70274c2386ad52d15a83470d26b658cd3871cbd7de0d0cd60009a13bbad1873dd20cc542ca3c4fd2c66ddde3f12eff31fb9aaa5609564312fc200134e5c0428af61dc30d1aac08542c21bb851a2d07fa939c1fe0c10810463285ea9d589fe9d4da586c2bc9df0bff0b1c80f60ef3343af146f83280cc72a64b47b634250ec4c5d0801f3f96933abb2f3245cf4defd4e8ce692c4566a2555c99b68e52fe48de7ebf1bdfba62c039908753bf122a84a0880ded5d90215190080f8af0ddb493a8f00e677b27f48e14cbdf4561a6d1e1e3f74371dc583771378d53b1a7fd2f07c19c740e19abd5ed5c13e9101821b33bf64aa338f81b3f3ce804447ccbffcc0acf17adf11539cbce58484ac33ce75f1742f95281db64dbbd6a3d647ab940f3c4658a2ab09492173c87c77c89ee606ce53d2052ef2f054a41f62091a3452aef8ea1c66857f575ea4f3708685440f1a60d8f1f856b9860ef7cdf070e14db00920cd5642a6dafad1a130b0da990b6ee4a6cc9c3c3fab2b98cbc2201236452c6743fbf1016af22508a7d5a502ede69d9f4da376a38edab7f38eaff35448fc06b1b8456fa612cee21925ff95b6d13673941400e5a2e4ab6b15a24f8114fbf9bcc29894dfb59a203526e1fc62fc83a3ad979d34536dadd3d1a5b6e058f3f2aec8d120ab234fa42352a8bd44e5ed605b0b80ef0400a6af20185bce2833a1a622ac48165025b33e7587bed8f9e344834b271c2d64c5bbda3c07c96dd8971332459aeba128391f4b9885e8bef5080f7daa4e08f5e81b4c913e809fc75bd29bf0231a540ccb606d00ba7f0d0f544f3900bb7764868fb72eb109a08d93c16f2982fa6a07511d5b1cdfd9a9fed086921c6017efa88e1a9b43412a460c3665f42fafa46937b7df976bc13741056d1ed1dd61a9413816ec3450f20025f7761106f5f41b1e42484635c984b3336aaece51635b36bea5c0c41e29eac16e84b8940255eeacdba54c32ccfaef820618ea9ddfb83d553bf4f44b30d3561ba9fbd4f9f02348947b58ab3eaa26c47ae2e4d4859b6110354c7ba7e08913f8ce93630e7568d0fd26c225cd25d996196513c1adde82374e94b8d510ee4725373c2dda85bbd673ca100385bfd6b840367ce499f12421b04742738d804deda1e57985682a5fe2c6e2dd5d8ef33b7e3c049cfb053c324a28396801c512a9fcdee262b4a57f2d0bee8a16d4c3d3f1a4659d322ba78be62395f661e19f56d9f62871688cbba47e6e72c4893f1e7e282228f132a20bfcfcf830a7294e142ae9a51499a5f196bdc28bf511a28ce10f023404903ca628b74a75ba1b463b822e27fdfc2e6fd6113cdd9e151fde50ab0f62a7a041a48e06125037eea6b8ad8058bec61132a1baa13de698cac3f59df00ca861840edd30f11f8281324d8f5452de88ab1a371f4c89394f40bbd3fc1e1387e2951034048bb1a3d2f8cd59355e644014ec2ac25226145394c11642c27b960bb7f40ddf89a51e8ef57cb4e671abd4e0d74a54a7ec94c4a5223fd12688606078ee0e6030ef63e971856bba1c5fe2b325fd5a2650216c5eee49c1d8d7a4ba8e6245ee3c448e1fc92c3ec1b1abe72e19d08fa9a71260f8eccaafaa76c9230d24581dbac6efe4dbd9084d3f3445ddada82622dd0eb3a9a171facfefaf7678741e71c78cc463043bacb113e18773404aeda451fdc6c4ee03fd438e3ef574dc62c525d1c0a09db7f4638c8fc07120008111ba977fbb80d6502e93f7614356a8c237c86018f2331311c3944dc3c18df1a336040d657b84df48ea5e717650fc002492b8c3f283d3d700d204b3d2ef81d51423f55872ca3943c988e1317725e10cdfffce55d86d0e56f984e5758fff11b745d5e29e2d22ec81f12ebd5e5613639ade2124d697c97e09003de58c9a1052c73b9319d40c094e831d2e5a9bb8fb517b33b1ce197424370fe527a0d1475ea64b6ec767635337a11ac18b9ef5f2e96be733aa59d422c1c270e86eb4ac39f093a896ca31b4b62d6280a3e6f6443067ef4e3a6240d55d2587e1f3c8fcd766747446a1643a8becf22976ff2e1d23f49bd384f09ad2bf2a68d9b94937eb6efa06ca7c572410ee5bacbf922b0235caba34319dc7c9c11a48db6b3a73383f8a00f240c537386db91530ba354878956cfede119e4d259d15a26cd9b800a227d7e84c09ed0b557f56968c9dbb2fbbe13382a35f37250a41bafa6dc7759106c69d64428208c7d24f4ffa1dfb5d77bfb1f5a04d01dede1f64eda1ab43202a2efd32bbdade4216bcd61085eee9106ac8f2460fc28d9da60d2d35d3a4e58bdbf82f3d18fb2e6853718d88d1f2a0de56459efdfeb1b72d4746b3af0615a7a089f4d3accddeeea85625ba95b097f9805b2ebe659e02cdb296743d7328e026aaf6656bc8778b932cdf080726614a86e0330524fb68ff0e9402288ab56510b92dfbcb8261aa8bcc04eacd86224dfdb190c5a7dd44d218b366b193fecfe93b5bbdba8bf67db0b10984df0b2c0573832807ad7ffaa804bc630e4f09fc5958450b5f639f55c2d85103648da5c5ca0bd1243eedfb0ab21ef62233c71f5ce841584f365dcdb4bf38e4898774341fc87ca9c42782b6fa567ce36b266342de0abf1cb132382c9eb1062f87256d141bfb9a66c88475d8c84590fbd58d4d33e83c157449d90ed1c021518be0242e1e5919c5950c6e2d7e86130bbf53911bc726feb1ffdd608ff19c5644a0350b6ac43db86b08ee666c6ced11ffa1e00e9b80e69bd45a7aebfa06e38fc5311f7dabe707d01678c1f7b822b80c27cc660c6fc83cfd7735bc9ccb0a8ba445f2ae9357ff2da957dee98454075f94cf1a27730e2902402f47cf74b4f6c7291b028041883c7df47aa1a3017e310fe7ad25662d85b824f60af8ff95953bcf44d126ca73371d54092da39a2214a9c88320bdf0d702ca78693aa36629f605c294a956f93e65e2dddbfae2368264b917924686a22eb222f504bcf4c955fecb6138380ba266ccb6c841e27ea01239c08e3e5178ee9b2b0f5081b483e8bf0ca381091d4a848611cfff510fb27e5f6451f38064a2809e8457b898438323816063f39c96018fe84f6188c40968d02d0d2f80fa12a109dad97ea90b765b8ff76843726b109f68ca9fb25faa26a9a34427131b04cebb4593fcc0e020a56998be28a8b12c8c57bb3bc0b0c88e32b43076b222cad3611c8da9b87bf84734df49c091e47fcf613799e949d1d0defdd7465dc5e228429d98c3d9848c5972ba8fb263a841bb1fda4dcc073ad8871bb7003a3cac97a54293b7f916b531b8a7184bb71c3994eb92e7bec2efc389a3984e94c0821451827c6a487d0d3908c75cff6269e77c0f45430870e83da3d64ff08863e4cfd796e1a1e56bcdfcbaa85fdaf3e19c346ad32f0e8775454c76261424ad32a30bd208b44ab551df2c5c738df2b60a916e246f287aa812dc920673f58cd32e626eebc06c7011f90728f193efd52d14bd1a938978c9b41386c102044be132ff5f946019ce7eda8ca2cfdda90dab1cd0982b2b36d503499090e82c7f06918cec2fbf6cd0c171146ca82a4eea86efdbca395109e9e49ee0ebac197363bf64501983bed2f610fbbe87f6b790aa5cf282472a7acf70d083b827d7cc43ecfe093a1968f2fe3d3089f28ceeeffbec071bc0ad9298d625d0b0197a92781fdcfc3037ca00902796cb5765f8203d0ebc5a348bd849024a87712168463e6a607d3e03bb6261c63e479394f62113d28bcf0ed89a6034a44bbcd15aa042f91c821219783324c94d0bc99e7f13ade6faa52beb279cc0cbbf29c73b0123c03e5ed5c666f44fd68b3c330798e9ea6ecd907c4e0c9f781b643bccd58004a67634d855a773c9d5bb37ab66d6c0fe84294b7986da0caae0f5fd46aa3d772fc4e7c47ef3a6d2353df37a9a3b931d5b5b4ca6db6f8aef9bfc9df07d63aca4304254089439b173aa8d3683f5d182540cbd958f50f360c163a2ce5aaa175a171ff9972ba193ce3f3c373f8c4aa49d776d256c58ed6e661a8426fbc6cd548cfdc5f28b5327bbe44ed94f83ffa5c3285d49b60523a155b998ad10e917ebd1a58d5afbea2d94595b9080b08d89e6fdc743ce283bb49dbf65e2f0d49b4192003d73e9ef53f5af7d96da8cdfd10360e02c4affa1e1b639e2b2ae45197de3b513c17cc8bb70943c5f889f68de5601e4d0142d56df550f1ed1da8e6688e0544b47671db9d93fdd2306da023857900826c92c3215bb9973b603b7b65fc619ecaf463faefa399cb39de4c745e509364c232e4b89460b1683a32a02023b99dd2f2dca1bb224beb3a95d256a1cce91b52581925c670682f5e0250aea66dc48fa68f8bd1493980a450dbd3a36d44d185f807934376a9f4ba3e0976c41f242693baf547bd27b68005c21508804c5037e7d49aba259d06d10e8e636281d04f672d2482ec034c181ce532b7e240c6b06962b2a867c0d8fb05fa7ca1aa9e2cbbb856e9d56c59be6e5c6a0fb7f4f46b4256cd5df442bb09ef46ff2f58b0c0c04f40a1db02c55144be0c8c094629394e492f0098e9652cfcc60c0d11c896e72529032903d794ecbe3c5a388dbd687f9396a5a7dec1c08b28fc40ff9ab2135f5fa0066ece0d8f25510ec860e9c6852a03da9a3370a8790b6c94c57a1ecf924d2d738472999b2cb5621c63c84ba1f91b3f354cfa430bc504357f62753fb9abca070a735976bb8f8253d3f0ba73f4e73efa5ee6044febe7934ba1b15bbab8bf539f101e362e28e2fa4aabcb6f490adb6e66ec8e815e6df72cada54b1df17898cb2ac515c150afc7c4d5f08b6afa341270e2855af03408104997ac584341c928253cb62d525481e85bc43bc9efaabeb17cfd15219065dabfcf9d19cbb153708d1c024cafda9ce871ee0d60ce149290d4499abdb7e391bfee0df6209c67c8805ed2cbf623f0fb0e5cfc819e703b1ab5d0356855f4ec0de4879fd84d7ce841df8dfc4c86adcd81ba3ba5425920049be2acc95ffaff510295cfc8e09a92321e82892db551813cf76235bed4dad279369e27fa10eb00e533df56427b64d43c5130485f76f31748d936af6de253ca29e1ded85260420b8cdfeb833133193f32dd14fa92d784f02b90c4dd7783a1e6af9a64ce61d1671bae59ef6ba6e44b66a2d437205cd21bbefc2f51961288199b0988c7c2688548efa3a0e397764b4d165fe681176dd8da084a27b741cbccac297112e6e0b1cc3d228912c6b2ec511a2a74fd6c34da72b58cd81c4aee2fc746d3ced9e5714f7be6b0d5aa36a41500f2754220b6c05e144a822aadca20d43509607e12835a8111c50e180d5f523e3af2b2219e1bfab4b66922f6f9242ad3f525f0e213e3f91724ed1b4b85f89fcdc59b68a0174015098e381b1e943320c5fae179c4a0d3e1b5005598894cdd11df85bae98a61cd04b3fc2986aae94432f6dc13388ce8bc16e9bad1e47076915117606ed4be3ee619b4e9bdf75ddf144d3dc8fb2fe557f7a02a610559a308a3a236e1e05a003befe536413dc0a4f9fe1f97bc7559d98722710b9636820502b7eb2cb5bcb851c5a61be4e880eda2f5a2e98f839b422e8c9c2d7f9de8c204c2d7701738057c3fb0c3e5a6527e74e7f396fcd5264904e867e4c3062fdaf36f86ef87506260309ac059a010a7eb0f9a5f4fa782ad4e95d4f47047f82a9142a8b0256ae64dd58e9aa43acba3275b37b159877cb2f6dcdef13a9898852130a2ccb034263e3eeec52e1e5c7be14a2310c6641fd8da3797288df26f2806fb216323f2ad35817c484b01c4acda459a581fdce9cb54f0f97b2d8b0f29cae3466eaf5c198c36dcd7231094147b90c68d4105d87d58645a022dfdbfa982c3e325319f0739f39551888576490e825e0524fda8946c302c3cf602f4d98d8d692df21a7579f3db16cd17b32ca90fe4bddf0300cdb77d9ba0a6098fcab420a991845774481e907202ae6ee8f397751851892f48b0c5c9040447fd026fe21a9d4e5d81dd0add80484c12e891e8e41b5802723ee01406ff63f6297d9bc2839f42d6b7352a13f749399a6de0440f5fb29fb73808a36073c6125338c2d8e7e7478dca01f6324c7c101a44f9b19cc08777abdc2db35fc197e33f5efd099a6b7f1ca2c8f80db2ca4d30c477129cd2932b01abba771d91e0e28144f59951850f604fc230bf44a6cef54186d59aac1eb6855694d8a9c1e01d89f1453afc7001a3a6d261ccf7c367e93756e89d4565e112cb45e5031b296127a12b78ed5dfb4225a7fc0a2786622bec4ba3d050004e057e69c3ed6c7652c0cbeefd8e7e5acac88de75f2cd3d86c32ac9428036753222c7cdc191d7b24a93129a7f089818bd867e6c7740cd818a64f09df3690a778b97e17db6d7372f118b8d096a065acde4032b432b3ccde46df4e001142a7b80917751797d14b36f637820853e6f02504520d6eb1e0b740badf9a23fa432cc834e7d026f7e3f64178aaa708a67318624e1aa44721082503d527f046dfb0a574bb5042c82cf123462624c074c7e73802e8e2f27b07d519f8e2caad7fee3758d1c5bc4e21166ea50e5ac1f9acbbdf86395c4eb3d22a0a65564e2bc0a30240101b4226cc5e53ddf9d2431a0ffdb35f4ff30af6558a73253544ebb947fd2345d9e9d451e55e2f2aac04a7967c6aa17929e7632c293daa95cb4c4db5e57423ec180324a8ddd5cd18067a6a958fe0b848bd1dd43f04cedeb8fa14551373408c297b9e53af6013a2e43084a8894cd8d20aecfa5282288e2aa55371ead7d5de04e20bf64baaa4a8842e20abb0bd4632fbbacc856887d8015416398caba4f94ab10335ec0182153481c461b2a340709de374740165ea25cac00f69273bfef3d720a5bc2d1f9c4911620a20c34fa33805e262bf0f5e923a37dc1e1c0b3a1c21d9602eedca094b10c1ddd83e093c98594b098d3cd8413dde1a3645b604e75192f2c08e414948d96433de2c1020e93c7aa9c0ba130ca2f15ea484bdbab3f1d143c8a202c72bd61d5929289fe05771aedc4c0cbfe728ef81c973d8af7ab948789d615d955c23b15470ff497703935edfe3804ed3791e115006001770ebcab335c2db57c5eb134fced842f4cfb276a5eb0c4c9d008343a7fd6b3521061502396a5876d3974101cd85354a66fb8ea8d52d4baa5cc31025ec00cebc568a9487d33d643ea52c0838a2db47d33e31cd1ba3e3b405cea9bf02e6be038a08a5402010553612fe959daa26067afe56894afe933d58b9decfc474911b676912df542b701213df441ec6076b30e38e602bb7c6084d152f18a35f5d073832837371c72a3078a69995bacf10a3d60498e7704a7a6c2f9216cec9991d4f005ea0faa2ca2948d992608661f1592fefc41dd622b5a556e50307cedeafefc61e15a1aac0f717a00dc2e5a0e42fcdd4b46707efa5ea825f3749c04b07f85f59e3ba592c9c60a9a62986117e1f2dcfa1db3c3130a558959906dbcbdfc6b42b5715271f68016afe2c255ffc75513b90d1c4718e587247463e415de9ec29a5d8b7aad0e636b9fd142af8d7a2e8da339781b00643a724b37801683365c948b555cbc1ec312efdf6a11638cd7e1134c1aa80b84c02607aaa97045e6610919d728749a33e1ed92de85a22212b608367f981681bcbcbf76940dde8ff46c8f7139baf47232eaa415335cd018b8e303d62a428b3687f4bc2c38025a6199e7d69e6bd2d3b3dc83aba2f76c10376fd972ba1e5e07d194250b9814dc8e8a3d92b48790a78c0fb9da6a5528dd9cb25bf92290362c20efc2943a9c11cf8b732a53f0056b4bcfb5c7c6a0133b43a4da364059aa309bc087ba2d891f2879c847840c5ec99abb2eb5709b141a588c400a4a4d49d21fe44b58cbd2c260380f8c47b2714fe2a8c8330df8330b0b68441020a8383ae3efae57454f3155b21e7901e507ffb9277c45a0790910f2a247f29741d62360d80a53530968ad382d4e50e657a003052a0bd24e3d7bc87f898d0beafb9ca3208007339eb094215eb19438e904d850d14c3a3a40bb88c1135e92d332b20341e46ab629629e1d8297811149d4ea074551c12ccf21b131bdcbb12da9a116e0d81faa764f48dad169f3890adfcac9a6130a807a6a7c0c52e442f8b2314b9e92308aba63bb85279c1c93c205883f9c7f71f512c1f1a69e46980bf46580041d74772a2d7c9d02d9f8faf4f0f54f0207fe2eb69c2bc23e9ac672e415d9946e17254bd5448883728d2b406b2586c2611e6de72700e5d99dc023e7c1e1c90bcb41ab37c2dc153fa108f44dee23eb9c1a2f6394c160457dbf232d20112120965b8b50098f40c8ae4de3c4ae3b7628a8fb2b80171cb5c9e2ba751da3cd41f78b898e25fab1d88a26d4a02e60e5bcb3bf7c3c34f602cd551dcabc9fff4c778730179d3a81e8b63840c36563d3fe34b6071eb31e306a125534fbf642b905d657c7d812de75856aaa20055cc968c6709ace167dd09af27b4809a8ec5b459d3d886a4e810656641a4bc676548672d7e3cbe833ff5b5b9404a04dda2df94100cec248d1696c136698486369c48c72ae5a1c87d2ad9ab216b8d6fd4cedc7a78dd132cf3148a344901c436c1e03c770559f3e50b853cee1b8766c3516f7aee9cf163bddba4632ecb260f6496453f30d2682fa71099413d8e15cc7e0008b89438f2564b8d7f0411cc975d6f4ea59e07e39c466bb0d56501b86ac43eff2b83ba29a0357f88a4f6056da799d4569765a2905ceb5279a93952ad1957f0010bab059ccbc80fc6a29d4b42284e0d2f5294cbef7a5d292aa8df9c2f57d68c743ed199a7c9e4680fac04bc99ebc179dc14c91d5dda74bddd80236629fde34d0e0aea9345236e097e4940e0be69f1ff9137ebddb20f25146a76bd504fb90b3af1a672c13e36fc628a9dcc6abb7017c782898fe1afdeb77cba95abde5a0073432cdb3e65e2a4cf8017eb80a348c31419b51768e1f477dd342e4aec0e4c5f1ca95f39a67d999cef0d99c0afbd08b71c7ca58381bb1d76c2f73fe0c8d9ef1d0bd22173108d9ad604f5b97885b125c02304d4495e15329328f9043daf26d47157b3442a07d25f8356186631436662638c31605a81b987391e5737497b543f7e16f85efdb9b3f272bea74fcb97ca920d4c5437f8673b3762b5e151ea2955ed2c619193b70b76f3c541098bcf0f2cfe7f41370c364becc08254e65f1d153cb48c49a68b98695561a5d347be2c89c77d2c36040ddee31406d212f83f0dba307e7d4fb85eb8ed1d7c7d9e8a483aa0d2b7ceee1b69a2ce85bdd164cf7472f6254640df4c2b94c6513bd7b55f340c1b56885183501cbe51522f2afbfbb383a9781e3335ce949f16f70b2629444c6f8cfbc084b04c061e6d2ad637de28cb50e3d6d6243418cc53440f7ffbb010cb1dd79eeb79925902e8a0719385b6545c35c67f055790bae7f6cfa73214cd11e22a1bc18e2badb74bb1d95ca12a72dc734448636ac15168b13762d238507d824efd72bfacc4f125ba295450e10e5a703dc60bc96c3453ba014fe82704e6fee14d64a8bc4af1d6033709ec304cbb34e1e12e571d0059a02c09c52d7defc0f875cf087544947393241567e06a8d258b533dcae305c15741caaab7025332127041d4a6676fc36efa4371db92478b4c92eb85159cf7d0d408a935a47546597dc7e605c140d33ed7ee0c0e988396993442879535cec5498aba155e419ff95949ca87183c0ac8b8d7d6288bef1071764fd0653906f2b83b3303d8d826252ffe2d024703908d0e03dcb664fbd8b89877e0ba019cfb04e5831478901ce19ef98315f0c9150092183f4e57ac6080901ef4b9eb6106ddbe6585af3df86df9c98ce8ce4a856710d674bde7c719d12f500501e26e00e89b111d85fa917c17af8e195cf09f66923145885945781b0df610dbbfc3c4798e26e543b64329d67326df39b914641956a36b886d59c91339e0f88e09620ad25b61189be78a1a769cb142257070bd9fbac49e86db9ffad27baf617f8c4f85a080427e6067be3b6bac5bdffb9a19844cb93e4e99c3facee0af50a065cca805da5d989fd6fc2c7a477b117ac2101a0348a665334da755d62835b0229f06d828c9c5a4ae565d59760736bb86e60923610e2eccc5f5c76c9082dabd7abed7762af0a20007f5b509096a6e919bb9b3f0910ce48450a0be038254b861fce4452fc70152722cc7d8b0489fd981397ce84cb16147ef0ef4d36fb4ea2785a41027a14f4f52f4a52a2ac431b8f7b58b9151aa0c72f36ea97153e2b9265aefc90def06c10e6bb42f60912331bf7ab11391c4c38e3f5860fbc1a076de9f94c25b8d7bd0de79c2dc306907c23809b1676a62feb1ccbb1f9ee95a44166ba3973e73b37328c3a803678806192d1516eebf1f1cb40da9c4f1c60ecc9778e8447982e0320f828ea3b56dc5bc4aec1c58b70cc41de719232bd066afa1afa396ee5bf65a1360ac227b66824b5cd6910972018bacfd78a90d2dbe6caa239e7e1779eed90060a83b0a021192cca437079442e60470189c3f0a372de2d4178720c2c228c0ba12fc35fffee036ff16b71a246de94ce11f99714df33d5821438580a14028419bc8b4843dd667ccf2b1519b489805622c191979432f4eb8644965a862419e6d1389cfa096220a1e306f126184abea26a23c473f24a4537a1e0d80861e46c3eaa3e3d3a596a6cc0719e042dcc1e8187b866588de30c8b141d083dc1c251db599f1a3657ad26d750544a763078d26fe05572414599c9f833fdbebb0b81be04450b7c14106e4cbcda251f63cc5509eb7689f0781d99d004521d49df6c05bc7776ccdcc573e987bf9f90ac0fc62b5ba6bdbc52732c683dea6e470e97fff9fce7c26375647c8f9381dfc0f54d482c9b6d8c25d19efd439a3aae71f1984c43d5c2ad042e288f63e0e25e9a97dba9519a2a4641ac4140ee67e32f326fb1a8ba6036558966bddef2efdab7ce9c81226661678152554a0038e30803331ef927b75258b10109514eef24390f721546bef1e1f6ce155acbb83a389c2b05507d73081c49d814e281cc3152fe8a97740085deec53a38259b831eb6e18d645ff2eed4d8bae0f56ad4cd7a47979ef04f85d009ff82dc4464f747cd756a92b828feefa1689d528d624f144070c5d539236ab2f47d8d1de2001eb299736e9bff2c5599015ac38abffc457fe08bef25e197da09e6ead20d3a65ce74a4709ab7df30fe41c7014d3a0a838bdd787bd9fb8cfff9237c21fadfbc408e81570a9474d3734084d0c1c9058e4c99c84f9631ee555965eed225f564360ea86d0426c96769e9ec4c4cc6dd954e562ae26002926979ecacdcbcc24f9cb6d804f4438d03470ef219b92c6109fb2eb8fc283d4a52b3e7d239d737b6ae7a554dab451f21072027e8dd7adf0cdd3017bdf890f7803a1423638753aaa87b4b0e752ed1ea14ab156c568c54e944f79706507c15d244c10214fd2d20dc4c3756b8541d1a451b023852ccd00cf4ec6d7d0bf7d37a1c0351f527e78714b6fa623e911738822ed815a8b90726e1ec44d25d4339108e5177dbd9f0f2bf83c407ab0396942044a0113ff0f8df241990ba4134553d01d9d304a478a1b176130fe10745fce52251301a367e23ebfe61f322397ec8124ce4097353711c95ee0d11ad02be712bcd17090911d559742603afe334339baf0058fa114195979388330443b57d0bf637045862bef182e609e732adb458aa8856444b93f310d9fc0e56df00107b27e0c8bfeb8716b33b5583eda5323a476a49a7fb7c09bc1161d3d96e443fbe37006ad3a4b21bc196c08421f784f074b9a4183f8ca5fc77ffe6e0a9bf2562b8ceb5aebfb8a52f914812299a86a4aaf9376cf1a72c89ad0db08de4958aca36f5f50fa64e7ab5edb22bbf560be5ae9d15581b20020679e07c29b07d3ef05269155387d4e3b341c950e3cd0aaaa236735fac3a6eefbdb98d23399bff8fc82ed32607d187bf5005593e79d41dd42b600cf7212e557ae4a88e0841b7519cb1d4a15c420ba8e6fa7f3ceee20ad4f8036e093f5e767dca337851393f1d7670f8c38c0d84ab2a86752a5c4a636a2fbd9f4376b20eec3489a74962aa2b16449b166126461cb47356a2714b8be23a9ec2cf88265f7a9a2e0b6922c1e374d44fe77107fc2176f78294f2d4949da7842ff7e3509203cbdda7c871644ed2e9a2e24e3fd203ebb53534d259a69981c631e8c264789b37bf3b3887d44db73c5d5fb1a08da234bf3846de35824bbf5cb8fa813f501152d32223f2e63935692cd712d0ce25ca7e8836b94303a3c4dd1939860c97f42f21b608442b3c0a778bcff3224f3a9f4e835e528c5c2feae13bcd8cf560e834acf01fcf5976cdd87e5f84f3c72ad3c192789d4a28707c404cdcdf2cdd194865284332e863fec3cb1e294d22e72bccd20d5a6088a1fe2f0f3f5772c1ba022aaca89336dc5b754ae04f4456bbe32f655517b962a1a66a25a0861fa38abc73294e91f4f09c34d684bd4ce0f111b4aae7ffaaac56c32dd722d3e06b598b03e78ba5be211dbc745e8b2c6c211e0cff00d41dc1f8e7e6cb483e6454ab97eb6cc1d83b5592b893ac38810d9cb828ad08f71af800528690fc83069e2182c68f238fad06a0ad02475653307ba7a0a2d59261314647edf512e4ce17aae9af4562688c20decf965ec1194563f701e95cdd9ddd2a7e6735b55de0fe239dc19cb5797ec77c4cf2215cc2f6c211d9c0266351989676733d3160633cfbfebde828edacb3a0d488dda431c8fa0c3b30a40b95861099e79af83d7db7d9b5d0878ff652a46202c83b96950af0c51ca457c6d6992592c94c997a5386269ff53eaaf41bf69bef41de546c1c05a2fc57eba8267d252835bea4cf6388491c269733304f5099f92df213511ff9ad9907ebffa4873561b0e20a495a4a426f44c5aa40099eade03cf6407b663d80616dcfd9a8f2d5e069d496dbd55949808ce537eb81e8354a85a3298a8fcd872ea33d347b07b011a1e75619d8cb4d14d7c80637d805aebad0e0dbd4909acd8d87d429735c676f72bc768f4ef8ca268e82e02ffbc458439a7526a9112413f2ad85ee4879c5c63e997438a403b6b540f12f7707e82066a203351a1484507fe88de443a461685cef6e6ac25dc68a50aad52c7772b0b02f3587b967f78b0889c300c6f36273600e43074d632913cc20e9061d973759f4b6174dbee2bb543269f737dbf692895056ec54f8c1d47d265d39cec3f9fca10053c80c0a1d234bb4de22649848ba28fcf36753347519c5936ddf4677575314a25a09300ffda2e664bc962fde41333408a72bd82fa72b0873ce154d65ebc80c8f1b067e895042360e0f91f3b90ea7ff85d7e60e2ba33a35aacb435db36746115ff98ab521b06c1f3e6476ae07cf310bc65ed8f9de28e134d212a15b5ab9e510ac82c740b99022b6cb259a7fb86fc8d2428f97b2efd289bcf4277c787b678fbc7e0d06dfe4879161f39078991ef337b86df1855cd8764f009b999b8cd2bc5429502d50743e765d3f56bcc6bf4aef62431684577ce480b27ca8525204168f8b2b05adcde077e4d6825f27bbcf147d4cd4adff482a30a2d39e9740398887e662a1906a057602e753ff716af042dc7aa26f11d912728ca02f8694536262fcc4e00952daa2a48a371a2e9c681a545477d46146e2ec2527d1eac3ca9254e07a44b3a286c5e7810da8e6d65e51c152602c81d3940faa41cec5c916326b8ef366beccb34ac3e6ccea438b7f89351c3af9ced7ddf5d0708b44acc49567090d6db98b5225e3ff955f78031de3c85f530b2e8d798cd315a7ff3e69ff6c7181f9055d6c18c1c3d3f70a3c6d2bdcd32d8e9c057275a1d254c922a78204e971cc3d48ec181b2041a10e2a7fd8e67f18b8b43a7f5b51eb9e5f80f02816f785c8543ec534f9e3f92b3eea1a6816d7831160d6c85ae4318cd60e40d3009b62e373992c5d3c21d19b1670cdd92d8cde0af786e2c93a2b285cb03f0b5ce5d29af5d5a5816b3251b296406e8e3aec07e8e50b294c7525b4b18f2719d133dd6bb69168a99d59233a367ba6d57be656ed2f3063d85114a7b6d3cb25e149b46d1473afed83078fe89d2592c83347844cb1ee9801fed1cce2c073c68a2eec4462268cc8f85a4cccaab173d274abcf4e8af615c9a055fc5ec76e8f6701d70fc89a9083903ce06f308280c1cf2429e21810751a2a713ec3934a1502510e4b36c52f055ede473229a6e060abfce75c0122cf0316eca867db76aface1861a3e0ae308892a9b4832530cf0d2f77c17ddfad9c29df336a615dd53cdc27819791dd3bbeb1f3c5fe5d3c04841e260650079450b4499f71f9705485423fac375d7f6378f97a1312083d5cd1be7c087b18205e4185ef4cb10986b67f379576d81105bd74e772869dcb97a244c92d48b4aec9926674b08720cded14b6600da5a268375519ab00d47fd196ce5f9cd057f87b2431f92bf96d141b200a4feb0363b372002f51b0fa10b845f6374ea3206bd5449420c77572f8db11f7ff11de2f0d9c54a257a402fa4d2651655b751a2f79745eebb056e3c24ea1c33ca33bfade976bdc8015c56e24689692e665ba8c625ea5205e40aff4494dd003360cc67b859be71af34a38f90fb83057a175ceb6e153bd2d9347da7f1debdb8473fc5350b03f894c709500a0091d413946c7eea1147fce715c0398694af77af3d4112a8abbf54efa3bdce4ac61db72ffdf00a30c5970f1ac35ed3f2394ffcd1a67457cea6064e3c2112325ffc6d0d79588956bb9e626580421d29ba99072f43d20544967267f0fe401106944418e736d08225f8c9e43574017bd155f432a08e708e16000470925bf9337be5888a470adbd38eed7487600bbb3b5bf985e0f28b015b72557e14f00e7663ee8735c110f0c09b4b6ffe7470f4cb91c5c2bb72f78c161d7833e8c0ec8bac5d5fc54683ad6c38eb789f41cd4e32862ca9fc4aadc617649d27f32c10357e4c160f39933ff263e90176831df39d6fbf86b3db4ef19ac31c907c5d0d3ff447012b607ab9db46916b69be0d75c34a01fc34e6c634c886eaf7d56e2f696b65ec15ef6152dac0737d6e62d16c864d1fc3a0dad89958acac363a0e88eda3fb694da9417e7005dca7a495566d9c8642f3a90585c9abca1f7d2d55d15e50d5795d3357029d93f5218446f8fd4f8c4f5cfe3ae4b5119333dcdf538d2a2e2414e1e5f62e69d21b17d8f57a613a1ce3dcd912b388ebb87124b8c05094bfe1e58f822e3379486b5c553b46c62a823c549212f90aedd31eecf5b651c3f5438040314952a56adfd4222a75b24250f074873348a509488c6d6f4ff75eae52baa1970d82c645fa3bbe8f3859d6790db2a0f10988f2227f759971704e357edcc246bf1436ba26986a044bc85d555c3a1035dba94bf5dfd2c9add9a1dee218caaa74f54f13115cafa359e59809e3378c2aeb84bdb7c60f27760d5868825e405888b8bb770542a2fca618566c83dda89147e6ddecdc65fe06e38b00ba6082521ebc38c1b0f812e4dcc6df24d3063bc46c966c9bdac2afe907e69407505b4574f54cc0485b967d0062f8df7d4326d751fd4503ba289692a95edf20306a1283c03f6b65de512714b1f2b0292b5e04aa72213bcba88398b7365b41eca6ed9a0c642712d6e118cb45bf812a7915eb961c401014703e7cfb19ab17d869153da62d16d5d3b2e3ffeb058dd56d9c529b07e5615c9ce6b4604f1fd9fd09d1dbba370b4cada3896ed76b98c8c47773b424edc9f27e0bca8e9f2fe379122a8db3a2a53be5d81afc02a0f4b17b311305565df07041b1a3fd6162bd36ee07608a4a94859a7ed00ff3e152f008370d2f529ff9bc1fcc9c68b88bdcb6bfe502c132a60ddfb58a5efdca5eee3052ad547fd21522424e39116c7fa3dccbad5e6df2b4b25de57d244a758958f6638dad98799b8d8ff185994eefa15134fa6188ac452a38b10f802e2cfd9c26a7680707f3b64657abef543b78995fc75141b66a8eb21c073501687b738bae076483d974b7658ef606556b11d8e4cb583fb04a2495917499fd3f0fc8b36fc3bd8792da631fe0b63e29a7598a991c04e19f75de2f0465b44272b411d927e73c226d8afd34763bc42e55bddbb38685c9874c4160459177cd00dba0cf87406e632c02bf1ec1f678214bb393cd44e2c49b31d98c75b9458df85b6dde8d955028dbcadb1b844ec9ce57012bbaa0b145a12daf03060cc7191d516f4d7e4737981e36a5ff221cee8a5e7ea4b0542d067e533ee129391ffba710df2d26d1c145feec7212b53ea5c528c0a523af884da8394cbd37a85103282620fcff7313ce7842a79a48f89fba738ae3143b4ef1f802e3c234f5a4a9c362133d2635a5af49de56a128e6469e904f5530045913afb5341c8a2a1dc8e903ae8855af1101cb161a302687f46ad289ad63fa08cd77587688faa00270967005be86a181942cf0b414636df75e498c88d7b006a599cfa7f6ad08bcf7760efa54d401268380459b425c167d290e7a2d422e071feff51e7746924943292f8f062bbc77283b277eac89838e22e20351d27521f030b0839cdb964d33d520a0af812207c497764e1ef0d3f0cc272e0073e09f2700b3ce98e293f321038f27ebc00112e497fbf3aa59a8839911e666a03625ddd23514e3e7b359638927f05ca19b8d4912c1a41eacb51a0582f9321403d6747f87b9d78cb521c8e5820788d0fe15a13004e44695ee2d879354dde781786347fc4ea6333acced57654158293ac54ae986ec9a778874a860e2d5a7c045e04c7864471e930eca806ffd86ebe7630712a5ed4ae2a5a875e058c726c2240271fb217ec5d9c7829782495ee808e88bb799987823f2d81491129a9ae782f216684d7a7f050343c4d6ce1b70710f14d27347e7581b5406a58069d6eaf5a5bb4db2063323676123f7472363b8fd2e5d073e8219f404fa9d3f38e93f1856203f75d1b192019376e81daf6d81192527ffbcda516f2f40a7c5a100723c5d56323d99a9eb48c80f12205bd5115b43e3f87f1b3ae0f9b40c2a297c0c7a4b06a573b19638150159f12f3c5a899d29f10ec18463a3a12c43d2cc08c3b6ef23743947ff6a80796e815154bc5b4b7bdf8af66818fe266f5cd0060b2aa5abf1eb380c536d9e68db5c88755b46e52583835a4d4eda66545478719a8d4421af75ba475cd3efbedcc1b9b0f474d6142f7368e43f85eb1fc5f82278ce1cc154a0fe3f07f3cf84e13d574b9c7c7b18bedf9137659a35cf54f40aacdda4d812cb8db5843af3be9f65df906bd553e149382355a45ee88f014cc385e926d4319b1a5fd046e4167570d69ef3adf23879be47d972443e1727e98fd88141eafae27e0e6945a46e69c13605370270fc01828086a6e79b38803c8638a11fb4635d4e1c5cfff82b17db153b24a0823fd19f831c8b18f9bc94f2a8b2f00c495f7e8a845304fdde46263f5e8c5b2fd7a027ee7b81f596456e5dcd48e0d17bbdbc3e20d58321a61b7baf1f27bd2fba89a7e429410643ac1d08375e0171f8f8900fcad86c84332a071da785a02b023b938449a5f4d7189e0b6c207d6ae66477141fd97407d11ac393bc72f82898155ee3f87bf8d56a241d6efa1073fcf330799de7a05f20a853de147e3e111b93e97ad2b281d5cb06a05641ef5d2788b073053c784e9f9e9838d807d634f977fb8dc6e17a5c046a035530915fa5721e5c5ef1446a3846de1ee0c88bab2d6c8f6a2b7c7de9dbcd6d1673a101fd8dfa6f2e0af656142309333268deab7897b2bcd32ff9a60888ce38b08b25ea8db0d25690c8542b9611b19c7d46e796174e7a7b44766a32cb912ea91dfadb3070d00528c2698efad3df8a8450362c800df5f6dacf9cba0fdb6834bad38eb8203af2bc84d4e7a55f61696f08832abe8d0a752fa0b138c7cad73fc77f7e266575f379e35fa3c24560ed67521c6e535090e136be3aa840575c49145e7702c3be29def506baec3ce3557c7c483b5c9b21aac7fd00d756523f317d48fbf50e4748b7a4a8fe9d096a3dd3f5c37f006a24c4d10e14fdbe3eb1fc613e531763c5ba78ac6c2a1bd0ce4394ef8e125daf4f35e0e7a79706c060008ac135954ea2ba57c919f0b720b0647012b0b8b3adf82ef4ad82a4ed4da5d56a4fec6980c9515da98aaec9d06e8c89c470c953e42e59418d7745490454541c303b6d79b011e0ec1471c330d396504a6c3ba70ddfcdaabb51ecd807a73eb7a15acea782ff70f7930b2e95e8f816e1c5e4bdb0fc4d84c23ad89f21499a5507e6eef4400adfa5ec048b89403a7711821ef3d80155f3f5ead1c0566a31e767e2a9ec4c95dff9dc4d8d2200b8005bfbfda94ce082dd9339b32e1c51b5e6c94f1bc614e30981ec9a7086a00a71b16479988e79611e8f983c18b4a2c3f4dc8293a835405ed7a8869d621528ee11053df06282e9816f810d29f3a2e49e90577325726de3ed085ceb131830ab8e25446dc91bd735c2c982f10002f3adfcfa132468bcd8a9db0436b63b2680065d3a0e1f4790cc925a5d3aa13026c119163e29a32a5305780d9dfe19cb83ee3bee752171147a23967f3a3d0dad38e3f2bd66cbdba6f1571b66ddb2a6020ecb217427fb1ed1e3246ee6f57f8a256e2da07cb4df8504a28facfa6e70b439b0995ae9f48b5471d10d0ce907f48a0aba75b9bd71fe04e452fc98e2cd8f1f3fbfda52d45e1f53ddaeca88f189d69bbfdeecdb3fe41a34d71bca825b9929d5a7a576d412f3cab9b88942c39d0af551f165bbeebc98e8a1dcad16d7e3335daba94c1cc548bd1b8d44939b8d8efb8cddfdf97c990ead57340e2c6544aacbd28fbf9db353761a774e587355a5260f90def0e788639b5b0e22c6c5651020de80175e7d34c80abc365f4b01d43fa4b386f5491ce923b4260a995efaadce872e13ff7501b9b469f4013660ff354a468af00c0dc111e94e9532d23a6e297d05c07b42bb8c0b84496c2ce5626e70b91d0b86be3b01549adb14a5487ea9cf8d07921a1c9aa8e775e966af2a25952787f581cbe8971a838e5b1d3fe37e15c147bd2618d7f84a3017c9ffa6c12f08e13afcf14e1e6b8d5547f28f9a3de4013ce1b93702c7dfc003cd01bca674894778e2663dcc7e6e590eb9dec7e4ac2a435a0441360a96e314ef9e7641eb560639912fd1f3dd5bc2596258bab8ca3585bbc2a56a01fc5e642df76772e9a7743078811e392b9b7b38c4db4051b154dc8cc2eaa232029f43795bf12ec7999df45a99787e12545f3ab1410acc560df3d2065d7203254de051c18264e084c1fc6e5f7e96dbc4c49d28bf04cab4e365558f26b92e2bb4614172d6ef6d44f6e504925a04bf008dbf304698a4089cb3cc97fd55b6bc1ffed16b3851b2c663270c1749f80de0e09c686fb0a159c4e51d471c1669f3af6ecdc613ca31ae33571d38e50577fc646d94a2d8a2db746e1fe5a66c17eeb9dd73a983702ac50ea92bea31f6af6a451cfdc6c2c2cfae5d51ce72b7173d99317f09aaa9423055389fc47332c2bb6031682a9e781c88ebbd052580e9981fd2008bf447c9c47d8a311002233b1bccfe95e2aa4ce3c282ca1f1a55170640eae05966e3c61667719936152ec3424337418d9c210102426720f6f237c0e009e9632e486101a14d57ab55cd5b38a1d26167f3d2631f091b1a99ae3dfb5fce22ac848b0d9c02acb2a82ba40b79286de9265033f436df6df9e55c950be55eeb979d5c8d58eb60e03a967f119a28502118ca166706cf89db5acbbf5e5032e6b54da3cd6b137fb18538bf7297f10beca913a658b56fc7538ebf8ae88b4d8973309b468ba51f831488a325a12bd864a503ef2c0666b2c8c8cf2b55237ec811cb4a0f5e919fcede5414fc51083c1b3dfc6e23e53a3a2f4ff370fd740c728017bb0b31c646b9364756490475dd5467dcb9e98bc020d7ed9f86b356f2111c58c883df280932d28dc767da2c074ec1ef29f41c489c5bea715a8a340415ace5a0896a5c4b5c7a0217ae4699b5b2cdbd60ef221a7f59dee93a578ec501a4c48f7ec219def0dc56cd7331eedcc64ed225fa58894f2cb677101611c8d82e4b129d1bf0aaeecfeead74447c3664a01aa2e5b7b3a993198a1f46a1fa4e632a92f4bf46902773d78d5ee97a9b4720e617a9dfd8ec98ad307c4df0b3d363c2fd9d7ba4689b3c4b65b74a25f3d2dc7547cc3a8eecc1ba18c5866a8de573839d3dc284a3954017478e305708c1bec910969835c7260500b9d5989799bf727e0fb6a0c539597d20f5c1c061c4daaeb0d5971dfb1c0cedfecd5b916b0a69da23290d6fc6ea2d8c4be5013c851b945f35c779596905ec2025e4567bfc18ddc8028524ab3ca618efbf8e39ddc63a727cd5b3e59ac53ee677ab45f925204e334c3d5553f509f708ebb224478f00b1fca3ca4d16dffdc435e9bd0e749abfbf77b413ef59eba2ff88f8672344bb6a8266ba2d1956656257dbd248bf367de40e89ee16e3e353afb2ff40f8d2aaf207ac82f8f012aaa284d5167e014e21a9bb9aa0e5633df279aed878fe4565e854b2f146655702bb6fdcd51cab5536e139d79c954edd879c1d733b3860b715a563d22f72be3e34b88eb0329dddddd8f9232c1e55721581e4d66d884c87f1176bbca848137431529a6d4b0d941ebd0ae050971f5022ef0032132008bdabc168eaaf965725355c00eb8e4d520bcf691584442a0bad4ccb779c73584bd3926d3cb45564946aa9c749bc20ec8bff3725374ede454cf6f9bc40cb2a58ec464f1cf9b9d2ce8c77138f93f16114e0c74184b1f80d15dd1a7bbead8228b527d7f03204f3af76027fee73a3caa36439bf28ce2a111c39f9dc05e1e91c254cf75b2b6b4a1beffc432304f35bb5a2e1bfdf120d3344f7b372a8b54f368f5868f2f1b3eab139685a2380d1b11431a3aa2a6a3d5da8742bec588e3957c5f1da4f0d11106b7b96e938476117e601349eebf8c2098b3cfb0b5fb86770cb98fadd3f132aab2dc2925007eca6d7231c336f91fe1c9bfc98205de84901ea6e9ad25db358c79ef08564e00af4b5082cbaedcafc842d60c938db76f084e7b45ef7a5d407b6b457cdfd9bbcaac668ef768ce67a6e3839f75785871b22e9739339402e34351c422a2f6af6a22c95d380b70a4b35af58fe33c4b71690f271f1c1b230e7e64a332d7b6d2fb0ecdb3fbe28c51f94cf5e031bdb7a11ddb91d0449b107e4cd332ede03560207936ec76e7d82b51b58f44253456d064dbe0d7afa30eb8733bb6020c78900289ad73c158006c63e82dfc89c42277f6ba74f5677792de5b488bf80a92f400addd025b13ec8f1033afd7c4db8050e82ebad4fafa7b586800047755be53bccd3c5c7c7a326320e49d321347f5bb59502687ff7cf8b78c42007a1da12efb3bc31da310b33947d4342136c061107046ad66a5e6f7cd85b1e1ae6cc963a6f0ad35d0354b937264ba126def82b8169dea1671b35b7320a7172f011ce3e1432ff68f07f2b35364c960b5dff9f6615f6f10364d0dc2571ef9bd2e3d866ebe8a90c426f1103b628fb2722e0c9efcd7e919c51604eb4b96b1cca943cfa415e0d2f1c0efaf61e3293e157bb479b7c8238277ccf764361c8ad548efddefe63a7223e1e58090c6034b194adaafbfddce59b34a69fc61d2b074a8885c0708d195736ab378966233b33f0a7b36f47b62c6bce7e109273db02c9d16e1af6c5a06356e28b09d370870e19e47218b49f06316d546391cf901ce932fa1458b3d6460e32a962084b97c73f3690b43d04f0dd67ecc14bbaa5eda5a2623e3e3c1a7eb639504fc66d82a6052bfc9e69d0636acc02d1729e438ebbdda81375955adb71413c7036c576737258625e822600e0eb12da8c48bc900787bb481d76c9c9e378cb1e9241e473b8e6a86ecebb9487e69c3d831585e1ed21b87c7d769c62cd526bb04244d47d909e3e761405b276731266e631ec8d6e37951a0b6a3bc9430c44c113f42a174c0b1248a832415302a16e384457396fa1e86cc6e8bca753395a3c9d9b7669993b6375d184aefb4a9f800fa21dcf9073eeb2e8ff3873cc3d441ba9687e44a1ace719eb0e74462745675016d5a2478c59ccb5cfe0dcd01408e5642535453e9488481c7632dcf1b2de774a71276fe11717478cad33081ff94abe8812a56915052e357572f950c7a617a59c507621ffb64937d939980901a2725a763ff113d599ff21983fafa8344b9e39c47ef3ab342458cc307f872a122c1b33586b430bce97470e49f9f72c7db4797e2450fcc17310d6b0d479f4439c8c8ac96b552aeaf12735b3633fe770f4d90b68b23c489ce2332222f6a242a409326381b31f1bd3afe1af08409a1c907a4d8cde93e2560b7ecbbbc57fca9e7d01e1b0204f971cd4ffcaaef787a0d672d52512f4341751399286e0c631111f82bbd4088171461020f2c4f900d00225e2b261a41c1896c1a0a7bcb0683ab2e36671919247916ca057c2a58896504966e9ae70f4016238f383188805f25ce3270699e9949c3c328294bc7fad67270fdda31b4353a22f50b7fb9a59347ebfe64e178fb45c73080c0ce8b7c2c7fa67bab510c3b7cd81d24b0794e51853c5291469a112ff015430687ec7773563ab66d23176cc0f3b521a82e6090beb8e4850966185984f8b42f24b30eedba362462cd1c236212bbc4800847d0c3017e3805b518207cbc1a6298c9f5f0f2c5364594c93c654b4e180f05b243a6de1f2c3c737a26f8e78aa2df4681f0f3ce3fb849d0dec10b11a676e42f398d1d2261b80aa80fe472071736488508fb927a60f7f707535167f71f87fa9c3a5b48f483b9fcc0cd12cd407da42c33c51ef3fd6a42806d4bac36136be65d3c923d07ad2e39e7264d228c0dafa5fb286107ee0ee164e01313e3d440bd1263fde03121264ffc874eb1620f91a138254ac15da45c204b6dcbd2ec656feb7e34437619930185340affc7841d435eebcce078f4dfab67aa7d790c7c472a2ceadf1e7e1c643b776683cebb46a1c95fd40090342291517f5f032ef4c0991b9f2977e57f103f2d31d221f6ea69a0be6dd4605fad95f841534254e53bf364fbdafee94b0babe601edfe3268e56dbe0dc6028c48e104ceba82ef510ef1f5080bca870d3f95aa5b99da6d1a006143ee03c3fbe94c67c9d8a66d66606f2d5932d964f07bd4e381ef08cd00a705e33027b02f3678344c3505661dc1f9988080362f7f9c2058360f59607ab8d5b502a9d60e8b8d3fb736be7f54bc1b5d517e3d18090dea560524560b09e80f27a3044ff38b4024fbca63ab30b3a238c22cf2b401a9ee75867373080bdd23c2344b837ea2596fd8b14434d37a061c8ed17246089563c0bda60f2ff5f977bcc6a4e51db29ec763b3471f97c422d91de51159dbfe7fd5dc4adb34060af80529ade5405c1cf576178f489e4b6742feca6fc8033db7d72287c0c85cd42f857739f6860f9abc5b5f4bd2f2af07a3491cbc3c080a5f19ccffb37ec820f333988841a41254de3e3325eca76259943180e600c0aab36605dca7a607cc1fd23abf670a9754b3b5b5daa511a26fb82fcda6d0322388a60af131ddabb88686dbd90966cf8a3defce386b4c6884430432d9c21e8ddca00ebb8281bf1af12b9ae3838fb18a8b26611aadc9fa794c8b0a94d93050ecd724b426a3d523e22a4480450c215cfe8de17ca0ef5525deca5804031803ac692a5847812d66c8637d98abeca152582b0aad81f7cf2a1214064cfa5ea1cf997b33d60e7bfc993f78337080e120f93290ea9cfb3f5a07075704a2111d52ef65e015931b4662cecabd6428db3bd206d37ae3148e664abf273d19ecece02877e0b5447809730c8b7bdde7860da695d9094ff01caa6544c584bf42e58721c9f588736150b0a7c58d40004cd5cf741c7bf967bc901c68739d49973280af71033f625f365de6403fa1749429be8d49f9588445b06cba40280720490932213fb228b7d567b7c82ca82bcb042200c02bd1c7d4e5a7e872c58a452aee9ee363e60e393ad37007a4b56700b9b9c4968ea9cedf01d100c8e64c4d760afa32c90a1016dcb7c723223bb428ef2ccc69f1df85d8032c229e2348ff9cd9ac4e77fed20e6bfbb242f036aa6f07c53195954fa77063d50572535673ff84d27f4a22ac4c183c3cad506e77b0f574d866f8031f70a9e6bd814ff066652460328f371eb81e4537bc3a4b11d17515b95d93084b1047a43793a57a59ff573b273425dc7d1e2cfe4568d7b75a33240825e11a076fbf09d6a2595e3879c1638ff679ff1895637ad03bfe85b815cbb436b2571e40322d1459124901e56ef0e6f613760c98eb2b1f4da8c208c7bf0505b7491a9114b8187a9c2189c21c5934057d0cf6cf91ec437579629e056b34528d038f474c4e6587703715c132067df4e47f9f8085d8ab0192dcf8bdd8c7e7ce0b459f7b5400645d40163c0f3a6b4c6a013f3dc44992a338091fb8d1d9384da724db647ec1164bb37e530c63a343a4f38c456502e20681ac1d389e82436a50ae04af9572db46312d946e5e08ed5752140c115b9007ec6752e5db94d4fd426e7f9f8221acc9cc61fe9f465658250bf72f98b8f04549356f19e98ad68b9ed4b518aaed65c9a8989b09ec1987ccf3491030be7144ab148bb9de1a0a1fea97f53ce959d75ea9c49cda999372984416543e81698674160a273938316241bdf15ab07e2be1e9d00d3e0db9dd769b3ddf0248d052d81c2cce48e2ed5ea718d733f00a97a2f9874618f0535f42f3a007e0970bc906077d324e0c687a53a11a5ce8960f8b4b1bd929bd09dac5ff7a886640cef14ed388be3e89b382788a53aa643167ae5060d7a33c7676e01306e8fb222aac245bf1161c8574ef952846d95c2a5a547f937c8545bb56268a4b071c6c981c05da4fc7c52a2cc9601f18f5112478293f3dcfdb284d8941d6984fbe6f4bc02978c525073e6049c73f9b7db1d68d8ac02dd0c823c8f42a7b6549a6e9c533ec0c3e0876313b0a762cd96210afd47d232987491528f3f556cd2ac75575c359cc204302a7bd11b3a23ea829d75a584e002f9dace2821cbbe701740cc7b32b05fa24ae9431a80d0289f2e2d2b6942f0c0b6918b817af17159ed0793d35d6666afab2f4d1ccfb0f6c0c1cd5d5560f790fbe4114b3641177841a75b58b4d5b31e128401354c9b31c8575c72bfee0685cc52fc923a17116891ce17fde9200ee17dc38f30784c0f37c601d15e1730f9ac5e05b30738e0f97071f8d86994f84f9699e2870cedf337c4cbf88a8a9c3aef5a433c8a3ce113739eac6f883bab3772d0ebff9b1b232aa72d1292909248c09cd5945c988da285b584ea2111e0dfb75a04f992ac27d47816a2cac6cac9cd107277f2fe0fc238347dc0a8526a4381a87f6d7b1e0ed8bc2e036ea702cdca3e558d45ce455036198c3da08a04e79ab889612e7243988b8e72750a2e3e253f650e321f0aa7fc2420f9599a6ea8d67f7cd93a6af655d951744e8eab9a49de4160ba4a657beb62cdf8f45ece233aa93b46ce621b634f4c06b66322dd0bc33cda70c3851c1c0ed9db7cb2d01a565e2240054c9fc26bbc1bfc290cab0be2c4b0c3e5a112d97b454a51ab1201e0e4d81a5fea44ade66c9291ca8d43480c1d48690186194e07769094137a94b9381d9e9c7494319e6d7e70b4719fc53e7be13bb632806876427c0249f77850f07884e5fdfa364867b2d8a80f7cefad6578a63bc933538926e709e4eef0d6ddd9aa8e82d8fa651c89c0dace1931b9e95d400e2389d16b1f9abd00d77055398f04ed70fd601a887a6b99d81c11e8d873e776dc3892bef39ad68ce30453cdf53f05fe3b202610f9a3565335b24169241021e4908d1b0eb0feadfe2d2815ed91297e832c29eb5c2ab601f0ae3b3da6702a9429547f2c3f259b3824ef30d1e8da968936ea1b96c64ea024fb98a213698e1f4526c56ecd92495c5b25c2dc4307cea28e62c28ab3587387bad0fc1eee415b44055a219ed42a43926d7b48e5ee10d6f14ce4fd05401e1e230efe47f90b1a591cc9e41f4f0102f3b812bd1f01cf4b72f90a248935edc7a207ead450e72cf063cfcc4f51352f932114811bc4deb38f4ede0158a8450c0cc441743ed39f9fea2e23e6336e56050c0e9fdaa8184f8e599a1abe25e8beffb5e28ceea14c2b129a6542a3806df35dcaa5a4b634bbaed6143c32bd01444cd5f688b3fa5682fc80021474bbd4ad1a81d725320c5fc3d8f6ae067b7e1f02a8e8219eac27e04596b1b047b9a96cd4ebaceb874c37317b40b7c4bae7e54c1e1e0144fc0aa59156b4ee12bf419ec2df1f4c3cec54bf08ef357e00921578c768a6eb0367af3d6ca6dd46496b27f16b79efb41231eaa26e7b44dedfe6420af442f986f38e316c99765eaf631134493d0c315da19df1864213a390673dd67098af7397edd819a1292234f7f8c85fc32340206cbf2ba949c32f849127e3ac6523a221421f22e804f2d6a4407600edbec3d28b503ba266b19d45a8163426ab644e8fc411f14712f77d3f6b231e0c60a160203ba3cbcd1492ea80db3cc775a7eb557469112684567f71a38ce8789d010d83469854008c4e85aaaba9e616c05d001303a51e75d9991d3fa7a9026d817154458730639d8bf7e7c094520186d1eca9bddf9350007e4305a175ef4ab242d8e5fcf402a9aee1eba74a1f9107f6911e39f1b1e76ce81f2ce5f8aeaf1d3bb6b43fad7cf4885c1ee85e3b51bc139a702785808acc20b6f614ae65f7014878396180477dcc55ee898aaff5be0fc903e500c27dc5a2a4c39938e943feaf410a3fdea765307170f191b7d69be67634c9335c450513a05b55033e30023e2108c00f0b2974a1a90f3b54ed8fb8471b2007e88030707f97dad64464cf452c07ba3403d4358c820e35cb9dbdf87cc0585e377600a200f85f09f5b35d9345e66df59a051665d89406e7926c189a96ad8577d2e155607cb53a38d608d97f5935e2cb0d5696848f9f0f434d9db5902c5579ee742d88b64c33aa3253de78707e1f4eb6b55de5a30060e3f799ca933ae1d398b7ecd72713208a9aea86d87ed544c956c5b90c7c30c57ba2c20e353e4c74607e62f01cfd8a797c302a9cbf42a6afb47433cfe47fcc0685d3f5b72973b3f43401f1ee01800f2d83b48ceca8b2fcf9371928d721d059f4af5f01be5c0e47a01422835b045858124aaa79725672c5e1c244088b2925eb84200fe859cd0fce93d28868503f9ac04b21f559344d3b169061f06758daea5951413d2fe5117011b400d3089160377ec4dec0ebdd3ea989d585089053098582cb58e920c03d2ef662d9b152ab0f2d674e192d1876f3d494e33e1816d37d12b7e67c60b40d2743013abb4e73f2bfedc8c67494e6f785353515dd225323005d9d451e801ca2408c1cf811c21e874a8e72530baa367f918a1e06575ddfc8af1266c68ca61ca51fefc6a00e4b6da9e33f8e64fcc14783a21a4fe4f3b2d561d3c99a2bb4a600f363b479c67e29a4cf759a7b00fb33ad320b1cff0a062e03bac4884f28ee7ff66ee1af942a5471328cde6e18215de89d6478ace167541a5bd874c88aa321e638a3752bd8b188ef90bd072d713103b7547e06161daddb91ca22ab47f5f30821641b41be886552c9897fca2359417b00d3fb006379efc857b6a47aec98128ba6049a07f4e0e5386b82fe326240ed4e473ebb81f76f318a8d31fbf3aba28e8e1576e3057072af0e62005c8a4d5ed2721b84f4fd91cca92101d4454b0bcaa5507f6ad23927242d6d7807a205780cb70059a840e6dbc767c75878200b305d9e4bc692d1443da83721d8f1004e23c2cc40ffeb3df883a43e736318100de15696811ec11e22518d23e8d8aecdfbc4f7991d16e313a16e1a4370b01c1e37fd659a0ecc70aefe1e29cce711ae7a5bb466d84d70e77c647bf546d395e9aebcec58e68e51aa563df22335715c2f4526eff36789006ed85ba84e9487ee00acaecdfec27c45a8cd4982e7c1e4840deb4bae70a77fb7bcb63b07138ad0382379934d664c0919f8c9c189dc721dd469c50796f93b697bd079a40a93b25c1bce2da10a95035dbe7707c23b5e88f0c607af084b7d1792375b94224c14af66b6caac0195318873b552e07afb20a0d04a3e9cf3907406d49aeb9f07b102e18761080b60bac078e412b208577547d6177799dbb8af872f4f49e8a7d6c0f354392f3326fd7a3d8f90f06a707058e281c11ca59da5e3a71c1cdf75bd090b18f11d46045533de13326439a4acb2ffcccd5109045780122e9ca21a6458f7701afbaddcccf08e5d257ca1ce214446c265ddf3c68654125cbb1c89903d46d2ebd78972635e2572236dd9d3da6799e29030f4a6e139b4492c8f9c5935fdcb6fad32a308978c1c619393aecd74b741dbfb2f1d7b340d46ba502603846e3693b0f0f1dffa6d7889d971cfea8248642271f340ccfa690cbad2f02a4520d39801c2f942ca0d1ff9e2ec47ebbf512ff02e81b2b658af3880334ff39c4c3ba34cbc5570725689c0b15a2b694f14c7a475f31cf0cd0bfa533bd66336cabc187497c4adc0460af6d5f8969e1e5ddc9d833f696d8ac56f53368c9c08bba69fc61a350e4c73f917977db4b5e44cf8d45cee7a416e12a7d645dea3794b4f103980fbc952f6d3d56c0b093f123352f30310f4dca20349d647db8508aae0a9ffc97e208132e8f20fe56e2a18b8fe93aef120446700add8d6084f5fc65ed7cc549ce871a410a8d040ef744cb7f73710ec77017c2f20f26210522cb0b359088b2968f108498487eb1efbfcaee08f50bd3d16333e21a9b9d5dd4b823a5ab1adb7e4ad31767df3cdac826f86b9537e492e30094f4785baa622f18f4142441e25315ec92f720db494b524e1af401b4a1e90c4939a5034e8b61a8e8e050e571b9b8d3c7b94495828b07093c5862e44c43ad015db4919539621adf23ffdbfa20b49b7bb6d614e2cc9a5b1dd8517522d0deb2b1e3e37a421b7571e2ace31d0e8a70601f81a3a2e0ec85be3c304f37de3f701860cc083abb054c42fbd7e11f09c8224c571c6251de497a3bfb256c74c9a02ccd54e0923f26f8c00508d70157d3ecbd2d43266ff7ffb7c236d1991ec2c94243547fd5b488ef8fc73c117c20a04302d508f3d60a433fa34fa75f05d1139b0856a0f84552b3882aa0701418f4967bbe8e16df3113b4299a191b6123bff4d2133577d379e7691257908275e90f5e43fcea2f23281805db11a2fa348ca1c61435954ce8fb0100c8f8674bbb0654738ec9cf52b0cf5dff000c74ed5e0997bd1433bd89ea7a1c6b6f8bacb5b31a5765ea2ed3afac0a135676e2fe84e0f24a44d96ac057c362c8123d990073109d9e3a8d96bb56055a268e992f3d3f9403e89cbb90490f7a170f61a5863d188e358a6183bb56c0225a8ff87be2e8992ba7b9c7930a2f1409503b5c480b19ba0476e769c9cca742d5eef07a542e263b407df57de1982199e4fae1f988040aa7d3e2c44f0b29f9b358f1c9fb8a59f3996a0150aa30d6c672d206584b351365004cbfe72f9e756f706bc314b43bfbb260660fe8f055fe3b46bab634362414e2b0a738f0560303f981524e96bd4bdd3ac57495fa7837565de563909d3b89e51bc895e282eafd2099ff8ab58c585de9e4f84b25843c6c964a22c5cc054310ccc837f2fefc6909b1dc038c1fe467af803bb272034a8f4497cf87267852e1fa729e7d6e66868febde1c6c641a7ba3617fd307a5669bc0e84059526e9b30734134c891e3cde6f19ee8fd859cb42c131640b2f1e243be4db6b017ffcf6852b8409d07f70d48fb8652e140573af731763791125cab4bf7928f77548f76273e498eeb489386aa62652b635bf29a6da2d0c9491ad6be0a001d4ecdf9e4f86833f1cc918f93a100f22350001774f19a0508f86e356227ec03701f7ce122924fec739b7ecf32cc0931554bffc8e5586d3d009ba4a2d16025eb2efb62cbe37e56bac8bb9260800e19a812263454423a8c63370767a39585c842de2e3f0a1c7866db447faaa3f105274dd2e4910dd4a018f958e0f07e1273b01d9218b1691138483a792323e931dd766594732932d5423fbd9fefaa325c3cabf27f3d0de111554a14834677718183ff54e34bab1365b71f2683ce1b17549f3d5c0e9027f146175c0fe0c5fff510180dd0b56df9aae3d00c3fb744564d2410f45b826cea9c43b954c595ed63111013dc20ab3e10765911415dfa061f48e013a85159fccac491611dd74b326442485623dcb1fd3841b74db64deeadf70a1b6967091ecdd4d74f30db421e316cc611ad6a3a5f76efbfc2e0a5aba4229b9c2a3346e5b44d5f9d531d1f625b4117e60296af37f73a64630b9c824e897ba16b20fb2d84ef94053c9c102d5cee33430d00ada9ce61ac3461cbf5c91778611e433c0b0efaaacee8d206f8f2bffd10c37fd093041efc4c1113e0093c7258c9f6c3c0f773c53649d955d26ad301f33a5221613bab5326a91e8db52210119e4143271cd636503f15bf556aed94675467c35b783c325ba4798052484b81a92365a4db708938589b7674057631d9b91e75c14cdc46ac3a7191c9cb4364aca3c712af4868c9255f6878d73ffe6e7162c547fcd297f077203e0a89ebd1159d31f04a4f0cb0e1959b678f96ddfedadcefac76023e73325cad20426158a7a280929e151673522d6f302a36e4ac6b6977269b2b6bf61e0eb0e627a8b206528ad752e9e7da4df301b0bfa4317ddf4914e4e8a6a8005c384ba73a848e86b4ee9f5d95614a7b8772f95e102c269aacbfa2303ad99ba97ab5be16b5b3d1d1242a933c346e9339ff59f9f4d3dd702c8f22845e1c911a03b6f38e45fc64189ee20b701694dff84fd0c34f308588be007ac1a06cfc9edec044c2c155ee530fcb402709b31c8e60c67a9e7caa51fe86191d4a739cf811bb2401670e7c35522fc46208bcc55ae2dde456309e1f0ac422af50a1881c67d63e2d4ea84536bc7573320fb8ffe8d10be0693147e9afe00bb4e704b17e25527250ecef39c8f4e37e324ce0d900651b8f5bf21d968e6de7729ff500a74c5d49215cf916b09a82dd40937c9ad876a2a7d2e58fa86b6a49c8d279dce2973422c3ffbe83706fe5e0f58e8081fc23677eec158297524d5b1bac94f7de25dbeeb03428dde837b7973a5eb55799e79e6eb22b87981cee2d4d29e1303aadfdc93b43754de670103ca66bb8f9bd0680bdb06401057af79823d1abe0f0ba9918e6f8475bb24ffc658878a396aa85096809fe07f1a651b42d04ec4ec6ba3573642089d3609d309a4b7e4a59da862f120acaaa7f0a876757497860be23e10f8690f697e1ccccc5d7fc4900f1464bea3081bada28c98a28de20d0428e5998121e0c8bf408102b88430128347d27a3e055f76abfb0a86e0be8cfbed9612c6987224250b3272e21f45dc212391153581468e28730bb8d8c9d37592552971859e43d91b923a6395391035ddd2aa9a0561f94b5788a4f3e6cd76132367cd917b82ccdb76c7f702cc0cbc1ec013a5bf0090ca302d7d514c92eaf95abcebcacecec4d5496755891df6607734c01ad2c845e948618154e60ebd8d8a98212bac1f0d2cd4249ee18fb29792af1bcc518dc72d2e7212a2ac71c9a60779419150c5c0e7783f58408c22cc2b3beb2160ed7b4c22c396a03067c26e23b264f9e4409733a05a1730cd26b1999ad40ed4ba18cbc88fbb1225bcb1a3c6ca903e81304bac921990a7445221d465694d9ed6455505dfcf1235774543a6883f83cffe2c3ed0a33c8e754c3a3669a9cb8e068d50e85e5c4647c098d9c943d5c0477f66734e5507f04da96e22f56db73298ef8985eb648c2ac6bc4ce4cafee1390e743a082c7322ff378f65ded22c5327297346a1c384b9d4653db07238b7884a90515b05dd066538798157baf715d30a5bde7c0c354a7cebc3325403118cc61a0e1ac061a0bea6886ae8be5a651f67f2942083b539be3bc2a96dfd65ccd70241c29bffb57451929be8ce367d06b8219e14035bd62b08405b011e73ef96dc1f8bbf4fe6d7a116e2ada893d6d66bb4f0e9a7202b9efacdd6a3655439eaec1d6fa891121557843a4628af8facd237c5b65063967bd6dfabdfc6a8442ac2a9cc5bc027ba69f3d5c7ef40fdf7e80beec65dd784c85ea6327dfcdd38dddad497876f2ac534693e66df50ceefba79f4285c067ae466581ac0abdee11c673ba75a2ab851ebd26885541801e65f80c9e48ef90c587dbd1e95fc70d3611d693022de40fd57a80b18493bfca77f2a503fd318251b26e09fa8a1527d624b4041326d296db3612a2aefe5869621d9206fba8a987c4fc42251b0659a678abfc1dc6287eeb961beda89ba25d519b4422adbf74d353d2834a208dbab2edf5655e97ab8b2f6563a11eb23cb4ffb0e8a77414ad283700b40b54ef6ee7bdb9ceb7db87f335419ee3069ae288026001e4b23b1947d7f7fdb331da0aec168326d5d8de697b71e2b0e8b25c61e374f5ba09cb7e307ae77b290e8cac6cd429c5a9e5e8846875602f9f85fb1bc85bc21bc9ff7f4ba1d953368786432f2283d46c883efdc515f882d2713180bb40726790096fb7c3f1fcffa68d22c02d038e27983ef16ecfb42434b9cf0d1c0f46d524365a13f3c4585c46749e22258c4e10e41935e7c8e90385945bce06e15f771a17ebed79868bbbf128288e2271fd62236a76852765ca62d4ef861722ec0aa9629cccf5d3dd4c71153de109b7935402ff668f540a42bd48c713a71187ddc77c5bd44c7d8ee0d3066fe802ad9cc45d40c5c31bc274f6708d98b634e638a4cab46419b1d10e59fc37c534e6f534af68879a021404278af1fbe89a95e69bbd9a911b2a6c92a7ea2b593e714e28da5d6f113102ed834c9919d7dd653a912e07a5bebf102575fd2272a253f35a20fd3f50441024313988473ba29edbf4c2916bb3e61def5bf5d7dc4fae281cb46a0821892f0591698f59e0daa334d52bd66f06d2a4edf6cb3d18c409675e8b363f8d8daca93fa8ae654e0cd60ac0ae4938bc15e5f2543dee42231388ee58be70c1cfe0a3f9e04be534574c2b958ee27f076a48ee42d465bf706ca8830a9b295574dcf03ab709af8c2ab7a7ff1797ae25d5f91bd8c5a0ad9e011a0a4a079c874389a0a601f924d7f3f8911eb100c81b6011e760fbf3e57c9e688aee81eb88f6c7c30a5ec86483c50a103637ba283b2e9c9001269f8e45f529b3f63926d1f1549c4a3ad3b35c993b0facb93788a1ec5e2c7f77f04821305acbfcdd8d5cdff5dbfefc9bbb659b6362445d2049a528a57c162806bd2f3feb75727ffc7139c1d48d60e032ff9f08e56b485dea6dac23f0df27918a1b7abcd4b22bb27b6df3a2195f1aec3bc3e4f1673b401444ac32fa6b41ffe09c432f6b0a5072e80db71bc720b5679fb6f8a5152198a9c0fbcfc8728279507df1ad9911730d2e15e04b33c9c9b6ee360b25c2f0d35eccb95fe7d4d558b6f197d091b53b2802ed754afa534136fa5f0ec003bc76688c8b2cf9c9c68cec43122c7f22093addf9b8651773038425a3e4b507a44076799932ee35131499ece217693958c12f287936001a00dd0da1a9819affe1528b88ae3820d077b8fb193a6bbf3b394939ef0362e9ef5e513f8824edb0eb289e26d4e045b969caf23d5a9a8578fa24ddb3b1a3d20cd2816f400d3e256926eac8aba6df85c14f1a2b2943e06babf61931db7ed8a197679a9f59ed8dcd776ae11c2e6d42a15ff57ca3ae7ca947510df6d2e6aecbba9133d1c2fdfb6e7734804d16cd79b9bbcf1a054997c20b811a7b8c4a041c5563d907f00395e1cbbc60a3b4e5d3ed071d66bb4a4a68524393bb0fe00744ae02d7d6a25f5bfd5751c4e9977b7537d3b833075be963a09c7cc23ed2d622d5410209727d49c0cf9b82bda186279183e560016786d5985800244ed8e1a17e06e52e18d0ac283f278a3c81500119e0517cc13408e0ba302ce470d420650d09b81bff7303dde051ddcac340cae1056fbfce42f3ae15ac9fb71fcc230bbe76fe45223c223ba7c4bcc289c750bbff95d02cf4a6c580582685030115eba8195d341a0950c186af0dcbf64897f5244d6d19a1375823c68fced36ed8d0287c6441e990e7a3cf73594596459795600719367efbf22912e935523b3fbdb6ef2aeb5f0fb527846f6d5848e15d2f1f47cc0586be56baeb1b6f4e41cff38b40435b73c6547b367565cb16eb91e1eb1cc762f54d404782eda99d367fa38186b77e7ffc99d6aef02527efd4606c45e270c14965da523020250c0a8f7fd6447a034620e5fdbc9d0023b790573e1a149b1070741d1991db158ebca22bc5b8c7c85f4753b19464c7205883155707948082755acbe6dc9321595578c84d1da134b6e528879608eff9e484724170ae0270ddd981d9ef0cdf317414c57f1cb5665f3791979701fd6ce7e808b7c41035789a83980dc04918782b2c040f38bc84ca6b03a1d2951af64bbc845c7356a51e8b4c20625178fef213bc35a4b3f7733e2db8d42026f4b0c0a7d21bedb4d6f9b9e7309b8d7db73c1f02a658d0188345ed084b85dd79c11e7c9772e5cf0cc950b01afa14c7c4e57ecbb8c636f709c0882390d26da1bcb0123f7b3e5632c26e9ecab78abab631f6e147a9ac46f8db06ff077728b64aeb3cd288accc4de1ed1e4279ffe83bc8dda60d8b3daf789f5cf9d46c57c77a32ebbffd57de84a614a4c010174e5685c366fae6756727b3b9416d77dcd2788fe480fd3eaadcbe09c07f2cdf0bbc71e2d40034cb10481789cb6dc0319111653d7fb303f2ab619d8b81f15a6f4d3cda61c44a18f6c22fa8f8dd1f6197108b3267ba92ed8fc1fcbaecf4e2c9791d8e24b3ddc3f955eef08d710790137e8556a05f0601fe47235df226620258d3a97d0abe6611ce068e568223e7954341b9413089b29369743a2c52e90e2473fb4106020d617c1d181e6479debbb47dbde237b8930a891c525d7d15307cdb832c78daacb691d5368f9dcc593af5de7d0267d0fe30addd996d9d7c66cdd9afdb0f1dbf6f055a6c13e0d310a65e6cd0b92063da820d72fd0af8a9fabbb57d0301f0283aa6afe075404e612573c2612032779ec5114fc44eda62b6bdfe8e24fd1eec694b0735027abf682e6d7b8de59d6362f20c96976f75c64e8be26b9fb638f65d6a5ab23e45ee100f0e113f297582e34d07b4230e3eb8cb33051a1c77cbb9f88c7fec4f3d0c3f4019d2c4e9ecac96c178fc38b046c08efb6ccd1d299a36e6712d0cf9743e512a427d5578398756cea2a4f3f7c964e5c6691f199caf3f39cd355b5fb75efe08ac2fb4f3a69eaf71439fea2a8d0cd601618049f490a969f0b099e8f72218f318bcc89731485767a3bb794f2d90781f8c4ea62e990d2c4afed99cfdafbe8ea06e82a87834de60e9232a72155346f1bf553c68547afbc7c23e1c646121018c02376f46d50df7e6111f4182f4f04804e08b4e382163c38ff0790ad971805d2c527d52d7e1451a6c0347f25546d5701f0932f685e0d90069ba3619e746031cba0b7cbfc0522879c3fb1eb5a7a039c3575a53f7e78ff2eca98b47039088ed0ebf6a20f9f609fddea91c316ae78c381d979a43759f3023d0a8997397ca82a657e65bd1d1eca0335962804368342475cf993dd28d99eeda274a806d654e044a73d2e2932d650e5d1e202267da82b9543355bdfee55c39fa14a791f142943e50d0be04879984e5204fe4b69e89d7ba5824e902fa529325065b95c123b52779714a74c7afb866e27135f48edded799c02cc9043f0e0275e9a9891dfc6b44ddb6fde0248cd43d8d23f932b71047c7415f1dfa9426e04579e506cfc6cd6a78e6e78399be199b632e6512ee46fdd01e0be83b9152ebeecf82a232387b8838cc96a30ac27bad5184705bea0f555a6a960750bc71f861a09d505c527c47850a864cd98c7c8c6cbfba82c60708efc6c625340e0e988a860fd05498b3f72e0b8ec83ee3c578508d15f5817da3f2893497aead47c53120d03706f76060124d297f89f245ad30e044d1da998ef690d463d992896dd48c5569d41a06b32b81f0c0b85dfc04bfabbb32e5872806579cb9ba59aba33176add172712eb501d5cc37ff96d218d7601ca31a457766977935131659abf21cb4e804dd276c7950cefdc79e2abd1c1b45c0b74060a97716999bb7de2b7f60fc012a522c16ba453b0dbb5f0f24739a7011e9dc96ca17cf4ea41f890c2c4cd292a0fccfcea6a773317",
    2  "header": "eca99df66c442c1f5fbd50075c06c311dd8b802ca97cbdc2637948f25d598720"
    3}
    
  71. jimpo force-pushed on Mar 12, 2019
  72. jimpo force-pushed on Mar 12, 2019
  73. jimpo force-pushed on Mar 12, 2019
  74. jimpo force-pushed on Mar 12, 2019
  75. jonasschnelli commented at 9:17 pm on March 14, 2019: contributor

    Currently testing a little bit… Would it make sense to log more infos? I think along the Pre-allocating up to position 0x700000 in fltr00006.dat it would be nice to know at what height the index currently is (maybe every 10k height-change or so).

    Sorry,.. saw Syncing basic block filter index with block chain from height XXX too late. NM

  76. flack commented at 9:39 pm on March 14, 2019: contributor

    just pasting this here so it doesn’t get lost:

    02019-03-14T20:08:25  <sipa> it should be called BIP158, there is no p2p protocol support in there :)
    
  77. jonasschnelli commented at 11:23 am on March 15, 2019: contributor

    Took ~45mins to build the index on Intel i7. Here is also a histogram of filter-sizes over heights.

    blk-filter-graph

  78. nopara73 commented at 2:38 pm on March 17, 2019: none

    Concept ACK.

    45 minutes is very impressive. In Wasabi our first iteration of building bech32 only filter table took two weeks on a powerful server. (Should be a couple of days now, but still bech32 constraint is a huge cheat for us.)

    Update: It took 11.5h with the latest code on Wasabi, so that’s how much optimization was added since the very start. Still nowhere near 45m.

  79. PatrickZGW commented at 5:53 pm on March 17, 2019: none

    Took me about 9 minutes to build the index on testnet on Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz

    Testnet block 1485036 (0000000000000070cef6099001404170fd4860ac15eede7b9947261fd54d8bf3):

    0{
    1  "filter": "3b42da6549c8cfe1037a1e0673b7ca7cf823cae390d8aa21c1074f732ba50f73cb765e8ba0bdc1e093a1fbce25cb35b0cb95c41be226d77080512422eb2278fc007984fca6eabb00fcbf7511da438ac9f14b571ae330914745414d85c40d92fb11e09940b6a75deecf2eafcd7c8909444c8480d62d925c234b07f629cc21a4ce7fc57a1982eab8e9f0cb48d7af3273184dd42f5696625424c7e234a4",
    2  "header": "a5d51fd3a49a361134fff81270edc210952f2250d71edaf5a16e234eea2910c2"
    3}
    
  80. ryanofsky commented at 5:42 pm on March 19, 2019: member
    This seems ready with no more dependencies. Should it be added to high priority review list (https://github.com/bitcoin/bitcoin/projects/8)?
  81. in src/index/base.cpp:167 in 96b3234b68 outdated
    165+    return true;
    166+}
    167+
    168+bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
    169+{
    170+    assert(current_tip = m_best_block_index);
    


    jamesob commented at 7:44 pm on March 19, 2019:
    This is wrong but is corrected later in a801e39472. Wonder if it’s worth amending here.

    ryanofsky commented at 5:46 pm on March 22, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (96b3234b682c0c8d639aa866c9888376e4f49cf0)

    re: #14121 (review)

    This is wrong but is corrected later in a801e39. Wonder if it’s worth amending here.

    This commit also doesn’t compile because the Rewind method isn’t declared until the next commit. Would fix this up, especially since the PR needs to be rebased anyway.

  82. in src/init.cpp:413 in 634693641d outdated
    408@@ -404,6 +409,10 @@ void SetupServerArgs()
    409     hidden_args.emplace_back("-sysperms");
    410 #endif
    411     gArgs.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), false, OptionsCategory::OPTIONS);
    412+    gArgs.AddArg("-blockfilterindex=<type>",
    413+                 strprintf("Maintain an index of compact filters by block (default: %u, values: %s).", 0, ListBlockFilterTypes()) +
    


    luke-jr commented at 8:14 pm on March 19, 2019:
    Please define the default in a single location (ie, not both here and GetArg calls), and use %s in case it is a string.
  83. in src/index/blockfilterindex.cpp:177 in ac208f820b outdated
    139+
    140+    size_t data_size =
    141+        GetSerializeSize(filter.GetBlockHash(), CLIENT_VERSION) +
    142+        GetSerializeSize(filter.GetEncodedFilter(), CLIENT_VERSION);
    143+
    144+    // If writing the filter would overflow the file, flush and move to the next one.
    


    jamesob commented at 2:19 pm on March 20, 2019:
    Conceptual nit: this logic seems like it should live in FlatFileSeq since it seems similar in nature to FlatFileSeq::Allocate().

    jimpo commented at 11:36 pm on March 22, 2019:

    I agree, but the logic for block/undo files is weird because they are synchronized (like the undo file numbers increment when the block file numbers do, not at a size limit).

    Would be a good thing to look at refactoring after this is merged maybe.

  84. in src/test/blockfilter_index_tests.cpp:111 in 4f7a4a593d outdated
    105+            return false;
    106+        }
    107+    }
    108+
    109+    return true;
    110+}
    


    jamesob commented at 4:09 pm on March 20, 2019:
    (not blocking) The above two functions are nice utilities and general beyond these tests. They’d probably be useful to future test writers and accordingly could live somewhere less specific.

    jimpo commented at 11:43 pm on March 22, 2019:
    I’d be happy to add to test_bitcoin or TestingSetup or something if people think these are useful in the test framework. I’m not really sure. Any opinions @jnewbery?

    MarcoFalke commented at 0:03 am on March 23, 2019:
    There are methods in src/test/validation_block_tests.cpp and src/bench/block_assemble.cpp that do something similar. I think copy-pasting them is fine unless it is more convenient to share code between them.
  85. MarcoFalke referenced this in commit 93623eea71 on Mar 20, 2019
  86. DrahtBot added the label Needs rebase on Mar 20, 2019
  87. in src/blockfilter.cpp:22 in 634693641d outdated
    17@@ -18,7 +18,7 @@ static constexpr int GCS_SER_TYPE = SER_NETWORK;
    18 static constexpr int GCS_SER_VERSION = 0;
    19 
    20 static const std::map<BlockFilterType, std::string> g_filter_types = {
    21-    {BASIC, "basic"},
    22+    {BlockFilterType::BASIC, "basic"},
    


    jamesob commented at 7:55 pm on March 20, 2019:
    This commit (consisting only of this line diff) could probably be squashed into https://github.com/bitcoin/bitcoin/pull/14121/commits/aab05e29d5d2e3f162f0453a3b0ba7205e57356e.

    ryanofsky commented at 7:37 pm on March 28, 2019:

    re: #14121 (review)

    In commit “blockfilter: Functions to translate filter types to/from names.” (a0bd77e2ad5bdbaeca529a38335b6a6c2f3fd5d9)

    Previous github comment can be marked resolved.

  88. jamesob approved
  89. jamesob commented at 8:17 pm on March 20, 2019: member

    Tested ACK https://github.com/bitcoin/bitcoin/pull/14121/commits/634693641d73f3bc70ba2c508bd4cb15d69e87b6

    Code and tests are really nice. Well done.

    The mainnet index took 154 minutes to generate on my Intel(R) Xeon(R) Silver 4116 CPU @ 2.10GHz (with an nvme SSD). Here’s a pretty unsurprising graph of progress per height:

    block-filter-gen

    My filter for block 0000000000000000001cd1a6b5d0d226d2af982c87d7c82dc033936af84c7d88 at height 565,900 matches @Sjors':

    0$ ./src/bitcoin-cli getblockfilter 0000000000000000001cd1a6b5d0d226d2af982c87d7c82dc033936af84c7d88
    1{
    2  "filter": "fd5927fd10419d25d44e3608af3c634ad45a6c6d7604a8ec4f587e7f0db41509cfdbffd588d25d4a8d5eb55155700285e8cbd266887ba7dbf357d1f6ac7aaa8748c945adaeb78ee6c4f1cf467b765a2d593352e65bc2e31bd3f7cf41fb6fbd10b6312b13b4831dac52f185403f62d0e4c92deeeefb23bc9565893fdc48c818e0ee5868febf07e21d2de51c6c7433de5430f1d0e2db0a9365966d8e8a420b4254756cccfad4a74b0d43dbe06d454c11c3da6523963dee995cdac584902929eee4db379d1061bede0ec204a2d27c6e58bc209b59cc3a8662588fd48635900bc48fadcc9e7efa500ebbde03ff22d0316824afc0b354d427da6014c47758ad2f7d198c98cd2c46e0134a58faaf0cd49d08ea2c8bcc485f92e61298321075868b6f12656d1f976c5779a9b45f169cb39e384cf010e444375331a2b8e2fe14c07e9eadf2c429ba30fc6e56eb05e4c209a09a1b3989fa84440be20ff5af2815c4e73cecbc6f171ba91829dfecbc4cc886b8c8fb59dba69b84740f3cc07ed8aa99a194effa2c8687fa8e71fcc2421014f92df79c761cd144dfb4840a24d87f9881fa5f0065684f8e0d553f7a9a002a15a38ab749591b112a0d75da19906948b9272b3727c6110d6dcf1af9b60d65f459f0e1f17bf74249bbd389b98736e37b0aad8d8a0534baff3cb9fe36a02d5cdfa494c8ac58ee73d71347f0e0fdbc8d22c2d942b3dead231c39dd78957dc2797bccf3d40c99601c6953812424e91cf94312e64d85dfef7dd0ee5aaa90d1ae03f8469860a42f822fc83e60ff2ae834eb53192287528f5d89d6f294239a29d5dd50c69a01ea37723dda5e231f7922851df5dc32133290202b0db6efd725e7a025cc9550d274e7a614eb8f1060dc6f263ccb96536b75575966f17b27c5002691a0a315c63feeb7df971276b25f9b6f202c41755e368f2a8c1e726a460a0e4fb100786920cfee01087cebc1a3fe045ffba16495e817b86802fd7bfce37dd07db0d3b0f36cbb1d531c2a571bc0399af10c8c2c4e5d2eba362bebf0cb4ef71f1a11d71e5a8a0c4d56d19cf13a87c354a62e69db909d38a8647b7e6273332f0eb6a9ca12afda5c885eba40bc0fa2206e929ff84457a4bd9375ad8f0784d6b7fa451d05e467ebf36407a30acb383c55ab9017292ee304bd3d3a74c55c59a7b552b5a5f73388641f1f64203d2ea27c6bf6081e16cea29c067594cb6573dccd3a842b67804fd1b59643887abf287aec4627ca85a9c708b07655ef18058c43c09adeb70274c2386ad52d15a83470d26b658cd3871cbd7de0d0cd60009a13bbad1873dd20cc542ca3c4fd2c66ddde3f12eff31fb9aaa5609564312fc200134e5c0428af61dc30d1aac08542c21bb851a2d07fa939c1fe0c10810463285ea9d589fe9d4da586c2bc9df0bff0b1c80f60ef3343af146f83280cc72a64b47b634250ec4c5d0801f3f96933abb2f3245cf4defd4e8ce692c4566a2555c99b68e52fe48de7ebf1bdfba62c039908753bf122a84a0880ded5d90215190080f8af0ddb493a8f00e677b27f48e14cbdf4561a6d1e1e3f74371dc583771378d53b1a7fd2f07c19c740e19abd5ed5c13e9101821b33bf64aa338f81b3f3ce804447ccbffcc0acf17adf11539cbce58484ac33ce75f1742f95281db64dbbd6a3d647ab940f3c4658a2ab09492173c87c77c89ee606ce53d2052ef2f054a41f62091a3452aef8ea1c66857f575ea4f3708685440f1a60d8f1f856b9860ef7cdf070e14db00920cd5642a6dafad1a130b0da990b6ee4a6cc9c3c3fab2b98cbc2201236452c6743fbf1016af22508a7d5a502ede69d9f4da376a38edab7f38eaff35448fc06b1b8456fa612cee21925ff95b6d13673941400e5a2e4ab6b15a24f8114fbf9bcc29894dfb59a203526e1fc62fc83a3ad979d34536dadd3d1a5b6e058f3f2aec8d120ab234fa42352a8bd44e5ed605b0b80ef0400a6af20185bce2833a1a622ac48165025b33e7587bed8f9e344834b271c2d64c5bbda3c07c96dd8971332459aeba128391f4b9885e8bef5080f7daa4e08f5e81b4c913e809fc75bd29bf0231a540ccb606d00ba7f0d0f544f3900bb7764868fb72eb109a08d93c16f2982fa6a07511d5b1cdfd9a9fed086921c6017efa88e1a9b43412a460c3665f42fafa46937b7df976bc13741056d1ed1dd61a9413816ec3450f20025f7761106f5f41b1e42484635c984b3336aaece51635b36bea5c0c41e29eac16e84b8940255eeacdba54c32ccfaef820618ea9ddfb83d553bf4f44b30d3561ba9fbd4f9f02348947b58ab3eaa26c47ae2e4d4859b6110354c7ba7e08913f8ce93630e7568d0fd26c225cd25d996196513c1adde82374e94b8d510ee4725373c2dda85bbd673ca100385bfd6b840367ce499f12421b04742738d804deda1e57985682a5fe2c6e2dd5d8ef33b7e3c049cfb053c324a28396801c512a9fcdee262b4a57f2d0bee8a16d4c3d3f1a4659d322ba78be62395f661e19f56d9f62871688cbba47e6e72c4893f1e7e282228f132a20bfcfcf830a7294e142ae9a51499a5f196bdc28bf511a28ce10f023404903ca628b74a75ba1b463b822e27fdfc2e6fd6113cdd9e151fde50ab0f62a7a041a48e06125037eea6b8ad8058bec61132a1baa13de698cac3f59df00ca861840edd30f11f8281324d8f5452de88ab1a371f4c89394f40bbd3fc1e1387e2951034048bb1a3d2f8cd59355e644014ec2ac25226145394c11642c27b960bb7f40ddf89a51e8ef57cb4e671abd4e0d74a54a7ec94c4a5223fd12688606078ee0e6030ef63e971856bba1c5fe2b325fd5a2650216c5eee49c1d8d7a4ba8e6245ee3c448e1fc92c3ec1b1abe72e19d08fa9a71260f8eccaafaa76c9230d24581dbac6efe4dbd9084d3f3445ddada82622dd0eb3a9a171facfefaf7678741e71c78cc463043bacb113e18773404aeda451fdc6c4ee03fd438e3ef574dc62c525d1c0a09db7f4638c8fc07120008111ba977fbb80d6502e93f7614356a8c237c86018f2331311c3944dc3c18df1a336040d657b84df48ea5e717650fc002492b8c3f283d3d700d204b3d2ef81d51423f55872ca3943c988e1317725e10cdfffce55d86d0e56f984e5758fff11b745d5e29e2d22ec81f12ebd5e5613639ade2124d697c97e09003de58c9a1052c73b9319d40c094e831d2e5a9bb8fb517b33b1ce197424370fe527a0d1475ea64b6ec767635337a11ac18b9ef5f2e96be733aa59d422c1c270e86eb4ac39f093a896ca31b4b62d6280a3e6f6443067ef4e3a6240d55d2587e1f3c8fcd766747446a1643a8becf22976ff2e1d23f49bd384f09ad2bf2a68d9b94937eb6efa06ca7c572410ee5bacbf922b0235caba34319dc7c9c11a48db6b3a73383f8a00f240c537386db91530ba354878956cfede119e4d259d15a26cd9b800a227d7e84c09ed0b557f56968c9dbb2fbbe13382a35f37250a41bafa6dc7759106c69d64428208c7d24f4ffa1dfb5d77bfb1f5a04d01dede1f64eda1ab43202a2efd32bbdade4216bcd61085eee9106ac8f2460fc28d9da60d2d35d3a4e58bdbf82f3d18fb2e6853718d88d1f2a0de56459efdfeb1b72d4746b3af0615a7a089f4d3accddeeea85625ba95b097f9805b2ebe659e02cdb296743d7328e026aaf6656bc8778b932cdf080726614a86e0330524fb68ff0e9402288ab56510b92dfbcb8261aa8bcc04eacd86224dfdb190c5a7dd44d218b366b193fecfe93b5bbdba8bf67db0b10984df0b2c0573832807ad7ffaa804bc630e4f09fc5958450b5f639f55c2d85103648da5c5ca0bd1243eedfb0ab21ef62233c71f5ce841584f365dcdb4bf38e4898774341fc87ca9c42782b6fa567ce36b266342de0abf1cb132382c9eb1062f87256d141bfb9a66c88475d8c84590fbd58d4d33e83c157449d90ed1c021518be0242e1e5919c5950c6e2d7e86130bbf53911bc726feb1ffdd608ff19c5644a0350b6ac43db86b08ee666c6ced11ffa1e00e9b80e69bd45a7aebfa06e38fc5311f7dabe707d01678c1f7b822b80c27cc660c6fc83cfd7735bc9ccb0a8ba445f2ae9357ff2da957dee98454075f94cf1a27730e2902402f47cf74b4f6c7291b028041883c7df47aa1a3017e310fe7ad25662d85b824f60af8ff95953bcf44d126ca73371d54092da39a2214a9c88320bdf0d702ca78693aa36629f605c294a956f93e65e2dddbfae2368264b917924686a22eb222f504bcf4c955fecb6138380ba266ccb6c841e27ea01239c08e3e5178ee9b2b0f5081b483e8bf0ca381091d4a848611cfff510fb27e5f6451f38064a2809e8457b898438323816063f39c96018fe84f6188c40968d02d0d2f80fa12a109dad97ea90b765b8ff76843726b109f68ca9fb25faa26a9a34427131b04cebb4593fcc0e020a56998be28a8b12c8c57bb3bc0b0c88e32b43076b222cad3611c8da9b87bf84734df49c091e47fcf613799e949d1d0defdd7465dc5e228429d98c3d9848c5972ba8fb263a841bb1fda4dcc073ad8871bb7003a3cac97a54293b7f916b531b8a7184bb71c3994eb92e7bec2efc389a3984e94c0821451827c6a487d0d3908c75cff6269e77c0f45430870e83da3d64ff08863e4cfd796e1a1e56bcdfcbaa85fdaf3e19c346ad32f0e8775454c76261424ad32a30bd208b44ab551df2c5c738df2b60a916e246f287aa812dc920673f58cd32e626eebc06c7011f90728f193efd52d14bd1a938978c9b41386c102044be132ff5f946019ce7eda8ca2cfdda90dab1cd0982b2b36d503499090e82c7f06918cec2fbf6cd0c171146ca82a4eea86efdbca395109e9e49ee0ebac197363bf64501983bed2f610fbbe87f6b790aa5cf282472a7acf70d083b827d7cc43ecfe093a1968f2fe3d3089f28ceeeffbec071bc0ad9298d625d0b0197a92781fdcfc3037ca00902796cb5765f8203d0ebc5a348bd849024a87712168463e6a607d3e03bb6261c63e479394f62113d28bcf0ed89a6034a44bbcd15aa042f91c821219783324c94d0bc99e7f13ade6faa52beb279cc0cbbf29c73b0123c03e5ed5c666f44fd68b3c330798e9ea6ecd907c4e0c9f781b643bccd58004a67634d855a773c9d5bb37ab66d6c0fe84294b7986da0caae0f5fd46aa3d772fc4e7c47ef3a6d2353df37a9a3b931d5b5b4ca6db6f8aef9bfc9df07d63aca4304254089439b173aa8d3683f5d182540cbd958f50f360c163a2ce5aaa175a171ff9972ba193ce3f3c373f8c4aa49d776d256c58ed6e661a8426fbc6cd548cfdc5f28b5327bbe44ed94f83ffa5c3285d49b60523a155b998ad10e917ebd1a58d5afbea2d94595b9080b08d89e6fdc743ce283bb49dbf65e2f0d49b4192003d73e9ef53f5af7d96da8cdfd10360e02c4affa1e1b639e2b2ae45197de3b513c17cc8bb70943c5f889f68de5601e4d0142d56df550f1ed1da8e6688e0544b47671db9d93fdd2306da023857900826c92c3215bb9973b603b7b65fc619ecaf463faefa399cb39de4c745e509364c232e4b89460b1683a32a02023b99dd2f2dca1bb224beb3a95d256a1cce91b52581925c670682f5e0250aea66dc48fa68f8bd1493980a450dbd3a36d44d185f807934376a9f4ba3e0976c41f242693baf547bd27b68005c21508804c5037e7d49aba259d06d10e8e636281d04f672d2482ec034c181ce532b7e240c6b06962b2a867c0d8fb05fa7ca1aa9e2cbbb856e9d56c59be6e5c6a0fb7f4f46b4256cd5df442bb09ef46ff2f58b0c0c04f40a1db02c55144be0c8c094629394e492f0098e9652cfcc60c0d11c896e72529032903d794ecbe3c5a388dbd687f9396a5a7dec1c08b28fc40ff9ab2135f5fa0066ece0d8f25510ec860e9c6852a03da9a3370a8790b6c94c57a1ecf924d2d738472999b2cb5621c63c84ba1f91b3f354cfa430bc504357f62753fb9abca070a735976bb8f8253d3f0ba73f4e73efa5ee6044febe7934ba1b15bbab8bf539f101e362e28e2fa4aabcb6f490adb6e66ec8e815e6df72cada54b1df17898cb2ac515c150afc7c4d5f08b6afa341270e2855af03408104997ac584341c928253cb62d525481e85bc43bc9efaabeb17cfd15219065dabfcf9d19cbb153708d1c024cafda9ce871ee0d60ce149290d4499abdb7e391bfee0df6209c67c8805ed2cbf623f0fb0e5cfc819e703b1ab5d0356855f4ec0de4879fd84d7ce841df8dfc4c86adcd81ba3ba5425920049be2acc95ffaff510295cfc8e09a92321e82892db551813cf76235bed4dad279369e27fa10eb00e533df56427b64d43c5130485f76f31748d936af6de253ca29e1ded85260420b8cdfeb833133193f32dd14fa92d784f02b90c4dd7783a1e6af9a64ce61d1671bae59ef6ba6e44b66a2d437205cd21bbefc2f51961288199b0988c7c2688548efa3a0e397764b4d165fe681176dd8da084a27b741cbccac297112e6e0b1cc3d228912c6b2ec511a2a74fd6c34da72b58cd81c4aee2fc746d3ced9e5714f7be6b0d5aa36a41500f2754220b6c05e144a822aadca20d43509607e12835a8111c50e180d5f523e3af2b2219e1bfab4b66922f6f9242ad3f525f0e213e3f91724ed1b4b85f89fcdc59b68a0174015098e381b1e943320c5fae179c4a0d3e1b5005598894cdd11df85bae98a61cd04b3fc2986aae94432f6dc13388ce8bc16e9bad1e47076915117606ed4be3ee619b4e9bdf75ddf144d3dc8fb2fe557f7a02a610559a308a3a236e1e05a003befe536413dc0a4f9fe1f97bc7559d98722710b9636820502b7eb2cb5bcb851c5a61be4e880eda2f5a2e98f839b422e8c9c2d7f9de8c204c2d7701738057c3fb0c3e5a6527e74e7f396fcd5264904e867e4c3062fdaf36f86ef87506260309ac059a010a7eb0f9a5f4fa782ad4e95d4f47047f82a9142a8b0256ae64dd58e9aa43acba3275b37b159877cb2f6dcdef13a9898852130a2ccb034263e3eeec52e1e5c7be14a2310c6641fd8da3797288df26f2806fb216323f2ad35817c484b01c4acda459a581fdce9cb54f0f97b2d8b0f29cae3466eaf5c198c36dcd7231094147b90c68d4105d87d58645a022dfdbfa982c3e325319f0739f39551888576490e825e0524fda8946c302c3cf602f4d98d8d692df21a7579f3db16cd17b32ca90fe4bddf0300cdb77d9ba0a6098fcab420a991845774481e907202ae6ee8f397751851892f48b0c5c9040447fd026fe21a9d4e5d81dd0add80484c12e891e8e41b5802723ee01406ff63f6297d9bc2839f42d6b7352a13f749399a6de0440f5fb29fb73808a36073c6125338c2d8e7e7478dca01f6324c7c101a44f9b19cc08777abdc2db35fc197e33f5efd099a6b7f1ca2c8f80db2ca4d30c477129cd2932b01abba771d91e0e28144f59951850f604fc230bf44a6cef54186d59aac1eb6855694d8a9c1e01d89f1453afc7001a3a6d261ccf7c367e93756e89d4565e112cb45e5031b296127a12b78ed5dfb4225a7fc0a2786622bec4ba3d050004e057e69c3ed6c7652c0cbeefd8e7e5acac88de75f2cd3d86c32ac9428036753222c7cdc191d7b24a93129a7f089818bd867e6c7740cd818a64f09df3690a778b97e17db6d7372f118b8d096a065acde4032b432b3ccde46df4e001142a7b80917751797d14b36f637820853e6f02504520d6eb1e0b740badf9a23fa432cc834e7d026f7e3f64178aaa708a67318624e1aa44721082503d527f046dfb0a574bb5042c82cf123462624c074c7e73802e8e2f27b07d519f8e2caad7fee3758d1c5bc4e21166ea50e5ac1f9acbbdf86395c4eb3d22a0a65564e2bc0a30240101b4226cc5e53ddf9d2431a0ffdb35f4ff30af6558a73253544ebb947fd2345d9e9d451e55e2f2aac04a7967c6aa17929e7632c293daa95cb4c4db5e57423ec180324a8ddd5cd18067a6a958fe0b848bd1dd43f04cedeb8fa14551373408c297b9e53af6013a2e43084a8894cd8d20aecfa5282288e2aa55371ead7d5de04e20bf64baaa4a8842e20abb0bd4632fbbacc856887d8015416398caba4f94ab10335ec0182153481c461b2a340709de374740165ea25cac00f69273bfef3d720a5bc2d1f9c4911620a20c34fa33805e262bf0f5e923a37dc1e1c0b3a1c21d9602eedca094b10c1ddd83e093c98594b098d3cd8413dde1a3645b604e75192f2c08e414948d96433de2c1020e93c7aa9c0ba130ca2f15ea484bdbab3f1d143c8a202c72bd61d5929289fe05771aedc4c0cbfe728ef81c973d8af7ab948789d615d955c23b15470ff497703935edfe3804ed3791e115006001770ebcab335c2db57c5eb134fced842f4cfb276a5eb0c4c9d008343a7fd6b3521061502396a5876d3974101cd85354a66fb8ea8d52d4baa5cc31025ec00cebc568a9487d33d643ea52c0838a2db47d33e31cd1ba3e3b405cea9bf02e6be038a08a5402010553612fe959daa26067afe56894afe933d58b9decfc474911b676912df542b701213df441ec6076b30e38e602bb7c6084d152f18a35f5d073832837371c72a3078a69995bacf10a3d60498e7704a7a6c2f9216cec9991d4f005ea0faa2ca2948d992608661f1592fefc41dd622b5a556e50307cedeafefc61e15a1aac0f717a00dc2e5a0e42fcdd4b46707efa5ea825f3749c04b07f85f59e3ba592c9c60a9a62986117e1f2dcfa1db3c3130a558959906dbcbdfc6b42b5715271f68016afe2c255ffc75513b90d1c4718e587247463e415de9ec29a5d8b7aad0e636b9fd142af8d7a2e8da339781b00643a724b37801683365c948b555cbc1ec312efdf6a11638cd7e1134c1aa80b84c02607aaa97045e6610919d728749a33e1ed92de85a22212b608367f981681bcbcbf76940dde8ff46c8f7139baf47232eaa415335cd018b8e303d62a428b3687f4bc2c38025a6199e7d69e6bd2d3b3dc83aba2f76c10376fd972ba1e5e07d194250b9814dc8e8a3d92b48790a78c0fb9da6a5528dd9cb25bf92290362c20efc2943a9c11cf8b732a53f0056b4bcfb5c7c6a0133b43a4da364059aa309bc087ba2d891f2879c847840c5ec99abb2eb5709b141a588c400a4a4d49d21fe44b58cbd2c260380f8c47b2714fe2a8c8330df8330b0b68441020a8383ae3efae57454f3155b21e7901e507ffb9277c45a0790910f2a247f29741d62360d80a53530968ad382d4e50e657a003052a0bd24e3d7bc87f898d0beafb9ca3208007339eb094215eb19438e904d850d14c3a3a40bb88c1135e92d332b20341e46ab629629e1d8297811149d4ea074551c12ccf21b131bdcbb12da9a116e0d81faa764f48dad169f3890adfcac9a6130a807a6a7c0c52e442f8b2314b9e92308aba63bb85279c1c93c205883f9c7f71f512c1f1a69e46980bf46580041d74772a2d7c9d02d9f8faf4f0f54f0207fe2eb69c2bc23e9ac672e415d9946e17254bd5448883728d2b406b2586c2611e6de72700e5d99dc023e7c1e1c90bcb41ab37c2dc153fa108f44dee23eb9c1a2f6394c160457dbf232d20112120965b8b50098f40c8ae4de3c4ae3b7628a8fb2b80171cb5c9e2ba751da3cd41f78b898e25fab1d88a26d4a02e60e5bcb3bf7c3c34f602cd551dcabc9fff4c778730179d3a81e8b63840c36563d3fe34b6071eb31e306a125534fbf642b905d657c7d812de75856aaa20055cc968c6709ace167dd09af27b4809a8ec5b459d3d886a4e810656641a4bc676548672d7e3cbe833ff5b5b9404a04dda2df94100cec248d1696c136698486369c48c72ae5a1c87d2ad9ab216b8d6fd4cedc7a78dd132cf3148a344901c436c1e03c770559f3e50b853cee1b8766c3516f7aee9cf163bddba4632ecb260f6496453f30d2682fa71099413d8e15cc7e0008b89438f2564b8d7f0411cc975d6f4ea59e07e39c466bb0d56501b86ac43eff2b83ba29a0357f88a4f6056da799d4569765a2905ceb5279a93952ad1957f0010bab059ccbc80fc6a29d4b42284e0d2f5294cbef7a5d292aa8df9c2f57d68c743ed199a7c9e4680fac04bc99ebc179dc14c91d5dda74bddd80236629fde34d0e0aea9345236e097e4940e0be69f1ff9137ebddb20f25146a76bd504fb90b3af1a672c13e36fc628a9dcc6abb7017c782898fe1afdeb77cba95abde5a0073432cdb3e65e2a4cf8017eb80a348c31419b51768e1f477dd342e4aec0e4c5f1ca95f39a67d999cef0d99c0afbd08b71c7ca58381bb1d76c2f73fe0c8d9ef1d0bd22173108d9ad604f5b97885b125c02304d4495e15329328f9043daf26d47157b3442a07d25f8356186631436662638c31605a81b987391e5737497b543f7e16f85efdb9b3f272bea74fcb97ca920d4c5437f8673b3762b5e151ea2955ed2c619193b70b76f3c541098bcf0f2cfe7f41370c364becc08254e65f1d153cb48c49a68b98695561a5d347be2c89c77d2c36040ddee31406d212f83f0dba307e7d4fb85eb8ed1d7c7d9e8a483aa0d2b7ceee1b69a2ce85bdd164cf7472f6254640df4c2b94c6513bd7b55f340c1b56885183501cbe51522f2afbfbb383a9781e3335ce949f16f70b2629444c6f8cfbc084b04c061e6d2ad637de28cb50e3d6d6243418cc53440f7ffbb010cb1dd79eeb79925902e8a0719385b6545c35c67f055790bae7f6cfa73214cd11e22a1bc18e2badb74bb1d95ca12a72dc734448636ac15168b13762d238507d824efd72bfacc4f125ba295450e10e5a703dc60bc96c3453ba014fe82704e6fee14d64a8bc4af1d6033709ec304cbb34e1e12e571d0059a02c09c52d7defc0f875cf087544947393241567e06a8d258b533dcae305c15741caaab7025332127041d4a6676fc36efa4371db92478b4c92eb85159cf7d0d408a935a47546597dc7e605c140d33ed7ee0c0e988396993442879535cec5498aba155e419ff95949ca87183c0ac8b8d7d6288bef1071764fd0653906f2b83b3303d8d826252ffe2d024703908d0e03dcb664fbd8b89877e0ba019cfb04e5831478901ce19ef98315f0c9150092183f4e57ac6080901ef4b9eb6106ddbe6585af3df86df9c98ce8ce4a856710d674bde7c719d12f500501e26e00e89b111d85fa917c17af8e195cf09f66923145885945781b0df610dbbfc3c4798e26e543b64329d67326df39b914641956a36b886d59c91339e0f88e09620ad25b61189be78a1a769cb142257070bd9fbac49e86db9ffad27baf617f8c4f85a080427e6067be3b6bac5bdffb9a19844cb93e4e99c3facee0af50a065cca805da5d989fd6fc2c7a477b117ac2101a0348a665334da755d62835b0229f06d828c9c5a4ae565d59760736bb86e60923610e2eccc5f5c76c9082dabd7abed7762af0a20007f5b509096a6e919bb9b3f0910ce48450a0be038254b861fce4452fc70152722cc7d8b0489fd981397ce84cb16147ef0ef4d36fb4ea2785a41027a14f4f52f4a52a2ac431b8f7b58b9151aa0c72f36ea97153e2b9265aefc90def06c10e6bb42f60912331bf7ab11391c4c38e3f5860fbc1a076de9f94c25b8d7bd0de79c2dc306907c23809b1676a62feb1ccbb1f9ee95a44166ba3973e73b37328c3a803678806192d1516eebf1f1cb40da9c4f1c60ecc9778e8447982e0320f828ea3b56dc5bc4aec1c58b70cc41de719232bd066afa1afa396ee5bf65a1360ac227b66824b5cd6910972018bacfd78a90d2dbe6caa239e7e1779eed90060a83b0a021192cca437079442e60470189c3f0a372de2d4178720c2c228c0ba12fc35fffee036ff16b71a246de94ce11f99714df33d5821438580a14028419bc8b4843dd667ccf2b1519b489805622c191979432f4eb8644965a862419e6d1389cfa096220a1e306f126184abea26a23c473f24a4537a1e0d80861e46c3eaa3e3d3a596a6cc0719e042dcc1e8187b866588de30c8b141d083dc1c251db599f1a3657ad26d750544a763078d26fe05572414599c9f833fdbebb0b81be04450b7c14106e4cbcda251f63cc5509eb7689f0781d99d004521d49df6c05bc7776ccdcc573e987bf9f90ac0fc62b5ba6bdbc52732c683dea6e470e97fff9fce7c26375647c8f9381dfc0f54d482c9b6d8c25d19efd439a3aae71f1984c43d5c2ad042e288f63e0e25e9a97dba9519a2a4641ac4140ee67e32f326fb1a8ba6036558966bddef2efdab7ce9c81226661678152554a0038e30803331ef927b75258b10109514eef24390f721546bef1e1f6ce155acbb83a389c2b05507d73081c49d814e281cc3152fe8a97740085deec53a38259b831eb6e18d645ff2eed4d8bae0f56ad4cd7a47979ef04f85d009ff82dc4464f747cd756a92b828feefa1689d528d624f144070c5d539236ab2f47d8d1de2001eb299736e9bff2c5599015ac38abffc457fe08bef25e197da09e6ead20d3a65ce74a4709ab7df30fe41c7014d3a0a838bdd787bd9fb8cfff9237c21fadfbc408e81570a9474d3734084d0c1c9058e4c99c84f9631ee555965eed225f564360ea86d0426c96769e9ec4c4cc6dd954e562ae26002926979ecacdcbcc24f9cb6d804f4438d03470ef219b92c6109fb2eb8fc283d4a52b3e7d239d737b6ae7a554dab451f21072027e8dd7adf0cdd3017bdf890f7803a1423638753aaa87b4b0e752ed1ea14ab156c568c54e944f79706507c15d244c10214fd2d20dc4c3756b8541d1a451b023852ccd00cf4ec6d7d0bf7d37a1c0351f527e78714b6fa623e911738822ed815a8b90726e1ec44d25d4339108e5177dbd9f0f2bf83c407ab0396942044a0113ff0f8df241990ba4134553d01d9d304a478a1b176130fe10745fce52251301a367e23ebfe61f322397ec8124ce4097353711c95ee0d11ad02be712bcd17090911d559742603afe334339baf0058fa114195979388330443b57d0bf637045862bef182e609e732adb458aa8856444b93f310d9fc0e56df00107b27e0c8bfeb8716b33b5583eda5323a476a49a7fb7c09bc1161d3d96e443fbe37006ad3a4b21bc196c08421f784f074b9a4183f8ca5fc77ffe6e0a9bf2562b8ceb5aebfb8a52f914812299a86a4aaf9376cf1a72c89ad0db08de4958aca36f5f50fa64e7ab5edb22bbf560be5ae9d15581b20020679e07c29b07d3ef05269155387d4e3b341c950e3cd0aaaa236735fac3a6eefbdb98d23399bff8fc82ed32607d187bf5005593e79d41dd42b600cf7212e557ae4a88e0841b7519cb1d4a15c420ba8e6fa7f3ceee20ad4f8036e093f5e767dca337851393f1d7670f8c38c0d84ab2a86752a5c4a636a2fbd9f4376b20eec3489a74962aa2b16449b166126461cb47356a2714b8be23a9ec2cf88265f7a9a2e0b6922c1e374d44fe77107fc2176f78294f2d4949da7842ff7e3509203cbdda7c871644ed2e9a2e24e3fd203ebb53534d259a69981c631e8c264789b37bf3b3887d44db73c5d5fb1a08da234bf3846de35824bbf5cb8fa813f501152d32223f2e63935692cd712d0ce25ca7e8836b94303a3c4dd1939860c97f42f21b608442b3c0a778bcff3224f3a9f4e835e528c5c2feae13bcd8cf560e834acf01fcf5976cdd87e5f84f3c72ad3c192789d4a28707c404cdcdf2cdd194865284332e863fec3cb1e294d22e72bccd20d5a6088a1fe2f0f3f5772c1ba022aaca89336dc5b754ae04f4456bbe32f655517b962a1a66a25a0861fa38abc73294e91f4f09c34d684bd4ce0f111b4aae7ffaaac56c32dd722d3e06b598b03e78ba5be211dbc745e8b2c6c211e0cff00d41dc1f8e7e6cb483e6454ab97eb6cc1d83b5592b893ac38810d9cb828ad08f71af800528690fc83069e2182c68f238fad06a0ad02475653307ba7a0a2d59261314647edf512e4ce17aae9af4562688c20decf965ec1194563f701e95cdd9ddd2a7e6735b55de0fe239dc19cb5797ec77c4cf2215cc2f6c211d9c0266351989676733d3160633cfbfebde828edacb3a0d488dda431c8fa0c3b30a40b95861099e79af83d7db7d9b5d0878ff652a46202c83b96950af0c51ca457c6d6992592c94c997a5386269ff53eaaf41bf69bef41de546c1c05a2fc57eba8267d252835bea4cf6388491c269733304f5099f92df213511ff9ad9907ebffa4873561b0e20a495a4a426f44c5aa40099eade03cf6407b663d80616dcfd9a8f2d5e069d496dbd55949808ce537eb81e8354a85a3298a8fcd872ea33d347b07b011a1e75619d8cb4d14d7c80637d805aebad0e0dbd4909acd8d87d429735c676f72bc768f4ef8ca268e82e02ffbc458439a7526a9112413f2ad85ee4879c5c63e997438a403b6b540f12f7707e82066a203351a1484507fe88de443a461685cef6e6ac25dc68a50aad52c7772b0b02f3587b967f78b0889c300c6f36273600e43074d632913cc20e9061d973759f4b6174dbee2bb543269f737dbf692895056ec54f8c1d47d265d39cec3f9fca10053c80c0a1d234bb4de22649848ba28fcf36753347519c5936ddf4677575314a25a09300ffda2e664bc962fde41333408a72bd82fa72b0873ce154d65ebc80c8f1b067e895042360e0f91f3b90ea7ff85d7e60e2ba33a35aacb435db36746115ff98ab521b06c1f3e6476ae07cf310bc65ed8f9de28e134d212a15b5ab9e510ac82c740b99022b6cb259a7fb86fc8d2428f97b2efd289bcf4277c787b678fbc7e0d06dfe4879161f39078991ef337b86df1855cd8764f009b999b8cd2bc5429502d50743e765d3f56bcc6bf4aef62431684577ce480b27ca8525204168f8b2b05adcde077e4d6825f27bbcf147d4cd4adff482a30a2d39e9740398887e662a1906a057602e753ff716af042dc7aa26f11d912728ca02f8694536262fcc4e00952daa2a48a371a2e9c681a545477d46146e2ec2527d1eac3ca9254e07a44b3a286c5e7810da8e6d65e51c152602c81d3940faa41cec5c916326b8ef366beccb34ac3e6ccea438b7f89351c3af9ced7ddf5d0708b44acc49567090d6db98b5225e3ff955f78031de3c85f530b2e8d798cd315a7ff3e69ff6c7181f9055d6c18c1c3d3f70a3c6d2bdcd32d8e9c057275a1d254c922a78204e971cc3d48ec181b2041a10e2a7fd8e67f18b8b43a7f5b51eb9e5f80f02816f785c8543ec534f9e3f92b3eea1a6816d7831160d6c85ae4318cd60e40d3009b62e373992c5d3c21d19b1670cdd92d8cde0af786e2c93a2b285cb03f0b5ce5d29af5d5a5816b3251b296406e8e3aec07e8e50b294c7525b4b18f2719d133dd6bb69168a99d59233a367ba6d57be656ed2f3063d85114a7b6d3cb25e149b46d1473afed83078fe89d2592c83347844cb1ee9801fed1cce2c073c68a2eec4462268cc8f85a4cccaab173d274abcf4e8af615c9a055fc5ec76e8f6701d70fc89a9083903ce06f308280c1cf2429e21810751a2a713ec3934a1502510e4b36c52f055ede473229a6e060abfce75c0122cf0316eca867db76aface1861a3e0ae308892a9b4832530cf0d2f77c17ddfad9c29df336a615dd53cdc27819791dd3bbeb1f3c5fe5d3c04841e260650079450b4499f71f9705485423fac375d7f6378f97a1312083d5cd1be7c087b18205e4185ef4cb10986b67f379576d81105bd74e772869dcb97a244c92d48b4aec9926674b08720cded14b6600da5a268375519ab00d47fd196ce5f9cd057f87b2431f92bf96d141b200a4feb0363b372002f51b0fa10b845f6374ea3206bd5449420c77572f8db11f7ff11de2f0d9c54a257a402fa4d2651655b751a2f79745eebb056e3c24ea1c33ca33bfade976bdc8015c56e24689692e665ba8c625ea5205e40aff4494dd003360cc67b859be71af34a38f90fb83057a175ceb6e153bd2d9347da7f1debdb8473fc5350b03f894c709500a0091d413946c7eea1147fce715c0398694af77af3d4112a8abbf54efa3bdce4ac61db72ffdf00a30c5970f1ac35ed3f2394ffcd1a67457cea6064e3c2112325ffc6d0d79588956bb9e626580421d29ba99072f43d20544967267f0fe401106944418e736d08225f8c9e43574017bd155f432a08e708e16000470925bf9337be5888a470adbd38eed7487600bbb3b5bf985e0f28b015b72557e14f00e7663ee8735c110f0c09b4b6ffe7470f4cb91c5c2bb72f78c161d7833e8c0ec8bac5d5fc54683ad6c38eb789f41cd4e32862ca9fc4aadc617649d27f32c10357e4c160f39933ff263e90176831df39d6fbf86b3db4ef19ac31c907c5d0d3ff447012b607ab9db46916b69be0d75c34a01fc34e6c634c886eaf7d56e2f696b65ec15ef6152dac0737d6e62d16c864d1fc3a0dad89958acac363a0e88eda3fb694da9417e7005dca7a495566d9c8642f3a90585c9abca1f7d2d55d15e50d5795d3357029d93f5218446f8fd4f8c4f5cfe3ae4b5119333dcdf538d2a2e2414e1e5f62e69d21b17d8f57a613a1ce3dcd912b388ebb87124b8c05094bfe1e58f822e3379486b5c553b46c62a823c549212f90aedd31eecf5b651c3f5438040314952a56adfd4222a75b24250f074873348a509488c6d6f4ff75eae52baa1970d82c645fa3bbe8f3859d6790db2a0f10988f2227f759971704e357edcc246bf1436ba26986a044bc85d555c3a1035dba94bf5dfd2c9add9a1dee218caaa74f54f13115cafa359e59809e3378c2aeb84bdb7c60f27760d5868825e405888b8bb770542a2fca618566c83dda89147e6ddecdc65fe06e38b00ba6082521ebc38c1b0f812e4dcc6df24d3063bc46c966c9bdac2afe907e69407505b4574f54cc0485b967d0062f8df7d4326d751fd4503ba289692a95edf20306a1283c03f6b65de512714b1f2b0292b5e04aa72213bcba88398b7365b41eca6ed9a0c642712d6e118cb45bf812a7915eb961c401014703e7cfb19ab17d869153da62d16d5d3b2e3ffeb058dd56d9c529b07e5615c9ce6b4604f1fd9fd09d1dbba370b4cada3896ed76b98c8c47773b424edc9f27e0bca8e9f2fe379122a8db3a2a53be5d81afc02a0f4b17b311305565df07041b1a3fd6162bd36ee07608a4a94859a7ed00ff3e152f008370d2f529ff9bc1fcc9c68b88bdcb6bfe502c132a60ddfb58a5efdca5eee3052ad547fd21522424e39116c7fa3dccbad5e6df2b4b25de57d244a758958f6638dad98799b8d8ff185994eefa15134fa6188ac452a38b10f802e2cfd9c26a7680707f3b64657abef543b78995fc75141b66a8eb21c073501687b738bae076483d974b7658ef606556b11d8e4cb583fb04a2495917499fd3f0fc8b36fc3bd8792da631fe0b63e29a7598a991c04e19f75de2f0465b44272b411d927e73c226d8afd34763bc42e55bddbb38685c9874c4160459177cd00dba0cf87406e632c02bf1ec1f678214bb393cd44e2c49b31d98c75b9458df85b6dde8d955028dbcadb1b844ec9ce57012bbaa0b145a12daf03060cc7191d516f4d7e4737981e36a5ff221cee8a5e7ea4b0542d067e533ee129391ffba710df2d26d1c145feec7212b53ea5c528c0a523af884da8394cbd37a85103282620fcff7313ce7842a79a48f89fba738ae3143b4ef1f802e3c234f5a4a9c362133d2635a5af49de56a128e6469e904f5530045913afb5341c8a2a1dc8e903ae8855af1101cb161a302687f46ad289ad63fa08cd77587688faa00270967005be86a181942cf0b414636df75e498c88d7b006a599cfa7f6ad08bcf7760efa54d401268380459b425c167d290e7a2d422e071feff51e7746924943292f8f062bbc77283b277eac89838e22e20351d27521f030b0839cdb964d33d520a0af812207c497764e1ef0d3f0cc272e0073e09f2700b3ce98e293f321038f27ebc00112e497fbf3aa59a8839911e666a03625ddd23514e3e7b359638927f05ca19b8d4912c1a41eacb51a0582f9321403d6747f87b9d78cb521c8e5820788d0fe15a13004e44695ee2d879354dde781786347fc4ea6333acced57654158293ac54ae986ec9a778874a860e2d5a7c045e04c7864471e930eca806ffd86ebe7630712a5ed4ae2a5a875e058c726c2240271fb217ec5d9c7829782495ee808e88bb799987823f2d81491129a9ae782f216684d7a7f050343c4d6ce1b70710f14d27347e7581b5406a58069d6eaf5a5bb4db2063323676123f7472363b8fd2e5d073e8219f404fa9d3f38e93f1856203f75d1b192019376e81daf6d81192527ffbcda516f2f40a7c5a100723c5d56323d99a9eb48c80f12205bd5115b43e3f87f1b3ae0f9b40c2a297c0c7a4b06a573b19638150159f12f3c5a899d29f10ec18463a3a12c43d2cc08c3b6ef23743947ff6a80796e815154bc5b4b7bdf8af66818fe266f5cd0060b2aa5abf1eb380c536d9e68db5c88755b46e52583835a4d4eda66545478719a8d4421af75ba475cd3efbedcc1b9b0f474d6142f7368e43f85eb1fc5f82278ce1cc154a0fe3f07f3cf84e13d574b9c7c7b18bedf9137659a35cf54f40aacdda4d812cb8db5843af3be9f65df906bd553e149382355a45ee88f014cc385e926d4319b1a5fd046e4167570d69ef3adf23879be47d972443e1727e98fd88141eafae27e0e6945a46e69c13605370270fc01828086a6e79b38803c8638a11fb4635d4e1c5cfff82b17db153b24a0823fd19f831c8b18f9bc94f2a8b2f00c495f7e8a845304fdde46263f5e8c5b2fd7a027ee7b81f596456e5dcd48e0d17bbdbc3e20d58321a61b7baf1f27bd2fba89a7e429410643ac1d08375e0171f8f8900fcad86c84332a071da785a02b023b938449a5f4d7189e0b6c207d6ae66477141fd97407d11ac393bc72f82898155ee3f87bf8d56a241d6efa1073fcf330799de7a05f20a853de147e3e111b93e97ad2b281d5cb06a05641ef5d2788b073053c784e9f9e9838d807d634f977fb8dc6e17a5c046a035530915fa5721e5c5ef1446a3846de1ee0c88bab2d6c8f6a2b7c7de9dbcd6d1673a101fd8dfa6f2e0af656142309333268deab7897b2bcd32ff9a60888ce38b08b25ea8db0d25690c8542b9611b19c7d46e796174e7a7b44766a32cb912ea91dfadb3070d00528c2698efad3df8a8450362c800df5f6dacf9cba0fdb6834bad38eb8203af2bc84d4e7a55f61696f08832abe8d0a752fa0b138c7cad73fc77f7e266575f379e35fa3c24560ed67521c6e535090e136be3aa840575c49145e7702c3be29def506baec3ce3557c7c483b5c9b21aac7fd00d756523f317d48fbf50e4748b7a4a8fe9d096a3dd3f5c37f006a24c4d10e14fdbe3eb1fc613e531763c5ba78ac6c2a1bd0ce4394ef8e125daf4f35e0e7a79706c060008ac135954ea2ba57c919f0b720b0647012b0b8b3adf82ef4ad82a4ed4da5d56a4fec6980c9515da98aaec9d06e8c89c470c953e42e59418d7745490454541c303b6d79b011e0ec1471c330d396504a6c3ba70ddfcdaabb51ecd807a73eb7a15acea782ff70f7930b2e95e8f816e1c5e4bdb0fc4d84c23ad89f21499a5507e6eef4400adfa5ec048b89403a7711821ef3d80155f3f5ead1c0566a31e767e2a9ec4c95dff9dc4d8d2200b8005bfbfda94ce082dd9339b32e1c51b5e6c94f1bc614e30981ec9a7086a00a71b16479988e79611e8f983c18b4a2c3f4dc8293a835405ed7a8869d621528ee11053df06282e9816f810d29f3a2e49e90577325726de3ed085ceb131830ab8e25446dc91bd735c2c982f10002f3adfcfa132468bcd8a9db0436b63b2680065d3a0e1f4790cc925a5d3aa13026c119163e29a32a5305780d9dfe19cb83ee3bee752171147a23967f3a3d0dad38e3f2bd66cbdba6f1571b66ddb2a6020ecb217427fb1ed1e3246ee6f57f8a256e2da07cb4df8504a28facfa6e70b439b0995ae9f48b5471d10d0ce907f48a0aba75b9bd71fe04e452fc98e2cd8f1f3fbfda52d45e1f53ddaeca88f189d69bbfdeecdb3fe41a34d71bca825b9929d5a7a576d412f3cab9b88942c39d0af551f165bbeebc98e8a1dcad16d7e3335daba94c1cc548bd1b8d44939b8d8efb8cddfdf97c990ead57340e2c6544aacbd28fbf9db353761a774e587355a5260f90def0e788639b5b0e22c6c5651020de80175e7d34c80abc365f4b01d43fa4b386f5491ce923b4260a995efaadce872e13ff7501b9b469f4013660ff354a468af00c0dc111e94e9532d23a6e297d05c07b42bb8c0b84496c2ce5626e70b91d0b86be3b01549adb14a5487ea9cf8d07921a1c9aa8e775e966af2a25952787f581cbe8971a838e5b1d3fe37e15c147bd2618d7f84a3017c9ffa6c12f08e13afcf14e1e6b8d5547f28f9a3de4013ce1b93702c7dfc003cd01bca674894778e2663dcc7e6e590eb9dec7e4ac2a435a0441360a96e314ef9e7641eb560639912fd1f3dd5bc2596258bab8ca3585bbc2a56a01fc5e642df76772e9a7743078811e392b9b7b38c4db4051b154dc8cc2eaa232029f43795bf12ec7999df45a99787e12545f3ab1410acc560df3d2065d7203254de051c18264e084c1fc6e5f7e96dbc4c49d28bf04cab4e365558f26b92e2bb4614172d6ef6d44f6e504925a04bf008dbf304698a4089cb3cc97fd55b6bc1ffed16b3851b2c663270c1749f80de0e09c686fb0a159c4e51d471c1669f3af6ecdc613ca31ae33571d38e50577fc646d94a2d8a2db746e1fe5a66c17eeb9dd73a983702ac50ea92bea31f6af6a451cfdc6c2c2cfae5d51ce72b7173d99317f09aaa9423055389fc47332c2bb6031682a9e781c88ebbd052580e9981fd2008bf447c9c47d8a311002233b1bccfe95e2aa4ce3c282ca1f1a55170640eae05966e3c61667719936152ec3424337418d9c210102426720f6f237c0e009e9632e486101a14d57ab55cd5b38a1d26167f3d2631f091b1a99ae3dfb5fce22ac848b0d9c02acb2a82ba40b79286de9265033f436df6df9e55c950be55eeb979d5c8d58eb60e03a967f119a28502118ca166706cf89db5acbbf5e5032e6b54da3cd6b137fb18538bf7297f10beca913a658b56fc7538ebf8ae88b4d8973309b468ba51f831488a325a12bd864a503ef2c0666b2c8c8cf2b55237ec811cb4a0f5e919fcede5414fc51083c1b3dfc6e23e53a3a2f4ff370fd740c728017bb0b31c646b9364756490475dd5467dcb9e98bc020d7ed9f86b356f2111c58c883df280932d28dc767da2c074ec1ef29f41c489c5bea715a8a340415ace5a0896a5c4b5c7a0217ae4699b5b2cdbd60ef221a7f59dee93a578ec501a4c48f7ec219def0dc56cd7331eedcc64ed225fa58894f2cb677101611c8d82e4b129d1bf0aaeecfeead74447c3664a01aa2e5b7b3a993198a1f46a1fa4e632a92f4bf46902773d78d5ee97a9b4720e617a9dfd8ec98ad307c4df0b3d363c2fd9d7ba4689b3c4b65b74a25f3d2dc7547cc3a8eecc1ba18c5866a8de573839d3dc284a3954017478e305708c1bec910969835c7260500b9d5989799bf727e0fb6a0c539597d20f5c1c061c4daaeb0d5971dfb1c0cedfecd5b916b0a69da23290d6fc6ea2d8c4be5013c851b945f35c779596905ec2025e4567bfc18ddc8028524ab3ca618efbf8e39ddc63a727cd5b3e59ac53ee677ab45f925204e334c3d5553f509f708ebb224478f00b1fca3ca4d16dffdc435e9bd0e749abfbf77b413ef59eba2ff88f8672344bb6a8266ba2d1956656257dbd248bf367de40e89ee16e3e353afb2ff40f8d2aaf207ac82f8f012aaa284d5167e014e21a9bb9aa0e5633df279aed878fe4565e854b2f146655702bb6fdcd51cab5536e139d79c954edd879c1d733b3860b715a563d22f72be3e34b88eb0329dddddd8f9232c1e55721581e4d66d884c87f1176bbca848137431529a6d4b0d941ebd0ae050971f5022ef0032132008bdabc168eaaf965725355c00eb8e4d520bcf691584442a0bad4ccb779c73584bd3926d3cb45564946aa9c749bc20ec8bff3725374ede454cf6f9bc40cb2a58ec464f1cf9b9d2ce8c77138f93f16114e0c74184b1f80d15dd1a7bbead8228b527d7f03204f3af76027fee73a3caa36439bf28ce2a111c39f9dc05e1e91c254cf75b2b6b4a1beffc432304f35bb5a2e1bfdf120d3344f7b372a8b54f368f5868f2f1b3eab139685a2380d1b11431a3aa2a6a3d5da8742bec588e3957c5f1da4f0d11106b7b96e938476117e601349eebf8c2098b3cfb0b5fb86770cb98fadd3f132aab2dc2925007eca6d7231c336f91fe1c9bfc98205de84901ea6e9ad25db358c79ef08564e00af4b5082cbaedcafc842d60c938db76f084e7b45ef7a5d407b6b457cdfd9bbcaac668ef768ce67a6e3839f75785871b22e9739339402e34351c422a2f6af6a22c95d380b70a4b35af58fe33c4b71690f271f1c1b230e7e64a332d7b6d2fb0ecdb3fbe28c51f94cf5e031bdb7a11ddb91d0449b107e4cd332ede03560207936ec76e7d82b51b58f44253456d064dbe0d7afa30eb8733bb6020c78900289ad73c158006c63e82dfc89c42277f6ba74f5677792de5b488bf80a92f400addd025b13ec8f1033afd7c4db8050e82ebad4fafa7b586800047755be53bccd3c5c7c7a326320e49d321347f5bb59502687ff7cf8b78c42007a1da12efb3bc31da310b33947d4342136c061107046ad66a5e6f7cd85b1e1ae6cc963a6f0ad35d0354b937264ba126def82b8169dea1671b35b7320a7172f011ce3e1432ff68f07f2b35364c960b5dff9f6615f6f10364d0dc2571ef9bd2e3d866ebe8a90c426f1103b628fb2722e0c9efcd7e919c51604eb4b96b1cca943cfa415e0d2f1c0efaf61e3293e157bb479b7c8238277ccf764361c8ad548efddefe63a7223e1e58090c6034b194adaafbfddce59b34a69fc61d2b074a8885c0708d195736ab378966233b33f0a7b36f47b62c6bce7e109273db02c9d16e1af6c5a06356e28b09d370870e19e47218b49f06316d546391cf901ce932fa1458b3d6460e32a962084b97c73f3690b43d04f0dd67ecc14bbaa5eda5a2623e3e3c1a7eb639504fc66d82a6052bfc9e69d0636acc02d1729e438ebbdda81375955adb71413c7036c576737258625e822600e0eb12da8c48bc900787bb481d76c9c9e378cb1e9241e473b8e6a86ecebb9487e69c3d831585e1ed21b87c7d769c62cd526bb04244d47d909e3e761405b276731266e631ec8d6e37951a0b6a3bc9430c44c113f42a174c0b1248a832415302a16e384457396fa1e86cc6e8bca753395a3c9d9b7669993b6375d184aefb4a9f800fa21dcf9073eeb2e8ff3873cc3d441ba9687e44a1ace719eb0e74462745675016d5a2478c59ccb5cfe0dcd01408e5642535453e9488481c7632dcf1b2de774a71276fe11717478cad33081ff94abe8812a56915052e357572f950c7a617a59c507621ffb64937d939980901a2725a763ff113d599ff21983fafa8344b9e39c47ef3ab342458cc307f872a122c1b33586b430bce97470e49f9f72c7db4797e2450fcc17310d6b0d479f4439c8c8ac96b552aeaf12735b3633fe770f4d90b68b23c489ce2332222f6a242a409326381b31f1bd3afe1af08409a1c907a4d8cde93e2560b7ecbbbc57fca9e7d01e1b0204f971cd4ffcaaef787a0d672d52512f4341751399286e0c631111f82bbd4088171461020f2c4f900d00225e2b261a41c1896c1a0a7bcb0683ab2e36671919247916ca057c2a58896504966e9ae70f4016238f383188805f25ce3270699e9949c3c328294bc7fad67270fdda31b4353a22f50b7fb9a59347ebfe64e178fb45c73080c0ce8b7c2c7fa67bab510c3b7cd81d24b0794e51853c5291469a112ff015430687ec7773563ab66d23176cc0f3b521a82e6090beb8e4850966185984f8b42f24b30eedba362462cd1c236212bbc4800847d0c3017e3805b518207cbc1a6298c9f5f0f2c5364594c93c654b4e180f05b243a6de1f2c3c737a26f8e78aa2df4681f0f3ce3fb849d0dec10b11a676e42f398d1d2261b80aa80fe472071736488508fb927a60f7f707535167f71f87fa9c3a5b48f483b9fcc0cd12cd407da42c33c51ef3fd6a42806d4bac36136be65d3c923d07ad2e39e7264d228c0dafa5fb286107ee0ee164e01313e3d440bd1263fde03121264ffc874eb1620f91a138254ac15da45c204b6dcbd2ec656feb7e34437619930185340affc7841d435eebcce078f4dfab67aa7d790c7c472a2ceadf1e7e1c643b776683cebb46a1c95fd40090342291517f5f032ef4c0991b9f2977e57f103f2d31d221f6ea69a0be6dd4605fad95f841534254e53bf364fbdafee94b0babe601edfe3268e56dbe0dc6028c48e104ceba82ef510ef1f5080bca870d3f95aa5b99da6d1a006143ee03c3fbe94c67c9d8a66d66606f2d5932d964f07bd4e381ef08cd00a705e33027b02f3678344c3505661dc1f9988080362f7f9c2058360f59607ab8d5b502a9d60e8b8d3fb736be7f54bc1b5d517e3d18090dea560524560b09e80f27a3044ff38b4024fbca63ab30b3a238c22cf2b401a9ee75867373080bdd23c2344b837ea2596fd8b14434d37a061c8ed17246089563c0bda60f2ff5f977bcc6a4e51db29ec763b3471f97c422d91de51159dbfe7fd5dc4adb34060af80529ade5405c1cf576178f489e4b6742feca6fc8033db7d72287c0c85cd42f857739f6860f9abc5b5f4bd2f2af07a3491cbc3c080a5f19ccffb37ec820f333988841a41254de3e3325eca76259943180e600c0aab36605dca7a607cc1fd23abf670a9754b3b5b5daa511a26fb82fcda6d0322388a60af131ddabb88686dbd90966cf8a3defce386b4c6884430432d9c21e8ddca00ebb8281bf1af12b9ae3838fb18a8b26611aadc9fa794c8b0a94d93050ecd724b426a3d523e22a4480450c215cfe8de17ca0ef5525deca5804031803ac692a5847812d66c8637d98abeca152582b0aad81f7cf2a1214064cfa5ea1cf997b33d60e7bfc993f78337080e120f93290ea9cfb3f5a07075704a2111d52ef65e015931b4662cecabd6428db3bd206d37ae3148e664abf273d19ecece02877e0b5447809730c8b7bdde7860da695d9094ff01caa6544c584bf42e58721c9f588736150b0a7c58d40004cd5cf741c7bf967bc901c68739d49973280af71033f625f365de6403fa1749429be8d49f9588445b06cba40280720490932213fb228b7d567b7c82ca82bcb042200c02bd1c7d4e5a7e872c58a452aee9ee363e60e393ad37007a4b56700b9b9c4968ea9cedf01d100c8e64c4d760afa32c90a1016dcb7c723223bb428ef2ccc69f1df85d8032c229e2348ff9cd9ac4e77fed20e6bfbb242f036aa6f07c53195954fa77063d50572535673ff84d27f4a22ac4c183c3cad506e77b0f574d866f8031f70a9e6bd814ff066652460328f371eb81e4537bc3a4b11d17515b95d93084b1047a43793a57a59ff573b273425dc7d1e2cfe4568d7b75a33240825e11a076fbf09d6a2595e3879c1638ff679ff1895637ad03bfe85b815cbb436b2571e40322d1459124901e56ef0e6f613760c98eb2b1f4da8c208c7bf0505b7491a9114b8187a9c2189c21c5934057d0cf6cf91ec437579629e056b34528d038f474c4e6587703715c132067df4e47f9f8085d8ab0192dcf8bdd8c7e7ce0b459f7b5400645d40163c0f3a6b4c6a013f3dc44992a338091fb8d1d9384da724db647ec1164bb37e530c63a343a4f38c456502e20681ac1d389e82436a50ae04af9572db46312d946e5e08ed5752140c115b9007ec6752e5db94d4fd426e7f9f8221acc9cc61fe9f465658250bf72f98b8f04549356f19e98ad68b9ed4b518aaed65c9a8989b09ec1987ccf3491030be7144ab148bb9de1a0a1fea97f53ce959d75ea9c49cda999372984416543e81698674160a273938316241bdf15ab07e2be1e9d00d3e0db9dd769b3ddf0248d052d81c2cce48e2ed5ea718d733f00a97a2f9874618f0535f42f3a007e0970bc906077d324e0c687a53a11a5ce8960f8b4b1bd929bd09dac5ff7a886640cef14ed388be3e89b382788a53aa643167ae5060d7a33c7676e01306e8fb222aac245bf1161c8574ef952846d95c2a5a547f937c8545bb56268a4b071c6c981c05da4fc7c52a2cc9601f18f5112478293f3dcfdb284d8941d6984fbe6f4bc02978c525073e6049c73f9b7db1d68d8ac02dd0c823c8f42a7b6549a6e9c533ec0c3e0876313b0a762cd96210afd47d232987491528f3f556cd2ac75575c359cc204302a7bd11b3a23ea829d75a584e002f9dace2821cbbe701740cc7b32b05fa24ae9431a80d0289f2e2d2b6942f0c0b6918b817af17159ed0793d35d6666afab2f4d1ccfb0f6c0c1cd5d5560f790fbe4114b3641177841a75b58b4d5b31e128401354c9b31c8575c72bfee0685cc52fc923a17116891ce17fde9200ee17dc38f30784c0f37c601d15e1730f9ac5e05b30738e0f97071f8d86994f84f9699e2870cedf337c4cbf88a8a9c3aef5a433c8a3ce113739eac6f883bab3772d0ebff9b1b232aa72d1292909248c09cd5945c988da285b584ea2111e0dfb75a04f992ac27d47816a2cac6cac9cd107277f2fe0fc238347dc0a8526a4381a87f6d7b1e0ed8bc2e036ea702cdca3e558d45ce455036198c3da08a04e79ab889612e7243988b8e72750a2e3e253f650e321f0aa7fc2420f9599a6ea8d67f7cd93a6af655d951744e8eab9a49de4160ba4a657beb62cdf8f45ece233aa93b46ce621b634f4c06b66322dd0bc33cda70c3851c1c0ed9db7cb2d01a565e2240054c9fc26bbc1bfc290cab0be2c4b0c3e5a112d97b454a51ab1201e0e4d81a5fea44ade66c9291ca8d43480c1d48690186194e07769094137a94b9381d9e9c7494319e6d7e70b4719fc53e7be13bb632806876427c0249f77850f07884e5fdfa364867b2d8a80f7cefad6578a63bc933538926e709e4eef0d6ddd9aa8e82d8fa651c89c0dace1931b9e95d400e2389d16b1f9abd00d77055398f04ed70fd601a887a6b99d81c11e8d873e776dc3892bef39ad68ce30453cdf53f05fe3b202610f9a3565335b24169241021e4908d1b0eb0feadfe2d2815ed91297e832c29eb5c2ab601f0ae3b3da6702a9429547f2c3f259b3824ef30d1e8da968936ea1b96c64ea024fb98a213698e1f4526c56ecd92495c5b25c2dc4307cea28e62c28ab3587387bad0fc1eee415b44055a219ed42a43926d7b48e5ee10d6f14ce4fd05401e1e230efe47f90b1a591cc9e41f4f0102f3b812bd1f01cf4b72f90a248935edc7a207ead450e72cf063cfcc4f51352f932114811bc4deb38f4ede0158a8450c0cc441743ed39f9fea2e23e6336e56050c0e9fdaa8184f8e599a1abe25e8beffb5e28ceea14c2b129a6542a3806df35dcaa5a4b634bbaed6143c32bd01444cd5f688b3fa5682fc80021474bbd4ad1a81d725320c5fc3d8f6ae067b7e1f02a8e8219eac27e04596b1b047b9a96cd4ebaceb874c37317b40b7c4bae7e54c1e1e0144fc0aa59156b4ee12bf419ec2df1f4c3cec54bf08ef357e00921578c768a6eb0367af3d6ca6dd46496b27f16b79efb41231eaa26e7b44dedfe6420af442f986f38e316c99765eaf631134493d0c315da19df1864213a390673dd67098af7397edd819a1292234f7f8c85fc32340206cbf2ba949c32f849127e3ac6523a221421f22e804f2d6a4407600edbec3d28b503ba266b19d45a8163426ab644e8fc411f14712f77d3f6b231e0c60a160203ba3cbcd1492ea80db3cc775a7eb557469112684567f71a38ce8789d010d83469854008c4e85aaaba9e616c05d001303a51e75d9991d3fa7a9026d817154458730639d8bf7e7c094520186d1eca9bddf9350007e4305a175ef4ab242d8e5fcf402a9aee1eba74a1f9107f6911e39f1b1e76ce81f2ce5f8aeaf1d3bb6b43fad7cf4885c1ee85e3b51bc139a702785808acc20b6f614ae65f7014878396180477dcc55ee898aaff5be0fc903e500c27dc5a2a4c39938e943feaf410a3fdea765307170f191b7d69be67634c9335c450513a05b55033e30023e2108c00f0b2974a1a90f3b54ed8fb8471b2007e88030707f97dad64464cf452c07ba3403d4358c820e35cb9dbdf87cc0585e377600a200f85f09f5b35d9345e66df59a051665d89406e7926c189a96ad8577d2e155607cb53a38d608d97f5935e2cb0d5696848f9f0f434d9db5902c5579ee742d88b64c33aa3253de78707e1f4eb6b55de5a30060e3f799ca933ae1d398b7ecd72713208a9aea86d87ed544c956c5b90c7c30c57ba2c20e353e4c74607e62f01cfd8a797c302a9cbf42a6afb47433cfe47fcc0685d3f5b72973b3f43401f1ee01800f2d83b48ceca8b2fcf9371928d721d059f4af5f01be5c0e47a01422835b045858124aaa79725672c5e1c244088b2925eb84200fe859cd0fce93d28868503f9ac04b21f559344d3b169061f06758daea5951413d2fe5117011b400d3089160377ec4dec0ebdd3ea989d585089053098582cb58e920c03d2ef662d9b152ab0f2d674e192d1876f3d494e33e1816d37d12b7e67c60b40d2743013abb4e73f2bfedc8c67494e6f785353515dd225323005d9d451e801ca2408c1cf811c21e874a8e72530baa367f918a1e06575ddfc8af1266c68ca61ca51fefc6a00e4b6da9e33f8e64fcc14783a21a4fe4f3b2d561d3c99a2bb4a600f363b479c67e29a4cf759a7b00fb33ad320b1cff0a062e03bac4884f28ee7ff66ee1af942a5471328cde6e18215de89d6478ace167541a5bd874c88aa321e638a3752bd8b188ef90bd072d713103b7547e06161daddb91ca22ab47f5f30821641b41be886552c9897fca2359417b00d3fb006379efc857b6a47aec98128ba6049a07f4e0e5386b82fe326240ed4e473ebb81f76f318a8d31fbf3aba28e8e1576e3057072af0e62005c8a4d5ed2721b84f4fd91cca92101d4454b0bcaa5507f6ad23927242d6d7807a205780cb70059a840e6dbc767c75878200b305d9e4bc692d1443da83721d8f1004e23c2cc40ffeb3df883a43e736318100de15696811ec11e22518d23e8d8aecdfbc4f7991d16e313a16e1a4370b01c1e37fd659a0ecc70aefe1e29cce711ae7a5bb466d84d70e77c647bf546d395e9aebcec58e68e51aa563df22335715c2f4526eff36789006ed85ba84e9487ee00acaecdfec27c45a8cd4982e7c1e4840deb4bae70a77fb7bcb63b07138ad0382379934d664c0919f8c9c189dc721dd469c50796f93b697bd079a40a93b25c1bce2da10a95035dbe7707c23b5e88f0c607af084b7d1792375b94224c14af66b6caac0195318873b552e07afb20a0d04a3e9cf3907406d49aeb9f07b102e18761080b60bac078e412b208577547d6177799dbb8af872f4f49e8a7d6c0f354392f3326fd7a3d8f90f06a707058e281c11ca59da5e3a71c1cdf75bd090b18f11d46045533de13326439a4acb2ffcccd5109045780122e9ca21a6458f7701afbaddcccf08e5d257ca1ce214446c265ddf3c68654125cbb1c89903d46d2ebd78972635e2572236dd9d3da6799e29030f4a6e139b4492c8f9c5935fdcb6fad32a308978c1c619393aecd74b741dbfb2f1d7b340d46ba502603846e3693b0f0f1dffa6d7889d971cfea8248642271f340ccfa690cbad2f02a4520d39801c2f942ca0d1ff9e2ec47ebbf512ff02e81b2b658af3880334ff39c4c3ba34cbc5570725689c0b15a2b694f14c7a475f31cf0cd0bfa533bd66336cabc187497c4adc0460af6d5f8969e1e5ddc9d833f696d8ac56f53368c9c08bba69fc61a350e4c73f917977db4b5e44cf8d45cee7a416e12a7d645dea3794b4f103980fbc952f6d3d56c0b093f123352f30310f4dca20349d647db8508aae0a9ffc97e208132e8f20fe56e2a18b8fe93aef120446700add8d6084f5fc65ed7cc549ce871a410a8d040ef744cb7f73710ec77017c2f20f26210522cb0b359088b2968f108498487eb1efbfcaee08f50bd3d16333e21a9b9d5dd4b823a5ab1adb7e4ad31767df3cdac826f86b9537e492e30094f4785baa622f18f4142441e25315ec92f720db494b524e1af401b4a1e90c4939a5034e8b61a8e8e050e571b9b8d3c7b94495828b07093c5862e44c43ad015db4919539621adf23ffdbfa20b49b7bb6d614e2cc9a5b1dd8517522d0deb2b1e3e37a421b7571e2ace31d0e8a70601f81a3a2e0ec85be3c304f37de3f701860cc083abb054c42fbd7e11f09c8224c571c6251de497a3bfb256c74c9a02ccd54e0923f26f8c00508d70157d3ecbd2d43266ff7ffb7c236d1991ec2c94243547fd5b488ef8fc73c117c20a04302d508f3d60a433fa34fa75f05d1139b0856a0f84552b3882aa0701418f4967bbe8e16df3113b4299a191b6123bff4d2133577d379e7691257908275e90f5e43fcea2f23281805db11a2fa348ca1c61435954ce8fb0100c8f8674bbb0654738ec9cf52b0cf5dff000c74ed5e0997bd1433bd89ea7a1c6b6f8bacb5b31a5765ea2ed3afac0a135676e2fe84e0f24a44d96ac057c362c8123d990073109d9e3a8d96bb56055a268e992f3d3f9403e89cbb90490f7a170f61a5863d188e358a6183bb56c0225a8ff87be2e8992ba7b9c7930a2f1409503b5c480b19ba0476e769c9cca742d5eef07a542e263b407df57de1982199e4fae1f988040aa7d3e2c44f0b29f9b358f1c9fb8a59f3996a0150aa30d6c672d206584b351365004cbfe72f9e756f706bc314b43bfbb260660fe8f055fe3b46bab634362414e2b0a738f0560303f981524e96bd4bdd3ac57495fa7837565de563909d3b89e51bc895e282eafd2099ff8ab58c585de9e4f84b25843c6c964a22c5cc054310ccc837f2fefc6909b1dc038c1fe467af803bb272034a8f4497cf87267852e1fa729e7d6e66868febde1c6c641a7ba3617fd307a5669bc0e84059526e9b30734134c891e3cde6f19ee8fd859cb42c131640b2f1e243be4db6b017ffcf6852b8409d07f70d48fb8652e140573af731763791125cab4bf7928f77548f76273e498eeb489386aa62652b635bf29a6da2d0c9491ad6be0a001d4ecdf9e4f86833f1cc918f93a100f22350001774f19a0508f86e356227ec03701f7ce122924fec739b7ecf32cc0931554bffc8e5586d3d009ba4a2d16025eb2efb62cbe37e56bac8bb9260800e19a812263454423a8c63370767a39585c842de2e3f0a1c7866db447faaa3f105274dd2e4910dd4a018f958e0f07e1273b01d9218b1691138483a792323e931dd766594732932d5423fbd9fefaa325c3cabf27f3d0de111554a14834677718183ff54e34bab1365b71f2683ce1b17549f3d5c0e9027f146175c0fe0c5fff510180dd0b56df9aae3d00c3fb744564d2410f45b826cea9c43b954c595ed63111013dc20ab3e10765911415dfa061f48e013a85159fccac491611dd74b326442485623dcb1fd3841b74db64deeadf70a1b6967091ecdd4d74f30db421e316cc611ad6a3a5f76efbfc2e0a5aba4229b9c2a3346e5b44d5f9d531d1f625b4117e60296af37f73a64630b9c824e897ba16b20fb2d84ef94053c9c102d5cee33430d00ada9ce61ac3461cbf5c91778611e433c0b0efaaacee8d206f8f2bffd10c37fd093041efc4c1113e0093c7258c9f6c3c0f773c53649d955d26ad301f33a5221613bab5326a91e8db52210119e4143271cd636503f15bf556aed94675467c35b783c325ba4798052484b81a92365a4db708938589b7674057631d9b91e75c14cdc46ac3a7191c9cb4364aca3c712af4868c9255f6878d73ffe6e7162c547fcd297f077203e0a89ebd1159d31f04a4f0cb0e1959b678f96ddfedadcefac76023e73325cad20426158a7a280929e151673522d6f302a36e4ac6b6977269b2b6bf61e0eb0e627a8b206528ad752e9e7da4df301b0bfa4317ddf4914e4e8a6a8005c384ba73a848e86b4ee9f5d95614a7b8772f95e102c269aacbfa2303ad99ba97ab5be16b5b3d1d1242a933c346e9339ff59f9f4d3dd702c8f22845e1c911a03b6f38e45fc64189ee20b701694dff84fd0c34f308588be007ac1a06cfc9edec044c2c155ee530fcb402709b31c8e60c67a9e7caa51fe86191d4a739cf811bb2401670e7c35522fc46208bcc55ae2dde456309e1f0ac422af50a1881c67d63e2d4ea84536bc7573320fb8ffe8d10be0693147e9afe00bb4e704b17e25527250ecef39c8f4e37e324ce0d900651b8f5bf21d968e6de7729ff500a74c5d49215cf916b09a82dd40937c9ad876a2a7d2e58fa86b6a49c8d279dce2973422c3ffbe83706fe5e0f58e8081fc23677eec158297524d5b1bac94f7de25dbeeb03428dde837b7973a5eb55799e79e6eb22b87981cee2d4d29e1303aadfdc93b43754de670103ca66bb8f9bd0680bdb06401057af79823d1abe0f0ba9918e6f8475bb24ffc658878a396aa85096809fe07f1a651b42d04ec4ec6ba3573642089d3609d309a4b7e4a59da862f120acaaa7f0a876757497860be23e10f8690f697e1ccccc5d7fc4900f1464bea3081bada28c98a28de20d0428e5998121e0c8bf408102b88430128347d27a3e055f76abfb0a86e0be8cfbed9612c6987224250b3272e21f45dc212391153581468e28730bb8d8c9d37592552971859e43d91b923a6395391035ddd2aa9a0561f94b5788a4f3e6cd76132367cd917b82ccdb76c7f702cc0cbc1ec013a5bf0090ca302d7d514c92eaf95abcebcacecec4d5496755891df6607734c01ad2c845e948618154e60ebd8d8a98212bac1f0d2cd4249ee18fb29792af1bcc518dc72d2e7212a2ac71c9a60779419150c5c0e7783f58408c22cc2b3beb2160ed7b4c22c396a03067c26e23b264f9e4409733a05a1730cd26b1999ad40ed4ba18cbc88fbb1225bcb1a3c6ca903e81304bac921990a7445221d465694d9ed6455505dfcf1235774543a6883f83cffe2c3ed0a33c8e754c3a3669a9cb8e068d50e85e5c4647c098d9c943d5c0477f66734e5507f04da96e22f56db73298ef8985eb648c2ac6bc4ce4cafee1390e743a082c7322ff378f65ded22c5327297346a1c384b9d4653db07238b7884a90515b05dd066538798157baf715d30a5bde7c0c354a7cebc3325403118cc61a0e1ac061a0bea6886ae8be5a651f67f2942083b539be3bc2a96dfd65ccd70241c29bffb57451929be8ce367d06b8219e14035bd62b08405b011e73ef96dc1f8bbf4fe6d7a116e2ada893d6d66bb4f0e9a7202b9efacdd6a3655439eaec1d6fa891121557843a4628af8facd237c5b65063967bd6dfabdfc6a8442ac2a9cc5bc027ba69f3d5c7ef40fdf7e80beec65dd784c85ea6327dfcdd38dddad497876f2ac534693e66df50ceefba79f4285c067ae466581ac0abdee11c673ba75a2ab851ebd26885541801e65f80c9e48ef90c587dbd1e95fc70d3611d693022de40fd57a80b18493bfca77f2a503fd318251b26e09fa8a1527d624b4041326d296db3612a2aefe5869621d9206fba8a987c4fc42251b0659a678abfc1dc6287eeb961beda89ba25d519b4422adbf74d353d2834a208dbab2edf5655e97ab8b2f6563a11eb23cb4ffb0e8a77414ad283700b40b54ef6ee7bdb9ceb7db87f335419ee3069ae288026001e4b23b1947d7f7fdb331da0aec168326d5d8de697b71e2b0e8b25c61e374f5ba09cb7e307ae77b290e8cac6cd429c5a9e5e8846875602f9f85fb1bc85bc21bc9ff7f4ba1d953368786432f2283d46c883efdc515f882d2713180bb40726790096fb7c3f1fcffa68d22c02d038e27983ef16ecfb42434b9cf0d1c0f46d524365a13f3c4585c46749e22258c4e10e41935e7c8e90385945bce06e15f771a17ebed79868bbbf128288e2271fd62236a76852765ca62d4ef861722ec0aa9629cccf5d3dd4c71153de109b7935402ff668f540a42bd48c713a71187ddc77c5bd44c7d8ee0d3066fe802ad9cc45d40c5c31bc274f6708d98b634e638a4cab46419b1d10e59fc37c534e6f534af68879a021404278af1fbe89a95e69bbd9a911b2a6c92a7ea2b593e714e28da5d6f113102ed834c9919d7dd653a912e07a5bebf102575fd2272a253f35a20fd3f50441024313988473ba29edbf4c2916bb3e61def5bf5d7dc4fae281cb46a0821892f0591698f59e0daa334d52bd66f06d2a4edf6cb3d18c409675e8b363f8d8daca93fa8ae654e0cd60ac0ae4938bc15e5f2543dee42231388ee58be70c1cfe0a3f9e04be534574c2b958ee27f076a48ee42d465bf706ca8830a9b295574dcf03ab709af8c2ab7a7ff1797ae25d5f91bd8c5a0ad9e011a0a4a079c874389a0a601f924d7f3f8911eb100c81b6011e760fbf3e57c9e688aee81eb88f6c7c30a5ec86483c50a103637ba283b2e9c9001269f8e45f529b3f63926d1f1549c4a3ad3b35c993b0facb93788a1ec5e2c7f77f04821305acbfcdd8d5cdff5dbfefc9bbb659b6362445d2049a528a57c162806bd2f3feb75727ffc7139c1d48d60e032ff9f08e56b485dea6dac23f0df27918a1b7abcd4b22bb27b6df3a2195f1aec3bc3e4f1673b401444ac32fa6b41ffe09c432f6b0a5072e80db71bc720b5679fb6f8a5152198a9c0fbcfc8728279507df1ad9911730d2e15e04b33c9c9b6ee360b25c2f0d35eccb95fe7d4d558b6f197d091b53b2802ed754afa534136fa5f0ec003bc76688c8b2cf9c9c68cec43122c7f22093addf9b8651773038425a3e4b507a44076799932ee35131499ece217693958c12f287936001a00dd0da1a9819affe1528b88ae3820d077b8fb193a6bbf3b394939ef0362e9ef5e513f8824edb0eb289e26d4e045b969caf23d5a9a8578fa24ddb3b1a3d20cd2816f400d3e256926eac8aba6df85c14f1a2b2943e06babf61931db7ed8a197679a9f59ed8dcd776ae11c2e6d42a15ff57ca3ae7ca947510df6d2e6aecbba9133d1c2fdfb6e7734804d16cd79b9bbcf1a054997c20b811a7b8c4a041c5563d907f00395e1cbbc60a3b4e5d3ed071d66bb4a4a68524393bb0fe00744ae02d7d6a25f5bfd5751c4e9977b7537d3b833075be963a09c7cc23ed2d622d5410209727d49c0cf9b82bda186279183e560016786d5985800244ed8e1a17e06e52e18d0ac283f278a3c81500119e0517cc13408e0ba302ce470d420650d09b81bff7303dde051ddcac340cae1056fbfce42f3ae15ac9fb71fcc230bbe76fe45223c223ba7c4bcc289c750bbff95d02cf4a6c580582685030115eba8195d341a0950c186af0dcbf64897f5244d6d19a1375823c68fced36ed8d0287c6441e990e7a3cf73594596459795600719367efbf22912e935523b3fbdb6ef2aeb5f0fb527846f6d5848e15d2f1f47cc0586be56baeb1b6f4e41cff38b40435b73c6547b367565cb16eb91e1eb1cc762f54d404782eda99d367fa38186b77e7ffc99d6aef02527efd4606c45e270c14965da523020250c0a8f7fd6447a034620e5fdbc9d0023b790573e1a149b1070741d1991db158ebca22bc5b8c7c85f4753b19464c7205883155707948082755acbe6dc9321595578c84d1da134b6e528879608eff9e484724170ae0270ddd981d9ef0cdf317414c57f1cb5665f3791979701fd6ce7e808b7c41035789a83980dc04918782b2c040f38bc84ca6b03a1d2951af64bbc845c7356a51e8b4c20625178fef213bc35a4b3f7733e2db8d42026f4b0c0a7d21bedb4d6f9b9e7309b8d7db73c1f02a658d0188345ed084b85dd79c11e7c9772e5cf0cc950b01afa14c7c4e57ecbb8c636f709c0882390d26da1bcb0123f7b3e5632c26e9ecab78abab631f6e147a9ac46f8db06ff077728b64aeb3cd288accc4de1ed1e4279ffe83bc8dda60d8b3daf789f5cf9d46c57c77a32ebbffd57de84a614a4c010174e5685c366fae6756727b3b9416d77dcd2788fe480fd3eaadcbe09c07f2cdf0bbc71e2d40034cb10481789cb6dc0319111653d7fb303f2ab619d8b81f15a6f4d3cda61c44a18f6c22fa8f8dd1f6197108b3267ba92ed8fc1fcbaecf4e2c9791d8e24b3ddc3f955eef08d710790137e8556a05f0601fe47235df226620258d3a97d0abe6611ce068e568223e7954341b9413089b29369743a2c52e90e2473fb4106020d617c1d181e6479debbb47dbde237b8930a891c525d7d15307cdb832c78daacb691d5368f9dcc593af5de7d0267d0fe30addd996d9d7c66cdd9afdb0f1dbf6f055a6c13e0d310a65e6cd0b92063da820d72fd0af8a9fabbb57d0301f0283aa6afe075404e612573c2612032779ec5114fc44eda62b6bdfe8e24fd1eec694b0735027abf682e6d7b8de59d6362f20c96976f75c64e8be26b9fb638f65d6a5ab23e45ee100f0e113f297582e34d07b4230e3eb8cb33051a1c77cbb9f88c7fec4f3d0c3f4019d2c4e9ecac96c178fc38b046c08efb6ccd1d299a36e6712d0cf9743e512a427d5578398756cea2a4f3f7c964e5c6691f199caf3f39cd355b5fb75efe08ac2fb4f3a69eaf71439fea2a8d0cd601618049f490a969f0b099e8f72218f318bcc89731485767a3bb794f2d90781f8c4ea62e990d2c4afed99cfdafbe8ea06e82a87834de60e9232a72155346f1bf553c68547afbc7c23e1c646121018c02376f46d50df7e6111f4182f4f04804e08b4e382163c38ff0790ad971805d2c527d52d7e1451a6c0347f25546d5701f0932f685e0d90069ba3619e746031cba0b7cbfc0522879c3fb1eb5a7a039c3575a53f7e78ff2eca98b47039088ed0ebf6a20f9f609fddea91c316ae78c381d979a43759f3023d0a8997397ca82a657e65bd1d1eca0335962804368342475cf993dd28d99eeda274a806d654e044a73d2e2932d650e5d1e202267da82b9543355bdfee55c39fa14a791f142943e50d0be04879984e5204fe4b69e89d7ba5824e902fa529325065b95c123b52779714a74c7afb866e27135f48edded799c02cc9043f0e0275e9a9891dfc6b44ddb6fde0248cd43d8d23f932b71047c7415f1dfa9426e04579e506cfc6cd6a78e6e78399be199b632e6512ee46fdd01e0be83b9152ebeecf82a232387b8838cc96a30ac27bad5184705bea0f555a6a960750bc71f861a09d505c527c47850a864cd98c7c8c6cbfba82c60708efc6c625340e0e988a860fd05498b3f72e0b8ec83ee3c578508d15f5817da3f2893497aead47c53120d03706f76060124d297f89f245ad30e044d1da998ef690d463d992896dd48c5569d41a06b32b81f0c0b85dfc04bfabbb32e5872806579cb9ba59aba33176add172712eb501d5cc37ff96d218d7601ca31a457766977935131659abf21cb4e804dd276c7950cefdc79e2abd1c1b45c0b74060a97716999bb7de2b7f60fc012a522c16ba453b0dbb5f0f24739a7011e9dc96ca17cf4ea41f890c2c4cd292a0fccfcea6a773317",
    3  "header": "eca99df66c442c1f5fbd50075c06c311dd8b802ca97cbdc2637948f25d598720"
    4}
    

    Feel free to ignore my nits. Nice work!

  90. Empact commented at 10:41 am on March 21, 2019: member

    Pushed some suggested changes as commits here: https://github.com/Empact/bitcoin/commits/bip157-index

    Nothing major - all in the vein of minimizing / simplifying / protecting. Split across small commits in hopes of being helpful.

  91. jamesob commented at 3:05 pm on March 21, 2019: member

    Pushed some suggested changes as commits here: Empact/bitcoin@bip157-index

    These are all pretty cosmetic changes that would necessitate comprehensive re-review from a lot of the people who have already looked at this code. I think this PR should be tested as-is for merge, and changes like those you’ve suggested should be filed as follow-up PRs.

    I like the direction that some of your changes go, but I think it’s wiser to marshal scarce review time towards the critical path of getting BIP157/158 to usability.

  92. Empact commented at 7:14 pm on March 21, 2019: member

    Concept ACK

    I’m also very much in favor of getting this in, and would like to see it on the high priority for review list. Note it needs a rebase at least before that happens, so I don’t think my nits are out of order. That said, feel free to ignore them.

    I would particularly make a case for https://github.com/Empact/bitcoin/commit/b1367c206852b62797b04139a85acce3506bc1d3, for usability. Here’s an example of Nicolas running into this issue: https://github.com/bitcoin/bitcoin/issues/15147

  93. laanwj added this to the "Blockers" column in a project

  94. ryanofsky commented at 8:12 pm on March 22, 2019: member

    I’ve looked though most of this PR and it really looks great. I need to look at some parts more closely, but I almost don’t have anything to comment on since it is so well put together. I should finish the review on Monday.

    I like most of Empact’s changes too, but a lot of them are subjective improvements. If you’re going to use them it might be nice to just incorporate them in the rebase like he mentioned.

  95. jimpo force-pushed on Mar 23, 2019
  96. DrahtBot removed the label Needs rebase on Mar 23, 2019
  97. jimpo commented at 4:29 am on March 23, 2019: contributor

    I have rebased and incorporated b1367c206852b62797b04139a85acce3506bc1d3 and 6c1a5adc68d704189825532522a9fa1b5a4aa235 of @Empact ’s changes.

    Thanks for the reviews!

  98. in src/index/base.h:35 in e6c6654506 outdated
    31@@ -32,7 +32,7 @@ class BaseIndex : public CValidationInterface
    32         bool ReadBestBlock(CBlockLocator& locator) const;
    33 
    34         /// Write block locator of the chain that the txindex is in sync with.
    35-        bool WriteBestBlock(const CBlockLocator& locator);
    36+        void WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator);
    


    lucayepa commented at 12:41 pm on March 24, 2019:
    ultranit, you can change the comment: txindex -> index.
  99. in src/index/base.cpp:175 in e3845e4a2a outdated
    163-        return error("%s: Failed to write locator to disk", __func__);
    164+    GetDB().WriteBestBlock(batch, chainActive.GetLocator(m_best_block_index));
    165+    return true;
    166+}
    167+
    168+bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
    


    ryanofsky commented at 7:52 pm on March 25, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    This commit still doesn’t compile because BaseIndex::Rewind declaration is not added until the next commit.

  100. in src/index/base.cpp:270 in e3845e4a2a outdated
    248@@ -224,9 +249,7 @@ void BaseIndex::ChainStateFlushed(const CBlockLocator& locator)
    249         return;
    250     }
    251 
    252-    if (!GetDB().WriteBestBlock(locator)) {
    253-        error("%s: Failed to write locator to disk", __func__);
    254-    }
    255+    Commit();
    


    ryanofsky commented at 7:58 pm on March 25, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    It would be good to check for failures on Commit() calls in this git commit. Even though the current implementation in this git commit always returns true, it can start to return false when the subclasses is added, and these calls don’t appear to be updated later.


    ryanofsky commented at 6:52 pm on March 28, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    Would be good to add NODISCARD to all methods that return errors on failure. Even in cases where you don’t want to handle or log the errors, it’s clarifying if invoking code shows that errors are being ignored, and maybe has comments saying why the errors are expected or ok to ignore.


    jimpo commented at 9:59 pm on April 5, 2019:
    I explicitly don’t want to handle failure as Commit() already logs on errors and in the places where the return value is ignored, the index is committing a later state so it is safe to just continue. I will add comments in the appropriate places saying this.
  101. in src/index/blockfilterindex.cpp:221 in 5964d2f97a outdated
    216+
    217+    if (!m_db->Write(DBHeightKey(pindex->nHeight), value)) {
    218+        return false;
    219+    }
    220+
    221+    m_next_filter_pos += bytes_written;
    


    ryanofsky commented at 8:05 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    This commit fails to compile with

    0index/blockfilterindex.cpp:221:23: error: no viable overloaded '+='
    1    m_next_filter_pos += bytes_written;
    
  102. in src/index/blockfilterindex.cpp:29 in 5964d2f97a outdated
    21+ * as big-endian so that sequential reads of filters by height are fast.
    22+ * Keys for the hash index have the type [DB_BLOCK_HASH, uint256].
    23+ */
    24+constexpr char DB_BLOCK_HASH = 's';
    25+constexpr char DB_BLOCK_HEIGHT = 't';
    26+constexpr char DB_FILTER_POS = 'P';
    


    ryanofsky commented at 8:10 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Can you mention the DB_FILTER_POS key in the comment above? I also think DB_NEXT_FILTER_POS would be a slightly more descriptive and consistent name.


    jimpo commented at 10:18 pm on April 5, 2019:
    Added a comment.
  103. in src/index/blockfilterindex.cpp:28 in 5964d2f97a outdated
    23+ */
    24+constexpr char DB_BLOCK_HASH = 's';
    25+constexpr char DB_BLOCK_HEIGHT = 't';
    26+constexpr char DB_FILTER_POS = 'P';
    27+
    28+constexpr unsigned int MAX_FILE_SIZE = 0x1000000; // 16 MiB
    


    ryanofsky commented at 8:14 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Comment saying this limit is for filter files would be nice. Or using less ambiguous names (MAX_FILT_FILE_SIZE, FILT_FILE_CHUNK_SIZE)

  104. in src/index/blockfilterindex.cpp:32 in 5964d2f97a outdated
    24+constexpr char DB_BLOCK_HASH = 's';
    25+constexpr char DB_BLOCK_HEIGHT = 't';
    26+constexpr char DB_FILTER_POS = 'P';
    27+
    28+constexpr unsigned int MAX_FILE_SIZE = 0x1000000; // 16 MiB
    29+/** The pre-allocation chunk size for fltr?????.dat files */
    


    ryanofsky commented at 8:16 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Obviously bikeshedding here but “filt” seems like a more obvious abbreviation for filter than “fltr”.

  105. in src/index/blockfilterindex.cpp:33 in 5964d2f97a outdated
    28+constexpr unsigned int MAX_FILE_SIZE = 0x1000000; // 16 MiB
    29+/** The pre-allocation chunk size for fltr?????.dat files */
    30+constexpr unsigned int FILE_CHUNK_SIZE = 0x100000; // 1 MiB
    31+
    32+namespace {
    33+    struct DBVal
    


    ryanofsky commented at 8:18 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Suggest running clang-format. Our style guide and clang-format-config don’t indent namespaces or put struct opening braces on new lines, iirc.

  106. in src/index/blockfilterindex.cpp:68 in 5964d2f97a outdated
    63+        template<typename Stream>
    64+        void Unserialize(Stream& s)
    65+        {
    66+            char prefix = ser_readdata8(s);
    67+            if (prefix != DB_BLOCK_HEIGHT) {
    68+                throw std::ios_base::failure("Invalid format for DB key");
    


    ryanofsky commented at 8:29 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Maybe use less generic error messages here and below like “Invalid format for block filter index DBHeightKey”

  107. in src/index/blockfilterindex.cpp:100 in 5964d2f97a outdated
     95+BlockFilterIndex::BlockFilterIndex(BlockFilterType filter_type,
     96+                                   size_t n_cache_size, bool f_memory, bool f_wipe)
     97+    : m_filter_type(filter_type)
     98+{
     99+    const std::string& filter_name = BlockFilterTypeName(filter_type);
    100+    if (filter_name == "") throw std::invalid_argument("unknown filter_type");
    


    ryanofsky commented at 8:30 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    if (filter_name.empty()) would be a little more idiomatic c++

  108. in src/index/blockfilterindex.cpp:116 in 5964d2f97a outdated
    107+    m_filter_fileseq = MakeUnique<FlatFileSeq>(std::move(path), "fltr", FILE_CHUNK_SIZE);
    108+}
    109+
    110+bool BlockFilterIndex::Init()
    111+{
    112+    if (!m_db->Read(DB_FILTER_POS, m_next_filter_pos)) {
    


    ryanofsky commented at 8:39 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    Is it possible to distinguish between entry not existing here and failure to deserialize? Might be a little nicer to be able to return an error if there’s something weird here, instead of resetting the next write to position 0.


    jimpo commented at 10:27 pm on April 5, 2019:
    Very good point!
  109. in src/index/blockfilterindex.cpp:15 in 5964d2f97a outdated
     9+#include <validation.h>
    10+
    11+/* The index database stores three items for each block: the disk location of the encoded filter,
    12+ * its dSHA256 hash, and the header. Those belonging to blocks on the active chain are indexed by
    13+ * height, and those belonging to blocks that have been reorganized out of the active chain are
    14+ * indexed by block hash. This ensures that filter data for any block that becomes part of the
    


    ryanofsky commented at 9:13 pm on March 25, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    It’d be good to mention that values and not just the keys are different in these two cases (height keys appear to have values prefixed by the block hash).

  110. ryanofsky commented at 9:22 pm on March 25, 2019: member

    I should finish the review on Monday

    Apologies! I didn’t get quite as far with the review as I would have liked. I left many minor suggestions, but as usual please just change whatever interests you and ignore anything else.

    Started review (will update list below with progress).

    • e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e index: Allow atomic commits of index state to be extended. (1/12)
    • c368acb6e341bb34e86f011164157ca5ad3d14ef index: Ensure block locator is not stale after chain reorg. (2/12)
    • a0bd77e2ad5bdbaeca529a38335b6a6c2f3fd5d9 blockfilter: Functions to translate filter types to/from names. (3/12)
    • 4fa1f82bd0db7e8b823e130130fef277d9b1005c serialize: Serialization support for big-endian 32-bit ints. (4/12)
    • 5964d2f97a221b898dbc477ca8f06ddfbe1d241e index: Implement block filter index with write operations. (5/12)
    • 88ecade18fd615add2550c89f6152bc44fc4ee5e index: Implement lookup methods on block filter index. (6/12)
    • 9283baae04dddf09576e541786cc429158077f82 test: Unit tests for block index filter. (7/12)
    • d85dd54792f7e1d4ab3d224930e35ed36c6db18a test: Unit test for block filter index reorg handling. (8/12)
    • 7a955d1ce6ecfce767c367bd2b787867c0767b47 index: Access functions for global block filter indexes. (9/12)
    • 88fac30e85e907891c92b8df456d925369d8bc06 init: Add CLI option to enable block filter index. (10/12)
    • 1426fb7128029e88ff0b3d6491d0f0e6d6d2ddab rpc: Add getblockfilter RPC method. (11/12)
    • e6c665450648dad8981ec981d6c8c6475e9aa1f8 blockfilter: Update BIP 158 test vectors. (12/12)
  111. in src/index/base.h:65 in e3845e4a2a outdated
    53@@ -54,8 +54,8 @@ class BaseIndex : public CValidationInterface
    54     /// over and the sync thread exits.
    55     void ThreadSync();
    56 
    57-    /// Write the current chain block locator to the DB.
    58-    bool WriteBestBlock(const CBlockIndex* block_index);
    59+    /// Write the current chain block locator and other index state to the DB.
    60+    bool Commit();
    


    ryanofsky commented at 8:35 pm on March 26, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e):

    Giving the private method the same name as the protected method inherited by subclasses makes the code harder to follow. Would suggest calling this InternalCommit() or something like that.


    ryanofsky commented at 3:03 pm on April 8, 2019:

    re: #14121 (review)

    In commit “index: Allow atomic commits of index state to be extended.” (4368384f1d267b011e03a805f934f5c47e2ca1b2)

    Current change isn’t what I was suggesting and I think is actually worse than the original (but still ok if this is what makes most sense to you). I was suggesting renaming the Commit() that’s internal to the base class InternalCommit(), not calling the other Commit() that’s shared and overridden externally by subclasses InternalCommit(). Calling the overridden Commit() internal also breaks consistency with the other overridden methods (init, rewind, writeblock).


    jimpo commented at 5:45 am on April 10, 2019:

    The difference from Init, Rewind, and WriteBlock and is that CommitInternal has a different method signature from Commit, which is why I thought you found it confusing, whereas the others have only a single method signature that the child classes override.

    I don’t have too much of a preference, though I do think switching the Internal would be weird, as I’ve generally found the convention to be that an outer function delegating to an inner helper function might call FuncInternal, not the other way around.

  112. in src/index/base.h:57 in e3845e4a2a outdated
    53@@ -54,8 +54,8 @@ class BaseIndex : public CValidationInterface
    54     /// over and the sync thread exits.
    55     void ThreadSync();
    56 
    57-    /// Write the current chain block locator to the DB.
    58-    bool WriteBestBlock(const CBlockIndex* block_index);
    59+    /// Write the current chain block locator and other index state to the DB.
    


    ryanofsky commented at 8:50 pm on March 26, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    It isn’t clear from this comment when this is called or what “other index state” might be referring to. Maybe expand comment to something like: “Write the current chain block locator and other index state from subclasses to the DB. This is called after blocks are added or rewound.”

  113. in src/index/base.h:74 in e3845e4a2a outdated
    68@@ -69,6 +69,10 @@ class BaseIndex : public CValidationInterface
    69     /// Write update index entries for a newly connected block.
    70     virtual bool WriteBlock(const CBlock& block, const CBlockIndex* pindex) { return true; }
    71 
    72+    /// Virtual method called internally that can be overridden to atomically
    73+    /// commit more index state.
    74+    virtual bool Commit(CDBBatch& batch);
    


    ryanofsky commented at 7:11 pm on March 28, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    Comment should mention how this relates to WriteBlock (and Rewind), that it’s called after one or more WriteBlock/Rewind calls in order to flush state to disk and store extra data. Comment should also mention that subclasses that override this must include a call to the overridden method (or the index position won’t be updated).

    It’s also not clear what’s supposed to happen if this returns false. Maybe a comment could clarify. It seems like the only thing that happens is that the batch update is not written, and the next block position is not saved, but the sync still continues? I don’t understand when this behavior would be useful, and why it wouldn’t be better to abort syncing the index in this case. Maybe the behavior is ok, but comment should at least mention what return value is expected from the overridden method and what effect it will have.

  114. in src/index/base.cpp:98 in e3845e4a2a outdated
    94@@ -95,17 +95,18 @@ void BaseIndex::ThreadSync()
    95         int64_t last_locator_write_time = 0;
    96         while (true) {
    97             if (m_interrupt) {
    98-                WriteBestBlock(pindex);
    99+                m_best_block_index = pindex;
    


    ryanofsky commented at 7:21 pm on March 28, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (e3845e4a2a27d69a09ac0f8744b23530ad3bbe9e)

    It seems good to set m_best_block_index here, but I don’t understand why it wasn’t being set before. Is this a bugfix?


    jimpo commented at 10:41 pm on April 5, 2019:
    Previously, since m_synced wasn’t set to and WriteBestBlock took an explicit pointer argument, it wasn’t necessary. But now WriteBestBlock reads m_best_block_index instead of taking an argument.
  115. in src/blockfilter.cpp:212 in a0bd77e2ad outdated
    207+    auto it = g_filter_types.find(filter_type);
    208+    return it != g_filter_types.end() ? it->second : unknown_retval;
    209+}
    210+
    211+bool BlockFilterTypeByName(const std::string& name, BlockFilterType& filter_type) {
    212+    for (auto entry : g_filter_types) {
    


    ryanofsky commented at 7:39 pm on March 28, 2019:

    In commit “blockfilter: Functions to translate filter types to/from names.” (a0bd77e2ad5bdbaeca529a38335b6a6c2f3fd5d9)

    const auto& to prevent string copies and allocs

  116. in src/index/blockfilterindex.cpp:227 in 5964d2f97a outdated
    222+    return true;
    223+}
    224+
    225+static bool CopyHeightIndexToHashIndex(CDBIterator& db_it, CDBBatch& batch,
    226+                                       const std::string& index_name, int start_height,
    227+                                       const CBlockIndex* stop_index)
    


    ryanofsky commented at 7:51 pm on March 28, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    It seems misleading that this function accepts a stop_index argument rather than a stop_height, because the stopping point is identified only by height, and no other fields of the CBlockIndex are actually accessed. It would also be more consistent to have start_height/stop_height arguments than start_height/stop_index.

  117. in src/index/blockfilterindex.cpp:298 in 5964d2f97a outdated
    260+    // overwritten.
    261+    if (!CopyHeightIndexToHashIndex(*db_it, batch, m_name, new_tip->nHeight, current_tip)) {
    262+        return false;
    263+    }
    264+
    265+    // The latest filter position gets written in Commit by the call to the BaseIndex::Rewind.
    


    ryanofsky commented at 8:08 pm on March 28, 2019:

    In commit “index: Implement block filter index with write operations.” (5964d2f97a221b898dbc477ca8f06ddfbe1d241e)

    This comment and the general control flow here where BlockFilterIndex::Rewind calls BaseIndex::Rewind which calls BlockFilterIndex::Commit which calls BaseIndex::Commit is confusing to follow (made worse by there being two BaseIndex::Commit methods). I flattened the control flow in a (+42/-49) simplification in 8bb65cedbaf4a84d4018cc194985aa02c8a51043 (pr/blockfilt), and I think it’d be good to squash these changes into the PR, especially since the early commit that introduces Commit()/Rewind() doesn’t compile right now anyway.


    jimpo commented at 6:39 pm on April 6, 2019:

    I looked over your proposed changes. I updated one of the commit methods to CommitInternal as suggested (though the opposite one from you – I think Commit calling CommitInternal makes more sense).

    However, I don’t really like the change in behavior to Commit/Rewind. Commit just does DB/disk persistence, which is why errors can generally be ignored, so giving it responsibility for writing m_best_block_index makes everything more confusing. However, Rewind is a special case where ignoring errors in Commit would be unsafe. I added more comments on the methods to explain this.


    ryanofsky commented at 10:54 pm on April 9, 2019:

    re: #14121 (review)

    I don’t really like the change in behavior to Commit/Rewind. Commit just does DB/disk persistence, which is why errors can generally be ignored

    Up to you but I’d encourage you to take another look. Your comment sounds more like a reaction to the method naming and not the substance of the change. I agree with you naming in my suggested change is not ideal. I actually wanted to call the base Commit() method UpdatePosition() and the overridden Commit() method Flush(), but I reverted back to your names to try to avoid adding unrelated differences. (IMO, all the mentions of committing and atomicity in this PR are misleading because almost nothing about the implementation is atomic. It is true that updates of m_best_block, DB_BEST_BLOCK, and fltr*.dat files are ordered, but operations being ordered is different from them being atomic, and it feels like too much is implied by the current naming even if the code is technically correct.)

    In any case, here are the advantages of my change beyond reducing the amount of code:

    1. It updates m_best_block_index in the same place as DB_BEST_BLOCK.
    2. It flattens control flow, getting rid the BlockFilterIndex::Rewind calling BaseIndex::Rewind calling BlockFilterIndex::Commit calling BaseIndex::Commit sequence and the whole call super anti-pattern.

    It’s sounds crazy to me to cite updating m_best_block_index and DB_BEST_BLOCK in different places instead of the same place as an advantage of the current code. This seems like a footgun and anti-ergonomic decision, like putting the steering wheel on the outside of a car. When you want to turn your car left, do you just want to turn the wheel? Or is it better to stop the car, get out, point the wheel, make the turn, stop again, straighten, then keep going? When you update the position, do you just want to call one method? Or is it better to manually set a member variable and go through a rube-goldberg sequence of virtual method calls?

  118. in src/index/blockfilterindex.cpp:254 in 88ecade18f outdated
    238@@ -218,7 +239,7 @@ bool BlockFilterIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex
    239         return false;
    240     }
    241 
    242-    m_next_filter_pos += bytes_written;
    243+    m_next_filter_pos.nPos += bytes_written;
    


    ryanofsky commented at 9:59 pm on March 28, 2019:

    In commit “index: Implement lookup methods on block filter index.” (88ecade18fd615add2550c89f6152bc44fc4ee5e)

    This change should be moved to the previous commit (5964d2f97a221b898dbc477ca8f06ddfbe1d241e), which doesn’t compile without it.

  119. in src/index/blockfilterindex.h:59 in 88ecade18f outdated
    54+    bool LookupFilter(const CBlockIndex* block_index, BlockFilter& filter_out) const;
    55+
    56+    /** Get a single filter header by block. */
    57+    bool LookupFilterHeader(const CBlockIndex* block_index, uint256& header_out) const;
    58+
    59+    /** Get a range of filters between two heights on a chain. */
    


    ryanofsky commented at 10:06 pm on March 28, 2019:

    In commit “index: Implement lookup methods on block filter index.” (88ecade18fd615add2550c89f6152bc44fc4ee5e)

    Might be worth commenting on how range lookups are intended to be used since they aren’t exposed via RPC in this PR.

  120. in src/test/blockfilter_index_tests.cpp:137 in 9283baae04 outdated
    132+    // Test lookups for a range of filters/hashes.
    133+    std::vector<BlockFilter> filters;
    134+    std::vector<uint256> filter_hashes;
    135+
    136+    const CBlockIndex* block_index = chainActive.Tip();
    137+    BOOST_CHECK(filter_index.LookupFilterRange(0, block_index, filters));
    


    ryanofsky commented at 10:19 pm on March 28, 2019:

    In commit “test: Unit tests for block index filter.” (9283baae04dddf09576e541786cc429158077f82)

    It might be good to add a test for good error handling (no crashes) over a range that includes blocks never added to the index (known blocks that were never connected, or connected blocks that haven’t been added to the index yet).

  121. in src/test/blockfilter_index_tests.cpp:82 in d85dd54792 outdated
    77+    block.hashPrevBlock = prev->GetBlockHash();
    78+    block.nTime = prev->nTime + 1;
    79+
    80+    // Replace mempool-selected txns with just coinbase plus passed-in txns:
    81+    block.vtx.resize(1);
    82+    for (const CMutableTransaction& tx : txns)
    


    ryanofsky commented at 10:21 pm on March 28, 2019:

    In commit “test: Unit test for block filter index reorg handling.” (d85dd54792f7e1d4ab3d224930e35ed36c6db18a)

    Style guide braces blah blah.

  122. in src/init.cpp:898 in 88fac30e85 outdated
    894@@ -886,6 +895,7 @@ int nUserMaxConnections;
    895 int nFD;
    896 ServiceFlags nLocalServices = ServiceFlags(NODE_NETWORK | NODE_NETWORK_LIMITED);
    897 int64_t peer_connect_timeout;
    898+std::vector<BlockFilterType> enabled_filter_types;
    


    ryanofsky commented at 10:26 pm on March 28, 2019:

    In commit “init: Add CLI option to enable block filter index.” (88fac30e85e907891c92b8df456d925369d8bc06)

    Please do use g_ prefix to make it clear this is a global.

  123. in src/init.cpp:1484 in 88fac30e85 outdated
    1476@@ -1448,6 +1477,13 @@ bool AppInitMain(InitInterfaces& interfaces)
    1477     nTotalCache -= nBlockTreeDBCache;
    1478     int64_t nTxIndexCache = std::min(nTotalCache / 8, gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? nMaxTxIndexCache << 20 : 0);
    1479     nTotalCache -= nTxIndexCache;
    1480+    int64_t filter_index_cache = 0;
    1481+    if (!enabled_filter_types.empty()) {
    1482+        size_t n_indexes = enabled_filter_types.size();
    1483+        int64_t max_cache = (max_filter_index_cache << 20) / n_indexes;
    1484+        filter_index_cache = std::min(nTotalCache / 8, max_cache);
    


    ryanofsky commented at 10:38 pm on March 28, 2019:

    In commit “init: Add CLI option to enable block filter index.” (88fac30e85e907891c92b8df456d925369d8bc06)

    Did you want to divide nTotalCache by n_indexes here? Shouldn’t matter now because n_indexes is one. But as n_indexes grows, nTotalCache on the next line could keep decreasing and even go below 0.


    jimpo commented at 10:54 pm on April 5, 2019:
    Updated the logic.
  124. in src/index/blockfilterindex.cpp:356 in 88ecade18f outdated
    339+        }
    340+
    341+        db_it->Next();
    342+    }
    343+
    344+    results.resize(results_size);
    


    ryanofsky commented at 11:04 pm on March 28, 2019:

    In commit “index: Implement lookup methods on block filter index.” (88ecade18fd615add2550c89f6152bc44fc4ee5e)

    It seems like this line isn’t doing anything and could be removed (new size should be same as previous size).


    jimpo commented at 10:46 pm on April 5, 2019:
    This is the first time results is accessed in this method, and the size has to be set appropriately.

    ryanofsky commented at 11:31 pm on April 9, 2019:

    re: #14121 (review)

    In commit “index: Implement lookup methods on block filter index.” (b5e8200db76f06d35099da502439dcbdfd0a1b3e)

    This is the first time results is accessed in this method, and the size has to be set appropriately.

    Sorry, you’re right. I confused results with the values variable above.

  125. in src/index/blockfilterindex.cpp:368 in 88ecade18f outdated
    351+        uint256 block_hash = block_index->GetBlockHash();
    352+
    353+        size_t i = static_cast<size_t>(block_index->nHeight - start_height);
    354+        if (block_hash == values[i].first) {
    355+            results[i] = std::move(values[i].second);
    356+            continue;
    


    ryanofsky commented at 11:14 pm on March 28, 2019:

    In commit “index: Implement lookup methods on block filter index.” (88ecade18fd615add2550c89f6152bc44fc4ee5e)

    Seems like this could break instead of continue. Would add a comment here if there’s a case where this needs to keep iterating.


    jimpo commented at 10:44 pm on April 5, 2019:
    Hmm? If it was a break, it would only copy one value to the result value rather than all of them.

    ryanofsky commented at 11:41 pm on April 9, 2019:

    re: #14121 (review)

    In commit “index: Implement lookup methods on block filter index.” (b5e8200db76f06d35099da502439dcbdfd0a1b3e)

    Hmm? If it was a break, it would only copy one value to the result value rather than all of them.

    Again this is me thinking results and values where the same variable, so it would be safe to break at the forking point because the earlier blocks would already be filled in. This could maybe be a legitimate optimization, but not worth implementing at this point.

  126. ryanofsky approved
  127. ryanofsky commented at 11:24 pm on March 28, 2019: member
    Slightly tested ACK e6c665450648dad8981ec981d6c8c6475e9aa1f8 (I just watched the “Syncing basic block filter index with block chain from height” messages and tested the RPC). Again no major comments, feel free to ignore them.
  128. index: Allow atomic commits of index state to be extended. 4368384f1d
  129. index: Ensure block locator is not stale after chain reorg. 62b7a4f094
  130. blockfilter: Functions to translate filter types to/from names. ba6ff9a6f7
  131. serialize: Serialization support for big-endian 32-bit ints. 2ad2338ef9
  132. index: Implement block filter index with write operations. 75a76e3619
  133. index: Implement lookup methods on block filter index. b5e8200db7
  134. test: Unit tests for block index filter. 6bcf0998c0
  135. test: Unit test for block filter index reorg handling. 2bc90e4e7b
  136. index: Access functions for global block filter indexes. accc8b8b18
  137. init: Add CLI option to enable block filter index. ff35105096
  138. rpc: Add getblockfilter RPC method.
    Retrieves and returns block filter and header from index.
    19308c9e21
  139. blockfilter: Update BIP 158 test vectors.
    New tests for the case of non-standard OP_RETURN outputs.
    c7efb652f3
  140. jimpo force-pushed on Apr 6, 2019
  141. in src/index/base.h:60 in 4368384f1d outdated
    53@@ -54,8 +54,15 @@ class BaseIndex : public CValidationInterface
    54     /// over and the sync thread exits.
    55     void ThreadSync();
    56 
    57-    /// Write the current chain block locator to the DB.
    58-    bool WriteBestBlock(const CBlockIndex* block_index);
    59+    /// Write the current index state (eg. chain block locator and subclass-specific items) to disk.
    60+    ///
    61+    /// Recommendations for error handling:
    62+    /// If called on a successor of the previous committed best block in the index, the index can
    


    ryanofsky commented at 3:10 pm on April 8, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (4368384f1d267b011e03a805f934f5c47e2ca1b2)

    Thank you for adding this comment, but I had to read this several times before I figured out that the “index can continue processing” isn’t describing something that happens externally but is describing what you are supposed to do when handling errors from this function. I’d suggest phrasing this less passively as:

    0/// Recommendation for handling errors returned by this function:
    1///
    2/// If calling this function fails, and m_best_block_index is a descendant
    3/// of a block that was previously committed, it is safe to ignore the error
    4/// because the index will not get corrupted (just needs to catch up from
    5/// further behind on reboot). If m_best_block_index is not a descendant of
    6/// the last block committed (due to a chain reorganization), the error can't
    7/// be ignored and the index should halt until Commit succeeds or it could 
    8/// end up getting corrupted.
    
  142. in src/index/base.cpp:100 in 4368384f1d outdated
     94@@ -95,17 +95,22 @@ void BaseIndex::ThreadSync()
     95         int64_t last_locator_write_time = 0;
     96         while (true) {
     97             if (m_interrupt) {
     98-                WriteBestBlock(pindex);
     99+                m_best_block_index = pindex;
    100+                // No need to handle errors in Commit. If it fails, the error will be already be
    101+                // logged. The best way to recover is to continue, as index cannot be corrupted by
    


    ryanofsky commented at 9:53 pm on April 9, 2019:

    In commit “index: Allow atomic commits of index state to be extended.” (4368384f1d267b011e03a805f934f5c47e2ca1b2)

    I don’t understand “index cannot be corrupted by a missed commit.” Does it mean that if this Commit call fails you expect a future Commit call to fix whatever the problem is? If so, this seems like a property that holds for TxIndex, but not necessarily for BlockFilterIndex since that index is writing external files. Should clarify whatever is meant by this comment.

    It would also be really helpful to give a specific example where this behavior would be desirable. Is there a case you are imagining where some commits fail and some succeed and after that everything is fine? I am having trouble trying to conjure up a scenario.

    Naively, as a user relying on the index I would expect that if updating the index ever failed, then the index would get disabled and lookups into the index would return RPC errors instead of silently returning bad data or incomplete data. I would probably also want writing to stop after the first error, so I could potentially debug the issue, and so my logs wouldn’t fill up with more and more write errors after the first one.

    Maybe this implementation makes more sense than what I’m describing and is better for some reason, but whatever the rationale is, it could definitely be stated more clearly.

  143. ryanofsky approved
  144. ryanofsky commented at 0:08 am on April 10, 2019: member

    Slightly tested ACK c7efb652f3543b001b4dd22186a354605b14f47e (I just rebuilt the index with the updated PR and tested the RPC). Changes since last review: rebase, fixed compile errors in internal commits, new comments, updated error messages, tweaked cache size logic, renamed commit method, renamed constants and globals, fixed whitespace, extra BlockFilterIndex::Init error check.

    I think this change is in a good state and could be merged in it’s current form. I left more comments but they are minor and you should ignore them if you don’t want to deal with them.

    I think this change has had almost enough review to be merged. It would benefit from a re-ack by @jamesob, and some more cursory reviews by other contributors to confirm that this only creates a new blockindex, and is disabled by default, and has no effect on the existing txindex or validation or wallet or anything crazy like that.

  145. jimpo commented at 6:16 am on April 10, 2019: contributor

    @ryanofsky Let me clarify the intended behavior of Commit, so we can figure out how to properly word it. The design really stems from the optimization where the block index is only flushed to disk infrequently or on shutdown, and the auxiliary indexes mimic this.

    During normal operation, when the chain is extended by new blocks, entries are written into the database height index and their corresponding filters are written to flat files. However, the in-database pointers to the latest block in the index and end position of the flat file sequence are not updated. This happens only on the ChainStateFlushed callback, or at the end of the initial catchup sync. There are a few reasons. 1) Until the ChainStateFlushed callback is received, we cannot guarantee that the new block entries added to the index will have corresponding block index entries that have been flushed to disk. This is in part why we store a block locator instead of a single block index (in case the index happens to get ahead of the block index), but it would be best to avoid writing ahead of the block index regardless. 2) BlockConnected can be implemented without taking cs_main, but if it had to generate a block locator to write to DB_BEST_BLOCK, then it would need to take cs_main. So the idea is that BlockConnected advances the in-memory state of the index for sure and writes some data to the database and to disk, but these database/disk writes will not be seen until the new block locator is written. This is why m_best_block is updated separately from DB_BEST_BLOCK. In the event of an unclean crash, we are not guaranteed that the index will resume from the state it was last in – it may have to catch up from further behind, but we do ensure that it is not corrupted.

    Now, with the block filter index, two things change. First, since it has an index of things by height, we need to ensure tip of the index is overwritten to the fork point (and committed to disk) before overwriting any height index entries, hence the Rewind method. Secondly, if we commit the new best block in the index, we also need to ensure the flat files have been flushed to disk, which is why CommitInternal is overridden in the subclass.

    So what happens if the call to Commit the latest flat files and DB_BEST_BLOCK fails? It’s not good (and it logs an error), but the in-memory and database state of the index are valid even if the database state is lagging behind. Which is why I think the best way to recover is to continue until the next Commit. If the index ever reaches some point where a database or disk write fails during BlockConnected, then a fatal error is raised and bitcoind shuts down.

    If you have a way of simplifying the logic so that 1) m_best_block_index is separate (and generally further ahead) than DB_BEST_BLOCK and 2) index state is guaranteed to be committed to the fork point of a reorg before connecting new blocks, then I’m open to suggestions. Thoughts?

  146. zquestz commented at 8:16 pm on April 10, 2019: none
    @jimpo do you have a list of the remaining tasks for full Neutrino support (BIP 157 and 158) that need to be done once this PR lands? Would love to get an idea of the remaining scope of work. =)
  147. in src/index/blockfilterindex.cpp:55 in 75a76e3619 outdated
    50+
    51+struct DBHeightKey {
    52+    int height;
    53+
    54+    DBHeightKey() : height(0) {}
    55+    DBHeightKey(int height_in) : height(height_in) {}
    


    MarcoFalke commented at 2:38 pm on April 12, 2019:

    in commit 75a76e36199eb

    Should this be explicit?


    jimpo commented at 11:39 pm on April 19, 2019:
    Yeah, probably a good idea.
  148. in src/test/blockfilter_index_tests.cpp:172 in 2bc90e4e7b outdated
    172+        tip = chainActive.Tip();
    173+    }
    174+    CScript coinbase_script_pub_key = GetScriptForDestination(coinbaseKey.GetPubKey().GetID());
    175+    std::vector<std::shared_ptr<CBlock>> chainA, chainB;
    176+    BOOST_REQUIRE(BuildChain(tip, coinbase_script_pub_key, 10, chainA));
    177+    BOOST_REQUIRE(BuildChain(tip, coinbase_script_pub_key, 10, chainB));
    


    MarcoFalke commented at 3:30 pm on April 12, 2019:

    in commit 2bc90e4e7bf7f:

    Should asset that the chains are different in this test to not accidentally reorg to the same chain?


    jimpo commented at 11:39 pm on April 19, 2019:
    I don’t understand.

    MarcoFalke commented at 3:29 pm on October 13, 2019:

    @jimpo I am adding this assert, and the tests still pass. What is the point of chainA and chainB, when they are identical?

     0diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp
     1index cf87aa9303..a7eb057b05 100644
     2--- a/src/test/blockfilter_index_tests.cpp
     3+++ b/src/test/blockfilter_index_tests.cpp
     4@@ -171,6 +171,7 @@ BOOST_FIXTURE_TEST_CASE(blockfilter_index_initial_sync, TestChain100Setup)
     5     std::vector<std::shared_ptr<CBlock>> chainA, chainB;
     6     BOOST_REQUIRE(BuildChain(tip, coinbase_script_pub_key, 10, chainA));
     7     BOOST_REQUIRE(BuildChain(tip, coinbase_script_pub_key, 10, chainB));
     8+    BOOST_CHECK_EQUAL(chainA.back()->GetHash(), chainB.back()->GetHash());
     9 
    10     // Check that new blocks on chain A get indexed.
    11     uint256 chainA_last_header = last_header;
    

    jimpo commented at 8:08 pm on October 14, 2019:
    You’re totally right, good catch! Opened #17140
  149. in src/init.cpp:989 in ff35105096 outdated
    984     if (gArgs.GetArg("-prune", 0)) {
    985         if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX))
    986             return InitError(_("Prune mode is incompatible with -txindex."));
    987+        if (!g_enabled_filter_types.empty()) {
    988+            return InitError(_("Prune mode is incompatible with -blockfilterindex."));
    989+        }
    


    MarcoFalke commented at 3:41 pm on April 12, 2019:

    in commit ff351050968f290:

    Why is it incompatible? Maybe should add a cpp comment to explain.

    See also https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki#node-operation

    “Nodes MAY prune block data after generating and storing all filters for a block.”


    jimpo commented at 11:38 pm on April 19, 2019:

    You can turn on the filter index and it will build in the background using the blocks on disk. This wouldn’t work in pruned mode. It might be safe though to allow pruning if the block filter index tip is already within chain tip height - MIN_BLOCKS_TO_KEEP. But then we couldn’t prune until the block filter index is in sync (it has its own sort of initial sync logic).

    Not prohibitively difficult though, just requires a bit more thought.

  150. MarcoFalke commented at 3:56 pm on April 12, 2019: member
    utACK c7efb652f3543b001b4dd22186a354605b14f47e
  151. ryanofsky commented at 9:40 pm on April 12, 2019: member

    re: #14121#pullrequestreview-223888002 from me

    I think this change has had almost enough review to be merged.

    There was a new ACK from Marco since I wrote this, so maybe this is about ready?

    re: #14121 (comment) from jimpo

    If you have a way of simplifying the logic so that 1) m_best_block_index is separate (and generally further ahead) than DB_BEST_BLOCK and 2) index state is guaranteed to be committed to the fork point of a reorg before connecting new blocks, then I’m open to suggestions. Thoughts?

    We’re just talking past each other. I already made the simplification in #14121 (review). Here is a rebased version: 00c3a91ae23b9f6951c51ee4060d1d20a852e721 (pr/blockfilt) with some extra renames, since I think neither of us liked the previous names. Here what I see as advantages of this change:

    1. Simpler control flow, no more rewind function calling other rewind function calling commit calling other commit.
    2. No more call super footgun and anti-pattern
    3. No misleading “commit” language and claims about atomicity
    4. Clearly distinct BaseIndex internal methods and hook methods implemented by subclasses.
    5. Slightly less code overall

    Unless there’s a bug or actual drawback to using this change, I hope you’ll consider it. But either way the PR looks good to me. I tested and ACKed it already and will keep reviewing if there are more updates.

  152. MarcoFalke merged this on Apr 18, 2019
  153. MarcoFalke closed this on Apr 18, 2019

  154. MarcoFalke referenced this in commit e4beef611a on Apr 18, 2019
  155. in src/validation.h:121 in c7efb652f3
    117@@ -118,6 +118,7 @@ static const int64_t MAX_FEE_ESTIMATION_TIP_AGE = 3 * 60 * 60;
    118 static const bool DEFAULT_PERMIT_BAREMULTISIG = true;
    119 static const bool DEFAULT_CHECKPOINTS_ENABLED = true;
    120 static const bool DEFAULT_TXINDEX = false;
    121+static const char* const DEFAULT_BLOCKFILTERINDEX = "0";
    


    MarcoFalke commented at 1:55 pm on April 18, 2019:
    I believe we make this extern in the header, so that it is not added to each obj file?

    jimpo commented at 11:40 pm on April 19, 2019:
    Yeah. This applies to all the defaults, right?

    MarcoFalke commented at 2:15 pm on April 22, 2019:
    I think only strings and cstrings and not POD types
  156. MarcoFalke commented at 1:59 pm on April 18, 2019: member
    Just some documentation feedback that I withheld earlier to avoid stalling this pull.
  157. MarcoFalke commented at 2:02 pm on April 18, 2019: member
    @ryanofsky Your proposed changes should probably go in a follow up pull request, as they also refactor txindex, which should be untouched by this pr
  158. MarcoFalke referenced this in commit 693c743a32 on Apr 18, 2019
  159. MarcoFalke removed this from the "Blockers" column in a project

  160. jimpo deleted the branch on Apr 18, 2019
  161. MarcoFalke referenced this in commit 4f42284fc0 on Oct 17, 2019
  162. sidhujag referenced this in commit 3def055b00 on Oct 18, 2019
  163. deadalnix referenced this in commit db7603658a on Jun 3, 2020
  164. deadalnix referenced this in commit d3684631bc on Jun 3, 2020
  165. deadalnix referenced this in commit 608bb6c7ca on Jun 3, 2020
  166. deadalnix referenced this in commit 12fb824820 on Jun 3, 2020
  167. deadalnix referenced this in commit ec4a36d489 on Jun 3, 2020
  168. deadalnix referenced this in commit 814db2b67c on Jun 3, 2020
  169. deadalnix referenced this in commit 5e0bf3785a on Jun 3, 2020
  170. deadalnix referenced this in commit bd0c10d418 on Jun 3, 2020
  171. deadalnix referenced this in commit d5f585b407 on Jun 4, 2020
  172. deadalnix referenced this in commit f79742f8f9 on Jun 4, 2020
  173. deadalnix referenced this in commit 85617361eb on Jun 4, 2020
  174. ftrader referenced this in commit d06d0bea75 on Aug 17, 2020
  175. ftrader referenced this in commit 04c3baeb04 on Aug 17, 2020
  176. kittywhiskers referenced this in commit 9dbd21b548 on Aug 2, 2021
  177. kittywhiskers referenced this in commit b5d7fc6755 on Aug 2, 2021
  178. kittywhiskers referenced this in commit 1111a7f340 on Aug 3, 2021
  179. kittywhiskers referenced this in commit bdd2c2bc39 on Aug 8, 2021
  180. UdjinM6 referenced this in commit 3b97c0558e on Aug 11, 2021
  181. kittywhiskers referenced this in commit 21c7e57493 on Aug 12, 2021
  182. UdjinM6 referenced this in commit 607bd4b6b5 on Aug 13, 2021
  183. Munkybooty referenced this in commit fe4f80a6ee on Sep 30, 2021
  184. Munkybooty referenced this in commit e46fef0205 on Oct 7, 2021
  185. Munkybooty referenced this in commit 1b738e74f1 on Oct 7, 2021
  186. Munkybooty referenced this in commit 62aac44c4f on Oct 12, 2021
  187. PastaPastaPasta referenced this in commit 3f32c9f056 on Nov 1, 2021
  188. Munkybooty referenced this in commit 01b4fe84f5 on Dec 21, 2021
  189. Munkybooty referenced this in commit 74c65712c9 on Dec 21, 2021
  190. DrahtBot locked this on Feb 15, 2022

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-07-01 10:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me