diff --git a/README.md b/README.md index 9f6d4fe..ece9261 100644 --- a/README.md +++ b/README.md @@ -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 +minichain> send [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 [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: @@ -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: `.`. +- **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`). @@ -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`. --- diff --git a/minichain/chain.py b/minichain/chain.py index 452becc..977f4ba 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -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") diff --git a/tests/test_chain_extra.py b/tests/test_chain_extra.py index 1a13908..3f8fe2e 100644 --- a/tests/test_chain_extra.py +++ b/tests/test_chain_extra.py @@ -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) +