|
55 | 55 | #include <stdint.h> |
56 | 56 |
|
57 | 57 | #include <condition_variable> |
| 58 | +#include <fstream> |
58 | 59 | #include <iterator> |
59 | 60 | #include <memory> |
60 | 61 | #include <mutex> |
@@ -2945,6 +2946,288 @@ class TemporaryRollback |
2945 | 2946 | }; |
2946 | 2947 | }; |
2947 | 2948 |
|
| 2949 | +class HintsWriter { |
| 2950 | + std::ostream& file; |
| 2951 | +public: |
| 2952 | + explicit HintsWriter(std::ostream& f) : file(f) {} |
| 2953 | + |
| 2954 | + // Writes a block's bits: writes 2 little-endian bytes for the size, |
| 2955 | + // then packs the bits 8 per byte. |
| 2956 | + void writeBlockBits(const std::vector<bool>& bitmap) { |
| 2957 | + std::string writebuf; |
| 2958 | + // Write the bitmap size as a 16-bit little-endian value. |
| 2959 | + uint16_t size = static_cast<uint16_t>(bitmap.size()); |
| 2960 | + writebuf.push_back(static_cast<char>(size & 0xFF)); // lower byte |
| 2961 | + writebuf.push_back(static_cast<char>((size >> 8) & 0xFF)); // higher byte |
| 2962 | + |
| 2963 | + uint8_t value = 0; |
| 2964 | + int bitpos = 0; |
| 2965 | + // Process each bit in the bitmap. |
| 2966 | + for (bool bit : bitmap) { |
| 2967 | + // Set the corresponding bit in 'value' if 'bit' is true. |
| 2968 | + if (bit) { |
| 2969 | + value |= (1 << bitpos); |
| 2970 | + } |
| 2971 | + ++bitpos; |
| 2972 | + // Once we've accumulated 8 bits, append them to writebuf. |
| 2973 | + if (bitpos == 8) { |
| 2974 | + writebuf.push_back(static_cast<char>(value)); |
| 2975 | + bitpos = 0; |
| 2976 | + value = 0; |
| 2977 | + } |
| 2978 | + } |
| 2979 | + // If there are remaining bits (less than 8), write them too. |
| 2980 | + if (bitpos != 0) { |
| 2981 | + writebuf.push_back(static_cast<char>(value)); |
| 2982 | + } |
| 2983 | + |
| 2984 | + // Write the entire buffer to the file. |
| 2985 | + file.write(writebuf.c_str(), writebuf.size()); |
| 2986 | + } |
| 2987 | + |
| 2988 | + // Writes a two-byte end marker (two 0 bytes). |
| 2989 | + void writeEndMarker() { |
| 2990 | + char marker[2] = {0, 0}; |
| 2991 | + file.write(marker, 2); |
| 2992 | + } |
| 2993 | +}; |
| 2994 | + |
| 2995 | + |
| 2996 | +static bool isOutputInUTXOSet(/*const CCoinsViewCache& coins, */NodeContext& node, const COutPoint& outpoint) |
| 2997 | +{ |
| 2998 | + LOCK(::cs_main); |
| 2999 | + CCoinsViewCache& chain_view = node.chainman->ActiveChainstate().CoinsTip(); |
| 3000 | + // std::optional<Coin> coin = coins.GetCoin(outpoint); |
| 3001 | + // auto coin{chain_view.GetCoin(outpoint)}; |
| 3002 | + // if (!coin) { |
| 3003 | + // return false; |
| 3004 | + // } // check if the coin exists |
| 3005 | + // return !coin->IsSpent(); |
| 3006 | + return chain_view.HaveCoin(outpoint); |
| 3007 | +} |
| 3008 | + |
| 3009 | +/** |
| 3010 | + * Serialize the UTXO set to a file for loading elsewhere. |
| 3011 | + * |
| 3012 | + * @see SnapshotMetadata |
| 3013 | + */ |
| 3014 | +static RPCHelpMan createhintfile() |
| 3015 | +{ |
| 3016 | + return RPCHelpMan{ |
| 3017 | + "createhintfile", |
| 3018 | + "Write the serialized UTXO set to a file. This can be used in loadtxoutset afterwards if this snapshot height is supported in the chainparams as well.\n\n" |
| 3019 | + "Unless the \"latest\" type is requested, the node will roll back to the requested height and network activity will be suspended during this process. " |
| 3020 | + "Because of this it is discouraged to interact with the node in any other way during the execution of this call to avoid inconsistent results and race conditions, particularly RPCs that interact with blockstorage.\n\n" |
| 3021 | + "This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)", |
| 3022 | + { |
| 3023 | + {"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."}, |
| 3024 | + {"type", RPCArg::Type::STR, RPCArg::Default(""), "The type of snapshot to create. Can be \"latest\" to create a snapshot of the current UTXO set or \"rollback\" to temporarily roll back the state of the node to a historical block before creating the snapshot of a historical UTXO set. This parameter can be omitted if a separate \"rollback\" named parameter is specified indicating the height or hash of a specific historical block. If \"rollback\" is specified and separate \"rollback\" named parameter is not specified, this will roll back to the latest valid snapshot block that can currently be loaded with loadtxoutset."}, |
| 3025 | + {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", |
| 3026 | + { |
| 3027 | + {"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, |
| 3028 | + "Height or hash of the block to roll back to before creating the snapshot. Note: The further this number is from the tip, the longer this process will take. Consider setting a higher -rpcclienttimeout value in this case.", |
| 3029 | + RPCArgOptions{.skip_type_check = true, .type_str = {"", "string or numeric"}}}, |
| 3030 | + }, |
| 3031 | + }, |
| 3032 | + }, |
| 3033 | + RPCResult{ |
| 3034 | + RPCResult::Type::OBJ, "", "", |
| 3035 | + { |
| 3036 | + // {RPCResult::Type::NUM, "coins_written", "the number of coins written in the snapshot"}, |
| 3037 | + // {RPCResult::Type::STR_HEX, "base_hash", "the hash of the base of the snapshot"}, |
| 3038 | + // {RPCResult::Type::NUM, "base_height", "the height of the base of the snapshot"}, |
| 3039 | + {RPCResult::Type::STR, "path", "the absolute path that the snapshot was written to"}, |
| 3040 | + // {RPCResult::Type::STR_HEX, "txoutset_hash", "the hash of the UTXO set contents"}, |
| 3041 | + // {RPCResult::Type::NUM, "nchaintx", "the number of transactions in the chain up to and including the base block"}, |
| 3042 | + } |
| 3043 | + }, |
| 3044 | + RPCExamples{ |
| 3045 | + HelpExampleCli("-rpcclienttimeout=0 createhintfile", "utxo.dat latest") + |
| 3046 | + HelpExampleCli("-rpcclienttimeout=0 createhintfile", "utxo.dat rollback") + |
| 3047 | + HelpExampleCli("-rpcclienttimeout=0 -named createhintfile", R"(utxo.dat rollback=853456)") |
| 3048 | + }, |
| 3049 | + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue |
| 3050 | +{ |
| 3051 | + NodeContext& node = EnsureAnyNodeContext(request.context); |
| 3052 | + const CBlockIndex* tip{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())}; |
| 3053 | + const CBlockIndex* target_index{nullptr}; |
| 3054 | + const std::string snapshot_type{self.Arg<std::string>("type")}; |
| 3055 | + const UniValue options{request.params[2].isNull() ? UniValue::VOBJ : request.params[2]}; |
| 3056 | + if (options.exists("rollback")) { |
| 3057 | + if (!snapshot_type.empty() && snapshot_type != "rollback") { |
| 3058 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified with rollback option", snapshot_type)); |
| 3059 | + } |
| 3060 | + target_index = ParseHashOrHeight(options["rollback"], *node.chainman); |
| 3061 | + } else if (snapshot_type == "rollback") { |
| 3062 | + auto snapshot_heights = node.chainman->GetParams().GetAvailableSnapshotHeights(); |
| 3063 | + CHECK_NONFATAL(snapshot_heights.size() > 0); |
| 3064 | + auto max_height = std::max_element(snapshot_heights.begin(), snapshot_heights.end()); |
| 3065 | + target_index = ParseHashOrHeight(*max_height, *node.chainman); |
| 3066 | + } else if (snapshot_type == "latest") { |
| 3067 | + target_index = tip; |
| 3068 | + } else { |
| 3069 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified. Please specify \"rollback\" or \"latest\"", snapshot_type)); |
| 3070 | + } |
| 3071 | + |
| 3072 | + const ArgsManager& args{EnsureAnyArgsman(request.context)}; |
| 3073 | + const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str())); |
| 3074 | + // Write to a temporary path and then move into `path` on completion |
| 3075 | + // to avoid confusion due to an interruption. |
| 3076 | + const fs::path temppath = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str() + ".incomplete")); |
| 3077 | + |
| 3078 | + if (fs::exists(path)) { |
| 3079 | + throw JSONRPCError( |
| 3080 | + RPC_INVALID_PARAMETER, |
| 3081 | + path.utf8string() + " already exists. If you are sure this is what you want, " |
| 3082 | + "move it out of the way first"); |
| 3083 | + } |
| 3084 | + |
| 3085 | + FILE* file{fsbridge::fopen(temppath, "wb")}; |
| 3086 | + AutoFile afile{file}; |
| 3087 | + if (afile.IsNull()) { |
| 3088 | + throw JSONRPCError( |
| 3089 | + RPC_INVALID_PARAMETER, |
| 3090 | + "Couldn't open file " + temppath.utf8string() + " for writing."); |
| 3091 | + } |
| 3092 | + |
| 3093 | + CConnman& connman = EnsureConnman(node); |
| 3094 | + const CBlockIndex* invalidate_index{nullptr}; |
| 3095 | + std::optional<NetworkDisable> disable_network; |
| 3096 | + std::optional<TemporaryRollback> temporary_rollback; |
| 3097 | + |
| 3098 | + // If the user wants to dump the txoutset of the current tip, we don't have |
| 3099 | + // to roll back at all |
| 3100 | + if (target_index != tip) { |
| 3101 | + // If the node is running in pruned mode we ensure all necessary block |
| 3102 | + // data is available before starting to roll back. |
| 3103 | + if (node.chainman->m_blockman.IsPruneMode()) { |
| 3104 | + LOCK(node.chainman->GetMutex()); |
| 3105 | + const CBlockIndex* current_tip{node.chainman->ActiveChain().Tip()}; |
| 3106 | + const CBlockIndex* first_block{node.chainman->m_blockman.GetFirstBlock(*current_tip, /*status_mask=*/BLOCK_HAVE_MASK)}; |
| 3107 | + if (first_block->nHeight > target_index->nHeight) { |
| 3108 | + throw JSONRPCError(RPC_MISC_ERROR, "Hint files cannot be generated in prune mode."); |
| 3109 | + } |
| 3110 | + } |
| 3111 | + |
| 3112 | + // Suspend network activity for the duration of the process when we are |
| 3113 | + // rolling back the chain to get a utxo set from a past height. We do |
| 3114 | + // this so we don't punish peers that send us that send us data that |
| 3115 | + // seems wrong in this temporary state. For example a normal new block |
| 3116 | + // would be classified as a block connecting an invalid block. |
| 3117 | + // Skip if the network is already disabled because this |
| 3118 | + // automatically re-enables the network activity at the end of the |
| 3119 | + // process which may not be what the user wants. |
| 3120 | + if (connman.GetNetworkActive()) { |
| 3121 | + disable_network.emplace(connman); |
| 3122 | + } |
| 3123 | + |
| 3124 | + invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index)); |
| 3125 | + temporary_rollback.emplace(*node.chainman, *invalidate_index); |
| 3126 | + } |
| 3127 | + |
| 3128 | + Chainstate* chainstate; |
| 3129 | + std::unique_ptr<CCoinsViewCursor> cursor; |
| 3130 | + |
| 3131 | + int block_height = 0; |
| 3132 | + |
| 3133 | + int startTime = 0; |
| 3134 | + int start_height = 0; |
| 3135 | + uint256 start_block; |
| 3136 | + bool start = node.chain->findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, interfaces::FoundBlock().hash(start_block).height(start_height)); |
| 3137 | + |
| 3138 | + int iteration = 0; |
| 3139 | + |
| 3140 | + uint256 block_hash = start_block; |
| 3141 | + |
| 3142 | + bool fetch_block{true}; |
| 3143 | + bool block_still_active = false; |
| 3144 | + bool next_block = false; |
| 3145 | + uint256 next_block_hash; |
| 3146 | + |
| 3147 | + // FILE* file{fsbridge::fopen(temppath, "wb")}; |
| 3148 | + std::ofstream outfile(temppath, std::ios::binary); |
| 3149 | + HintsWriter writer(outfile); |
| 3150 | + |
| 3151 | + while (!node.chain->shutdownRequested()) { |
| 3152 | + |
| 3153 | + node.chain->findBlock(block_hash, interfaces::FoundBlock().inActiveChain(block_still_active).nextBlock(interfaces::FoundBlock().inActiveChain(next_block).hash(next_block_hash))); |
| 3154 | + |
| 3155 | + if (fetch_block) { |
| 3156 | + // Read block data |
| 3157 | + CBlock block; |
| 3158 | + node.chain->findBlock(block_hash, interfaces::FoundBlock().data(block)); |
| 3159 | + |
| 3160 | + if (!block.IsNull()) { |
| 3161 | + if (!block_still_active) { |
| 3162 | + throw JSONRPCError(RPC_MISC_ERROR, strprintf("Hint files cannot be generated because the block_hash %s is inactive", block_hash.GetHex())); |
| 3163 | + } |
| 3164 | + } |
| 3165 | + |
| 3166 | + std::vector<bool> block_outputs_bitmap(block.vtx.size(), false); |
| 3167 | + |
| 3168 | + // std::cout << "Processing block " << block_hash.GetHex() << " at height " << block_height << std::endl; |
| 3169 | + |
| 3170 | + for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { |
| 3171 | + // auto vtx = block.vtx[posInBlock]; |
| 3172 | + // vtx->vout |
| 3173 | + // SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast<int>(posInBlock)}, fUpdate, /*rescanning_old_block=*/true); |
| 3174 | + |
| 3175 | + |
| 3176 | + auto vtx = block.vtx[posInBlock]; |
| 3177 | + // for (const CTxOut& txout : vtx->vout) { |
| 3178 | + // COutPoint(txid, i) |
| 3179 | + // // isOutputInUTXOSet(txout. |
| 3180 | + // } |
| 3181 | + std::vector<bool> tx_outputs_bitmap(vtx->vout.size(), false); |
| 3182 | + |
| 3183 | + const Txid& txid = vtx->GetHash(); |
| 3184 | + for (size_t voutIndex = 0; voutIndex < vtx->vout.size(); ++voutIndex) { |
| 3185 | + auto availabeCoin = isOutputInUTXOSet(node, COutPoint(txid, voutIndex)); |
| 3186 | + |
| 3187 | + // auto xx = strprintf("txid %s voutIndex %d is available: %s", txid.GetHex(), voutIndex, availabeCoin ? "available" : "spent"); |
| 3188 | + // std::cout << xx << std::endl; |
| 3189 | + tx_outputs_bitmap[voutIndex] = availabeCoin; |
| 3190 | + } |
| 3191 | + |
| 3192 | + block_outputs_bitmap.insert(block_outputs_bitmap.end(), tx_outputs_bitmap.begin(), tx_outputs_bitmap.end()); |
| 3193 | + } |
| 3194 | + |
| 3195 | + writer.writeBlockBits(block_outputs_bitmap); |
| 3196 | + } |
| 3197 | + |
| 3198 | + |
| 3199 | + // std::cout << "Processing block " << block_hash.GetHex() << " at height " << block_height << std::endl; |
| 3200 | + // std::cout << "next_block_hash " << next_block_hash.GetHex() << std::endl; |
| 3201 | + |
| 3202 | + |
| 3203 | + if (block_height >= target_index->nHeight) { |
| 3204 | + break; |
| 3205 | + } |
| 3206 | + |
| 3207 | + if (!next_block) { |
| 3208 | + // break successfully when rescan has reached the tip, or |
| 3209 | + // previous block is no longer on the chain due to a reorg |
| 3210 | + break; |
| 3211 | + } |
| 3212 | + |
| 3213 | + // increment block and verification progress |
| 3214 | + block_hash = next_block_hash; |
| 3215 | + ++block_height; |
| 3216 | + |
| 3217 | + } |
| 3218 | + |
| 3219 | + writer.writeEndMarker(); |
| 3220 | + outfile.close(); |
| 3221 | + |
| 3222 | + fs::rename(temppath, path); |
| 3223 | + |
| 3224 | + UniValue result(UniValue::VOBJ); |
| 3225 | + result.pushKV("path", path.utf8string()); |
| 3226 | + return result; |
| 3227 | +}, |
| 3228 | + }; |
| 3229 | +} |
| 3230 | + |
2948 | 3231 | /** |
2949 | 3232 | * Serialize the UTXO set to a file for loading elsewhere. |
2950 | 3233 | * |
@@ -3403,6 +3686,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) |
3403 | 3686 | {"blockchain", &scanblocks}, |
3404 | 3687 | {"blockchain", &getdescriptoractivity}, |
3405 | 3688 | {"blockchain", &getblockfilter}, |
| 3689 | + {"blockchain", &createhintfile}, |
3406 | 3690 | {"blockchain", &dumptxoutset}, |
3407 | 3691 | {"blockchain", &loadtxoutset}, |
3408 | 3692 | {"blockchain", &getchainstates}, |
|
0 commit comments