Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,18 @@ This runs the Proof-of-Work algorithm, validates transactions, computes the new
Once your node is running, you can perform basic blockchain operations directly in your terminal.

**Making a Transfer**
Send coins to another public key:
Send native coins or custom assets to another public key:
```text
minichain> send <receiver_address> <amount> <fee>
minichain> send <receiver_address> <amount> [gas_limit] [fee_per_gas]
```
*Example: `send 8b3401abedb875aff7279b5ab58cb9a0c... 100 1`*
*Example (native currency): `send 8b3401abedb875aff7279b5ab58cb9a0c... 100 21000 1`*
*Example (multi-asset dictionary): `send 8b3401abedb875aff7279b5ab58cb9a0c... '{"": 100, "alice.GOLD": 50}' 21000 1`*

**Creating & Minting Native Assets**
```text
minichain> create-asset <ticker> <amount> <recipient> [gas_limit] [fee_per_gas]
```
*Example: `create-asset GOLD 1000 8b3401abedb875aff7279b5ab58cb9a0c...`*

**Checking Balances**
View the state of all active accounts and contracts on the chain:
Expand All @@ -130,10 +137,31 @@ minichain> address # View your own public key

---

## Native Multi-Asset Architecture

MiniChain treats native currency and custom tokens uniformly:
- **Account Balances:** Stored as `dict[str, int]` mapping asset identifiers to balances. The native asset is represented by the empty string `""`, while custom tokens are namespaced by the creator's address: `<creator_address>.<ticker>`.
- **Transaction Flags:** Transactions include bitmask `flags: int`:
- `ALLOW_MINT = 1`: Allows authorized creators to mint tokens namespaced to their own address without requiring pre-existing balance.
- `ALLOW_BURN = 2`: Allows burning tokens by sending them to `BURN_ADDRESS` (`"0000000000000000000000000000000000000000"`).
- **Genesis Allocation:** `genesis.json` supports direct multi-asset distribution at genesis:
```json
"alloc": {
"0000000000000000000000000000000000000001": {
"balances": {
"": 1000000000,
"0000000000000000000000000000000000000001.GOV": 50000000
}
}
}
```

---

## Smart Contracts

MiniChain supports fully-functional smart contracts written directly in Python!
The execution engine uses `sys.settrace` for precise **Gas Metering** (charging 1 gas per executed opcode) and `multiprocessing` for **Sandboxed Execution** to ensure network security.
The execution engine uses `sys.settrace` for precise **Gas Metering** (charging 1 gas per executed opcode) and `multiprocessing` for **Sandboxed Execution** to ensure network security. Contracts can hold, receive, and transfer multi-asset dictionaries via `msg["value"]`, `transfer_out`, and `call_contract`.

### Writing a Contract
Smart contracts in MiniChain have access to a persistent `storage` dictionary and a `msg` dictionary containing transaction context (`sender`, `value`, `data`).
Expand Down Expand Up @@ -166,7 +194,7 @@ curl -X POST http://127.0.0.1:8545/ \
-d '{"jsonrpc": "2.0", "method": "mc_blockNumber", "id": 1}'
```

Available endpoints include: `mc_blockNumber`, `mc_getBlockByNumber`, `mc_getBalance`, and `mc_sendTransaction`.
Available endpoints include: `mc_blockNumber`, `mc_getBlockByNumber`, `mc_getBalance`, `mc_getAssetBalance`, and `mc_sendTransaction`.

---

Expand Down
31 changes: 23 additions & 8 deletions minichain/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,34 @@ def _create_genesis_block(self, genesis_path):

# Apply genesis allocations
alloc = config.get("alloc", {})
total_alloc = 0
total_native_alloc = 0
for address, data in alloc.items():
balance = data.get("balance", 0)
if not isinstance(balance, int) or balance < 0:
logger.error("Invalid genesis balance for %s: %s. Must be a non-negative integer.", address, balance)
if not isinstance(data, dict):
logger.error("Invalid genesis alloc data for %s: must be a dict.", address)
sys.exit(1)
account = self.state.get_account(address)
account['balances'][''] = balance
total_alloc += balance
raw_balances = data.get("balances") if "balances" in data else data.get("balance", 0)
if isinstance(raw_balances, int) and not isinstance(raw_balances, bool):
if raw_balances < 0:
logger.error("Invalid genesis balance for %s: %s. Must be a non-negative integer.", address, raw_balances)
sys.exit(1)
account['balances'][''] = account['balances'].get('', 0) + raw_balances
total_native_alloc += raw_balances
elif isinstance(raw_balances, dict):
for asset, amt in raw_balances.items():
if not isinstance(asset, str) or isinstance(amt, bool) or not isinstance(amt, int) or amt < 0:
logger.error("Invalid genesis balance for %s [%s]: %s. Must be non-negative integer.", address, asset, amt)
sys.exit(1)
account['balances'][asset] = account['balances'].get(asset, 0) + amt
if asset == "":
total_native_alloc += amt
else:
logger.error("Invalid genesis balance for %s: %s. Must be an integer or dict.", address, raw_balances)
sys.exit(1)

initial_supply = config.get("initial_supply")
if initial_supply is not None and initial_supply != total_alloc:
logger.error("Genesis allocation mismatch: initial_supply is %s but alloc sum is %s", initial_supply, total_alloc)
if initial_supply is not None and initial_supply != total_native_alloc:
logger.error("Genesis allocation mismatch: initial_supply is %s but alloc sum is %s", initial_supply, total_native_alloc)
sys.exit(1)

self.chain_id = config.get("chain_id", "minichain-default")
Expand Down
69 changes: 69 additions & 0 deletions tests/test_chain_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,72 @@ def test_genesis_hash_matching_config_succeeds(tmpdir):
path2 = _write_genesis(tmpdir, overrides={"hash": real_hash})
bc2 = Blockchain(genesis_path=path2)
assert bc2.chain[0].hash == real_hash


def test_genesis_multi_asset_alloc_succeeds(tmpdir):
addr = "deadbeef" * 5
path = _write_genesis(tmpdir, overrides={
"alloc": {
addr: {
"balances": {
"": 100,
"alice.GOLD": 500,
"bob.SILVER": 250,
}
}
},
"initial_supply": 100,
})
bc = Blockchain(genesis_path=path)
acc = bc.state.get_account(addr)
assert acc["balances"][""] == 100
assert acc["balances"]["alice.GOLD"] == 500
assert acc["balances"]["bob.SILVER"] == 250


def test_genesis_multi_asset_dict_balance_succeeds(tmpdir):
addr = "deadbeef" * 5
path = _write_genesis(tmpdir, overrides={
"alloc": {
addr: {
"balance": {
"": 200,
"custom.TOKEN": 300,
}
}
},
"initial_supply": 200,
})
bc = Blockchain(genesis_path=path)
acc = bc.state.get_account(addr)
assert acc["balances"][""] == 200
assert acc["balances"]["custom.TOKEN"] == 300


def test_genesis_multi_asset_negative_balance_exits(tmpdir):
addr = "deadbeef" * 5
path = _write_genesis(tmpdir, overrides={
"alloc": {
addr: {
"balances": {
"": 100,
"alice.GOLD": -5,
}
}
},
"initial_supply": 100,
})
with pytest.raises(SystemExit):
Blockchain(genesis_path=path)


def test_genesis_multi_asset_non_dict_alloc_entry_exits(tmpdir):
addr = "deadbeef" * 5
path = _write_genesis(tmpdir, overrides={
"alloc": {
addr: "not-a-dict"
}
})
with pytest.raises(SystemExit):
Blockchain(genesis_path=path)

Loading