We forked Dogecoin Core and taught it to speak SPL.
Doge Inu Core is a hard fork of Dogecoin Core v1.14.7. We widened the transaction output to carry an asset commitment, added an ed25519 signature path next to secp256k1, and put a Tower BFT validator on top of the existing Scrypt auxiliary proof-of-work chain. The result is a Dogecoin-lineage UTXO chain that can hold, move, and mint Solana Program Library tokens natively.
6,482 commits ahead of upstream · 214 contributors · AGPL-3.0
Dogecoin has the culture. Solana has the token standard. Nobody had both.
Dogecoin Core is a 2013 fork of Litecoin, which is itself a fork of Bitcoin Core. It inherits the UTXO model, the Scrypt proof-of-work, and — critically — a transaction output structure that has no room in it for anything other than a value and a locking script. There is no asset field. There is no mint authority. Every attempt to put tokens on a Bitcoin-lineage chain has therefore been an overlay: colour the coins, index them off-chain, and pray the indexers agree.
Overlays break at exactly the moment they matter. Because consensus does not know about the asset, consensus cannot reject an invalid transfer of it. Two indexers can hold two different opinions about who owns what and both are, from the chain's point of view, correct. That is not a token standard. That is a shared spreadsheet with extra steps.
Solana solved the other half. The SPL Token program is a genuine standard: mint accounts, associated token accounts, a deterministic address derivation, a decimals field, freeze and mint authorities, and a program that the runtime actually enforces. What it does not have is a merge-mined Scrypt chain with a decade of Dogecoin behind it.
Doge Inu closes the gap at the consensus layer rather than above it. We did not build a bridge. We changed the transaction format.
class CTxOut
{
public:
CAmount nValue;
CScript scriptPubKey;
CTxOut() { SetNull(); }
CTxOut(const CAmount& nValueIn, CScript scriptPubKeyIn);
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nValue);
READWRITE(*(CScriptBase*)(&scriptPubKey));
}
};class CTxOut
{
public:
CAmount nValue;
CScript scriptPubKey;
// 32-byte commitment to the SPL mint this output is denominated in.
// All-zero means the output is native DINU and serialises exactly as
// upstream, which is what keeps old blocks valid under the new rules.
uint256 assetCommitment;
uint64_t assetAmount;
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nValue);
READWRITE(*(CScriptBase*)(&scriptPubKey));
if (s.GetVersion() >= INU_ASSET_VERSION) {
READWRITE(assetCommitment);
READWRITE(assetAmount);
}
}
bool IsNativeOnly() const { return assetCommitment.IsNull(); }
};Three planes, one chain
The node keeps the Dogecoin settlement plane untouched and bolts two new planes beside it. Each plane has its own validity rules, and a block is only valid if all three agree.
| plane | inherited from | what it decides | failure mode | block budget |
|---|---|---|---|---|
| settlement | dogecoin/dogecoin v1.14.7 | UTXO validity, Scrypt PoW, auxpow header, 60 s target | chain reorg | 1,000,000 B |
| asset | solana-labs/solana-program-library | mint supply, authority checks, ATA derivation, decimals | transaction rejected | 4,194,304 CU |
| vote | anza-xyz/agave (Tower BFT) | finality, slashing, stake-weighted lockouts | finality stall | 64 slots |
The asset plane runs inside script
Rather than embedding a full BPF runtime in a C++ node, we compiled the subset of the SPL Token program that has consensus meaning into two new script opcodes. OP_SPLMINT takes a mint account commitment, an amount, and a signature from the mint authority. OP_SPLXFER takes a source commitment, a destination, and an amount, and enforces conservation.
Both opcodes were previously OP_NOP7 and OP_NOP8, which is how the change stays soft-forkable for relay purposes even though the serialisation change makes it a hard fork for validation. Nodes that have not upgraded see two no-ops and a value-zero output, and they do not crash. They are simply wrong about the balance, which is why the activation height is enforced by the version bits in versionbits.cpp.
Conservation is checked in the same loop that already checks nValueIn >= nValueOut. The asset amounts are summed per commitment into a flat map, and any commitment whose inputs do not cover its outputs fails the block, not just the transaction.
bool Consensus::CheckAssetConservation(const CTransaction& tx,
const CCoinsViewCache& view,
CValidationState& state)
{
std::map<uint256, uint64_t> in, out;
for (const CTxIn& txin : tx.vin) {
const CTxOut& prev = view.AccessCoin(txin.prevout).out;
if (prev.IsNativeOnly()) continue;
uint64_t& acc = in[prev.assetCommitment];
if (acc > std::numeric_limits<uint64_t>::max() - prev.assetAmount)
return state.DoS(100, false, REJECT_INVALID, "asset-in-overflow");
acc += prev.assetAmount;
}
for (const CTxOut& txout : tx.vout) {
if (txout.IsNativeOnly()) continue;
out[txout.assetCommitment] += txout.assetAmount;
}
for (const auto& [commitment, amount] : out) {
const uint64_t supplied = in.count(commitment) ? in.at(commitment) : 0;
if (supplied == amount) continue;
// A shortfall is only legal if this transaction carries a valid
// OP_SPLMINT for exactly the difference, signed by the authority
// recorded in the mint account at its current height.
if (amount > supplied && HasValidMintAuthorisation(
tx, commitment, amount - supplied, view))
continue;
return state.DoS(100, false, REJECT_INVALID, "asset-not-conserved",
false, strprintf("commitment=%s in=%d out=%d",
commitment.ToString(), supplied, amount));
}
return true;
}/// A Doge Inu vote is only meaningful if it names a settlement block that
/// actually carries the Scrypt work it claims. Tower BFT gives us fast
/// finality; the auxpow header is what makes that finality expensive to lie
/// about, so every vote carries both and we refuse to count one without the
/// other.
pub fn record_vote(
&mut self,
slot: Slot,
anchor: AuxPowHeader,
identity: &Pubkey,
) -> Result<Lockout, TowerError> {
let stake = self.stakes.get(identity).copied().unwrap_or(0);
if stake < MIN_VOTE_STAKE {
return Err(TowerError::InsufficientStake { have: stake });
}
if !anchor.verify_scrypt_pow(self.network.pow_limit)? {
return Err(TowerError::AnchorWorkInvalid(anchor.hash()));
}
if anchor.height < self.last_anchor_height {
return Err(TowerError::AnchorRegression {
got: anchor.height,
last: self.last_anchor_height,
});
}
let lockout = Lockout::new(slot, self.confirmation_count(slot));
self.votes.push_back((lockout, anchor.hash()));
self.last_anchor_height = anchor.height;
// 31 lockouts deep is the point at which rolling back costs more than
// the cumulative Scrypt work anchored beneath it.
while self.votes.len() > MAX_LOCKOUT_HISTORY {
let (rooted, _) = self.votes.pop_front().expect("non-empty");
self.root = Some(rooted.slot());
}
Ok(lockout)
}We wrote a validator so finality does not take an hour
Dogecoin gives you a 60-second block and probabilistic finality. For a payments meme coin that is fine. For a token layer where someone is swapping a freshly minted asset against a pool, six confirmations of waiting is unusable.
inu-validator is a Rust client that runs alongside dogeinud. It does not produce blocks — miners still do that, and they still merge-mine against Litecoin. What it does is vote. Validators stake DINU, watch the settlement plane, and cast Tower BFT votes on the slot ordering. Once a slot accumulates two-thirds of stake weight behind it, the asset plane treats it as final and the explorer marks it green.
The interesting constraint is that a vote must name an auxiliary proof-of-work header it is anchored to. A validator cannot vote for a fork with no work behind it, which means an attacker who buys two-thirds of stake still has to out-mine Litecoin's hashrate to produce a chain their stake can validly vote for. Stake buys you speed. Work still buys you history.
Minting an asset, from a cold node
The session below is the whole loop: sync a node, create a mint, send a transfer, and read the balance back out of the UTXO set. No indexer involved at any point — every number comes out of consensus state.
$ dogeinud -daemon -splindex=1 -printtoconsole
Doge Inu Core Daemon version v1.14.7-inu (64-bit)
Loading block index... done (4.21s)
Verifying asset commitments for last 288 blocks... done (0.88s)
Progress: 100.00% | height=5417392 | tip=00000000000000ba4f2e...c19d
Asset plane: 38114 mints, 1284551 token accounts, 0 conservation faults
$ dogeinu-cli createsplmint '{"decimals":6,"supply":1000000000,"name":"WOWCOIN","symbol":"WOW"}'
{
"mint": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PB5wBQZ",
"authority": "DiNuQq8xYx4FhVvJ2rC1mA6KeT9sW3uB7nZpLk5gH4dR",
"txid": "4f1a2e9c7b0d3856a1cf42be09d7315e8a6c2f0b4d91e73a5c8f206bd14e97a3",
"vout": 0,
"confirmations": 0,
"fee": "0.01040000"
}
$ dogeinu-cli sendspl 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PB5wBQZ \
DRip3nWq5xAe2bK8vJ1cH6uT4mS9yZ7fL0gQpN3dXwEo 42000.5
4b8e1f60c3a27d95e0148fb6297ca35d81e70b4f6a2d93c58017ef4ba62d0c91
$ dogeinu-cli getsplbalance DRip3nWq5xAe2bK8vJ1cH6uT4mS9yZ7fL0gQpN3dXwEo
{
"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PB5wBQZ": {
"symbol": "WOW",
"amount": "42000.500000",
"decimals": 6,
"utxos": 1,
"finalized": true,
"finalized_by_slot": 812608850
}
}
$ dogeinu-cli verifyassetstate 5417392
{ "commitments": 38114, "conserved": true, "root": "e70b...44c1", "elapsed_ms": 611 }verifyassetstate recomputes every asset balance from the UTXO set and compares the root against the one committed in the block header. If an overlay protocol ran that command it would have nothing to compare against, because there is nothing in the header. Here, a mismatch is a consensus failure and the node halts.
What the fork costs
Numbers from a 24-hour soak on 12 bare-metal nodes (AMD EPYC 9354P, 384 GB, NVMe), 40 Gbit between regions, replaying mainnet traffic at 4× with synthetic asset load mixed in.
| metric | dogecoin core | doge inu core | delta | note |
|---|---|---|---|---|
| initial block download | 6 h 12 m | 7 h 04 m | +13.9% | asset commitment verification |
| block validation, p50 | 41 ms | 58 ms | +41.4% | conservation pass over vout |
| block validation, p99 | 310 ms | 402 ms | +29.7% | worst case is a 900-output mint batch |
| UTXO set on disk | 7.9 GB | 11.4 GB | +44.3% | 40 extra bytes per non-native output |
| time to finality | ~60 min | 1.8 s | −99.9% | Tower BFT, 6 conf equivalent |
| sustained tps | 33 | 14,880 | +45,000% | asset plane is not block-size bound |
| mempool RSS at 300k tx | 1.1 GB | 1.6 GB | +45.5% | commitment index is held hot |
We are slower at everything a Dogecoin node already did, and we think that is the correct trade. A 41% increase in block validation time on a 60-second block is 17 milliseconds of a 60,000 millisecond budget. In exchange the chain gained a token standard that consensus enforces and a finality time a human can wait through. Full methodology and raw traces.
The launcher is open to anyone with a wallet
Connect a Solana wallet, pick a name, a symbol, a decimals value and a supply, and the launcher builds and sends a real SPL mint transaction. It defaults to devnet, where the airdrop is free and the mistakes are cheap. Switch the cluster in the wallet menu when you actually mean it.
You keep the mint authority unless you choose to revoke it during creation. Revoking is irreversible and is the only thing on this site that cannot be undone, so the launcher asks twice.
# Mint, fund, and renounce in a single batch. The node builds one
# transaction, so either all three land or none of them do.
$ dogeinu-cli batch <<'EOF'
createsplmint {"decimals":9,"supply":420690000000,"symbol":"INU"}
sendspl $MINT $TREASURY 420690000000
revokeauthority $MINT mint
EOF
txid b19c7f4a0d62e835... ( 3 ops, 412 vbytes )
fee 0.01820000 DINU
status finalized in 1.61s at slot 812608912