From 7dc8c6f25d856c6b3232448925409e26a55cd6fe Mon Sep 17 00:00:00 2001
From: CalDescent <>
Date: Tue, 10 May 2022 07:56:22 +0100
Subject: [PATCH] Added (incomplete) Pirate Chain mainnet params

---
 .../params/AbstractPirateChainParams.java     | 318 ++++++++++++++++++
 .../params/PirateChainMainNetParams.java      | 126 +++++++
 2 files changed, 444 insertions(+)
 create mode 100644 core/src/main/java/org/libdohj/params/AbstractPirateChainParams.java
 create mode 100644 core/src/main/java/org/libdohj/params/PirateChainMainNetParams.java

diff --git a/core/src/main/java/org/libdohj/params/AbstractPirateChainParams.java b/core/src/main/java/org/libdohj/params/AbstractPirateChainParams.java
new file mode 100644
index 00000000..f7b4321e
--- /dev/null
+++ b/core/src/main/java/org/libdohj/params/AbstractPirateChainParams.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Adapted for Pirate Chain in May 2022 by Qortal dev team
+ */
+
+package org.libdohj.params;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.store.BlockStore;
+import org.bitcoinj.store.BlockStoreException;
+import org.bitcoinj.utils.MonetaryFormat;
+import org.libdohj.core.AltcoinNetworkParameters;
+import org.libdohj.core.AltcoinSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.math.BigInteger;
+
+import static org.bitcoinj.core.Coin.COIN;
+
+/**
+ * Common parameters for Litecoin networks.
+ */
+public abstract class AbstractPirateChainParams extends NetworkParameters implements AltcoinNetworkParameters {
+    /** Standard format for the PIRATE denomination. */
+    public static final MonetaryFormat PIRATE;
+    /** Standard format for the mPIRATE denomination. */
+    public static final MonetaryFormat MPIRATE;
+    /** Standard format for the BITS denomination. */
+    public static final MonetaryFormat BITS;
+
+    public static final int ARRR_TARGET_TIMESPAN = (int) (3.5 * 24 * 60 * 60); // 3.5 days // TODO
+    public static final int ARRR_TARGET_SPACING = (int) (1 * 60); // 1 minute
+    public static final int ARRR_INTERVAL = ARRR_TARGET_TIMESPAN / ARRR_TARGET_SPACING;
+
+    /**
+     * The maximum number of coins to be generated
+     */
+    public static final long MAX_ARRR = 200000000;
+
+    /**
+     * The maximum money to be generated
+     */
+    public static final Coin MAX_ARRR_MONEY = COIN.multiply(MAX_ARRR);
+
+    /** Currency code for base 1 Litecoin. */
+    public static final String CODE_PIRATE = "PIRATE";
+    /** Currency code for base 1/1,000 Litecoin. */
+    public static final String CODE_MPIRATE = "mPIRATE";
+    /** Currency code for base 1/100,000,000 Litecoin. */
+    public static final String CODE_BITS = "bits";
+
+    static {
+        PIRATE = MonetaryFormat.BTC.noCode()
+            .code(0, CODE_PIRATE)
+            .code(3, CODE_MPIRATE)
+            .code(7, CODE_BITS);
+        MPIRATE = PIRATE.shift(3).minDecimals(2).optionalDecimals(2);
+        BITS = PIRATE.shift(7).minDecimals(0).optionalDecimals(2);
+    }
+
+    /** The string returned by getId() for the main, production network where people trade things. */
+    public static final String ID_ARRR_MAINNET = "main";
+    /** The string returned by getId() for the testnet. */
+    public static final String ID_ARRR_TESTNET = "test";
+    /** The string returned by getId() for regtest. */
+    public static final String ID_ARRR_REGTEST = "regtest";
+
+    public static final int PIRATE_PROTOCOL_VERSION_MINIMUM = 70002;
+    public static final int PIRATE_PROTOCOL_VERSION_CURRENT = 70003;
+
+    private static final Coin BASE_SUBSIDY = COIN.multiply(256);
+
+    protected Logger log = LoggerFactory.getLogger(AbstractPirateChainParams.class);
+
+    public AbstractPirateChainParams() {
+        super();
+        interval = ARRR_INTERVAL;
+        targetTimespan = ARRR_TARGET_TIMESPAN;
+        maxTarget = Utils.decodeCompactBits(0x1f07ffffL);
+
+        packetMagic = 0xf9beb4d9;
+        bip32HeaderP2PKHpub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". (?)
+        bip32HeaderP2PKHpriv = 0x0488ADE4; //The 4 byte header that serializes in base58 to "xprv" (?)
+    }
+
+    @Override
+    public Coin getBlockSubsidy(final int height) {
+        return BASE_SUBSIDY.shiftRight(height / getSubsidyDecreaseBlockCount());
+    }
+
+    /**
+     * Get the hash to use for a block.
+     */
+    @Override
+    public Sha256Hash getBlockDifficultyHash(Block block) {
+        return ((AltcoinBlock) block).getScryptHash();
+    }
+
+    public MonetaryFormat getMonetaryFormat() {
+        return PIRATE;
+    }
+
+    @Override
+    public Coin getMaxMoney() {
+        return MAX_ARRR_MONEY;
+    }
+
+    @Override
+    public Coin getMinNonDustOutput() {
+        return Coin.valueOf(100000);
+    }
+
+    @Override
+    public String getUriScheme() {
+        return "pirate";
+    }
+
+    @Override
+    public boolean hasMaxMoney() {
+        return true;
+    }
+
+
+    @Override
+    public void checkDifficultyTransitions(StoredBlock storedPrev, Block nextBlock, BlockStore blockStore)
+        throws VerificationException, BlockStoreException {
+        try {
+            final long newTargetCompact = calculateNewDifficultyTarget(storedPrev, nextBlock, blockStore);
+            final long receivedTargetCompact = nextBlock.getDifficultyTarget();
+
+            if (newTargetCompact != receivedTargetCompact)
+                throw new VerificationException("Network provided difficulty bits do not match what was calculated: " +
+                        newTargetCompact + " vs " + receivedTargetCompact);
+        } catch (CheckpointEncounteredException ex) {
+            // Just have to take it on trust then
+        }
+    }
+
+    /**
+     * Get the difficulty target expected for the next block. This includes all
+     * the weird cases for Litecoin such as testnet blocks which can be maximum
+     * difficulty if the block interval is high enough.
+     *
+     * @throws CheckpointEncounteredException if a checkpoint is encountered while
+     * calculating difficulty target, and therefore no conclusive answer can
+     * be provided.
+     */
+    public long calculateNewDifficultyTarget(StoredBlock storedPrev, Block nextBlock, BlockStore blockStore)
+        throws VerificationException, BlockStoreException, CheckpointEncounteredException {
+        final Block prev = storedPrev.getHeader();
+        final int previousHeight = storedPrev.getHeight();
+        final int retargetInterval = this.getInterval();
+
+        // Is this supposed to be a difficulty transition point?
+        if ((storedPrev.getHeight() + 1) % retargetInterval != 0) {
+            if (this.allowMinDifficultyBlocks()) {
+                // Special difficulty rule for testnet:
+                // If the new block's timestamp is more than 5 minutes
+                // then allow mining of a min-difficulty block.
+                if (nextBlock.getTimeSeconds() > prev.getTimeSeconds() + getTargetSpacing() * 2) {
+                    return Utils.encodeCompactBits(maxTarget);
+                } else {
+                    // Return the last non-special-min-difficulty-rules-block
+                    StoredBlock cursor = storedPrev;
+
+                    while (cursor.getHeight() % retargetInterval != 0
+                            && cursor.getHeader().getDifficultyTarget() == Utils.encodeCompactBits(this.getMaxTarget())) {
+                        StoredBlock prevCursor = cursor.getPrev(blockStore);
+                        if (prevCursor == null) {
+                            break;
+                        }
+                        cursor = prevCursor;
+                    }
+
+                    return cursor.getHeader().getDifficultyTarget();
+                }
+            }
+
+            // No ... so check the difficulty didn't actually change.
+            return prev.getDifficultyTarget();
+        }
+
+        // We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every
+        // two weeks after the initial block chain download.
+        StoredBlock cursor = storedPrev;
+        int goBack = retargetInterval - 1;
+
+        // Litecoin: This fixes an issue where a 51% attack can change difficulty at will.
+        // Go back the full period unless it's the first retarget after genesis.
+        // Code based on original by Art Forz
+        if (cursor.getHeight()+1 != retargetInterval)
+            goBack = retargetInterval;
+
+        for (int i = 0; i < goBack; i++) {
+            if (cursor == null) {
+                // This should never happen. If it does, it means we are following an incorrect or busted chain.
+                throw new VerificationException(
+                        "Difficulty transition point but we did not find a way back to the genesis block.");
+            }
+            cursor = blockStore.get(cursor.getHeader().getPrevBlockHash());
+        }
+
+        //We used checkpoints...
+        if (cursor == null) {
+            log.debug("Difficulty transition: Hit checkpoint!");
+            throw new CheckpointEncounteredException();
+        }
+
+        Block blockIntervalAgo = cursor.getHeader();
+        return this.calculateNewDifficultyTargetInner(previousHeight, prev.getTimeSeconds(),
+            prev.getDifficultyTarget(), blockIntervalAgo.getTimeSeconds(),
+            nextBlock.getDifficultyTarget());
+    }
+
+    /**
+     * Calculate the difficulty target expected for the next block after a normal
+     * recalculation interval. Does not handle special cases such as testnet blocks
+     * being setting the target to maximum for blocks after a long interval.
+     *
+     * @param previousHeight height of the block immediately before the retarget.
+     * @param prev the block immediately before the retarget block.
+     * @param nextBlock the block the retarget happens at.
+     * @param blockIntervalAgo The last retarget block.
+     * @return New difficulty target as compact bytes.
+     */
+    protected long calculateNewDifficultyTargetInner(int previousHeight, final Block prev,
+            final Block nextBlock, final Block blockIntervalAgo) {
+        return this.calculateNewDifficultyTargetInner(previousHeight, prev.getTimeSeconds(),
+            prev.getDifficultyTarget(), blockIntervalAgo.getTimeSeconds(),
+            nextBlock.getDifficultyTarget());
+    }
+
+    /**
+     *
+     * @param previousHeight Height of the block immediately previous to the one we're calculating difficulty of.
+     * @param previousBlockTime Time of the block immediately previous to the one we're calculating difficulty of.
+     * @param lastDifficultyTarget Compact difficulty target of the last retarget block.
+     * @param lastRetargetTime Time of the last difficulty retarget.
+     * @param nextDifficultyTarget The expected difficulty target of the next
+     * block, used for determining precision of the result.
+     * @return New difficulty target as compact bytes.
+     */
+    protected long calculateNewDifficultyTargetInner(int previousHeight, long previousBlockTime,
+        final long lastDifficultyTarget, final long lastRetargetTime,
+        final long nextDifficultyTarget) {
+        final int retargetTimespan = this.getTargetTimespan();
+        int actualTime = (int) (previousBlockTime - lastRetargetTime);
+        final int minTimespan = retargetTimespan / 4;
+        final int maxTimespan = retargetTimespan * 4;
+
+        actualTime = Math.min(maxTimespan, Math.max(minTimespan, actualTime));
+
+        BigInteger newTarget = Utils.decodeCompactBits(lastDifficultyTarget);
+        newTarget = newTarget.multiply(BigInteger.valueOf(actualTime));
+        newTarget = newTarget.divide(BigInteger.valueOf(retargetTimespan));
+
+        if (newTarget.compareTo(this.getMaxTarget()) > 0) {
+            log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16));
+            newTarget = this.getMaxTarget();
+        }
+
+        int accuracyBytes = (int) (nextDifficultyTarget >>> 24) - 3;
+
+        // The calculated difficulty is to a higher precision than received, so reduce here.
+        BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8);
+        newTarget = newTarget.and(mask);
+        return Utils.encodeCompactBits(newTarget);
+    }
+
+    @Override
+    public AltcoinSerializer getSerializer(boolean parseRetain) {
+        return new AltcoinSerializer(this, parseRetain);
+    }
+
+    @Override
+    public int getProtocolVersionNum(final ProtocolVersion version) {
+        switch (version) {
+            case PONG:
+            case BLOOM_FILTER:
+                return version.getBitcoinProtocolVersion();
+            case CURRENT:
+                return PIRATE_PROTOCOL_VERSION_CURRENT;
+            case MINIMUM:
+            default:
+                return PIRATE_PROTOCOL_VERSION_MINIMUM;
+        }
+    }
+
+    /**
+     * Whether this network has special rules to enable minimum difficulty blocks
+     * after a long interval between two blocks (i.e. testnet).
+     */
+    public boolean allowMinDifficultyBlocks() {
+        return this.isTestNet();
+    }
+
+    public int getTargetSpacing() {
+        return this.getTargetTimespan() / this.getInterval();
+    }
+
+    private static class CheckpointEncounteredException extends Exception {  }
+}
diff --git a/core/src/main/java/org/libdohj/params/PirateChainMainNetParams.java b/core/src/main/java/org/libdohj/params/PirateChainMainNetParams.java
new file mode 100644
index 00000000..e7d2d7a8
--- /dev/null
+++ b/core/src/main/java/org/libdohj/params/PirateChainMainNetParams.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2014 Andreas Schildbach
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Adapted for Pirate Chain in May 2022 by Qortal dev team
+ */
+
+package org.libdohj.params;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.params.MainNetParams;
+import org.bitcoinj.script.Script;
+import org.bitcoinj.script.ScriptOpCodes;
+import org.bouncycastle.util.encoders.Hex;
+
+import java.io.ByteArrayOutputStream;
+
+import static org.bitcoinj.core.Coin.COIN;
+
+/**
+ * Parameters for the Pirate Chain main production network on which people trade
+ * goods and services.
+ * Note: these are incomplete and can only be used for limited use cases
+ */
+public class PirateChainMainNetParams extends AbstractPirateChainParams {
+    public static final int MAINNET_MAJORITY_WINDOW = MainNetParams.MAINNET_MAJORITY_WINDOW;
+    public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
+    public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
+
+    public PirateChainMainNetParams() {
+        super();
+        id = ID_ARRR_MAINNET;
+        // Genesis hex is 0x027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71
+        packetMagic = 0xf9beb4d9;
+
+        maxTarget = Utils.decodeCompactBits(0x200f0f0fL);
+        port = 7770;
+        addressHeader = 60;
+        p2shHeader = 85;
+        segwitAddressHrp = "zs";
+        // acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
+        dumpedPrivateKeyHeader = 188;
+
+        this.genesisBlock = createGenesis(this);
+        spendableCoinbaseDepth = 100;
+        subsidyDecreaseBlockCount = 210000;
+
+        String genesisHash = genesisBlock.getHashAsString();
+        // TODO: The genesis hash currently does not match, so checking has temporarily been disabled. This will need fixing before the complete implementation can be used.
+        //checkState(genesisHash.equals("")); // TODO: 0x027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71
+        alertSigningKey = Hex.decode("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f");
+
+        majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
+        majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
+        majorityWindow = MAINNET_MAJORITY_WINDOW;
+
+        dnsSeeds = new String[] {
+                "pirate1.cryptoforge.cc", // Forge
+                "pirate2.cryptoforge.cc", // Forge
+                "pirate3.cryptoforge.cc", // Forge
+                "explorer.cryptoforge.cc", // Forge
+                "explorer.pirate.black", // Explorer
+                "mseed.dexstats.info", // Dexstats chmex
+                "seed.komodostats.com", // komodostats webworker01
+                "bootstrap.arrr.black" // geo optimized routing enabled chmex
+        };
+        bip32HeaderP2PKHpub = 0x0488B21E;
+        bip32HeaderP2PKHpriv = 0x0488ADE4;
+    }
+
+    private static AltcoinBlock createGenesis(NetworkParameters params) {
+        AltcoinBlock genesisBlock = new AltcoinBlock(params, 4L);
+        Transaction t = new Transaction(params);
+        try {
+            byte[] bytes = Hex.decode
+                    ("000a889f00854b8665cd555f4656f68179d31ccadc1b1f7fb0952726313b16941da348284d67add4686121d4e3d930160c1348d8191c25f12b267a6a9c131b5031cbf8af1f79c9d513076a216ec87ed045fa966e01214ed83ca02dc1797270a454720d3206ac7d931a0a680c5c5e099057592570ca9bdf6058343958b31901fce1a15a4f38fd347750912e14004c73dfe588b903b6c03166582eeaf30529b14072a7b3079e3a684601b9b3024054201f7440b0ee9eb1a7120ff43f713735494aa27b1f8bab60d7f398bca14f6abb2adbf29b04099121438a7974b078a11635b594e9170f1086140b4173822dd697894483e1c6b4e8b8dcd5cb12ca4903bc61e108871d4d915a9093c18ac9b02b6716ce1013ca2c1174e319c1a570215bc9ab5f7564765f7be20524dc3fdf8aa356fd94d445e05ab165ad8bb4a0db096c097618c81098f91443c719416d39837af6de85015dca0de89462b1d8386758b2cf8a99e00953b308032ae44c35e05eb71842922eb69797f68813b59caf266cb6c213569ae3280505421a7e3a0a37fdf8e2ea354fc5422816655394a9454bac542a9298f176e211020d63dee6852c40de02267e2fc9d5e1ff2ad9309506f02a1a71a0501b16d0d36f70cdfd8de78116c0c506ee0b8ddfdeb561acadf31746b5a9dd32c21930884397fb1682164cb565cc14e089d66635a32618f7eb05fe05082b8a3fae620571660a6b89886eac53dec109d7cbb6930ca698a168f301a950be152da1be2b9e07516995e20baceebecb5579d7cdbc16d09f3a50cb3c7dffe33f26686d4ff3f8946ee6475e98cf7b3cf9062b6966e838f865ff3de5fb064a37a21da7bb8dfd2501a29e184f207caaba364f36f2329a77515dcb710e29ffbf73e2bbd773fab1f9a6b005567affff605c132e4e4dd69f36bd201005458cfbd2c658701eb2a700251cefd886b1e674ae816d3f719bac64be649c172ba27a4fd55947d95d53ba4cbc73de97b8af5ed4840b659370c556e7376457f51e5ebb66018849923db82c1c9a819f173cccdb8f3324b239609a300018d0fb094adf5bd7cbb3834c69e6d0b3798065c525b20f040e965e1a161af78ff7561cd874f5f1b75aa0bc77f720589e1b810f831eac5073e6dd46d00a2793f70f7427f0f798f2f53a67e615e65d356e66fe40609a958a05edb4c175bcc383ea0530e67ddbe479a898943c6e3074c6fcc252d6014de3a3d292b03f0d88d312fe221be7be7e3c59d07fa0f2f4029e364f1f355c5d01fa53770d0cd76d82bf7e60f6903bc1beb772e6fde4a70be51d9c7e03c8d6d8dfb361a234ba47c470fe630820bbd920715621b9fbedb49fcee165ead0875e6c2b1af16f50b5d6140cc981122fcbcf7c5a4e3772b3661b628e08380abc545957e59f634705b1bbde2f0b4e055a5ec5676d859be77e20962b645e051a880fddb0180b4555789e1f9344a436a84dc5579e2553f1e5fb0a599c137be36cabbed0319831fea3fddf94ddc7971e4bcf02cdc93294a9aab3e3b13e3b058235b4f4ec06ba4ceaa49d675b4ba80716f3bc6976b1fbf9c8bf1f3e3a4dc1cd83ef9cf816667fb94f1e923ff63fef072e6a19321e4812f96cb0ffa864da50ad74deb76917a336f31dce03ed5f0303aad5e6a83634f9fcc371096f8288b8f02ddded5ff1bb9d49331e4a84dbe1543164438fde9ad71dab024779dcdde0b6602b5ae0a6265c14b94edd83b37403f4b78fcd2ed555b596402c28ee81d87a909c4e8722b30c71ecdd861b05f61f8b1231795c76adba2fdefa451b283a5d527955b9f3de1b9828e7b2e74123dd47062ddcc09b05e7fa13cb2212a6fdbc65d7e852cec463ec6fd929f5b8483cf3052113b13dac91b69f49d1b7d1aec01c4a68e41ce157");
+            t.addInput(new TransactionInput(params, t, bytes));
+            ByteArrayOutputStream scriptPubKeyBytes = new ByteArrayOutputStream();
+            Script.writeBytes(scriptPubKeyBytes, Hex.decode
+                    ("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"));
+            scriptPubKeyBytes.write(ScriptOpCodes.OP_CHECKSIG);
+            t.addOutput(new TransactionOutput(params, t, COIN.multiply(50), scriptPubKeyBytes.toByteArray()));
+        } catch (Exception e) {
+            // Cannot happen.
+            throw new RuntimeException(e);
+        }
+        genesisBlock.addTransaction(t);
+        genesisBlock.setTime(1231006505L);
+        genesisBlock.setDifficultyTarget(0x200f0f0fL);
+        genesisBlock.setNonce(0x000000000000000000000000000000000000000000000000000000000000000bL);
+        // TODO: merkle root
+        return genesisBlock;
+    }
+
+    private static PirateChainMainNetParams instance;
+    public static synchronized PirateChainMainNetParams get() {
+        if (instance == null) {
+            instance = new PirateChainMainNetParams();
+        }
+        return instance;
+    }
+
+    @Override
+    public String getPaymentProtocolId() {
+        return ID_ARRR_MAINNET;
+    }
+
+    @Override
+    public boolean isTestNet() {
+        return false;
+    }
+}