// Copyright (c) 2012-2013 The PPCoin developers
// Copyright (c) 2014 The BlackCoin developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <boost/assign/list_of.hpp>

#include <pos.h>
#include <txdb.h>
#include <validation.h>
#include <arith_uint256.h>
#include <hash.h>
#include <timedata.h>
#include <chainparams.h>
#include <script/sign.h>
#include <consensus/consensus.h>
#include <util/signstr.h>
#include <qtum/qtumdelegation.h>

using namespace std;

// Delegation contract function
QtumDelegation& GetQtumDelegation()
{
    static QtumDelegation qtumDelegation;
    return qtumDelegation;
}

// Stake Modifier (hash modifier of proof-of-stake):
// The purpose of stake modifier is to prevent a txout (coin) owner from
// computing future proof-of-stake generated by this txout at the time
// of transaction confirmation. To meet kernel protocol, the txout
// must hash with a future stake modifier to generate the proof.
uint256 ComputeStakeModifier(const CBlockIndex* pindexPrev, const uint256& kernel)
{
    if (!pindexPrev)
        return uint256();  // genesis block's modifier is 0

    CDataStream ss(SER_GETHASH, 0);
    ss << kernel << pindexPrev->nStakeModifier;
    return Hash(ss.begin(), ss.end());
}

// BlackCoin kernel protocol
// coinstake must meet hash target according to the protocol:
// kernel (input 0) must meet the formula
//     hash(nStakeModifier + blockFrom.nTime + txPrev.vout.hash + txPrev.vout.n + nTime) < bnTarget * nWeight
// this ensures that the chance of getting a coinstake is proportional to the
// amount of coins one owns.
// The reason this hash is chosen is the following:
//   nStakeModifier: scrambles computation to make it very difficult to precompute
//                   future proof-of-stake
//   blockFrom.nTime: slightly scrambles computation
//   txPrev.vout.hash: hash of txPrev, to reduce the chance of nodes
//                     generating coinstake at the same time
//   txPrev.vout.n: output number of txPrev, to reduce the chance of nodes
//                  generating coinstake at the same time
//   nTime: current timestamp
//   block/tx hash should not be used here as they can be generated in vast
//   quantities so as to generate blocks faster, degrading the system back into
//   a proof-of-work situation.
//
bool CheckStakeKernelHash(CBlockIndex* pindexPrev, unsigned int nBits, uint32_t blockFromTime, CAmount prevoutValue, const COutPoint& prevout, unsigned int nTimeBlock, uint256& hashProofOfStake, uint256& targetProofOfStake, bool fPrintProofOfStake)
{
    if (nTimeBlock < blockFromTime)  // Transaction timestamp violation
        return error("CheckStakeKernelHash() : nTime violation");

    // Base target
    arith_uint256 bnTarget;
    bnTarget.SetCompact(nBits);

    // Weighted target
    int64_t nValueIn = prevoutValue;
    arith_uint256 bnWeight = arith_uint256(nValueIn);
    bnTarget *= bnWeight;

    targetProofOfStake = ArithToUint256(bnTarget);

    uint256 nStakeModifier = pindexPrev->nStakeModifier;

    // Calculate hash
    CDataStream ss(SER_GETHASH, 0);
    ss << nStakeModifier;
    ss << blockFromTime << prevout.hash << prevout.n << nTimeBlock;
    hashProofOfStake = Hash(ss.begin(), ss.end());

    if (fPrintProofOfStake)
    {
        LogPrintf("CheckStakeKernelHash() : check modifier=%s nTimeBlockFrom=%u nPrevout=%u nTimeBlock=%u hashProof=%s\n",
            nStakeModifier.GetHex().c_str(),
            blockFromTime, prevout.n, nTimeBlock,
            hashProofOfStake.ToString());
    }

    // Now check if proof-of-stake hash meets target protocol
    if (UintToArith256(hashProofOfStake) > bnTarget)
        return false;

    if (LogInstance().WillLogCategory(BCLog::COINSTAKE) && !fPrintProofOfStake)
    {
        LogPrintf("CheckStakeKernelHash() : check modifier=%s nTimeBlockFrom=%u nPrevout=%u nTimeBlock=%u hashProof=%s\n",
            nStakeModifier.GetHex().c_str(),
            blockFromTime, prevout.n, nTimeBlock,
            hashProofOfStake.ToString());
    }

    return true;
}

bool GetStakeCoin(const COutPoint& prevout, Coin& coinPrev, CBlockIndex*& blockFrom, CBlockIndex* pindexPrev, BlockValidationState& state, CCoinsViewCache& view)
{
    // Get the coin
    if(!view.GetCoin(prevout, coinPrev)){
        return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-prevout-not-exist", strprintf("CheckProofOfStake() : Stake prevout does not exist %s", prevout.hash.ToString()));
    }

    // Check that the coin is mature
    int nHeight = pindexPrev->nHeight + 1;
    if(nHeight - coinPrev.nHeight < COINBASE_MATURITY){
        return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-prevout-not-mature", strprintf("CheckProofOfStake() : Stake prevout is not mature, expecting %i and only matured to %i", COINBASE_MATURITY, nHeight - coinPrev.nHeight));
    }

    // Get the block header from the coin
    blockFrom = pindexPrev->GetAncestor(coinPrev.nHeight);
    if(!blockFrom) {
        return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-prevout-not-loaded", strprintf("CheckProofOfStake() : Block at height %i for prevout can not be loaded", coinPrev.nHeight));
    }

    // Check that the coin is not used in the last COINBASE_MATURITY headers
    // Delegated utxo is not spent when block is created using that coin, so additional check to the last headers needed
    int coinHeight = -1;
    CBlockIndex* prev = pindexPrev;
    for(int i = 0; i < COINBASE_MATURITY; i++) {
        if(prev->prevoutStake == prevout) {
            coinHeight = prev->nHeight;
            break;
        }
        prev = prev->pprev;

        if(!prev) break;
    }
    if(coinHeight != -1) {
        if(nHeight - coinHeight < COINBASE_MATURITY){
            return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-prevout-not-mature", strprintf("CheckProofOfStake() : Stake prevout is not mature, expecting %i and only matured to %i", COINBASE_MATURITY, nHeight - coinHeight));
        }
    }

    return true;
}

// Check kernel hash target and coinstake signature
bool CheckProofOfStake(CBlockIndex* pindexPrev, BlockValidationState& state, const CTransaction& tx, unsigned int nBits, uint32_t nTimeBlock, const std::vector<unsigned char>& vchPoD,  const COutPoint& headerPrevout, uint256& hashProofOfStake, uint256& targetProofOfStake, CCoinsViewCache& view)
{
    if (!tx.IsCoinStake())
        return error("CheckProofOfStake() : called on non-coinstake %s", tx.GetHash().ToString());

    // Kernel (input 0) must match the stake hash target (nBits)
    const CTxIn& txin = tx.vin[0];

    // Get the PoS transaction coin from the first input
    Coin coinTxPrev;
    CBlockIndex* blockTxFrom = 0;
    if(!GetStakeCoin(txin.prevout, coinTxPrev, blockTxFrom, pindexPrev, state, view))
        return error("CheckProofOfStake() : fail to get prevout %s", txin.prevout.hash.ToString());

    // Get the PoS header coin from prevoutStake
    Coin coinHeaderPrev;
    CBlockIndex* blockHeaderFrom = 0;
    if(txin.prevout == headerPrevout)
    {
        coinHeaderPrev = coinTxPrev;
        blockHeaderFrom = blockTxFrom;
    }
    else
    {
        // The PoS transaction and PoS header coins are different when proof of delegation exist
        if(!GetStakeCoin(headerPrevout, coinHeaderPrev, blockHeaderFrom, pindexPrev, state, view))
            return error("CheckProofOfStake() : fail to get prevout %s", headerPrevout.hash.ToString());
    }

    int nHeight = pindexPrev->nHeight + 1;
    bool checkDelegation = false;
    int nOfflineStakeHeight = Params().GetConsensus().nOfflineStakeHeight;
    if (nHeight >= nOfflineStakeHeight && !Params().GetConsensus().delegationsAddress.IsNull())
    {
        ////////////////////////////////////////////////// deploy offline staking contract
        if(nHeight == nOfflineStakeHeight){
            globalState->deployDelegationsContract();
        }
        /////////////////////////////////////////////////

        // Check if the delegation contract exist
        QtumDelegation& qtumDelegation = GetQtumDelegation();
        if(!qtumDelegation.ExistDelegationContract())
            return state.Invalid(BlockValidationResult::BLOCK_HEADER_REJECT, "stake-delegation-contract-not-exist", strprintf("CheckProofOfStake() : The delegation contract doesn't exist, block height %i", nOfflineStakeHeight)); // Internal error, delegation contract not exist

        // Get the delegation from the contract
        uint160 address = uint160(ExtractPublicKeyHash(coinHeaderPrev.out.scriptPubKey));
        Delegation delegation;
        if(!qtumDelegation.GetDelegation(address, delegation)) {
            return state.Invalid(BlockValidationResult::BLOCK_HEADER_REJECT, "stake-get-delegation-failed", strprintf("CheckProofOfStake() : Failed to get delegation from the delegation contract")); // Internal error, get delegation from the delegation contract
        }

        // Verify delegation received from the contract
        bool verifiedDelegation = qtumDelegation.VerifyDelegation(address, delegation);
        bool hasDelegationProof = vchPoD.size() > 0;

        // Check that if PoD is present then the delegation received from the contract can be verified
        if(hasDelegationProof && hasDelegationProof != verifiedDelegation) {
            return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-delegation-not-verified", strprintf("CheckProofOfStake() : Delegation for block at height %i cannot be verified", nHeight));
        }

        // Check that if PoD is not present then the delegation received from the contract is null
        if(!hasDelegationProof && !delegation.IsNull()) {
            return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-delegation-not-used", strprintf("CheckProofOfStake() : Delegation for block at height %i is present but not used to create the block", nHeight));
        }

        checkDelegation = hasDelegationProof;
        if(checkDelegation)
        {
            // Check that the staker have the permission to use that coin to create the coinstake transaction
            CScript stakerPubKey = tx.vout[1].scriptPubKey;
            uint160 staker = uint160(ExtractPublicKeyHash(stakerPubKey));
            if(!SignStr::VerifyMessage(CKeyID(address), staker.GetReverseHex(), vchPoD))
                return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-verify-delegation-failed", strprintf("CheckProofOfStake() : VerifyDelegation failed on coinstake %s", tx.GetHash().ToString()));

            // Check the super staker min utxo value
            if(coinTxPrev.out.nValue < DEFAULT_STAKING_MIN_UTXO_VALUE)
                return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-delegation-not-min-utxo", strprintf("CheckProofOfStake() : Stake for block at height %i do not have the minimum amount required for super staker", nHeight));

            // Check that the block delegation data is the same as the data received from the contract, this is to avoid using old/removed delegation
            bool delegateOutputExist = IsDelegateOutputExist(delegation.fee);
            int fee = GetDelegationFeeTx(tx, coinTxPrev, delegateOutputExist);
            if(delegation.staker != staker ||
                    (int)delegation.fee != fee ||
                    (int)delegation.blockHeight > nHeight ||
                    delegation.PoD != vchPoD) {
                return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-delegation-not-match", strprintf("CheckProofOfStake() : Delegation for block at height %i is not the same with the delegation received from the delegation contract", nHeight));
            }
        }
    }

    // Check stake and block prevout
    if(!checkDelegation && txin.prevout != headerPrevout)
        return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-prevout-diff-block-prevout", strprintf("CheckProofOfStake() : Stake prevout %s is different then block prevout %s", txin.prevout.hash.ToString(), headerPrevout.hash.ToString()));

    // Verify signature
    if (!VerifySignature(coinTxPrev, txin.prevout.hash, tx, 0, SCRIPT_VERIFY_NONE))
        return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "stake-verify-signature-failed", strprintf("CheckProofOfStake() : VerifySignature failed on coinstake %s", tx.GetHash().ToString()));

    if (!CheckStakeKernelHash(pindexPrev, nBits, blockHeaderFrom->nTime, coinHeaderPrev.out.nValue, headerPrevout, nTimeBlock, hashProofOfStake, targetProofOfStake, LogInstance().WillLogCategory(BCLog::COINSTAKE)))
        return state.Invalid(BlockValidationResult::BLOCK_HEADER_SYNC, "stake-check-kernel-failed", strprintf("CheckProofOfStake() : INFO: check kernel failed on coinstake %s, hashProof=%s", tx.GetHash().ToString(), hashProofOfStake.ToString())); // may occur during initial download or if behind on block chain sync

    return true;
}

// Check whether the coinstake timestamp meets protocol
bool CheckCoinStakeTimestamp(uint32_t nTimeBlock)
{
    return (nTimeBlock & STAKE_TIMESTAMP_MASK) == 0;
}

bool CheckBlockInputPubKeyMatchesOutputPubKey(const CBlock& block, CCoinsViewCache& view, bool delegateOutputExist) {

    Coin coinIn;
    if(!view.GetCoin(block.prevoutStake, coinIn)) {
        return error("%s: Could not fetch prevoutStake from UTXO set", __func__);
    }

    uint32_t hasDelegation = block.HasProofOfDelegation() ? 1 : 0;
    if(hasDelegation && !delegateOutputExist)
        return true; // Delegate output doesn't exist in case of 100% fee, so the check cannot be performed

    CTransactionRef coinstakeTx = block.vtx[1];
    if(coinstakeTx->vout.size() < 2 + hasDelegation) {
        return error("%s: coinstake transaction does not have the minimum number of outputs", __func__);
    }

    const CTxOut& txout = coinstakeTx->vout[1 + hasDelegation];

    if(coinIn.out.scriptPubKey == txout.scriptPubKey) {
        return true;
    }

    // If the input does not exactly match the output, it MUST be on P2PKH spent and P2PK out.
    CTxDestination inputAddress;
    txnouttype inputTxType=TX_NONSTANDARD;
    if(!ExtractDestination(coinIn.out.scriptPubKey, inputAddress, &inputTxType)) {
        return error("%s: Could not extract address from input", __func__);
    }

    if(inputTxType != TX_PUBKEYHASH || inputAddress.type() != typeid(PKHash)) {
        return error("%s: non-exact match input must be P2PKH", __func__);
    }

    CTxDestination outputAddress;
    txnouttype outputTxType=TX_NONSTANDARD;
    if(!ExtractDestination(txout.scriptPubKey, outputAddress, &outputTxType)) {
        return error("%s: Could not extract address from output", __func__);
    }

    if(outputTxType != TX_PUBKEY || outputAddress.type() != typeid(PKHash)) {
        return error("%s: non-exact match output must be P2PK", __func__);
    }

    if(boost::get<PKHash>(inputAddress) != boost::get<PKHash>(outputAddress)) {
        return error("%s: input P2PKH pubkey does not match output P2PK pubkey", __func__);
    }

    return true;
}

bool CheckRecoveredPubKeyFromBlockSignature(CBlockIndex* pindexPrev, const CBlockHeader& block, CCoinsViewCache& view) {
    Coin coinPrev;
    if(!view.GetCoin(block.prevoutStake, coinPrev)){
        if(!GetSpentCoinFromMainChain(pindexPrev, block.prevoutStake, &coinPrev)) {
            return error("CheckRecoveredPubKeyFromBlockSignature(): Could not find %s and it was not at the tip", block.prevoutStake.hash.GetHex());
        }
    }

    uint256 hash = block.GetHashWithoutSign();
    CPubKey pubkey;
    std::vector<unsigned char> vchBlockSig = block.GetBlockSignature();
    std::vector<unsigned char> vchPoD = block.GetProofOfDelegation();
    bool hasDelegation = block.HasProofOfDelegation();

    if(vchBlockSig.empty()) {
        return error("CheckRecoveredPubKeyFromBlockSignature(): Signature is empty\n");
    }

    // Recover the public key
    if (pindexPrev->nHeight + 1 >= Params().GetConsensus().nOfflineStakeHeight)
    {
        // Recover the public key from compact signature
        if(hasDelegation)
        {
            // Has delegation
            CTxDestination address;
            txnouttype txType=TX_NONSTANDARD;
            if(pubkey.RecoverCompact(hash, vchBlockSig) &&
                    ExtractDestination(coinPrev.out.scriptPubKey, address, &txType)){
                if ((txType == TX_PUBKEY || txType == TX_PUBKEYHASH) && address.type() == typeid(PKHash)) {
                    if(SignStr::VerifyMessage(CKeyID(boost::get<PKHash>(address)), pubkey.GetID().GetReverseHex(), vchPoD)) {
                        return true;
                    }
                }
            }
        }
        else
        {
            // No delegation
            CTxDestination address;
            txnouttype txType=TX_NONSTANDARD;
            if(pubkey.RecoverCompact(hash, vchBlockSig) &&
                    ExtractDestination(coinPrev.out.scriptPubKey, address, &txType)){
                if ((txType == TX_PUBKEY || txType == TX_PUBKEYHASH) && address.type() == typeid(PKHash)) {
                    if(pubkey.GetID() == boost::get<PKHash>(address)) {
                        return true;
                    }
                }
            }
        }
    }
    else
    {
        // Recover the public key from LowS signature
        for(uint8_t recid = 0; recid <= 3; ++recid) {
            for(uint8_t compressed = 0; compressed < 2; ++compressed) {
                if(!pubkey.RecoverLaxDER(hash, vchBlockSig, recid, compressed)) {
                    continue;
                }

                CTxDestination address;
                txnouttype txType=TX_NONSTANDARD;
                if(ExtractDestination(coinPrev.out.scriptPubKey, address, &txType)){
                    if ((txType == TX_PUBKEY || txType == TX_PUBKEYHASH) && address.type() == typeid(PKHash)) {
                        if(pubkey.GetID() == boost::get<PKHash>(address)) {
                            return true;
                        }
                    }
                }
            }
        }
    }

    return false;
}

bool CheckKernel(CBlockIndex* pindexPrev, unsigned int nBits, uint32_t nTimeBlock, const COutPoint& prevout, CCoinsViewCache& view)
{
    std::map<COutPoint, CStakeCache> tmp;
    return CheckKernel(pindexPrev, nBits, nTimeBlock, prevout, view, tmp);
}

bool CheckKernel(CBlockIndex* pindexPrev, unsigned int nBits, uint32_t nTimeBlock, const COutPoint& prevout, CCoinsViewCache& view, const std::map<COutPoint, CStakeCache>& cache)
{
    uint256 hashProofOfStake, targetProofOfStake;
    auto it=cache.find(prevout);
    if(it == cache.end()) {
        //not found in cache (shouldn't happen during staking, only during verification which does not use cache)
        Coin coinPrev;
        if(!view.GetCoin(prevout, coinPrev)){
            if(!GetSpentCoinFromMainChain(pindexPrev, prevout, &coinPrev)) {
                return error("CheckKernel(): Could not find coin and it was not at the tip");
            }
        }

        if(pindexPrev->nHeight + 1 - coinPrev.nHeight < COINBASE_MATURITY){
            return error("CheckKernel(): Coin not matured");
        }
        CBlockIndex* blockFrom = pindexPrev->GetAncestor(coinPrev.nHeight);
        if(!blockFrom) {
            return error("CheckKernel(): Could not find block");
        }
        if(coinPrev.IsSpent()){
            return error("CheckKernel(): Coin is spent");
        }

        return CheckStakeKernelHash(pindexPrev, nBits, blockFrom->nTime, coinPrev.out.nValue, prevout,
                                    nTimeBlock, hashProofOfStake, targetProofOfStake);
    }else{
        //found in cache
        const CStakeCache& stake = it->second;
        if(CheckStakeKernelHash(pindexPrev, nBits, stake.blockFromTime, stake.amount, prevout,
                                    nTimeBlock, hashProofOfStake, targetProofOfStake)){
            //Cache could potentially cause false positive stakes in the event of deep reorgs, so check without cache also
            return CheckKernel(pindexPrev, nBits, nTimeBlock, prevout, view);
        }
    }
    return false;
}

void CacheKernel(std::map<COutPoint, CStakeCache>& cache, const COutPoint& prevout, CBlockIndex* pindexPrev, CCoinsViewCache& view){
    if(cache.find(prevout) != cache.end()){
        //already in cache
        return;
    }

    Coin coinPrev;
    if(!view.GetCoin(prevout, coinPrev)){
        return;
    }

    if(pindexPrev->nHeight + 1 - coinPrev.nHeight < COINBASE_MATURITY){
        return;
    }
    CBlockIndex* blockFrom = pindexPrev->GetAncestor(coinPrev.nHeight);
    if(!blockFrom) {
        return;
    }

    CStakeCache c(blockFrom->nTime, coinPrev.out.nValue);
    cache.insert({prevout, c});
}

/**
 * Proof-of-stake functions needed in the wallet but wallet independent
 */
struct BlockScript{
    CScript stakerScript;
    CScript delegateScript;
    uint8_t fee;
    bool hasDelegate;

    BlockScript(const CScript& _stakerScript = CScript()):
        stakerScript(_stakerScript),
        fee(0),
        hasDelegate(false)
    {}
};

struct ScriptsElement{
    BlockScript script;
    uint256 hash;
};

/**
 * Cache of the recent mpos scripts for the block reward recipients
 * The max size of the map is 2 * nCacheScripts - nMPoSRewardRecipients, so in this case it is 20
 */
std::map<int, ScriptsElement> scriptsMap;

unsigned int GetStakeMaxCombineInputs() { return 100; }

int64_t GetStakeCombineThreshold() { return 100 * COIN; }

unsigned int GetStakeSplitOutputs() { return 2; }

int64_t GetStakeSplitThreshold() { return GetStakeSplitOutputs() * GetStakeCombineThreshold(); }

bool SplitOfflineStakeReward(const int64_t& nReward, const uint8_t& fee, int64_t& nRewardOffline, int64_t& nRewardStaker)
{
    if(fee > 100) return false;
    nRewardStaker = nReward * fee / 100;
    nRewardOffline = nReward - nRewardStaker;
    return true;
}

bool IsDelegateOutputExist(int inFee)
{
    return inFee >= 0 && inFee < 100;
}

int GetDelegationFeeTx(const CTransaction& tx, const Coin& coin, bool delegateOutputExist)
{
    CAmount nValueCoin = coin.out.nValue;
    size_t minVoutSize = delegateOutputExist ? 3 : 2;
    if(!tx.IsCoinStake() || tx.vout.size() < minVoutSize || nValueCoin <= 0)
        return -1;

    CAmount nValueStaker = tx.vout[1].nValue - nValueCoin;
    CAmount nValueDelegate = delegateOutputExist ? tx.vout[2].nValue : 0;
    CAmount nReward = nValueStaker + nValueDelegate;
    if(nReward <= 0)
        return -1;

    return (nValueStaker * 100 + nReward - 1) / nReward;
}

bool GetDelegationFeeFromContract(const uint160& address, uint8_t& fee)
{
    Delegation delegation;
    QtumDelegation& qtumDelegation = GetQtumDelegation();
    bool ret = qtumDelegation.GetDelegation(address, delegation);
    if(ret) ret &= qtumDelegation.VerifyDelegation(address, delegation);
    if(ret)
    {
        fee = delegation.fee;
    }
    return ret;
}

bool NeedToEraseScriptFromCache(int nBlockHeight, int nCacheScripts, int nScriptHeight, const ScriptsElement& scriptElement)
{
    // Erase element from cache if not in range [nBlockHeight - nCacheScripts, nBlockHeight + nCacheScripts]
    if(nScriptHeight < (nBlockHeight - nCacheScripts) ||
            nScriptHeight > (nBlockHeight + nCacheScripts))
        return true;

    // Erase element from cache if hash different
    CBlockIndex* pblockindex = ChainActive()[nScriptHeight];
    if(pblockindex && pblockindex->GetBlockHash() != scriptElement.hash)
        return true;

    return false;
}

void CleanScriptCache(int nHeight, const Consensus::Params& consensusParams)
{
    int nCacheScripts = consensusParams.nMPoSRewardRecipients * 1.5;

    // Remove the scripts from cache that are not used
    for (std::map<int, ScriptsElement>::iterator it=scriptsMap.begin(); it!=scriptsMap.end();){
        if(NeedToEraseScriptFromCache(nHeight, nCacheScripts, it->first, it->second))
        {
            it = scriptsMap.erase(it);
        }
        else{
            it++;
        }
    }
}

bool ReadFromScriptCache(BlockScript &script, CBlockIndex* pblockindex, int nHeight, const Consensus::Params& consensusParams)
{
    CleanScriptCache(nHeight, consensusParams);

    // Find the script in the cache
    std::map<int, ScriptsElement>::iterator it = scriptsMap.find(nHeight);
    if(it != scriptsMap.end())
    {
        if(it->second.hash == pblockindex->GetBlockHash())
        {
            script = it->second.script;
            return true;
        }
    }

    return false;
}

void AddToScriptCache(BlockScript script, CBlockIndex* pblockindex, int nHeight, const Consensus::Params& consensusParams)
{
    CleanScriptCache(nHeight, consensusParams);

    // Add the script into the cache
    ScriptsElement listElement;
    listElement.script = script;
    listElement.hash = pblockindex->GetBlockHash();
    scriptsMap.insert(std::pair<int, ScriptsElement>(nHeight, listElement));
}

bool AddMPoSScript(std::vector<BlockScript> &mposScriptList, int nHeight, const Consensus::Params& consensusParams)
{
    // Check if the block index exist into the active chain
    CBlockIndex* pblockindex = ChainActive()[nHeight];
    if(!pblockindex)
    {
        LogPrint(BCLog::COINSTAKE, "Block index not found\n");
        return false;
    }

    // Try find the script from the cache
    BlockScript blockScript;
    if(ReadFromScriptCache(blockScript, pblockindex, nHeight, consensusParams))
    {
        mposScriptList.push_back(blockScript);
        return true;
    }

    // Read the block
    uint160 stakeAddress;
    if(!pblocktree->ReadStakeIndex(nHeight, stakeAddress)){
        return false;
    }

    // The block reward for PoS is in the second transaction (coinstake) and the second or third output
    if(pblockindex->IsProofOfStake())
    {
        if(stakeAddress == uint160())
        {
            LogPrint(BCLog::COINSTAKE, "Fail to solve script for mpos reward recipient\n");
            //This should never fail, but in case it somehow did we don't want it to bring the network to a halt
            //So, use an OP_RETURN script to burn the coins for the unknown staker
            blockScript = CScript() << OP_RETURN;
        }else{
            // Make public key hash script
            blockScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(stakeAddress) << OP_EQUALVERIFY << OP_CHECKSIG;
        }

        if(pblockindex->HasProofOfDelegation())
        {
            uint160 delegateAddress;
            uint8_t fee;
            if(!pblocktree->ReadDelegateIndex(nHeight, delegateAddress, fee)){
                return false;
            }

            if(delegateAddress == uint160())
            {
                LogPrint(BCLog::COINSTAKE, "Fail to solve script for mpos delegate reward recipient\n");
                blockScript.delegateScript = CScript() << OP_RETURN;
            }else{
                // Make public key hash script
                blockScript.delegateScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(delegateAddress) << OP_EQUALVERIFY << OP_CHECKSIG;
            }

            blockScript.fee = fee;
            blockScript.hasDelegate = true;
        }

        // Add the script into the list
        mposScriptList.push_back(blockScript);

        // Update script cache
        AddToScriptCache(blockScript, pblockindex, nHeight, consensusParams);
    }
    else
    {
        if(Params().MineBlocksOnDemand()){
            //this could happen in regtest. Just ignore and add an empty script
            blockScript = CScript() << OP_RETURN;
            mposScriptList.push_back(blockScript);
            return true;

        }
        LogPrint(BCLog::COINSTAKE, "The block is not proof-of-stake\n");
        return false;
    }

    return true;
}

bool GetMPoSOutputScripts(std::vector<BlockScript>& mposScriptList, int nHeight, const Consensus::Params& consensusParams)
{
    bool ret = true;
    nHeight -= COINBASE_MATURITY;

    // Populate the list of scripts for the reward recipients
    for(int i = 0; (i < consensusParams.nMPoSRewardRecipients - 1) && ret; i++)
    {
        ret &= AddMPoSScript(mposScriptList, nHeight - i, consensusParams);
    }

    return ret;
}

bool GetMPoSOutputs(std::vector<CTxOut>& mposOutputList, int64_t nRewardPiece, int nHeight, const Consensus::Params& consensusParams)
{
    std::vector<BlockScript> mposScriptList;
    if(!GetMPoSOutputScripts(mposScriptList, nHeight, consensusParams))
    {
        LogPrint(BCLog::COINSTAKE, "Fail to get the list of recipients\n");
        return false;
    }

    // Create the outputs for the recipients
    for(unsigned int i = 0; i < mposScriptList.size(); i++)
    {
        BlockScript blockScript = mposScriptList[i];
        if(blockScript.hasDelegate)
        {
            int64_t nRewardDelegate, nRewardStaker;
            if(!SplitOfflineStakeReward(nRewardPiece, blockScript.fee, nRewardDelegate, nRewardStaker))
            {
                LogPrint(BCLog::COINSTAKE, "Fail to to split the offline staking reward\n");
                return false;
            }

            mposOutputList.push_back(CTxOut(nRewardStaker, blockScript.stakerScript));
            if(IsDelegateOutputExist(blockScript.fee))
            {
                mposOutputList.push_back(CTxOut(nRewardDelegate, blockScript.delegateScript));
            }
        }
        else
        {
            mposOutputList.push_back(CTxOut(nRewardPiece, blockScript.stakerScript));
        }
    }

    return true;
}

bool CreateMPoSOutputs(CMutableTransaction& txNew, int64_t nRewardPiece, int nHeight, const Consensus::Params& consensusParams)
{
    std::vector<CTxOut> mposOutputList;
    if(!GetMPoSOutputs(mposOutputList, nRewardPiece, nHeight, consensusParams))
    {
        return false;
    }

    // Split the block reward with the recipients
    for(unsigned int i = 0; i < mposOutputList.size(); i++)
    {
        txNew.vout.push_back(mposOutputList[i]);
    }

    return true;
}


/**
 * @file envelope_collection.cc
 * Computes the reverberation envelope time series for all combinations of
 * receiver azimuth, source beam number, receiver beam number.
 */
#include <usml/eigenverb/envelope_collection.h>
#include <boost/numeric/ublas/matrix_proxy.hpp>
#include <boost/foreach.hpp>
#include <netcdfcpp.h>

using namespace usml::eigenverb;

/**
 * Reserve memory in which to store results as a series of
 * nested dynamic arrays.
 */
envelope_collection::envelope_collection(
	const seq_vector* envelope_freq,
	size_t src_freq_first,
	const seq_vector* travel_time,
	double reverb_duration,
	double pulse_length,
	double threshold,
	size_t num_azimuths,
	size_t num_src_beams,
	size_t num_rcv_beams,
	double initial_time,
	sensor_model::id_type source_id,
    sensor_model::id_type receiver_id,
    wposition1 src_position,
    wposition1 rcv_position
) :
	_envelope_freq(envelope_freq->clone()),
	_travel_time( travel_time->clip(0.0,reverb_duration) ),
	_reverb_duration(reverb_duration),
	_pulse_length(pulse_length),
	_threshold( threshold ),
	_num_azimuths(num_azimuths),
	_num_src_beams(num_src_beams),
	_num_rcv_beams(num_rcv_beams),
	_initial_time(initial_time),
	_source_id(source_id),
	_receiver_id(receiver_id),
	_source_position(src_position),
	_receiver_position(rcv_position),
	_envelope_model( _envelope_freq, src_freq_first, _travel_time,
	                        _initial_time, _pulse_length, _threshold)
{
    // Store range from source to receiver when eigenverbs were obtained.
    _slant_range = _receiver_position.distance(_source_position);

	_envelopes = new matrix<double>***[_num_azimuths];
	matrix<double>**** pa = _envelopes;
	for (size_t a = 0; a < _num_azimuths; ++a, ++pa) {
		*pa = new matrix<double>**[_num_src_beams];
		matrix<double>*** ps = *pa;
		for (size_t s = 0; s < _num_src_beams; ++s, ++ps) {
			*ps = new matrix<double>*[_num_rcv_beams];
			matrix<double>** pr = *ps;
			for (size_t r = 0; r < _num_rcv_beams; ++r, ++pr) {
				*pr = new matrix< double >(
						_envelope_freq->size(), _travel_time->size() ) ;
				(*pr)->clear();
			}
		}
	}
}

/**
 * Delete dynamic memory in each of the nested dynamic arrays.
 */
envelope_collection::~envelope_collection() {
	matrix<double>**** pa = _envelopes;
	for (size_t a = 0; a < _num_azimuths; ++a, ++pa) {
		matrix<double>*** ps = *pa;
		for (size_t s = 0; s < _num_src_beams; ++s, ++ps) {
			matrix<double>** pr = *ps;
			for (size_t r = 0; r < _num_rcv_beams; ++r, ++pr) {
				delete *pr ;
			}
			delete[] *ps ;
		}
		delete[] *pa ;
	}
	delete[] _envelopes ;
	delete _envelope_freq ;
	delete _travel_time ;
}

/**
 * Adds the intensity contribution for a single combination of source
 * and receiver eigenverbs.
 */
void envelope_collection::add_contribution(
	const eigenverb& src_verb, const eigenverb& rcv_verb,
	const matrix<double>& src_beam, const matrix<double>& rcv_beam,
	const vector<double>& scatter, double xs2, double ys2 )
{
	size_t azimuth = rcv_verb.az_index ;
	bool ok = _envelope_model.compute_intensity(src_verb,rcv_verb,scatter,xs2,ys2) ;
	if ( ok ) {
		for ( size_t s=0 ; s < src_beam.size2() ; ++s ) {
			for ( size_t r=0 ; r < rcv_beam.size2() ; ++r ) {
				for ( size_t f=0 ; f < _envelope_freq->size() ; ++f ) {
					matrix_row< matrix<double> > intensity(_envelope_model.intensity(), f);
					matrix_row< matrix<double> > envelope(*_envelopes[azimuth][s][r], f);
					envelope += src_beam(f, s) * rcv_beam(f, r) * intensity;
				}
			}
		}
	}
}

/**
 * Updates the envelope_collection data with the parameters provided.
 */
void envelope_collection::dead_reckon(double delta_time,
                                    double slant_range, double prev_range) {
    // Set new slant_range
    _slant_range = slant_range;

    // Shift the time series
    delete _travel_time;
    boost::numeric::ublas::vector<double> temp_data = (*_travel_time);
    temp_data = temp_data + delta_time;
    _travel_time = new seq_data( temp_data );

    { // Scope for lock

        // Perform copy and intensity update
        double gain = slant_range/prev_range;
        gain *= gain ;

        write_lock_guard guard(this->_envelopes_mutex);
        // Copy "this" envelopes to new_collection
        matrix<double>**** pa = _envelopes;
        for (size_t a = 0; a < _num_azimuths; ++a, ++pa)
        {
            matrix<double>*** ps = *pa;
            for (size_t s = 0; s < _num_src_beams; ++s, ++ps)
            {
                matrix<double>** pr = *ps;
                for (size_t r = 0; r < _num_rcv_beams; ++r, ++pr)
                {
                    (**pr) = this->envelope(a, s, r);
                    (**pr) *= gain;
                }
            }
        }
    }
}

/**
 * Writes the envelope data to disk
 */
void envelope_collection::write_netcdf(const char* filename) const {
	NcFile* nc_file = new NcFile(filename, NcFile::Replace);

	// dimensions

	NcDim* azimuth_dim = nc_file->add_dim("azimuth", (long) _num_azimuths ) ;
	NcDim* src_beam_dim = nc_file->add_dim("src_beam", (long) _num_src_beams ) ;
	NcDim* rcv_beam_dim = nc_file->add_dim("rcv_beam", (long) _num_rcv_beams ) ;
	NcDim* freq_dim = nc_file->add_dim("frequency", (long) _envelope_freq->size()) ;
	NcDim* time_dim = nc_file->add_dim("travel_time", (long) _travel_time->size()) ;

	// variables

	NcVar* pulse_length_var = nc_file->add_var("pulse_length", ncDouble );
	NcVar* threshold_var = nc_file->add_var("threshold", ncDouble );
	NcVar* initial_time_var = nc_file->add_var("initial_time", ncDouble );
	NcVar* freq_var = nc_file->add_var("frequency", ncDouble, freq_dim);
	NcVar* time_var = nc_file->add_var("travel_time", ncDouble, time_dim);
	NcVar* envelopes_var = nc_file->add_var("intensity", ncDouble,
		azimuth_dim, src_beam_dim, rcv_beam_dim, freq_dim, time_dim ) ;

	// units

	pulse_length_var->add_att("units", "seconds");
	threshold_var->add_att("units", "dB");
	initial_time_var->add_att("units", "seconds");
	time_var->add_att("units", "seconds");
	freq_var->add_att("units", "hertz");
	envelopes_var->add_att("units", "dB");

	// data

	pulse_length_var->put( &_pulse_length ) ;
	threshold_var->put( &_threshold ) ;
	initial_time_var->put( &_initial_time ) ;
	freq_var->put( _envelope_freq->data().begin(), (long) _envelope_freq->size());
	time_var->put( _travel_time->data().begin(), (long) _travel_time->size());

	for (size_t a = 0; a < _num_azimuths; ++a) {
		for (size_t s = 0; s < _num_src_beams; ++s) {
			for (size_t r = 0; r < _num_rcv_beams; ++r) {
				for (size_t f = 0; f < _envelope_freq->size(); ++f) {
					matrix_row< matrix<double> > row(*_envelopes[a][s][r], f);
					vector<double> envelope = 10.0*log10(max(row, 1e-30));
					envelopes_var->set_cur((long)a, (long)s, (long)r, (long) f, 0L );
					envelopes_var->put(envelope.data().begin(), 1L, 1L, 1L, 1L,
						(long) _travel_time->size() );
				}
			}
		}
	}

    // close file

    delete nc_file; // destructor frees all netCDF temp variables
}


#include <thread>
#include <unordered_map>
#include "kvmsg.hpp"
#include <random>
#include <format>
#include <chrono>
#include <boost/algorithm/string.hpp>
#include <fmt/core.h>

int gen_num(int a, int b)
{
	std::random_device rd;  //Will be used to obtain a seed for the random number engine
	std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
	std::uniform_int_distribution<> distrib(a, b);
	return distrib(gen);
}

int main()
{
	using namespace std::string_literals;
	const std::string SUBTREE = "/client/";
	std::unordered_map<std::string, kvmsg> mp;

	zmqpp::context_t ctx;
	zmqpp::socket_t snapshot(ctx, zmqpp::socket_type::dealer);
	snapshot.connect("tcp://localhost:5556");

	zmqpp::socket_t subscriber(ctx, zmqpp::socket_type::subscribe);
	subscriber.connect("tcp://localhost:5557");
	subscriber.subscribe(SUBTREE);

	zmqpp::socket_t push(ctx, zmqpp::socket_type::push);
	push.connect("tcp://localhost:5558");

	// get state snapshot
	zmqpp::message_t snmsg;
	snmsg << "ICANHAZ?";
	snmsg << SUBTREE;
	snapshot.send(snmsg);

	int seq{};

	while (1) {
		kvmsg reply(1);
		reply.recv(snapshot);
		seq = reply.getSequence();

		if (boost::iequals(reply.getKey(), "KTHXBAI"s)) {
			fmt::print("Received snaphot {}\n", seq);
			break;
		}
		fmt::print("Receiving {}\n", seq);
		mp.insert_or_assign(reply.getKey(), reply);
	}

	zmqpp::poller_t poller;
	poller.add(subscriber);

	constexpr int INTERVAL = 2000;
	auto alarm = std::chrono::steady_clock::now();
	while (true) {
		poller.poll(1000);
		if (poller.events(subscriber) == zmqpp::poller_t::poll_in) {
			kvmsg reply2(0);
			reply2.recv(subscriber);

			if (reply2.getSequence() > seq) {
				fmt::print("Received snaphot {}\n", reply2.getSequence());
				mp.insert_or_assign(reply2.getKey(), reply2);
			}
		}

		auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - alarm).count();
		if (diff > INTERVAL) {
			auto key = std::format("{}{}", SUBTREE, gen_num(0, 10000));
			auto body = std::format("{}", gen_num(0, 1000000));

			kvmsg msg(0);
			msg.setKey(key);
			msg.setBody(body);
			msg.setProp("ttl", std::to_string(gen_num(1, 3)));
			msg.send(push);
			alarm = std::chrono::steady_clock::now();
		}
	}
	return 0;
}

zeromq/5-Advanced Pub-Sub Patterns/5.4-Reliable Pub-Sub (Clone Pattern)/clonecli5.cpp