Skip to content

TON vs Ethereum, Solana & Bitcoin

💡
Before you start

Python 3, a terminal, and the file tonaddr.py from the TON introduction tutorial. If you do not have it, open Introduction to TON and do its step 1 — two minutes, one file. Check Python with python3 --version; nothing else needs installing.

Comparison articles usually end in opinion. This one ends in numbers you generated: two address formats measured against the same corruption test, two transaction models implemented side by side, and the arithmetic behind “wait six confirmations” against “final immediately”. Nothing here connects to a network.

Why Compare?

Understanding how TON differs from other major blockchains helps you make informed decisions about which network to use for different purposes. Each blockchain makes different tradeoffs between speed, security, decentralization, and features.

Architecture Comparison

Bitcoin Single chain, UTXO model. Every node processes every transaction. Simplest and most battle-tested but limited throughput (~7 TPS).
Ethereum Single chain with Layer 2 rollups. Account-based model with shared global state. All smart contracts share one execution environment.
Solana Single high-performance chain. Uses Proof-of-History (PoH) for ordering. Parallel transaction processing via Sealevel runtime.
TON Multi-blockchain with dynamic sharding. Asynchronous smart contracts communicate via messages. Infinite horizontal scaling.

Speed and Throughput

  • Bitcoin: ~7 TPS, 10-minute blocks, ~60-minute finality (6 confirmations)
  • Ethereum: ~15-30 TPS (L1), 12-second blocks, ~12-15 minute finality. Layer 2s (Arbitrum, Optimism) add 2,000-4,000+ TPS
  • Solana: ~4,000 TPS (actual, not theoretical), 400ms blocks, ~12-second finality
  • TON: ~10,000+ TPS (current), 2-5 second blocks, under 6 seconds to finality. Theoretical limit: millions of TPS with full sharding
💡
Theoretical vs actual TPS

Many blockchains advertise theoretical maximums. What matters is sustained real-world throughput under load. TON's current ~10,000 TPS is actual on-chain capacity, with the sharding design allowing much more as demand grows.

Transaction Fees

  • Bitcoin: $1-50+ depending on network congestion
  • Ethereum (L1): $1-100+ for simple transfers, more for smart contract interactions. Gas spikes during high demand
  • Ethereum (L2): $0.01-0.50 on Arbitrum/Optimism, under $0.01 on newer L2s
  • Solana: $0.001-0.01 per transaction. Very cheap but priority fees can spike during congestion
  • TON: $0.01-0.05 per transaction. Stable and predictable due to the sharding model distributing load

Smart Contract Model

  • Bitcoin: Limited scripting (Bitcoin Script). Not Turing-complete. Designed for value transfer, not complex logic
  • Ethereum: Solidity/Vyper on EVM. Synchronous execution — all contracts share global state. Atomic transactions (everything succeeds or everything reverts)
  • Solana: Rust/C on Sealevel VM. Parallel execution but requires explicit account declarations. Programs are stateless; state lives in separate accounts
  • TON: FunC/Tact on TVM. Asynchronous message-passing — contracts communicate like microservices. No atomic cross-contract calls. Each contract manages its own state

Security Model

  • Bitcoin: Proof-of-Work. 15+ years of battle testing. The most secure blockchain by hash rate. 51% attack is economically impractical
  • Ethereum: Proof-of-Stake since 2022. 900,000+ validators. Slashing for malicious behavior. Large validator set makes attacks expensive
  • Solana: Proof-of-Stake with Proof-of-History ordering. ~1,900 validators. Has experienced multiple network outages under load (7+ in 2022-2023)
  • TON: BFT Proof-of-Stake. ~300+ validators. High minimum stake (~300,000 TON). Fewer validators means faster consensus but higher concentration risk
⚠️
Fewer validators = higher trust requirement

TON's ~300 validators is significantly fewer than Ethereum's 900,000+. While the BFT model only needs honest majority (2/3), a smaller set means each validator has more power and the cost to corrupt the network is lower in absolute terms.

Ecosystem Maturity

  • Bitcoin (2009): Most mature. Primarily used as a store of value. Limited smart contract ecosystem (Ordinals, Lightning Network)
  • Ethereum (2015): Largest smart contract ecosystem. Thousands of dApps. Most tooling, auditors, and developer resources
  • Solana (2020): Fast-growing ecosystem. Strong in DeFi and NFTs. Good developer tooling.
  • TON (2021): Youngest ecosystem. Growing rapidly due to Telegram. Fewer dApps and auditors. Unique advantage: 900M+ potential users via Telegram

When to Use Which

  • Long-term store of value: Bitcoin — simplest, most secure, most decentralized, longest track record
  • Complex DeFi and smart contracts: Ethereum (or its L2s) — largest ecosystem, most auditors, most battle-tested contracts
  • High-frequency trading and performance: Solana — fastest single-chain execution, lowest latency
  • Payments and Telegram integration: TON — sub-second finality, lowest barrier to entry via Telegram, ideal for micropayments and social payments

Now Measure the Differences Yourself, in Five Steps

Networks are usually compared with adjectives. In the next twenty-five minutes you will compare four design decisions with measurements instead: how two address formats behave when the address is damaged, what a UTXO chain has to do that an account chain does not, what a confirmation is actually worth, and what happens to your fee when a block is full. Every figure below came from running these files.

1
Put one key on two networks

Go: open a terminal in the folder holding tonaddr.py.

Do: save this as btcaddr.py. It prints nothing; it builds an older-style address — a hash of the public key, a network byte, and a four-byte checksum, written in base58.

import hashlib

ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

def b58(data: bytes) -> str:
    n = int.from_bytes(data, "big")
    out = ""
    while n:
        n, rem = divmod(n, 58)
        out = ALPHABET[rem] + out
    return "1" * (len(data) - len(data.lstrip(b"\x00"))) + out

def b58_decode(s: str) -> bytes:
    n = 0
    for ch in s:
        n = n * 58 + ALPHABET.index(ch)
    body = n.to_bytes((n.bit_length() + 7) // 8, "big")
    return b"\x00" * (len(s) - len(s.lstrip("1"))) + body

def address(public_key: bytes) -> str:
    h = hashlib.new("ripemd160", hashlib.sha256(public_key).digest()).digest()
    payload = b"\x00" + h                              # 0x00 = the main Bitcoin network
    checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
    return b58(payload + checksum)

def valid(addr: str) -> bool:
    raw = b58_decode(addr)
    payload, given = raw[:-4], raw[-4:]
    return hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] == given

Now save this as both.py and run python3 both.py:

import hashlib
from btcaddr import address
from tonaddr import encode

pubkey = hashlib.sha256(b"one key, two networks").digest()

btc = address(pubkey)
ton = encode(pubkey)
print("the same public key, on two networks:")
print(f"  bitcoin-style: {btc}  ({len(btc)} characters)")
print(f"  ton          : {ton}  ({len(ton)} characters)")

You should see: two addresses with the right shapes:

the same public key, on two networks:
  bitcoin-style: 1Ej1P6CQRGsxbiCBfMDiLBeN4TsL5gzu4N  (34 characters)
  ton          : EQD0jnavzuRcCT5EU143Qn5QpURR3ZassbNTPZdQ5VAo_6qK  (48 characters)

Both results check themselves against something you already know: a Bitcoin address of this type begins with 1 and runs to about 34 characters, and a TON address begins with EQ and is 48. Neither convention was written into the code — they fall out of the network byte and the flags byte.

If not: ValueError: unsupported hash type ripemd160 means your Python was built against an OpenSSL that disables it; the rest of this page still works, so skip to step 3. ModuleNotFoundError: No module named 'tonaddr' means the file from the introduction tutorial is not in this folder.

2
Damage both addresses and see which notices

Go: same folder. A checksum is a safety net, and nets have gauges.

Do: save this as corrupt.py and run it. It takes about a minute.

import hashlib, random
from btcaddr import address, valid, ALPHABET
from tonaddr import encode, decode

pubkey = hashlib.sha256(b"one key, two networks").digest()
btc, ton = address(pubkey), encode(pubkey)
TON_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

def missed(good, alphabet, check, changes, trials=40_000, seed=5):
    random.seed(seed)
    slipped = 0
    for _ in range(trials):
        chars = list(good)
        for _ in range(changes):
            i = random.randrange(len(good) - 1)
            chars[i] = random.choice([c for c in alphabet if c != good[i]])
        try:
            slipped += bool(check("".join(chars)))
        except Exception:
            pass
    return slipped

for changes in (1, 3):
    print(f"{changes} character(s) corrupted, 40,000 attempts each:")
    print(f"  bitcoin-style (4-byte checksum, 32 bits): {missed(btc, ALPHABET, valid, changes)} slipped through")
    print(f"  ton           (2-byte checksum, 16 bits): "
          f"{missed(ton, TON_ALPHABET, lambda a: decode(a)['checksum_ok'], changes)} slipped through")

You should see: both designs perfect against a single typo, and the margin showing only under heavier damage:

1 character(s) corrupted, 40,000 attempts each:
  bitcoin-style (4-byte checksum, 32 bits): 0 slipped through
  ton           (2-byte checksum, 16 bits): 0 slipped through
3 character(s) corrupted, 40,000 attempts each:
  bitcoin-style (4-byte checksum, 32 bits): 0 slipped through
  ton           (2-byte checksum, 16 bits): 2 slipped through

Two in forty thousand is a small number and a real one, and it is what a shorter checksum buys: a shorter address. Neither result should change how you behave, because both are defences against your fingers and neither is a defence against an attacker substituting a different valid address — which is the failure that actually costs people money.

If not: if the bitcoin-style rows raise an exception rather than printing, ripemd160 is unavailable in your build (see step 1). If both rows read 0 at three characters, your trial count is lower than 40,000 — the effect is rare by design.

3
Spend money in both transaction models

Go: same folder. This is the difference people feel without being able to name it.

Do: save this as models.py and run it.

# UTXO model: you spend whole coins and get change back.
coins = [{"id": "c1", "value": 3}, {"id": "c2", "value": 5}, {"id": "c3", "value": 1}]
pay = 6

chosen, total = [], 0
for coin in sorted(coins, key=lambda c: -c["value"]):
    chosen.append(coin)
    total += coin["value"]
    if total >= pay:
        break

print("UTXO model")
print(f"  you must pay {pay}; you hold coins of", [c["value"] for c in coins])
print(f"  spent whole coins {[c['id'] for c in chosen]} worth {total}")
print(f"  change returned to a NEW address of yours: {total - pay}")
print(f"  coins left over: {[c['id'] for c in coins if c not in chosen]}")

print()
print("account model")
balances = {"you": 9, "shop": 0}
balances["you"] -= pay
balances["shop"] += pay
print(f"  one subtraction, one addition: {balances}")
print("  no change, no coin selection, but every payment needs a sequence number")

You should see: two ways to move six coins:

UTXO model
  you must pay 6; you hold coins of [3, 5, 1]
  spent whole coins ['c2', 'c1'] worth 8
  change returned to a NEW address of yours: 2
  coins left over: ['c3']

account model
  one subtraction, one addition: {'you': 3, 'shop': 6}
  no change, no coin selection, but every payment needs a sequence number

The UTXO side explains several things at once: why a wallet’s fee depends on how many small coins it must combine, why a change address appears in your history that you never created, and why the clustering attack works — spending c1 and c2 together proves one person holds both. The account side has none of that, and pays for it with the sequence number, which is what stops a signed payment being replayed forever.

If not: if the change comes out negative, the coin-selection loop is breaking before it has enough — the break belongs after the running total reaches the amount.

4
Work out what a confirmation is worth

Go: same folder. “Wait for six confirmations” is repeated everywhere and justified almost nowhere.

Do: save this as finality.py and run it. The model is the simple race: an attacker rebuilding the chain from behind must out-produce everyone else.

print("probabilistic finality: an attacker with some share of the mining power")
print("must out-run the honest chain to reverse a payment\n")

for share in (0.10, 0.25, 0.40):
    row = []
    for depth in (1, 3, 6):
        chance = (share / (1 - share)) ** depth
        row.append(f"{depth} block(s): {chance:>8.4%}")
    print(f"  attacker holds {share:>4.0%} of the power -> " + "   ".join(row))

print()
print("BFT finality (TON, and other proof-of-stake designs):")
print("  a block signed by the required majority of validators is final immediately;")
print("  reversing it requires those validators to sign a conflicting block,")
print("  which is publicly provable and costs them their stake")

You should see: where the number six comes from:

probabilistic finality: an attacker with some share of the mining power
must out-run the honest chain to reverse a payment

  attacker holds  10% of the power -> 1 block(s): 11.1111%   3 block(s):  0.1372%   6 block(s):  0.0002%
  attacker holds  25% of the power -> 1 block(s): 33.3333%   3 block(s):  3.7037%   6 block(s):  0.1372%
  attacker holds  40% of the power -> 1 block(s): 66.6667%   3 block(s): 29.6296%   6 block(s):  8.7791%

BFT finality (TON, and other proof-of-stake designs):
  a block signed by the required majority of validators is final immediately;
  reversing it requires those validators to sign a conflicting block,
  which is publicly provable and costs them their stake

Six confirmations is not a magic number — it is where the risk becomes negligible against a modest attacker. Read the bottom row: against one holding 40% of the power, six confirmations still leaves nearly a 9% chance, which is why large transfers wait longer. The two designs make different promises: one gives you a probability that improves with patience, the other gives certainty conditional on validators not destroying their own stake.

If not: if the percentages exceed 100%, the attacker’s share was set above 0.5, where the formula no longer applies — a majority attacker succeeds eventually, at any depth.

5
Find out what a fee really is

Go: same folder. Fees are quoted as prices. They behave like bids.

Do: save this as fees.py and run it.

import random

random.seed(2)
BLOCK_CAPACITY = 20

waiting = [{"id": i, "fee": round(random.uniform(0.1, 20), 2)} for i in range(100)]

included = sorted(waiting, key=lambda t: -t["fee"])[:BLOCK_CAPACITY]
cheapest_in = min(t["fee"] for t in included)

print(f"{len(waiting)} transactions waiting, {BLOCK_CAPACITY} fit in the next block")
print(f"the cheapest fee that got in: {cheapest_in}")
print(f"transactions left waiting   : {len(waiting) - BLOCK_CAPACITY}")
print()
for offer in (0.5, 15.0, 19.0):
    print(f"  you offer {offer:>5}: "
          f"{'included' if offer >= cheapest_in else 'still waiting -- and the price rises with demand'}")

You should see: an auction, not a price list:

100 transactions waiting, 20 fit in the next block
the cheapest fee that got in: 17.62
transactions left waiting   : 80

  you offer   0.5: still waiting -- and the price rises with demand
  you offer  15.0: still waiting -- and the price rises with demand
  you offer  19.0: included

This is the mechanism behind every “fees are high today” complaint, and it is also the honest form of TON’s scalability claim: a network that can add shards raises capacity so the auction rarely binds, while a fixed-capacity chain has no answer to demand except price. The trade — asynchronous cross-shard transfers, which you can measure in the TON architecture tutorial — is what buys that headroom.

If not: if every offer is included, BLOCK_CAPACITY is at or above 100, so nothing is competing. If the clearing fee differs from 17.62, the random.seed(2) line is missing.

🎉
Check yourself before moving on

Without scrolling up: you are choosing a network for a shop that accepts payments of about fifty dollars, dozens of times a day. Which two of the five measurements matter most, and which one is nearly irrelevant? Answer: step 4 and step 5 matter most. Finality decides how long a customer stands at your counter before you can hand over the goods, and the fee auction decides whether a fifty-dollar payment costs you cents or a meaningful share of the sale when the network is busy. Step 2 is nearly irrelevant — both address formats catch typing mistakes reliably, and the real address risk is substitution rather than corruption, which no checksum addresses. Step 3 matters mainly for privacy and fee predictability rather than for the shop’s day-to-day operation.

Now do it without the page: change fees.py so capacity grows with demand — say one block slot for every five waiting transactions — and watch the clearing fee collapse. Then ask what that model leaves out. You will have found the real argument between these designs: not whether capacity can scale, but what is given up to scale it.

Summary

  • TON is the fastest by throughput and finality, with unique multi-blockchain sharding
  • Ethereum has the most mature ecosystem and security tooling
  • Bitcoin is the most battle-tested and decentralized
  • Solana offers the best single-chain performance
  • TON's unique advantage is Telegram integration and mass-market accessibility
  • TON's unique risk is fewer validators and a younger, less-audited ecosystem
  • No blockchain is "best" — each optimizes for different tradeoffs
🎉
You can now compare blockchains!

With this knowledge, you can evaluate which platform is best suited for your specific use case and risk tolerance.