const jsonfile = require('jsonfile');
const elliptic = require('elliptic');
const path = require('path');

const Wormhole = artifacts.require("Wormhole");
const MockImplementation = artifacts.require("MockImplementation");
const Implementation = artifacts.require("Implementation");

const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe";
const testSigner3PK = "87b45997ea577b93073568f06fc4838cffc1d01f90fc4d57f936957f3c4d99fb";
const testBadSigner1PK = "87b45997ea577b93073568f06fc4838cffc1d01f90fc4d57f936957f3c4d99fc";


const core = '0x' + Buffer.from("Core").toString("hex").padStart(64,0)
const actionContractUpgrade = "01"
const actionGuardianSetUpgrade = "02"
const actionMessageFee = "03"
const actionTransferFee = "04"


const ImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi

// Taken from https://medium.com/fluidity/standing-the-time-of-test-b906fcc374a9
advanceTimeAndBlock = async (time) => {
    await advanceTime(time);
    await advanceBlock();

    return Promise.resolve(web3.eth.getBlock('latest'));
}

advanceTime = (time) => {
    return new Promise((resolve, reject) => {
        web3.currentProvider.send({
            jsonrpc: "2.0",
            method: "evm_increaseTime",
            params: [time],
            id: new Date().getTime()
        }, (err, result) => {
            if (err) {
                return reject(err);
            }
            return resolve(result);
        });
    });
}

advanceBlock = () => {
    return new Promise((resolve, reject) => {
        web3.currentProvider.send({
            jsonrpc: "2.0",
            method: "evm_mine",
            id: new Date().getTime()
        }, (err, result) => {
            if (err) {
                return reject(err);
            }
            const newBlockHash = web3.eth.getBlock('latest').hash;

            return resolve(newBlockHash)
        });
    });
}

contract("Wormhole", function () {
    const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK);
    const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK);
    const testSigner3 = web3.eth.accounts.privateKeyToAccount(testSigner3PK);
    const testChainId = "2";
    const testGovernanceChainId = "1";
    const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004";

    it("should be initialized with the correct signers and values", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const index = await initialized.methods.getCurrentGuardianSetIndex().call();
        const set = (await initialized.methods.getGuardianSet(index).call());

        // check set
        assert.lengthOf(set[0], 1);
        assert.equal(set[0][0], testSigner1.address);

        // check expiration
        assert.equal(set.expirationTime, "0");

        // chain id
        const chainId = await initialized.methods.chainId().call();
        assert.equal(chainId, testChainId);

        // governance
        const governanceChainId = await initialized.methods.governanceChainId().call();
        assert.equal(governanceChainId, testGovernanceChainId);
        const governanceContract = await initialized.methods.governanceContract().call();
        assert.equal(governanceContract, testGovernanceContract);
    })

    it("should log a published message correctly", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const log = await initialized.methods.publishMessage(
            "0x123",
            "0x123321",
            32
        ).send({
            value: 0, // fees are set to 0 initially
            from: accounts[0]
        })

        assert.equal(log.events.LogMessagePublished.returnValues.sender.toString(), accounts[0]);
        assert.equal(log.events.LogMessagePublished.returnValues.sequence.toString(), "0");
        assert.equal(log.events.LogMessagePublished.returnValues.nonce, 291);
        assert.equal(log.events.LogMessagePublished.returnValues.payload.toString(), "0x123321");
        assert.equal(log.events.LogMessagePublished.returnValues.consistencyLevel, 32);
    })

    it("should increase the sequence for an account", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const log = await initialized.methods.publishMessage(
            "0x1",
            "0x1",
            32
        ).send({
            value: 0, // fees are set to 0 initially
            from: accounts[0]
        })

        assert.equal(log.events.LogMessagePublished.returnValues.sequence.toString(), "1");
    })

    it("parses VMs correctly", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            1337,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );

        let result
        try {
            result = await initialized.methods.parseAndVerifyVM("0x" + vm).call();
        } catch (err) {
            console.log(err)
            assert.fail("parseAndVerifyVM failed")
        }

        assert.equal(result.vm.version, 1);
        assert.equal(result.vm.timestamp, timestamp);
        assert.equal(result.vm.nonce, nonce);
        assert.equal(result.vm.emitterChainId, emitterChainId);
        assert.equal(result.vm.emitterAddress, emitterAddress);
        assert.equal(result.vm.payload, data);
        assert.equal(result.vm.guardianSetIndex, 0);
        assert.equal(result.vm.sequence, 1337);
        assert.equal(result.vm.consistencyLevel, 2);

        assert.equal(result.valid, true);

        assert.equal(result.reason, "");
    })


    it("should fail quorum on VMs with no signers", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            1337,
            data,
            [], // no valid signers present
            0,
            2
        );

        let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call();
        assert.equal(result[1], false)
        assert.equal(result[2], "no quorum")
    })


    it("should fail to verify on VMs with bad signer", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            1337,
            data,
            [
                testBadSigner1PK, // not a valid signer
            ],
            0,
            2
        );

        let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call();
        assert.equal(result[1], false)
        assert.equal(result[2], "VM signature invalid")
    })

    it("should error on VMs with invalid guardian set index", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            1337,
            data,
            [
                testSigner1PK,
            ],
            200,
            2
        );

        let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call();
        assert.equal(result[1], false)
        assert.equal(result[2], "invalid guardian set")
    })

    it("should revert on VMs with duplicate non-monotonic signature indexes", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVMFixedIndex(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            1337,
            data,
            [
                testSigner1PK,
                testSigner1PK,
                testSigner1PK,
            ],
            0,
            2
        );

        try {
            await initialized.methods.parseAndVerifyVM("0x" + vm).call();
            assert.fail("accepted signature indexes being the same in a VM");
        } catch (e) {
            assert.equal(e.data[Object.keys(e.data)[0]].reason, 'signature indices must be ascending')
        }
    })


    it("should set and enforce fees", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = testGovernanceChainId;
        const emitterAddress = testGovernanceContract

        data = [
            //Core
            core,
            // Action 3 (Set Message Fee)
            actionMessageFee,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // Message Fee
            web3.eth.abi.encodeParameter("uint256", 1111).substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            0,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );


        let before = await initialized.methods.messageFee().call();

        let set = await initialized.methods.submitSetMessageFee("0x" + vm).send({
            value: 0,
            from: accounts[0],
            gasLimit: 1000000
        });

        let after = await initialized.methods.messageFee().call();

        assert.notEqual(before, after);
        assert.equal(after, 1111);

        // test message publishing
        await initialized.methods.publishMessage(
            "0x123",
            "0x123321",
            32
        ).send({
            from: accounts[0],
            value: 1111
        })

        let failed = false;
        try {
            await initialized.methods.publishMessage(
                "0x123",
                "0x123321",
                32
            ).send({
                value: 1110,
                from: accounts[0]
            })
        } catch (e) {
            failed = true
        }

        assert.equal(failed, true);
    })

    it("should transfer out collected fees", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const receiver = "0x" + zeroPadBytes(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16), 20);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = testGovernanceChainId;
        const emitterAddress = testGovernanceContract

        data = [
            // Core
            core,
            // Action 4 (Transfer Fees)
            actionTransferFee,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // Amount
            web3.eth.abi.encodeParameter("uint256", 11).substring(2),
            // Recipient
            web3.eth.abi.encodeParameter("address", receiver).substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            0,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );

        let WHBefore = await web3.eth.getBalance(Wormhole.address);
        let receiverBefore = await web3.eth.getBalance(receiver);

        let set = await initialized.methods.submitTransferFees("0x" + vm).send({
            value: 0,
            from: accounts[0],
            gasLimit: 1000000
        });

        let WHAfter = await web3.eth.getBalance(Wormhole.address);
        let receiverAfter = await web3.eth.getBalance(receiver);

        assert.equal(WHBefore - WHAfter, 11);
        assert.equal(receiverAfter - receiverBefore, 11);
    })

    it("should accept a new guardian set", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = testGovernanceChainId;
        const emitterAddress = testGovernanceContract

        let oldIndex = Number(await initialized.methods.getCurrentGuardianSetIndex().call());

        data = [
            // Core
            core,
            // Action 2 (Guardian Set Upgrade)
            actionGuardianSetUpgrade,
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            web3.eth.abi.encodeParameter("uint32", oldIndex + 1).substring(2 + (64 - 8)),
            web3.eth.abi.encodeParameter("uint8", 3).substring(2 + (64 - 2)),
            web3.eth.abi.encodeParameter("address", testSigner1.address).substring(2 + (64 - 40)),
            web3.eth.abi.encodeParameter("address", testSigner2.address).substring(2 + (64 - 40)),
            web3.eth.abi.encodeParameter("address", testSigner3.address).substring(2 + (64 - 40)),
        ].join('')

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            0,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );

        let set = await initialized.methods.submitNewGuardianSet("0x" + vm).send({
            value: 0,
            from: accounts[0],
            gasLimit: 1000000
        });

        let index = await initialized.methods.getCurrentGuardianSetIndex().call();

        assert.equal(oldIndex + 1, index);

        assert.equal(index, 1);

        let guardians = await initialized.methods.getGuardianSet(index).call();

        assert.equal(guardians.expirationTime, 0);

        assert.lengthOf(guardians[0], 3);
        assert.equal(guardians[0][0], testSigner1.address);
        assert.equal(guardians[0][1], testSigner2.address);
        assert.equal(guardians[0][2], testSigner3.address);

        let oldGuardians = await initialized.methods.getGuardianSet(oldIndex).call();

        const time = (await web3.eth.getBlock("latest")).timestamp;

        // old guardian set expiry is set
        assert.ok(
            oldGuardians.expirationTime > Number(time) + 86000
            && oldGuardians.expirationTime < Number(time) + 88000
        );
    })

    it("should accept smart contract upgrades", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        const mock = await MockImplementation.new();

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = testGovernanceChainId;
        const emitterAddress = testGovernanceContract

        data = [
            // Core
            core,
            // Action 1 (Contract Upgrade)
            actionContractUpgrade,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // New Contract Address
            web3.eth.abi.encodeParameter("address", mock.address).substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            0,
            data,
            [
                testSigner1PK,
                testSigner2PK,
                testSigner3PK
            ],
            1,
            2
        );

        let before = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");

        assert.equal(before.toLowerCase(), Implementation.address.toLowerCase());

        let set = await initialized.methods.submitContractUpgrade("0x" + vm).send({
            value: 0,
            from: accounts[0],
            gasLimit: 1000000
        });

        let after = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");

        assert.equal(after.toLowerCase(), mock.address.toLowerCase());

        const mockImpl = new web3.eth.Contract(MockImplementation.abi, Wormhole.address);

        let isUpgraded = await mockImpl.methods.testNewImplementationActive().call();

        assert.ok(isUpgraded);
    })

    it("should revert governance packets from old guardian set", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        data = [
            // Core
            core,
            // Action 4 (Transfer Fee)
            actionTransferFee,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // Amount
            web3.eth.abi.encodeParameter("uint256", 1).substring(2),
            // Recipient            
            web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            0,
            0,
            testGovernanceChainId,
            testGovernanceContract,
            0,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );

        let failed = false;
        try {
            await initialized.methods.submitTransferFees("0x" + vm).send({
                value: 0,
                from: accounts[0],
                gasLimit: 1000000
            });
            assert.fail("governance packet of old guardian set accepted")
        } catch (e) {
            assert.equal(e.data[Object.keys(e.data)[0]].reason, "not signed by current guardian set")
        }
    })

    it("should time out old gardians", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);

        const timestamp = 1000;
        const nonce = 1001;
        const emitterChainId = 11;
        const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee"
        const data = "0xaaaaaa";

        const vm = await signAndEncodeVM(
            timestamp,
            nonce,
            emitterChainId,
            emitterAddress,
            0,
            data,
            [
                testSigner1PK,
            ],
            0,
            2
        );

        // this should pass
        const current = await initialized.methods.parseAndVerifyVM("0x" + vm).call();

        assert.equal(current.valid, true)

        await advanceTimeAndBlock(100000);

        const expired = await initialized.methods.parseAndVerifyVM("0x" + vm).call();

        assert.equal(expired.valid, false)
    })

    it("should revert governance packets from wrong governance chain", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        data = [
            // Core
            core,
            // Action 4 (set fees)
            actionTransferFee,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // Amount
            web3.eth.abi.encodeParameter("uint256", 1).substring(2),
            // Recipient
            web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            0,
            0,
            999,
            testGovernanceContract,
            0,
            data,
            [
                testSigner1PK,
                testSigner2PK,
                testSigner3PK,
            ],
            1,
            2
        );

        try {
            await initialized.methods.submitTransferFees("0x" + vm).send({
                value: 0,
                from: accounts[0],
                gasLimit: 1000000
            });
            assert.fail("governance packet from wrong governance chain accepted")
        } catch (e) {
            assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance chain")
        }
    })

    it("should revert governance packets from wrong governance contract", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        data = [
            // Core
            core,
            // Action 4 (Transfer Fee)
            actionTransferFee,
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            web3.eth.abi.encodeParameter("uint256", 1).substring(2),
            web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            0,
            0,
            testGovernanceChainId,
            core,
            0,
            data,
            [
                testSigner1PK,
                testSigner2PK,
                testSigner3PK,
            ],
            1,
            2
        );

        try {
            await initialized.methods.submitTransferFees("0x" + vm).send({
                value: 0,
                from: accounts[0],
                gasLimit: 1000000
            });
            assert.fail("governance packet from wrong governance contract accepted")
        } catch (e) {
            assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance contract")
        }
    })

    it("should revert on governance packets that already have been applied", async function () {
        const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address);
        const accounts = await web3.eth.getAccounts();

        data = [
            // Core
            core,
            // Action 4 (Transfer Fee)
            actionTransferFee,
            // ChainID
            web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)),
            // Amount
            web3.eth.abi.encodeParameter("uint256", 1).substring(2),
            // Recipient
            web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2),
        ].join('')

        const vm = await signAndEncodeVM(
            0,
            0,
            testGovernanceChainId,
            testGovernanceContract,
            0,
            data,
            [
                testSigner1PK,
                testSigner2PK,
                testSigner3PK,
            ],
            1,
            2
        );

        await initialized.methods.submitTransferFees("0x" + vm).send({
            value: 0,
            from: accounts[0],
            gasLimit: 1000000
        });

        try {
            await initialized.methods.submitTransferFees("0x" + vm).send({
                value: 0,
                from: accounts[0],
                gasLimit: 1000000
            });

            assert.fail("governance packet accepted twice")
        } catch (e) {
            assert.equal(e.data[Object.keys(e.data)[0]].reason, "governance action already consumed")
        }
    })

});

const signAndEncodeVM = async function (
    timestamp,
    nonce,
    emitterChainId,
    emitterAddress,
    sequence,
    data,
    signers,
    guardianSetIndex,
    consistencyLevel
) {
    const body = [
        web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)),
        web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2),
        web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)),
        web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)),
        data.substr(2)
    ]

    const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join("")))

    let signatures = "";

    for (let i in signers) {
        const ec = new elliptic.ec("secp256k1");
        const key = ec.keyFromPrivate(signers[i]);
        const signature = key.sign(hash.substr(2), {canonical: true});

        const packSig = [
            web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)),
            zeroPadBytes(signature.r.toString(16), 32),
            zeroPadBytes(signature.s.toString(16), 32),
            web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)),
        ]

        signatures += packSig.join("")
    }

    const vm = [
        web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)),
        web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)),

        signatures,
        body.join("")
    ].join("");

    return vm
}

const signAndEncodeVMFixedIndex = async function (
    timestamp,
    nonce,
    emitterChainId,
    emitterAddress,
    sequence,
    data,
    signers,
    guardianSetIndex,
    consistencyLevel
) {
    const body = [
        web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)),
        web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2),
        web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)),
        web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)),
        data.substr(2)
    ]

    const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join("")))

    let signatures = "";

    for (let i in signers) {
        const ec = new elliptic.ec("secp256k1");
        const key = ec.keyFromPrivate(signers[i]);
        const signature = key.sign(hash.substr(2), {canonical: true});

        const packSig = [
            // Fixing the index to be zero to product a non-monotonic VM
            web3.eth.abi.encodeParameter("uint8", 0).substring(2 + (64 - 2)),
            zeroPadBytes(signature.r.toString(16), 32),
            zeroPadBytes(signature.s.toString(16), 32),
            web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)),
        ]

        signatures += packSig.join("")
    }

    const vm = [
        web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)),
        web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)),
        web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)),

        signatures,
        body.join("")
    ].join("");

    return vm
}


function zeroPadBytes(value, length) {
    while (value.length < 2 * length) {
        value = "0" + value;
    }
    return value;
}
