"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_browserify_1 = require("crypto-browserify");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const light_1 = require("protobufjs/light");
const protobuf_1 = require("../protobuf");
const skipchain_1 = require("../skipchain");
const skipblock_1 = require("../skipchain/skipblock");
const instance_1 = tslib_1.__importDefault(require("./instance"));
const data_header_1 = tslib_1.__importDefault(require("./proto/data-header"));
/**
 * The proof class represents a proof that a given instance with its data is either present or absent in the global
 * state. It does this by proving three different things:
 *
 * 1. that there is a valid chain of blocks from the genesis to the latest block
 * 2. a copy of the latest block to get the root hash of the global state trie
 * 3. an inclusion proof against the root hash that can be positive (element is there) or negative (absence of element)
 *
 * As the element that is proven is always an instance, this class also has convenience methods to access the
 * instance data in case it is a proof of existence. For absence proofs, these methods will throw an error.
 */
class Proof extends light_1.Message {
    constructor(props) {
        super(props);
        this.links = this.links || [];
    }
    /**
     * Get the state change stored in the inclusion proof
     *
     * @returns the state change body
     */
    get stateChangeBody() {
        return this.inclusionproof.stateChangeBody;
    }
    /**
     * Returns the contractID this proof represents. Throws an error if it
     * is a proof of absence.
     *
     * @returns the contract ID as a buffer
     */
    get contractID() {
        return this.stateChangeBody.contractID;
    }
    /**
     * Get the darc ID of the instance
     *
     * @returns the darcID responsible for the instanceID this proof represents.
     */
    get darcID() {
        return this.stateChangeBody.darcID;
    }
    /**
     * The value of the instance is different from the value stored in the global state.
     *
     * @returns the value of the instance this proof represents.
     */
    get value() {
        return this.stateChangeBody.value;
    }
    /**
     * Get the instance ID for the proof
     *
     * @returns the instance ID as a buffer
     */
    get key() {
        return this.inclusionproof.key;
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Proof", Proof, InclusionProof, skipblock_1.SkipBlock, skipblock_1.ForwardLink);
    }
    /**
     * Verifies that the proof is of the given type and starts at the given byzcoin-id.
     * @param genesisID the hash of the first block, the byzcoin-id
     * @param contractID what contract the instance is supposed to be of
     * @throws an error if the proof is not based on genesisID, if it is a proof of absence,
     * or if the instance is not of type contractID.
     * @deprecated this function is unsecure
     */
    getVerifiedInstance(genesisID, contractID) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const err = this.verify(genesisID);
            if (err != null) {
                return Promise.reject(err);
            }
            if (!this.exists(this.key)) {
                return Promise.reject("cannot return an Instance from a proof of absence");
            }
            if (this.contractID !== contractID) {
                return Promise.reject("not of correct contractID");
            }
            return new instance_1.default({ id: this.key, contractID: this.contractID, darcID: this.darcID, data: this.value });
        });
    }
    /**
     * Verify that the proof contains a correct chain from the given block.
     *
     * @param block the first block of the proof
     * @returns an error if something is wrong, null otherwise
     */
    verifyFrom(block) {
        if (this.links.length === 0) {
            return new Error("missing forward-links");
        }
        if (!this.links[0].newRoster.id.equals(block.roster.id)) {
            return new Error("invalid first roster found in proof");
        }
        return this.verify(block.hash);
    }
    /**
     * Verify that the proof contains a correct chain from the given genesis. Note that
     * this function doesn't verify the first roster of the chain.
     *
     * @param latestID ID of the latest known and trusted block the proof is based upon
     * @returns an error if something is wrong, null otherwise
     * @deprecated use verifyFrom for a complete verification
     */
    verify(latestID) {
        if (!this.latest.computeHash().equals(this.latest.hash)) {
            return new Error("invalid latest block");
        }
        const header = data_header_1.default.decode(this.latest.data);
        if (!this.inclusionproof.hashInterior(0).equals(header.trieRoot)) {
            return new Error("invalid root");
        }
        const links = this.links;
        if (!links[0].to.equals(latestID)) {
            return new Error("mismatching block ID in the first link");
        }
        let publics = links[0].newRoster.getServicePublics(skipchain_1.SkipchainRPC.serviceName);
        let prev = links[0].to;
        if (Proof.latestID && links[links.length - 1].to.equals(Proof.latestID)) {
            // This latest block is already known, so no use of doing forward-link checking
            prev = Proof.latestID;
        }
        else {
            // Check that all forward-links are correct.
            for (let i = 1; i < links.length; i++) {
                const link = links[i];
                const err = link.verifyWithScheme(publics, this.latest.signatureScheme);
                if (err) {
                    return new Error("invalid forward link signature: " + err.message);
                }
                if (!link.from.equals(prev)) {
                    return new Error("invalid chain of forward links");
                }
                prev = link.to;
                if (link.newRoster) {
                    publics = link.newRoster.getServicePublics(skipchain_1.SkipchainRPC.serviceName);
                }
            }
            Proof.latestID = prev;
        }
        if (!prev.equals(this.latest.hash)) {
            return new Error("last forward link does not point to the latest block");
        }
        return undefined;
    }
    /**
     * Check if the key exists in the proof
     *
     * @returns true when it exists, false otherwise
     * @throws for corrupted proofs
     */
    exists(key) {
        return this.inclusionproof.exists(key);
    }
    /**
     * @param cid contractID to check
     * @returns true if it is a proof of existence and the given type of contract matches.
     */
    matchContract(cid) {
        return this.stateChangeBody.contractID.toString() === cid;
    }
    /**
     * @returns a nicely formatted representation of the proof.
     */
    toString() {
        return `Proof for contractID(${this.contractID}) for ${this.key}`;
    }
}
exports.default = Proof;
/**
 * Get an array of booleans depending on the binary representation
 * of the key
 *
 * @param key the key to hash
 * @returns an array of booleans matching the key binary value
 */
function hashToBits(key) {
    const h = crypto_browserify_1.createHash("sha256");
    h.update(key);
    const hash = h.digest();
    const bits = new Array(hash.length * 8);
    for (let i = 0; i < bits.length; i++) {
        // tslint:disable-next-line no-bitwise
        bits[i] = ((hash[i >> 3] << (i % 8)) & (1 << 7)) > 0;
    }
    return bits;
}
/**
 * Get a buffer from an array of boolean converted in binary
 *
 * @param bits the array of booleans
 * @returns a buffer of the binary shape
 */
function boolToBuffer(bits) {
    // tslint:disable-next-line no-bitwise
    const buf = Buffer.alloc((bits.length + 7) >> 3, 0);
    for (let i = 0; i < bits.length; i++) {
        if (bits[i]) {
            // tslint:disable-next-line no-bitwise
            buf[i >> 3] |= (1 << 7) >> (i % 8);
        }
    }
    return buf;
}
/**
 * Interior node of an inclusion proof
 */
class InteriorNode extends light_1.Message {
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("trie.InteriorNode", InteriorNode);
    }
}
/**
 * Empty node of an inclusion proof
 */
class EmptyNode extends light_1.Message {
    constructor(props) {
        super(props);
        this.prefix = this.prefix || [];
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("trie.EmptyNode", EmptyNode);
    }
}
/**
 * Leaf node of an inclusion proof
 */
class LeafNode extends light_1.Message {
    constructor(props) {
        super(props);
        this.prefix = this.prefix || [];
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("trie.LeafNode", LeafNode);
    }
}
/**
 * InclusionProof represents the proof that an instance is present or not in the global state trie.
 */
class InclusionProof extends light_1.Message {
    constructor(props) {
        super(props);
        this.interiors = this.interiors || [];
    }
    /**
     * @return {Buffer} the key in the leaf for this inclusionProof. This is not the same as the key this proof has
     * been created for!
     */
    get key() {
        return this.leaf.key;
    }
    /**
     * @return {Buffer} the value stored in the instance. The value of an instance holds the contractID, darcID,
     * version and the data of the instance.
     */
    get value() {
        return this.leaf.value;
    }
    /**
     * Returns the stateChangeBody of this InclusionProof.
     */
    get stateChangeBody() {
        if (!this._state) {
            // cache the decoding
            this._state = StateChangeBody.decode(this.value);
        }
        return this._state;
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("trie.Proof", InclusionProof, InteriorNode, LeafNode, EmptyNode);
    }
    /**
     * Get the hash of the interior node at the given index
     *
     * @param index the index of the interior node
     * @returns the hash as a buffer
     */
    hashInterior(index) {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(this.interiors[index].left);
        h.update(this.interiors[index].right);
        return h.digest();
    }
    /**
     * Get the hash of the leaf of the inclusion proof
     *
     * @returns the hash as a buffer
     */
    hashLeaf() {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(Buffer.from([3]));
        h.update(this.nonce);
        const prefix = boolToBuffer(this.leaf.prefix);
        h.update(prefix);
        const length = Buffer.allocUnsafe(4);
        length.writeIntLE(this.leaf.prefix.length, 0, 4);
        h.update(length);
        h.update(this.leaf.key);
        h.update(this.leaf.value);
        return h.digest();
    }
    /**
     * Get the hash of the empty node of the inclusion proof
     *
     * @returns the hash of the empty node
     */
    hashEmpty() {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(Buffer.from([2]));
        h.update(this.nonce);
        const prefix = boolToBuffer(this.empty.prefix);
        h.update(prefix);
        const length = Buffer.allocUnsafe(4);
        length.writeIntLE(this.empty.prefix.length, 0, 4);
        h.update(length);
        return h.digest();
    }
    /**
     * Check if the key exists in the proof
     *
     * @returns true when it exists, false otherwise
     * @throws for corrupted proofs
     */
    exists(key) {
        if (key.length === 0) {
            throw new Error("key is nil");
        }
        if (this.interiors.length === 0) {
            throw new Error("no interior node");
        }
        const bits = hashToBits(key);
        let expectedHash = this.hashInterior(0);
        let i = 0;
        for (; i < this.interiors.length; i++) {
            if (!expectedHash.equals(this.hashInterior(i))) {
                throw new Error("invalid interior node");
            }
            if (bits[i]) {
                expectedHash = this.interiors[i].left;
            }
            else {
                expectedHash = this.interiors[i].right;
            }
        }
        if (expectedHash.equals(this.hashLeaf())) {
            if (lodash_1.default.difference(bits.slice(0, i), this.leaf.prefix).length !== 0) {
                throw new Error("invalid prefix in leaf node");
            }
            return this.key.equals(key);
        }
        else if (expectedHash.equals(this.hashEmpty())) {
            if (lodash_1.default.difference(bits.slice(0, i), this.empty.prefix).length !== 0) {
                throw new Error("invalid prefix in empty node");
            }
            return false;
        }
        throw new Error("no corresponding leaf/empty node with respect to the interior node");
    }
}
exports.InclusionProof = InclusionProof;
class StateChangeBody extends light_1.Message {
    constructor(props) {
        super(props);
    }
    get contractID() {
        return this.contractid;
    }
    get darcID() {
        return this.darcid;
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("StateChangeBody", StateChangeBody);
    }
    static fromBytes(b) {
        return StateChangeBody.decode(b);
    }
    /**
     * Helper to encode the StateChangeBody using protobuf
     * @returns the bytes
     */
    toBytes() {
        return Buffer.from(StateChangeBody.encode(this).finish());
    }
}
exports.StateChangeBody = StateChangeBody;
Proof.register();
InclusionProof.register();
InteriorNode.register();
LeafNode.register();
EmptyNode.register();
StateChangeBody.register();
