// Copyright (c) 2019-2020 The PIVX developers
// Copyright (c) 2020-2021 The PENGOLINCOIN developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "zpgo/zpgomodule.h"

#include "hash.h"
#include "libzerocoin/Commitment.h"
#include "libzerocoin/Coin.h"
#include "validation.h"
#include "zpgochain.h"

template <typename Stream>
PublicCoinSpend::PublicCoinSpend(libzerocoin::ZerocoinParams* params, Stream& strm): pubCoin(params) {
    strm >> *this;
    this->spendType = libzerocoin::SpendType::SPEND;

    if (this->version < PUBSPEND_SCHNORR) {
        // coinVersion is serialized only from v4 spends
        this->coinVersion = libzerocoin::ExtractVersionFromSerial(this->coinSerialNumber);

    } else {
        // from v4 spends, serialNumber is not serialized for v2 coins anymore.
        // in this case, we extract it from the coin public key
        if (this->coinVersion >= libzerocoin::PUBKEY_VERSION)
            this->coinSerialNumber = libzerocoin::ExtractSerialFromPubKey(this->pubkey);

    }

}

bool PublicCoinSpend::Verify() const {
    bool fUseV1Params = getCoinVersion() < libzerocoin::PUBKEY_VERSION;
    if (version < PUBSPEND_SCHNORR) {
        // spend contains the randomness of the coin
        if (fUseV1Params) {
            // Only v2+ coins can publish the randomness
            std::string errMsg = strprintf("PublicCoinSpend version %d with coin version 1 not allowed. "
                    "Minimum spend version required: %d", version, PUBSPEND_SCHNORR);
            return error("%s: %s", __func__, errMsg);
        }

        // Check that the coin is a commitment to serial and randomness.
        libzerocoin::ZerocoinParams* params = Params().GetConsensus().Zerocoin_Params(false);
        libzerocoin::Commitment comm(&params->coinCommitmentGroup, getCoinSerialNumber(), randomness);
        if (comm.getCommitmentValue() != pubCoin.getValue()) {
            return error("%s: commitments values are not equal", __func__);
        }

    } else {
        // for v1 coins, double check that the serialized coin serial is indeed a v1 serial
        if (coinVersion < libzerocoin::PUBKEY_VERSION &&
                libzerocoin::ExtractVersionFromSerial(this->coinSerialNumber) != coinVersion) {
            return error("%s: invalid coin version", __func__);
        }

        // spend contains a shnorr signature of ptxHash with the randomness of the coin
        libzerocoin::ZerocoinParams* params = Params().GetConsensus().Zerocoin_Params(fUseV1Params);
        if (!schnorrSig.Verify(params, getCoinSerialNumber(), pubCoin.getValue(), getTxOutHash())) {
            return error("%s: schnorr signature does not verify", __func__);
        }

    }

    // Now check that the signature validates with the serial
    if (!HasValidSignature()) {
        return error("%s: signature invalid", __func__);;
    }

    return true;
}

bool PublicCoinSpend::HasValidSignature() const
{
    if (coinVersion < libzerocoin::PUBKEY_VERSION)
        return true;

    // for spend version 3 we must check that the provided pubkey and serial number match
    if (version < PUBSPEND_SCHNORR) {
        CBigNum extractedSerial = libzerocoin::ExtractSerialFromPubKey(this->pubkey);
        if (extractedSerial != this->coinSerialNumber)
            return error("%s: hashedpubkey is not equal to the serial!", __func__);
    }

    return pubkey.Verify(signatureHash(), vchSig);
}


const uint256 PublicCoinSpend::signatureHash() const
{
    CHashWriter h(0, 0);
    h << ptxHash << denomination << getCoinSerialNumber() << randomness << txHash << outputIndex << getSpendType();
    return h.GetHash();
}

namespace ZPGOModule {

    // Return stream of CoinSpend from tx input scriptsig
    CDataStream ScriptSigToSerializedSpend(const CScript& scriptSig)
    {
        std::vector<char, zero_after_free_allocator<char> > data;
        // skip opcode and data-len
        uint8_t byteskip = ((uint8_t) scriptSig[1] + 2);
        data.insert(data.end(), scriptSig.begin() + byteskip, scriptSig.end());
        return CDataStream(data, SER_NETWORK, PROTOCOL_VERSION);
    }

    PublicCoinSpend parseCoinSpend(const CTxIn &in)
    {
        libzerocoin::ZerocoinParams *params = Params().GetConsensus().Zerocoin_Params(false);
        CDataStream serializedCoinSpend = ScriptSigToSerializedSpend(in.scriptSig);
        return PublicCoinSpend(params, serializedCoinSpend);
    }

    bool parseCoinSpend(const CTxIn &in, const CTransaction &tx, const CTxOut &prevOut, PublicCoinSpend &publicCoinSpend) {
        if (!in.IsZerocoinPublicSpend() || !prevOut.IsZerocoinMint())
            return error("%s: invalid argument/s", __func__);

        PublicCoinSpend spend = parseCoinSpend(in);
        spend.outputIndex = in.prevout.n;
        spend.txHash = in.prevout.hash;
        CMutableTransaction txNew(tx);
        txNew.vin.clear();
        spend.setTxOutHash(txNew.GetHash());

        // Check prev out now
        CValidationState state;
        if (!TxOutToPublicCoin(prevOut, spend.pubCoin, state))
            return error("%s: cannot get mint from output", __func__);

        spend.setDenom(spend.pubCoin.getDenomination());
        publicCoinSpend = spend;
        return true;
    }

    bool validateInput(const CTxIn &in, const CTxOut &prevOut, const CTransaction &tx, PublicCoinSpend &publicSpend) {
        // Now prove that the commitment value opens to the input
        if (!parseCoinSpend(in, tx, prevOut, publicSpend)) {
            return false;
        }
        if (libzerocoin::ZerocoinDenominationToAmount(
                libzerocoin::IntToZerocoinDenomination(in.nSequence)) != prevOut.nValue) {
            return error("PublicCoinSpend validateInput :: input nSequence different to prevout value");
        }
        return publicSpend.Verify();
    }

    bool ParseZerocoinPublicSpend(const CTxIn &txIn, const CTransaction& tx, CValidationState& state, PublicCoinSpend& publicSpend)
    {
        CTxOut prevOut;
        if(!GetOutput(txIn.prevout.hash, txIn.prevout.n ,state, prevOut)){
            return state.DoS(100, error("%s: public zerocoin spend prev output not found, prevTx %s, index %d",
                                        __func__, txIn.prevout.hash.GetHex(), txIn.prevout.n));
        }
        if (!ZPGOModule::parseCoinSpend(txIn, tx, prevOut, publicSpend)) {
            return state.Invalid(error("%s: invalid public coin spend parse %s\n", __func__,
                                       tx.GetHash().GetHex()), REJECT_INVALID, "bad-txns-invalid-zpgo");
        }
        return true;
    }
}
