Doge Inucore v1.14.7-inu
Doge Inu on X
specification · revised 2026-09-14

Doge Inu Core protocol documentation

This document specifies the consensus rules, data structures, and network behaviour of Doge Inu Core v1.14.7-inu. It assumes you know how a Bitcoin-lineage UTXO node works and describes only what we changed, plus the two subsystems that did not exist upstream.

Source and lineage

Doge Inu Core is a hard fork of dogecoin/dogecoin at tag v1.14.7, commit 3a29ba6. That tree is itself descended from Litecoin 0.8, which descends from Bitcoin Core 0.8. We keep the upstream remote wired up and rebase security fixes forward; as of this revision we are 6,482 commits ahead and 0 behind.

dogeinu-core
The node. C++17, autotools, AGPL-3.0. Drop-in replacement for dogecoind with an extra -splindex flag.
inu-validator
The vote client. Rust 1.79, Tokio, connects to the node over the ZMQ block notifier and its own gossip mesh.
spl-shim
The translation layer. Maps SPL Token instruction encodings onto Doge Inu script and back, so Solana tooling can address the chain without modification.
inu-rpc-proxy
A JSON-RPC front end that answers Solana getAccountInfo and getTokenAccountsByOwner calls from UTXO state.

Architecture

A Doge Inu block is a Dogecoin block. Byte for byte, a v1.14.7 node can parse the header, verify the Scrypt proof-of-work, and check the merkle root. What it cannot do is validate the asset plane, because the asset plane lives in output fields that the old deserialiser stops reading before it reaches.

The three planes are evaluated in a fixed order and each can veto the block. Settlement runs first because it is cheapest to fail. The asset plane runs second, against the coins view that settlement just produced. The vote plane runs last and asynchronously — it cannot reject a block, only decline to finalise it.

src/validation.cppConnectBlock — plane ordering
bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state,
                               CBlockIndex* pindex, CCoinsViewCache& view,
                               const CChainParams& chainparams, bool fJustCheck)
{
    // Plane 1 — settlement. Unmodified from upstream apart from the call
    // into the asset pass at the bottom of the tx loop.
    if (!CheckBlock(block, state, chainparams.GetConsensus(), !fJustCheck, !fJustCheck))
        return error("%s: CheckBlock failed", __func__);

    int64_t nAssetOps = 0;
    for (const auto& tx : block.vtx) {
        if (!CheckTxInputs(*tx, state, view, pindex->nHeight))
            return false;

        // Plane 2 — asset. Conservation is a consensus rule, so a single
        // bad transfer invalidates the whole block rather than being
        // silently dropped from the mempool.
        if (pindex->nHeight >= chainparams.GetConsensus().nInuAssetHeight) {
            if (!Consensus::CheckAssetConservation(*tx, view, state))
                return false;
            nAssetOps += CountAssetOps(*tx);
        }
    }

    if (nAssetOps > MAX_BLOCK_ASSET_OPS)
        return state.DoS(100, false, REJECT_INVALID, "bad-blk-assetops");

    const uint256 assetRoot = view.ComputeAssetRoot();
    if (assetRoot != block.hashAssetRoot)
        return state.DoS(100, false, REJECT_INVALID, "bad-asset-root", false,
                         strprintf("computed=%s header=%s",
                                   assetRoot.ToString(), block.hashAssetRoot.ToString()));

    // Plane 3 — vote. Advisory at connect time; inu-validator observes the
    // ZMQ notification and votes out of band.
    g_vote_notifier.NotifyBlockConnected(pindex->nHeight, block.GetHash(), assetRoot);

    return true;
}

Consensus changes

Eleven consensus rules changed. Nine are additive and apply only to transactions that carry a non-null asset commitment. Two touch existing behaviour and are the reason this is a hard fork rather than a soft one.

ruleactivationkinddescription
INU_TXOUT_WIDEN5,100,000breakingCTxOut gains assetCommitment and assetAmount
INU_HEADER_ASSETROOT5,100,000breakingBlock header commits to the asset state root
INU_OP_SPLMINT5,100,000additiveOP_NOP7 redefined as a mint authorisation
INU_OP_SPLXFER5,100,000additiveOP_NOP8 redefined as an asset transfer
INU_ED255195,100,000additiveCHECKSIG accepts ed25519 when the pubkey prefix is 0x05
INU_ATA_DERIVE5,100,000additiveAssociated token account derivation is consensus-fixed
INU_DECIMALS_MAX5,100,000additiveMint decimals capped at 18
INU_FREEZE5,214,000additiveFreeze authority can immobilise a token account
INU_VOTE_ANCHOR5,214,000additiveVote accounts and stake registration
INU_ASSETOP_LIMIT5,301,500additive4,194,304 compute units per block
INU_MINT_RENT5,301,500additiveMint accounts require a dust-exempt native output
hard fork notice

Nodes below v1.14.7-inu stopped following the canonical chain at height 5,100,000 on 14 March 2026. There is no replay protection between the two chains for native DINU transfers because the pre-fork UTXO set is shared. If you held DINU across the fork, move it once on each chain with distinct outputs before doing anything else.

Serialization

The wire format change is gated on the stream version, which is how the same code path serves both a modern peer and an archival block read off disk. Version INU_ASSET_VERSION is 70016; anything below it round-trips as upstream.

A non-native output costs 40 extra bytes: 32 for the commitment and 8 for the amount. We considered a varint amount and rejected it, because a fixed width keeps the conservation arithmetic branch-free and the UTXO database record size constant, which matters more to LevelDB than 6 bytes of average saving.

wire formatnon-native output, 78 bytes minimum
  offset  size  field                description
  ------  ----  -------------------  ---------------------------------------
       0     8  nValue               native DINU in koinu, LE
       8   1-9  scriptLen            compact size
       9     n  scriptPubKey         locking script
     9+n    32  assetCommitment      sha256d(mint_pubkey || b"inu-asset-v1")
    41+n     8  assetAmount          base units, LE, respects mint decimals
    ------  ----  -----------------  ---------------------------------------
                                     all-zero commitment ⇒ fields omitted
                                     and the output is byte-identical to
                                     an upstream Dogecoin output

SPL compatibility

“SPL compatible” is a claim people make loosely, so here is the precise one. Doge Inu implements the consensus-relevant subset of the SPL Token program: InitializeMint, InitializeAccount, Transfer, MintTo, Burn, SetAuthority, FreezeAccount, ThawAccount, and CloseAccount. Instruction encodings are byte-identical to the upstream program, which is what lets unmodified Solana tooling build transactions against us.

What we deliberately do not implement: Approve and the delegate model, multisig authorities beyond 3-of-5, the Token-2022 extension set, and confidential transfers. Delegates assume an account model where a third party can mutate state you own without spending your output. In a UTXO chain that concept has no home, and faking it produces a worse security model than leaving it out.

How a Solana instruction becomes a script

spl-shim takes a serialised SPL instruction, extracts the fields consensus cares about, and emits a Doge Inu script. The mapping is total and reversible for the supported instruction set, so the shim can also decompile a script back into an instruction for display in wallets.

shim/src/translate.rsSPL instruction → Doge Inu script
pub fn translate(ix: &TokenInstruction, ctx: &Ctx) -> Result<Script, ShimError> {
    match ix {
        TokenInstruction::Transfer { amount } => {
            let src = ctx.account(0)?;
            let dst = ctx.account(1)?;
            let owner = ctx.signer(2)?;

            Ok(Script::builder()
                .push_slice(&commitment_of(src)?)   // which asset
                .push_slice(&dst.to_bytes())        // where it goes
                .push_int(*amount as i64)           // how much, base units
                .push_slice(&owner.to_bytes())      // who authorised it
                .push_opcode(OP_SPLXFER)
                .into_script())
        }

        TokenInstruction::MintTo { amount } => {
            let mint = ctx.account(0)?;
            let dst = ctx.account(1)?;
            let authority = ctx.signer(2)?;

            // The authority must match what the chain currently records for
            // this mint, not what the caller claims. Consensus re-reads it.
            Ok(Script::builder()
                .push_slice(&commitment_of(mint)?)
                .push_slice(&dst.to_bytes())
                .push_int(*amount as i64)
                .push_slice(&authority.to_bytes())
                .push_opcode(OP_SPLMINT)
                .into_script())
        }

        TokenInstruction::Approve { .. } => Err(ShimError::Unsupported {
            instruction: "Approve",
            reason: "delegated spend has no UTXO equivalent; use a 2-of-2 multisig output",
        }),

        other => Err(ShimError::Unsupported {
            instruction: other.name(),
            reason: "not in the consensus-relevant subset",
        }),
    }
}

New opcodes

Two opcodes carry the entire asset layer. Both were no-ops upstream, so a pre-fork node executing them succeeds trivially rather than failing — which is why the serialisation change, not the opcodes, is what makes this a hard fork.

opcodebytewasstack in (top last)CU
OP_SPLMINT0xb6OP_NOP7commitment dst amount authority3,400
OP_SPLXFER0xb7OP_NOP8commitment dst amount owner1,200
OP_SPLBURN0xb8OP_NOP9commitment amount owner900
OP_SPLAUTH0xb9OP_NOP10commitment kind newauth oldauth2,100
src/script/interpreter.cppOP_SPLXFER evaluation
case OP_SPLXFER:
{
    if (!(flags & SCRIPT_VERIFY_INU_ASSETS)) break;   // pre-activation: no-op
    if (stack.size() < 4)
        return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

    const valtype& vchOwner      = stacktop(-1);
    const CScriptNum amount      (stacktop(-2), fRequireMinimal, 8);
    const valtype& vchDest       = stacktop(-3);
    const valtype& vchCommitment = stacktop(-4);

    if (vchCommitment.size() != 32) return set_error(serror, SCRIPT_ERR_INU_BAD_COMMITMENT);
    if (vchDest.size() != 32)       return set_error(serror, SCRIPT_ERR_INU_BAD_DEST);
    if (amount <= 0)                return set_error(serror, SCRIPT_ERR_INU_BAD_AMOUNT);

    // The signature covers the commitment, destination and amount, so a
    // relay node cannot re-point a transfer at itself without invalidating it.
    if (!checker.CheckAssetAuthorisation(vchOwner, vchCommitment, vchDest,
                                         amount.getint64(), SIGVERSION_INU))
        return set_error(serror, SCRIPT_ERR_INU_BAD_AUTH);

    popstack(stack); popstack(stack); popstack(stack); popstack(stack);
    stack.push_back(vchTrue);
    nComputeUnits += CU_SPLXFER;
    if (nComputeUnits > MAX_TX_COMPUTE_UNITS)
        return set_error(serror, SCRIPT_ERR_INU_CU_EXCEEDED);
}
break;

Address derivation

Doge Inu addresses come in two flavours. Native DINU still uses base58check with version byte 0x1e, so a legacy Dogecoin address is a valid Doge Inu address and looks like D…. Asset-bearing addresses are ed25519 public keys rendered as raw base58, exactly as on Solana, so a Phantom address works unmodified.

Associated token accounts are derived with the same program-derived-address algorithm Solana uses, with a different seed suffix so the two chains never collide.

src/asset/ata.cppATA derivation
// Mirrors spl_associated_token_account::get_associated_token_address,
// with "inu-ata-v1" appended so a Solana ATA and a Doge Inu ATA for the
// same (owner, mint) pair are always different accounts.
uint256 DeriveAssociatedTokenAccount(const CPubKey256& owner,
                                     const uint256& mint)
{
    std::vector<unsigned char> seeds;
    seeds.insert(seeds.end(), owner.begin(), owner.end());
    seeds.insert(seeds.end(), INU_TOKEN_PROGRAM_ID.begin(), INU_TOKEN_PROGRAM_ID.end());
    seeds.insert(seeds.end(), mint.begin(), mint.end());
    seeds.insert(seeds.end(), INU_ATA_DOMAIN.begin(), INU_ATA_DOMAIN.end());

    for (uint8_t bump = 255; bump > 0; --bump) {
        seeds.push_back(bump);
        uint256 candidate = Sha256(seeds);
        seeds.pop_back();

        // Reject anything that lands on the ed25519 curve: those have a
        // private key somewhere and must not be program-controlled.
        if (!IsOnEd25519Curve(candidate)) return candidate;
    }
    throw std::runtime_error("no off-curve ATA found (probability ~2^-255)");
}

Mint and freeze authority

A mint account records two optional authorities. The mint authority may increase supply. The freeze authority may immobilise an individual token account. Either can be set to none, permanently, and there is no mechanism to restore one — OP_SPLAUTH rejects a transition out of the null authority state.

We take a position here that upstream does not: a mint whose authority is still live is flagged as such everywhere in our tooling. The explorer shows a mint open badge, the RPC returns "authority_live": true, and the launcher warns before you send. Whether that is a risk is the holder's call; whether it is visible is ours.

mint_authority
32-byte ed25519 pubkey, or null. Controls MintTo. One-way revoke.
freeze_authority
32-byte ed25519 pubkey, or null. Controls FreezeAccount / ThawAccount.
decimals
uint8, 0–18 inclusive. Immutable after InitializeMint.
supply
uint64 base units. Derived from the UTXO set, never stored.
rent
1.0 DINU held in a dust-exempt output, reclaimable on CloseAccount.

inu-validator

The validator is a separate process from the node. This is deliberate: a bug in the vote client should degrade finality, not halt settlement. If every validator on the network crashed simultaneously, Doge Inu would keep producing blocks at 60 seconds with Dogecoin's original probabilistic finality, and the asset plane would keep validating. You would just wait longer to be sure.

validator configminimum viable operator setup
$ cargo install --locked inu-validator
$ inu-validator init --identity ~/.inu/identity.json --network mainnet
$ inu-validator stake register \
    --amount 250000 \
    --commission 5 \
    --withdraw-authority ~/.inu/withdraw.json

registered vote account  VoTeQx7n2mK9pL4rW8sT1yB6cH3gJ5dF0aZ...
stake activating         250000 DINU, live at epoch 1482 (in 6h 11m)
commission               5%
identity                 iNuId3nT8wQ2eR7tY1uI6oP0aS9dF4gH...

$ inu-validator run --node http://127.0.0.1:22555 --zmq tcp://127.0.0.1:28332
[00:00:00] gossip: joined mesh, 1204 peers, shred version 41207
[00:00:02] tower: loaded 31 lockouts, root slot 812604118
[00:00:02] anchor: settlement tip 5417392, scrypt work verified
[00:00:03] vote: slot 812608850 -> submitted (lockout 2)
[00:00:03] vote: slot 812608851 -> submitted (lockout 2)
[00:00:04] finality: slot 812608850 rooted, 68.4% stake, 1.61s

Auxpow anchoring

The security argument for Doge Inu rests on one idea, so it is worth stating it plainly.

Stake decides the order of things quickly. Work decides whether that order is expensive to rewrite. A vote that does not name work is not a vote.

Every Tower BFT vote carries an AuxPowHeader — the Litecoin-merge-mined header of the settlement block the validator is voting relative to. The vote is discarded unless the Scrypt work in that header verifies against the current difficulty target and its height is monotonically non-decreasing for that validator.

The consequence: an attacker who acquires two-thirds of DINU stake can stall finality (they can refuse to vote) but cannot finalise a competing history, because producing the anchors for that history requires out-mining the Litecoin hashrate that Dogecoin merge-mines against. Stake is a liveness resource here, not a safety one.

honest limitation

This does mean Doge Inu inherits Dogecoin's dependence on Litecoin's merge-mining hashrate. If that hashrate collapsed, our finality guarantee would degrade to “whatever stake says”, which is a materially weaker claim. We monitor the ratio and publish it on the explorer.

Slashing conditions

conditiondetectionpenaltyappeal
equivocation (two votes, same slot, different hash)any peer submits both signed votes100% of stakenone
lockout violationtower state replay100% of stakenone
anchoring to unverified workheader re-verification at vote ingest5% of stake14-epoch window
liveness failure1,000 consecutive missed slots0.5% per 1,000automatic on resume
commission fraudepoch reward reconciliation2× the overchargegovernance vote

Run a node

A full node with the asset index needs 16 GB of RAM and about 340 GB of NVMe. You can run without -splindex on 8 GB, but then getsplbalance and the explorer RPCs are unavailable — the node still validates the asset plane, it just does not keep a queryable index of it.

build from sourceDebian 12 / Ubuntu 24.04
$ sudo apt install -y build-essential libtool autotools-dev automake pkg-config \
      bsdmainutils python3 libevent-dev libboost-dev libdb5.3++-dev libsodium-dev

$ git clone https://github.com/dogeinu/dogeinu-core.git
$ cd dogeinu-core
$ git checkout v1.14.7-inu
$ ./autogen.sh
$ ./configure --with-incompatible-bdb --enable-inu-assets --disable-tests
$ make -j$(nproc)

$ cat > ~/.dogeinu/dogeinu.conf <<'EOF'
server=1
txindex=1
splindex=1
dbcache=6000
maxmempool=1200
zmqpubhashblock=tcp://127.0.0.1:28332
rpcbind=127.0.0.1
rpcport=22555
EOF

$ ./src/dogeinud -daemon
$ ./src/dogeinu-cli getblockchaininfo | jq '{blocks, assetcommitments, verificationprogress}'
{
  "blocks": 5417392,
  "assetcommitments": 38114,
  "verificationprogress": 0.9999981
}

RPC reference

Every upstream Dogecoin RPC still works. These are the additions. All of them are available over the standard JSON-RPC endpoint on port 22555; the Solana-shaped endpoints live behind inu-rpc-proxy on 8899.

methodparamsreturnsneeds splindex
createsplmint{decimals, supply, name, symbol}mint address, txid, feeno
sendsplmint, dest, amounttxidyes
getsplbalanceaddress [, mint]map of mint → balanceyes
listsplassets[start, count]paginated mint recordsyes
getsplmintinfomintdecimals, supply, authoritiesyes
revokeauthoritymint, kindtxidno
verifyassetstate[height]root, conserved, elapsed_msno
getfinalitystatustxidslot, stake weight, rootedno
getvalidatorset[epoch]identities, stake, commissionno
JSON-RPCgetsplmintinfo response
{
  "mint": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PB5wBQZ",
  "symbol": "WOW",
  "name": "WOWCOIN",
  "decimals": 6,
  "supply": "1000000000.000000",
  "holders": 4182,
  "mint_authority": "DiNuQq8xYx4FhVvJ2rC1mA6KeT9sW3uB7nZpLk5gH4dR",
  "freeze_authority": null,
  "authority_live": true,
  "created_at_height": 5401882,
  "created_at_slot": 810288211,
  "rent_output": { "txid": "4f1a2e9c...97a3", "vout": 1, "value": "1.00000000" },
  "asset_root_at_tip": "e70bd5c81f9a2364...44c1"
}

Benchmarks

The soak harness replays a captured mainnet block range at an accelerated rate and mixes in synthetic asset traffic at a configurable ratio. Everything below is the 60/40 native/asset mix, which is roughly what the live chain has settled at.

bench/soak.log24h soak, 12 nodes, excerpt
  hour   blocks   tx/blk   asset%   val p50   val p99   utxo GB   reorgs   faults
  ----   ------   ------   ------   -------   -------   -------   ------   ------
     1      240    1,880    39.4%     54 ms    381 ms      9.91        0        0
     4      960    2,140    40.1%     56 ms    394 ms     10.28        1        0
     8    1,920    2,301    41.8%     57 ms    402 ms     10.74        1        0
    12    2,880    2,288    40.6%     58 ms    399 ms     11.02        2        0
    16    3,840    2,412    40.2%     58 ms    404 ms     11.19        2        0
    20    4,800    2,390    39.8%     59 ms    411 ms     11.31        3        0
    24    5,760    2,366    40.0%     58 ms    402 ms     11.44        3        0

  peak sustained throughput      14,880 tx/s  (asset plane, 400ms slots)
  peak settlement throughput         39 tx/s  (block-size bound, unchanged)
  finality p50 / p99             1.61s / 12.4s
  conservation faults                     0
  asset root mismatches                   0

The gap between 14,880 and 39 is the honest headline. The asset plane is fast because it is bounded by compute units and slot time. The settlement plane is slow because it is bounded by a 1 MB block every 60 seconds, which we did not change and do not intend to. Asset transfers that never touch a native output settle on the fast path; anything that moves DINU waits for a block like it always did.

Upgrade history

versiondateheightcontents
v1.14.7-inu2026-03-145,100,000Fork from Dogecoin Core. Asset layer, ed25519, opcodes.
v1.14.7-inu.22026-05-025,214,000Freeze authority, vote accounts, stake registration.
v1.14.7-inu.32026-07-195,301,500Compute unit metering, mint rent, fee market v2.
v1.14.7-inu.42026-09-085,398,200Gossip v1.16, ATA batch derivation, LevelDB 1.23.
v1.15.0-inuplanned Q1 2027Token-2022 transfer hooks, 2 MB settlement blocks.

Known limits

Things that are true and inconvenient, collected in one place so nobody has to discover them the hard way.

  • No delegate model. Approve is unimplemented. Contracts written against SPL delegation will not port without a redesign around multisig outputs.
  • UTXO fragmentation is real. A wallet holding one asset across 400 outputs pays 400 inputs' worth of fees to consolidate. The reference wallet auto-consolidates below 80 outputs; third-party wallets generally do not.
  • Finality is not settlement. A slot can be rooted by stake while the settlement block containing it is still four confirmations deep. Exchanges should key off getfinalitystatus, not confirmation count, and should understand the difference before they do.
  • We inherit Litecoin's hashrate risk. See auxpow anchoring.
  • The asset root adds 32 bytes to every header. Light clients that hard-coded the 80-byte Dogecoin header will fail to parse Doge Inu headers. There is no way around this and we did not find a nicer one.
  • It is still a meme coin. None of the engineering above changes that, and we would rather say so here than let the documentation imply otherwise.

Corrections and spec bugs: open an issue on dogeinu/dogeinu-core with the spec label. Consensus-affecting changes require an INU-IP and two independent implementations before activation.