chore: initial commit for v0.0.1

DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
This commit is contained in:
vsecoder
2026-04-17 14:16:44 +03:00
commit 7e7393e4f8
196 changed files with 55947 additions and 0 deletions

45
docs/api/README.md Normal file
View File

@@ -0,0 +1,45 @@
# REST API
DChain-нода предоставляет HTTP API на порту `--stats-addr` (по умолчанию `:8080`).
## Базовые URL
| Окружение | URL |
|---------|-----|
| Локально | `http://localhost:8081` |
| Docker node1 | `http://node1:8080` |
| Docker node2 | `http://node2:8080` |
| Docker node3 | `http://node3:8080` |
## Разделы API
| Документ | Эндпоинты |
|---------|----------|
| [Chain API](chain.md) | Блоки, транзакции, балансы, адреса, stats |
| [Contracts API](contracts.md) | Деплой, вызов, state, логи |
| [Relay API](relay.md) | Отправка сообщений, inbox, контакты |
## Формат ошибок
```json
{"error": "описание ошибки"}
```
HTTP-статус: 400 для клиентских ошибок, 500 для серверных.
## Аутентификация
REST API не требует аутентификации. Транзакции подписываются на стороне клиента (CLI-командами) и отправляются как подписанные JSON-объекты. API не имеет admin-эндпоинтов требующих токенов.
## Пример
```bash
# Статистика сети
curl http://localhost:8081/api/stats
# Баланс адреса
curl http://localhost:8081/api/balance/03a1b2c3...
# Последние блоки
curl http://localhost:8081/api/blocks?limit=10
```

314
docs/api/chain.md Normal file
View File

@@ -0,0 +1,314 @@
# Chain API
Эндпоинты для чтения блокчейна: блоки, транзакции, балансы, идентичности, валидаторы.
## Статистика сети
### `GET /api/netstats`
Агрегированная статистика сети.
```bash
curl http://localhost:8081/api/netstats
```
```json
{
"total_blocks": 1024,
"total_txs": 4821,
"validator_count": 3,
"relay_count": 1,
"total_supply_ut": 10000000000
}
```
---
## Блоки
### `GET /api/blocks?limit=N`
Последние `N` блоков (по умолчанию 20).
```bash
curl "http://localhost:8081/api/blocks?limit=10"
```
```json
[
{
"index": 1024,
"hash": "a1b2c3...",
"prev_hash": "...",
"timestamp": 1710000000,
"validator": "03abcd...",
"tx_count": 3,
"total_fees_ut": 15000
},
...
]
```
---
### `GET /api/block/{index}`
Детали блока по высоте.
```bash
curl http://localhost:8081/api/block/1024
```
```json
{
"index": 1024,
"hash": "a1b2c3...",
"prev_hash": "...",
"timestamp": 1710000000,
"validator": "03abcd...",
"transactions": [
{
"id": "tx-abc123",
"type": "TRANSFER",
"from": "03...",
"to": "04...",
"amount_ut": 1000000,
"fee_ut": 1000
}
]
}
```
---
## Транзакции
### `GET /api/txs/recent?limit=N`
Последние транзакции (по умолчанию 20).
```bash
curl "http://localhost:8081/api/txs/recent?limit=5"
```
---
### `GET /api/tx/{txid}`
Транзакция по ID.
```bash
curl http://localhost:8081/api/tx/tx-abc123def456
```
```json
{
"id": "tx-abc123",
"block_index": 1024,
"type": "CALL_CONTRACT",
"from": "03abcd...",
"timestamp": 1710000000,
"payload": { ... },
"signature": "..."
}
```
---
### `POST /api/tx`
Отправить подписанную транзакцию.
```bash
curl -X POST http://localhost:8081/api/tx \
-H "Content-Type: application/json" \
-d '{"type":"TRANSFER","from":"03...","payload":{...},"signature":"..."}'
```
```json
{"id": "tx-abc123", "status": "accepted"}
```
---
## Chain API v2
Расширенный API для работы с транзакциями.
### `GET /v2/chain/transactions/{tx_id}`
```bash
curl http://localhost:8081/v2/chain/transactions/tx-abc123
```
```json
{
"id": "tx-abc123",
"block_index": 1024,
"payload": { ... },
"payload_hex": "...",
"signature_hex": "..."
}
```
---
### `GET /v2/chain/accounts/{account_id}/transactions`
Транзакции для аккаунта с пагинацией.
**Query параметры:**
| Параметр | По умолчанию | Описание |
|---------|------------|---------|
| `limit` | 100 | Максимум (max 1000) |
| `after_block` | — | Только блоки после N |
| `before_block` | — | Только блоки до N |
| `order` | `desc` | `asc` или `desc` |
```bash
curl "http://localhost:8081/v2/chain/accounts/03abcd.../transactions?limit=20&order=desc"
```
---
### `POST /v2/chain/transactions/draft`
Создать черновик транзакции для подписания.
```bash
curl -X POST http://localhost:8081/v2/chain/transactions/draft \
-H "Content-Type: application/json" \
-d '{"from":"03...","to":"04...","amount_ut":1000000}'
```
```json
{
"tx": { ... },
"sign_bytes_hex": "...",
"sign_bytes_base64": "..."
}
```
---
### `POST /v2/chain/transactions`
Отправить подписанную транзакцию (расширенный формат).
```bash
curl -X POST http://localhost:8081/v2/chain/transactions \
-H "Content-Type: application/json" \
-d '{"tx":{...},"signature":"..."}'
```
---
## Адреса
### `GET /api/address/{addr}?limit=N&offset=N`
Информация об адресе (DC-адрес или hex pubkey).
```bash
curl "http://localhost:8081/api/address/DCabc123?limit=20&offset=0"
```
```json
{
"address": "DCabc123...",
"pub_key": "03abcd...",
"balance_ut": 9500000,
"tx_count": 12,
"transactions": [...],
"has_more": false
}
```
---
## Идентичности и валидаторы
### `GET /api/identity/{pubkey|addr}`
Информация об идентичности.
```bash
curl http://localhost:8081/api/identity/03abcd...
```
```json
{
"pub_key": "03abcd...",
"address": "DCabc123...",
"nick": "alice",
"x25519_pub": "...",
"registered_at": 100,
"stake_ut": 1000000000
}
```
---
### `GET /api/validators`
Список активных валидаторов.
```bash
curl http://localhost:8081/api/validators
```
```json
[
{"pub_key": "03...", "stake_ut": 1000000000},
{"pub_key": "04...", "stake_ut": 1000000000}
]
```
---
### `GET /api/node/{pubkey|addr}?window=N`
Информация об узле (статистика, репутация).
```bash
curl http://localhost:8081/api/node/03abcd...
```
---
### `GET /api/relays`
Зарегистрированные relay-провайдеры.
```bash
curl http://localhost:8081/api/relays
```
```json
[
{
"pub_key": "03...",
"relay_pub": "...",
"fee_ut": 100,
"endpoint": ""
}
]
```
---
## Live Events
### `GET /api/events`
Server-Sent Events поток новых блоков и транзакций.
```bash
curl -N http://localhost:8081/api/events
```
```
data: {"type":"block","index":1025,"hash":"...","tx_count":2}
data: {"type":"tx","id":"tx-abc","block":1025,"from":"03..."}
```

285
docs/api/contracts.md Normal file
View File

@@ -0,0 +1,285 @@
# Contracts API
REST API для работы с WASM-контрактами: деплой, вызов, чтение state и логов.
> Деплой и вызов контрактов выполняются через CLI-команды `client deploy-contract` и `client call-contract`, которые формируют и отправляют подписанные транзакции через `POST /api/tx`. Прямой HTTP-деплой не поддерживается.
## Список контрактов
### `GET /api/contracts`
Все задеплоенные контракты.
```bash
curl http://localhost:8081/api/contracts
```
```json
{
"count": 4,
"contracts": [
{
"contract_id": "a1b2c3d4e5f60001",
"deployer_pub": "03abcd...",
"deployed_at": 100,
"wasm_size": 1842,
"abi": {
"contract": "governance",
"version": "1.0.0",
"methods": [...]
}
},
...
]
}
```
---
## Детали контракта
### `GET /api/contracts/{contractID}`
```bash
curl http://localhost:8081/api/contracts/a1b2c3d4e5f60001
```
```json
{
"contract_id": "a1b2c3d4e5f60001",
"deployer_pub": "03abcd...",
"deployed_at": 100,
"wasm_size": 1842,
"abi_json": "{...}"
}
```
| Поле | Тип | Описание |
|------|-----|---------|
| `contract_id` | string | 16-символьный hex ID |
| `deployer_pub` | string | Pubkey деплоера |
| `deployed_at` | uint64 | Высота блока деплоя |
| `wasm_size` | int | Размер WASM в байтах |
| `abi_json` | string | JSON ABI контракта |
---
## State контракта
### `GET /api/contracts/{contractID}/state/{key}`
Читает значение из state контракта по ключу.
```bash
# Строковое значение
curl http://localhost:8081/api/contracts/a1b2c3d4e5f60001/state/admin
# Вложенный ключ
curl "http://localhost:8081/api/contracts/a1b2c3d4e5f60001/state/param:gas_price"
# Ключ эскроу
curl "http://localhost:8081/api/contracts/.../state/e:deal-001:b"
```
```json
{
"key": "admin",
"value_b64": "MDNhYmNk...",
"value_hex": "30336162636...",
"value_u64": null
}
```
| Поле | Тип | Описание |
|------|-----|---------|
| `value_b64` | string | Base64-encoded raw bytes |
| `value_hex` | string | Hex-encoded raw bytes |
| `value_u64` | uint64\|null | Если значение ровно 8 байт big-endian |
**Примеры чтения state контрактов:**
```bash
# Governance: live параметр
curl "http://localhost:8081/api/contracts/$GOV_ID/state/param:gas_price"
# Governance: pending предложение
curl "http://localhost:8081/api/contracts/$GOV_ID/state/prop:gas_price"
# Username registry: pubkey по имени
curl "http://localhost:8081/api/contracts/$REG_ID/state/name:alice"
# value_hex = hex(pubkey_bytes) → hex.DecodeString(value_hex) = pubkey
# Escrow: статус сделки
curl "http://localhost:8081/api/contracts/$ESC_ID/state/e:deal-001:x"
# value_hex: 61='a' (active), 64='d' (disputed), 72='r' (released), 66='f' (refunded)
```
---
## Логи контракта
### `GET /api/contracts/{contractID}/logs?limit=N`
Последние `N` лог-записей (по умолчанию 50, максимум 100).
```bash
curl "http://localhost:8081/api/contracts/a1b2c3d4e5f60001/logs?limit=20"
```
```json
{
"contract_id": "a1b2c3d4e5f60001",
"count": 5,
"logs": [
{
"tx_id": "tx-abc123",
"block_index": 1024,
"timestamp": 1710000000,
"message": "registered: alice"
},
{
"tx_id": "tx-def456",
"block_index": 1020,
"timestamp": 1709999800,
"message": "initialized"
}
]
}
```
Логи отсортированы от новейших к старейшим.
---
## Деплой контракта (через CLI)
```bash
client deploy-contract \
--key key.json \
--wasm mycontract.wasm \
--abi mycontract_abi.json \
--node http://localhost:8081
```
Успешный деплой печатает:
```
contract_id: a1b2c3d4e5f60001
```
В Docker:
```bash
docker exec node1 client deploy-contract \
--key /keys/node1.json \
--wasm /contracts/mycontract.wasm \
--abi /contracts/mycontract_abi.json \
--node http://node1:8080
```
---
## Вызов контракта (через CLI)
```bash
# Без аргументов
client call-contract \
--contract a1b2c3d4e5f60001 \
--method increment \
--gas 5000 \
--key key.json --node http://localhost:8081
# Строковый аргумент
client call-contract \
--contract $REG_ID \
--method register \
--arg alice \
--gas 30000 \
--key key.json --node http://localhost:8081
# Числовой аргумент (uint64)
client call-contract \
--contract $ESC_ID \
--method create \
--arg "deal-001" \
--arg "$SELLER_PUB" \
--arg64 5000000 \
--gas 30000 \
--key key.json --node http://localhost:8081
# Несколько аргументов в JSON
client call-contract \
--contract $AUCTION_ID \
--method create \
--args '["Rare item #1", 1000000, 100]' \
--gas 20000 \
--key key.json --node http://localhost:8081
```
### Флаги call-contract
| Флаг | Тип | Описание |
|------|-----|---------|
| `--contract` | string | ID контракта (16 hex) |
| `--method` | string | Имя метода |
| `--arg` | string | Добавить строковый аргумент (повторяемый) |
| `--arg64` | uint64 | Добавить числовой аргумент (повторяемый) |
| `--args` | JSON | Массив всех аргументов `["str", 42, ...]` |
| `--gas` | uint64 | Gas лимит |
| `--key` | path | Файл ключа |
| `--node` | URL | Node URL |
---
## Токены и NFT
### `GET /api/tokens`
```bash
curl http://localhost:8081/api/tokens
```
```json
{"count": 2, "tokens": [{...}]}
```
### `GET /api/tokens/{id}`
```bash
curl http://localhost:8081/api/tokens/abc123
```
### `GET /api/tokens/{id}/balance/{pubkey}`
```bash
curl http://localhost:8081/api/tokens/abc123/balance/03abcd...
```
```json
{
"token_id": "abc123",
"pub_key": "03abcd...",
"address": "DCabc...",
"balance": 1000
}
```
### `GET /api/nfts`
```bash
curl http://localhost:8081/api/nfts
```
### `GET /api/nfts/{id}`
```bash
curl http://localhost:8081/api/nfts/nft-abc123
```
### `GET /api/nfts/owner/{pubkey}`
```bash
curl http://localhost:8081/api/nfts/owner/03abcd...
```
```json
{"count": 3, "nfts": [{...}]}
```

246
docs/api/relay.md Normal file
View File

@@ -0,0 +1,246 @@
# Relay API
REST API для работы с шифрованными сообщениями через relay-сеть.
Сообщения шифруются E2E с использованием NaCl (X25519 + XSalsa20-Poly1305). Relay хранит зашифрованные конверты и доставляет их получателям.
## Отправить сообщение
### `POST /relay/send`
Зашифровать и отправить сообщение получателю.
**Request body:**
```json
{
"recipient_pub": "<x25519_hex>",
"msg_b64": "<base64_encoded_message>"
}
```
| Поле | Тип | Описание |
|------|-----|---------|
| `recipient_pub` | string | X25519 public key получателя (hex) |
| `msg_b64` | string | Сообщение в base64 |
```bash
MSG=$(echo -n "Hello, Bob!" | base64)
curl -X POST http://localhost:8081/relay/send \
-H "Content-Type: application/json" \
-d "{\"recipient_pub\":\"$BOB_X25519\",\"msg_b64\":\"$MSG\"}"
```
**Response:**
```json
{
"id": "env-abc123",
"recipient_pub": "...",
"status": "sent"
}
```
---
## Broadcast конверта
### `POST /relay/broadcast`
Опубликовать pre-sealed конверт (для light-клиентов, которые шифруют на своей стороне).
**Request body:**
```json
{
"envelope": {
"id": "...",
"recipient_pub": "...",
"sender_pub": "...",
"payload_b64": "...",
"timestamp": 1710000000,
"fee_ut": 100
}
}
```
```bash
curl -X POST http://localhost:8081/relay/broadcast \
-H "Content-Type: application/json" \
-d '{"envelope": {...}}'
```
**Response:**
```json
{"id": "env-abc123", "status": "broadcast"}
```
---
## Inbox
### `GET /relay/inbox?pub=<x25519hex>&since=<ts>&limit=N`
Получить сообщения из inbox.
**Query параметры:**
| Параметр | Обязательный | Описание |
|---------|------------|---------|
| `pub` | Да | X25519 pubkey получателя (hex) |
| `since` | Нет | Unix timestamp — только сообщения новее |
| `limit` | Нет | Максимум (по умолчанию 50) |
```bash
# Получить все сообщения
curl "http://localhost:8081/relay/inbox?pub=$MY_X25519"
# Только новые (после timestamp)
curl "http://localhost:8081/relay/inbox?pub=$MY_X25519&since=1710000000&limit=20"
```
**Response:**
```json
{
"pub": "...",
"count": 2,
"has_more": false,
"items": [
{
"id": "env-abc123",
"sender_pub": "...",
"payload_b64": "...",
"timestamp": 1710000000,
"fee_ut": 100
}
]
}
```
> `payload_b64` содержит зашифрованное сообщение. Расшифровка выполняется на стороне клиента с помощью X25519 private key.
---
### `GET /relay/inbox/count?pub=<hex>`
Количество сообщений в inbox.
```bash
curl "http://localhost:8081/relay/inbox/count?pub=$MY_X25519"
```
**Response:**
```json
{"pub": "...", "count": 3}
```
---
### `DELETE /relay/inbox/{envID}?pub=<hex>`
Удалить сообщение из inbox.
```bash
curl -X DELETE "http://localhost:8081/relay/inbox/env-abc123?pub=$MY_X25519"
```
**Response:**
```json
{"id": "env-abc123", "status": "deleted"}
```
---
## Контакты
### `GET /relay/contacts?pub=<ed25519hex>`
Входящие запросы на контакт.
> Используйте **ed25519** pubkey (не X25519).
```bash
curl "http://localhost:8081/relay/contacts?pub=$MY_ED25519"
```
**Response:**
```json
{
"pub": "...",
"count": 1,
"contacts": [
{
"from_pub": "03abcd...",
"from_nick": "alice",
"intro": "Hi! Let's connect.",
"fee_ut": 1000,
"timestamp": 1710000000
}
]
}
```
---
## CLI команды для relay
Прямой вызов relay API через CLI:
```bash
# Отправить сообщение (автоматически ищет X25519 ключ в registry)
client send-msg \
--to $RECIPIENT_PUB \
--msg "Hello!" \
--key key.json \
--node http://localhost:8081
# Отправить через @username
client send-msg \
--to @alice \
--msg "Hello Alice!" \
--registry $REGISTRY_ID \
--key key.json \
--node http://localhost:8081
# Получить сообщения из inbox
client inbox \
--key key.json \
--node http://localhost:8081 \
--limit 20
# Удалить прочитанные
client inbox \
--key key.json \
--node http://localhost:8081 \
--delete
# Запросить контакт
client request-contact \
--to $RECIPIENT_PUB \
--fee 1000 \
--intro "Hi, I want to connect" \
--key key.json \
--node http://localhost:8081
```
---
## Архитектура relay
```
Отправитель Relay Node Получатель
│ │ │
│── POST /relay/send ───────────────▶│ │
│ {recipient_pub, msg_b64} │ Encrypt (NaCl box) │
│ │ Broadcast via gossipsub │
│ │◀── gossip ─────────────────▶│
│ │ │
│ │ Store in mailbox │
│ │ │
│ │◀── GET /relay/inbox?pub=... ─│
│ │ │
│ │─── {items:[{payload_b64}]} ▶│
│ │ │
│ │ Submit RELAY_PROOF tx │
│ │ (claim fee from sender) │
```
**Gossipsub топик:** `dchain/relay/v1`
**Fee:** Relay берёт fee (задаётся при регистрации `--relay-fee`). Sender должен иметь достаточный баланс. Fee списывается при доставке через RELAY_PROOF транзакцию.

207
docs/architecture.md Normal file
View File

@@ -0,0 +1,207 @@
# Архитектура DChain
## Обзор
DChain — это L1-блокчейн для децентрализованного мессенджера. Архитектура разделена на четыре слоя:
```
┌─────────────────────────────────────────────────────────────┐
│ L3 Application — messaging, usernames, auctions, escrow │
│ (Smart contracts: username_registry, governance, auction, │
│ escrow; deployed on-chain via DEPLOY_CONTRACT) │
├─────────────────────────────────────────────────────────────┤
│ L2 Transport — relay mailbox, E2E NaCl encryption │
│ (relay/, mailbox, GossipSub envelopes, RELAY_PROOF tx) │
├─────────────────────────────────────────────────────────────┤
│ L1 Chain — PBFT consensus, WASM VM, BadgerDB │
│ (blockchain/, consensus/, vm/, identity/) │
├─────────────────────────────────────────────────────────────┤
│ L0 Network — libp2p, GossipSub, DHT, mDNS │
│ (p2p/) │
└─────────────────────────────────────────────────────────────┘
```
---
## Консенсус (PBFT)
**Алгоритм:** Practical Byzantine Fault Tolerance, кворум 2/3.
**Фазы:**
```
Leader Validator-2 Validator-3
│── PRE-PREPARE ──▶│ │
│── PRE-PREPARE ────────────────────────▶ │
│◀─ PREPARE ───────│ │
│◀─ PREPARE ────────────────────────────── │
│── COMMIT ────────▶│ │
│── COMMIT ──────────────────────────────▶ │
│◀─ COMMIT ────────│ │
│◀─ COMMIT ────────────────────────────── │
AddBlock()
```
**Свойства:**
- Safety при ≤ f Byzantine нодах где N ≥ 3f+1
- Текущий testnet: N=2 валидатора, f=0 (узел node3 — relay-only observer)
- View-change при недоступном лидере: таймаут 10 секунд
**Блок-производство:**
- Fast ticker (500 ms) — при наличии транзакций в mempool
- Idle ticker (5 s) — пустые блоки для heartbeat и синхронизации
**Ключевые файлы:**
```
consensus/engine.go — PBFT engine
consensus/msg.go — ConsensusMsg типы (PRE-PREPARE, PREPARE, COMMIT, VIEW-CHANGE)
```
---
## Хранилище (BadgerDB)
Весь state хранится в BadgerDB (LSM-дерево, pure Go).
**Пространства ключей:**
| Префикс | Тип | Описание |
|---------|-----|---------|
| `block:<index_20d>` | JSON | Блоки по индексу |
| `height` | uint64 JSON | Текущая высота |
| `balance:<pubkey>` | uint64 JSON | Балансы токенов |
| `id:<pubkey>` | JSON | Identity (RegisterKey payload) |
| `validator:<pubkey>` | presence | Активный сет валидаторов |
| `relay:<pubkey>` | JSON | Зарегистрированные relay-ноды |
| `contract:<id>` | JSON | ContractRecord (метаданные + WASM) |
| `cstate:<id>:<key>` | raw bytes | Состояние контракта |
| `clog:<id>:<height_20d>:<seq_05d>` | JSON | Логи контракта |
| `rep:<pubkey>` | JSON | Репутация (blocks, relays, slashes) |
| `contact_in:<to>:<from>` | JSON | Входящие contact requests |
| `mail:<x25519>:<ts>:<id>` | JSON | Relay mailbox (TTL 7 дней) |
**Две отдельные БД:**
- `--db ./chaindata` — chain state (consensus state machine)
- `--mailbox-db ./mailboxdata` — relay mailbox (отдельная изоляция)
---
## P2P сеть (libp2p)
**Компоненты:**
- **Transport:** TCP `/ip4/0.0.0.0/tcp/4001`
- **Identity:** Ed25519 peer key (вывод из node identity)
- **Discovery:** mDNS (локалка) + Kademlia DHT (WAN)
- **Pub/Sub:** GossipSub с тремя топиками:
| Топик | Содержимое |
|-------|-----------|
| `dchain/tx/v1` | Транзакции (gossip) |
| `dchain/blocks/v1` | Готовые блоки (gossip от лидера) |
| `dchain/relay/v1` | Relay envelopes (зашифрованные сообщения) |
- **Direct streams:** PBFT consensus messages (pre-prepare/prepare/commit/view-change) идут напрямую между валидаторами через `/dchain/consensus/1.0.0` протокол
- **Sync:** block range sync по `/dchain/sync/1.0.0` при подключении нового пира
---
## WASM Virtual Machine
**Runtime:** wazero v1.7.3, interpreter mode (детерминированный на всех платформах).
**Жизненный цикл контракта:**
```
DEPLOY_CONTRACT tx
├── validate: wazero.CompileModule() — если ошибка, tx отклоняется
├── contractID = hex(sha256(deployerPubKey || wasmBytes))[:16]
├── BadgerDB: contract:<id> → ContractRecord{WASMBytes, ABI, ...}
└── state: cstate:<id>:* — изначально пусто
CALL_CONTRACT tx
├── ABI validation: метод существует, число аргументов совпадает
├── pre-charge: tx.Fee + gasLimit × gasPrice
├── vm.Call(contractID, wasmBytes, method, argsJSON, gasLimit, hostEnv)
│ ├── compile (cached) + instrument (gas_tick в loop headers)
│ ├── register "env" host module (14 функций)
│ ├── [optional] wasi_snapshot_preview1 (для TinyGo контрактов)
│ └── fn.Call(ctx) → gasUsed, error
├── refund: (gasLimit - gasUsed) × gasPrice → обратно sender
└── logs: clog:<id>:<height>:<seq> → BadgerDB
```
**Gas модель:** каждый вызов функции (WASM или host) = 100 gas units.
`gasCost (µT) = gasUsed × gasPrice` (gasPrice управляется governance или константа 1 µT).
**Типы контрактов:**
- **Binary WASM** — написаны на Go через кодогенераторы (`contracts/*/gen/main.go`)
- **TinyGo WASM** — написаны на Go, компилируются с `tinygo -target wasip1`
---
## Экономика
**Supply:** Фиксированный. Genesis-блок минтит **21 000 000 T** на ключ node1. Последующей эмиссии нет.
**Unit:** µT (микро-токен). 1 T = 1 000 000 µT.
**Доходы валидаторов:** только комиссии из транзакций блока (`TotalFees`). Без блок-реварда.
**Комиссии:**
| Операция | Минимальная fee |
|---------|----------------|
| Все транзакции | 1 000 µT (MinFee) |
| CONTACT_REQUEST | tx.Amount → recipient (anti-spam) |
| DEPLOY_CONTRACT | 10 000 µT |
| CALL_CONTRACT | MinFee + gasUsed × gasPrice |
| RELAY_PROOF | sender → relay node (произвольно) |
**Governance:** gas_price, relay_fee и другие параметры можно менять on-chain через governance-контракт без хардфорка.
---
## Relay (E2E мессенджер)
**Шифрование:** NaCl box (X25519 Diffie-Hellman + XSalsa20 + Poly1305).
**Ключи:** каждый Identity имеет два ключа:
- Ed25519 (подпись транзакций, chain identity)
- X25519 (вывод из Ed25519 seed, шифрование сообщений)
**Поток сообщения:**
```
Alice node1 (relay) Bob
│ │ │
│── seal(msg, bob.X25519) ──▶ │ │
│ POST /relay/broadcast │ │
│ │── gossip envelope ──▶ │
│ │ store mailbox │
│ │ (TTL 7 days) │
│ │ │
│ │◀── GET /relay/inbox ──│
│ │ │── open(envelope)
│ │ │ → plaintext
```
**Anti-spam:**
- Первый контакт — платный (CONTACT_REQUEST tx, fee идёт получателю)
- Envelope: max 64 KB, max 500 envelopes на получателя (FIFO)
- RELAY_PROOF: подписанное доказательство доставки, fee снимается со sender и кредитуется relay
---
## Динамические валидаторы
Сет валидаторов хранится on-chain в `validator:<pubkey>`. Любой текущий валидатор может добавить или убрать другого (ADD_VALIDATOR / REMOVE_VALIDATOR tx). После commit такого блока PBFT-движок перезагружает сет без рестарта ноды.
**Ключевые файлы:**
```
blockchain/chain.go — state machine, applyTx, VMHostEnv
consensus/engine.go — PBFT, UpdateValidators()
vm/vm.go — wazero runtime, NewVM()
vm/host.go — host module "env" (14 функций)
vm/gas.go — gas counter, Remaining()
relay/mailbox.go — BadgerDB TTL mailbox
relay/crypto.go — NaCl seal/open
p2p/host.go — libp2p host, GossipSub
node/api_routes.go — HTTP API routing
```

411
docs/cli/README.md Normal file
View File

@@ -0,0 +1,411 @@
# CLI Reference
Справочник по командам `client` — инструмента для работы с DChain.
## Установка
```bash
go build -o client ./cmd/client/
# или
go install ./cmd/client/
```
## Глобальные флаги
Большинство команд принимают:
| Флаг | Описание |
|------|---------|
| `--key <path>` | Файл ключа (JSON с Ed25519 + X25519) |
| `--node <url>` | URL ноды (например `http://localhost:8081`) |
---
## Ключи и идентичность
### `keygen`
Сгенерировать новый ключ.
```bash
client keygen --out alice.json
```
Создаёт `alice.json` с Ed25519 (подпись транзакций) и X25519 (E2E relay шифрование).
---
### `register`
Зарегистрировать идентичность в блокчейне.
```bash
client register \
--key alice.json \
--nick alice \
--node http://localhost:8081
```
Выполняет PoW, затем отправляет `REGISTER_KEY` транзакцию.
---
## Токены (нативные)
### `balance`
```bash
client balance \
--key alice.json \
--node http://localhost:8081
```
```
Balance: 9.5 T (9500000 µT)
```
---
### `transfer`
```bash
# По pubkey
client transfer \
--to 03abcd... \
--amount 1.5 \
--key alice.json \
--node http://localhost:8081
# По DC-адресу
client transfer \
--to DCabc123... \
--amount 0.5 \
--key alice.json \
--node http://localhost:8081
# По @username (через registry)
client transfer \
--to @bob \
--amount 1.0 \
--registry $REG_ID \
--key alice.json \
--node http://localhost:8081
```
`--amount` в T (токенах). 1 T = 1 000 000 µT.
---
## Смарт-контракты
### `deploy-contract`
```bash
client deploy-contract \
--key alice.json \
--wasm mycontract.wasm \
--abi mycontract_abi.json \
--node http://localhost:8081
```
Выводит `contract_id: <hex16>`.
---
### `call-contract`
```bash
# Без аргументов
client call-contract \
--contract $CONTRACT_ID \
--method increment \
--gas 5000 \
--key alice.json --node http://localhost:8081
# Строковые аргументы
client call-contract \
--contract $REG_ID \
--method register \
--arg alice \
--gas 30000 \
--key alice.json --node http://localhost:8081
# Смешанные аргументы (строка + uint64)
client call-contract \
--contract $ESC_ID \
--method create \
--arg "deal-001" \
--arg "$SELLER_PUB" \
--arg64 5000000 \
--gas 30000 \
--key alice.json --node http://localhost:8081
# JSON-массив аргументов
client call-contract \
--contract $AUC_ID \
--method create \
--args '["Rare item", 1000000, 100]' \
--gas 20000 \
--key alice.json --node http://localhost:8081
```
| Флаг | Описание |
|------|---------|
| `--contract` | ID контракта |
| `--method` | Имя метода |
| `--arg` | Строковый аргумент (повторяемый) |
| `--arg64` | uint64 аргумент (повторяемый) |
| `--args` | JSON-массив всех аргументов |
| `--gas` | Gas лимит |
---
## Relay / Сообщения
### `send-msg`
```bash
# По pubkey
client send-msg \
--to $RECIPIENT_PUB \
--msg "Hello!" \
--key alice.json --node http://localhost:8081
# По @username
client send-msg \
--to @bob \
--msg "Hey Bob!" \
--registry $REG_ID \
--key alice.json --node http://localhost:8081
```
---
### `inbox`
```bash
# Читать сообщения
client inbox \
--key alice.json \
--node http://localhost:8081
# Последние N
client inbox \
--key alice.json \
--limit 20 \
--node http://localhost:8081
# Читать и удалять
client inbox \
--key alice.json \
--delete \
--node http://localhost:8081
```
---
### `request-contact`
```bash
client request-contact \
--to $RECIPIENT_PUB \
--fee 1000 \
--intro "Hi, let's connect!" \
--key alice.json --node http://localhost:8081
```
---
### `accept-contact` / `block-contact`
```bash
client accept-contact \
--from $SENDER_PUB \
--key alice.json --node http://localhost:8081
client block-contact \
--from $SENDER_PUB \
--key alice.json --node http://localhost:8081
```
---
## Валидаторы
### `stake`
```bash
client stake \
--amount 1000 \
--key alice.json --node http://localhost:8081
```
---
### `unstake`
```bash
client unstake \
--key alice.json --node http://localhost:8081
```
---
## Утилиты
### `info`
```bash
client info --node http://localhost:8081
```
Показывает высоту блока, количество валидаторов, total supply.
---
### `wait-tx`
Ждать подтверждения транзакции (через SSE).
```bash
client wait-tx \
--id tx-abc123 \
--timeout 30 \
--node http://localhost:8081
```
---
## Токены (fungible)
### `issue-token`
```bash
client issue-token \
--name "MyToken" \
--symbol MTK \
--decimals 6 \
--supply 1000000 \
--key alice.json --node http://localhost:8081
```
---
### `transfer-token`
```bash
client transfer-token \
--token $TOKEN_ID \
--to $RECIPIENT_PUB \
--amount 100 \
--key alice.json --node http://localhost:8081
```
---
### `burn-token`
```bash
client burn-token \
--token $TOKEN_ID \
--amount 50 \
--key alice.json --node http://localhost:8081
```
---
### `token-balance`
```bash
client token-balance \
--token $TOKEN_ID \
--key alice.json --node http://localhost:8081
# Другой адрес
client token-balance \
--token $TOKEN_ID \
--address $BOB_PUB \
--node http://localhost:8081
```
---
## NFT
### `mint-nft`
```bash
client mint-nft \
--name "CryptoArt #1" \
--desc "First edition" \
--uri "https://example.com/nft/1" \
--attrs '{"rarity":"rare","color":"blue"}' \
--key alice.json --node http://localhost:8081
```
---
### `transfer-nft`
```bash
client transfer-nft \
--nft $NFT_ID \
--to $BOB_PUB \
--key alice.json --node http://localhost:8081
```
---
### `burn-nft`
```bash
client burn-nft \
--nft $NFT_ID \
--key alice.json --node http://localhost:8081
```
---
### `nft-info`
```bash
client nft-info \
--nft $NFT_ID \
--node http://localhost:8081
# Проверить владельца
client nft-info \
--nft $NFT_ID \
--owner $ALICE_PUB \
--node http://localhost:8081
```
---
## @Username resolution
Команды `transfer` и `send-msg` поддерживают `@username` синтаксис при наличии `--registry`:
```bash
# Resolve @alice → pubkey через username_registry контракт
client transfer \
--to @alice \
--amount 1.0 \
--registry $REGISTRY_ID \
--key key.json --node http://localhost:8081
```
Под капотом: `GET /api/contracts/<registry_id>/state/name:alice``value_hex``hex.DecodeString` → pubkey.
---
## Переменные окружения
Для удобства можно задать в `.env` или shell:
```bash
export NODE_URL=http://localhost:8081
export MY_KEY=./keys/alice.json
export GOV_ID=a1b2c3d4e5f60001
export REG_ID=b2c3d4e5f6000002
```

59
docs/contracts/README.md Normal file
View File

@@ -0,0 +1,59 @@
# Production Smart Contracts
DChain поставляется с четырьмя production-контрактами, задеплоенными из genesis-кошелька.
## Обзор
| Контракт | Назначение | Ключевые фичи |
|---------|-----------|--------------|
| [username_registry](username_registry.md) | Username ↔ адрес | Тарифная сетка, treasury fees, reverse lookup |
| [governance](governance.md) | On-chain параметры | Propose/approve workflow, admin role |
| [auction](auction.md) | English auction | Token escrow, автоматический refund, settle |
| [escrow](escrow.md) | Двусторонний escrow | Dispute/resolve, admin arbitration |
## Деплой
```bash
docker compose --profile deploy run --rm deploy
```
Все 4 контракта деплоятся автоматически. ID сохраняются в `/tmp/contracts.env`.
## Вызов контракта
```bash
docker exec node1 client call-contract \
--key /keys/node1.json \
--contract <CONTRACT_ID> \
--method <METHOD> \
--arg <STRING_ARG> # строковый аргумент (можно несколько)
--arg64 <UINT64_ARG> # числовой аргумент uint64
--gas <GAS_LIMIT> # рекомендуется 20000 для записи, 5000 для чтения
--node http://node1:8080
```
## Contract Treasury
У каждого контракта есть **ownerless treasury address**`hex(sha256(contractID + ":treasury"))`.
Это эскроу-адрес без private key. Только сам контракт может снять с него деньги через host function `transfer`.
Используется в `auction` и `escrow` для хранения заблокированных токенов.
## Просмотр состояния
```bash
# Через REST API
curl http://localhost:8081/api/contracts/<ID>/state/<key>
# Через Explorer
open http://localhost:8081/contract?id=<ID>
```
## Логи контракта
```bash
# REST
curl "http://localhost:8081/api/contracts/<ID>/logs?limit=20"
# Explorer → вкладка Logs
```

213
docs/contracts/auction.md Normal file
View File

@@ -0,0 +1,213 @@
# auction
English auction с on-chain token escrow. Ставки хранятся в contract treasury. При перебивании ставки предыдущий топ-биддер получает автоматический refund.
## Жизненный цикл
```
seller buyer-1 buyer-2 anyone
│ │ │ │
│─ create ──────▶│ │ │
│ (min_bid, │ │ │
│ duration) │ │ │
│ │ │ │
│ │─ bid(500) ───▶│ │
│ │ treasury ←500│ │
│ │ │ │
│ │ │─ bid(800) ───▶│
│ │ refund→500 │ treasury ←800│
│ │ │ │
│ │ │ │ [end_block reached]
│ │ │ │
│◀─────────────────────────────────── settle ────│
│ treasury→800 │ │ │
│ (seller gets │ │ │
│ winning bid) │ │ │
```
**Статусы:** `open``settled` / `cancelled`
## Auction ID
Формат: `<block_height>:<seq>`, например `42:0`, `42:1`.
Генерируется автоматически при `create`, логируется как `created: <id>`.
## Методы
### `create`
Создать новый аукцион.
**Аргументы:**
| # | Имя | Тип | Описание |
|---|-----|-----|---------|
| 0 | `title` | string | Описание лота (max 128 байт) |
| 1 | `min_bid` | uint64 | Минимальная ставка в µT |
| 2 | `duration` | uint64 | Длительность в блоках |
**Поведение:**
- Сохраняет seller = caller
- `end_block = current_block + duration`
- Лог: `created: <auction_id>`
```bash
# Аукцион: мин. ставка 1 T, длительность 100 блоков
client call-contract --method create \
--arg "Rare NFT #42" --arg64 1000000 --arg64 100 \
--contract $AUC_ID --key /keys/node1.json \
--gas 20000 --node http://node1:8080
```
---
### `bid`
Поставить ставку. Средства переводятся из кошелька caller в treasury контракта.
**Аргументы:**
| # | Имя | Тип | Описание |
|---|-----|-----|---------|
| 0 | `auction_id` | string | ID аукциона |
| 1 | `amount` | uint64 | Ставка в µT |
**Проверки:**
- Аукцион в статусе `open`
- `current_block ≤ end_block`
- `amount > current_top_bid` (или `amount ≥ min_bid` если ставок нет)
**Поведение:**
- Переводит `amount` с caller → treasury
- Если был предыдущий топ-биддер → refund ему его ставки из treasury
- Обновляет `top_bidder` и `top_bid`
- Лог: `bid: <auction_id>`
```bash
client call-contract --method bid \
--arg "42:0" --arg64 2000000 \
--contract $AUC_ID --key /keys/node1.json \
--gas 30000 --node http://node1:8080
```
---
### `settle`
Завершить аукцион после истечения `end_block`. Переводит топ-ставку продавцу.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `auction_id` | string |
**Проверки:** `current_block > end_block`, статус `open`
**Поведение:**
- Если есть ставки: переводит `top_bid` из treasury → seller, статус → `settled`
- Если ставок нет: статус → `cancelled`
- Лог: `settled: <id>` или `cancelled: <id> (no bids)`
- Может вызвать **любой** (не только seller)
```bash
client call-contract --method settle --arg "42:0" \
--contract $AUC_ID --key /keys/node1.json \
--gas 20000 --node http://node1:8080
```
---
### `cancel`
Отменить аукцион без ставок. Только seller.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `auction_id` | string |
**Проверки:** статус `open`, ставок нет, `caller == seller`
```bash
client call-contract --method cancel --arg "42:0" \
--contract $AUC_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
---
### `info`
Запросить состояние аукциона.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `auction_id` | string |
**Логи:**
```
seller: <pubkey>
title: <title>
top_bid: <amount>
end_block: <block>
status: open|settled|cancelled
```
```bash
client call-contract --method info --arg "42:0" \
--contract $AUC_ID --key /keys/node1.json \
--gas 5000 --node http://node1:8080
```
---
## State Layout
```
cstate:<contractID>:seq → uint64 (глобальный счётчик аукционов)
cstate:<contractID>:a:<id>:s → seller pubkey
cstate:<contractID>:a:<id>:t → title
cstate:<contractID>:a:<id>:b → top_bid (uint64 big-endian)
cstate:<contractID>:a:<id>:e → end_block (uint64 big-endian)
cstate:<contractID>:a:<id>:w → top_bidder pubkey
cstate:<contractID>:a:<id>:x → status byte ('o', 's', 'c')
```
## Полный пример сценария
```bash
# 1. Alice создаёт аукцион
docker exec node1 client call-contract \
--key /keys/node1.json --contract $AUC_ID \
--method create \
--arg "Special Edition #1" --arg64 500000 --arg64 50 \
--gas 20000 --node http://node1:8080
# Лог: created: 5:0
AUC_ITEM="5:0"
# 2. Bob делает ставку 1 T
docker exec node1 client call-contract \
--key /tmp/bob.json --contract $AUC_ID \
--method bid --arg $AUC_ITEM --arg64 1000000 \
--gas 30000 --node http://node1:8080
# 3. Charlie перебивает ставку — Bob получает refund автоматически
docker exec node1 client call-contract \
--key /tmp/charlie.json --contract $AUC_ID \
--method bid --arg $AUC_ITEM --arg64 1500000 \
--gas 30000 --node http://node1:8080
# 4. Проверить статус
docker exec node1 client call-contract \
--key /keys/node1.json --contract $AUC_ID \
--method info --arg $AUC_ITEM \
--gas 5000 --node http://node1:8080
# 5. После end_block — завершить (любой может)
docker exec node1 client call-contract \
--key /keys/node1.json --contract $AUC_ID \
--method settle --arg $AUC_ITEM \
--gas 20000 --node http://node1:8080
# Лог: settled: 5:0
# Alice получает 1.5 T на кошелёк
```

267
docs/contracts/escrow.md Normal file
View File

@@ -0,0 +1,267 @@
# escrow
Двусторонний trustless escrow. Buyer блокирует средства в contract treasury. Seller выполняет условие. Buyer подтверждает или открывает спор. При споре admin-арбитр решает исход.
## Жизненный цикл
```
buyer seller admin
│ │ │
│─ create ─────▶│ │ treasury ← amount (locked)
│ (seller, │ │ status: active
│ amount) │ │
│ │ │
│ [seller delivered] │
│ │ │
│─ release ────▶│ │ treasury → seller
│ │ │ status: released
OR:
│─ dispute ────▶│ │ status: disputed
│ │─ dispute ────▶│ (either party can)
│ │ │
│ │ │─ resolve(winner=buyer) ──▶
│ │ │ treasury → buyer
│ │ │ status: refunded
│ │ │
│ │ │─ resolve(winner=seller) ──▶
│ │ │ treasury → seller
│ │ │ status: released
OR:
│ │─ refund ─────▶│ treasury → buyer (voluntary)
│ │ │ status: refunded
```
**Статусы:** `active``released` / `refunded` / `disputed`
## Методы
### `init`
Установить caller как admin (арбитр споров).
**Аргументы:** нет
Вызывается один раз после деплоя (deploy-скрипт делает автоматически).
```bash
client call-contract --method init \
--contract $ESC_ID --key /keys/node1.json \
--gas 5000 --node http://node1:8080
```
---
### `create`
Buyer создаёт escrow и блокирует средства.
**Аргументы:**
| # | Имя | Тип | Описание |
|---|-----|-----|---------|
| 0 | `id` | string | Уникальный ID эскроу (задаёт пользователь) |
| 1 | `seller` | string | Pubkey продавца (hex) |
| 2 | `amount` | uint64 | Сумма в µT |
**Поведение:**
- Переводит `amount` с caller (buyer) → treasury
- Сохраняет buyer, seller, amount, status=active
- Лог: `created: <id>`
**Ограничения:**
- ID должен быть уникальным — если `cstate[e:<id>:b]` уже есть, tx отклоняется
```bash
client call-contract --method create \
--arg "deal-001" \
--arg <SELLER_PUBKEY> \
--arg64 10000000 \
--contract $ESC_ID --key /tmp/buyer.json \
--gas 30000 --node http://node1:8080
```
---
### `release`
Buyer подтверждает получение и освобождает средства seller'у.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `id` | string |
**Права:** только buyer.
**Статус:** должен быть `active`.
**Поведение:**
- Переводит `amount` с treasury → seller
- Статус → released
- Лог: `released: <id>`
```bash
client call-contract --method release --arg "deal-001" \
--contract $ESC_ID --key /tmp/buyer.json \
--gas 20000 --node http://node1:8080
```
---
### `refund`
Seller добровольно возвращает деньги buyer'у.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `id` | string |
**Права:** только seller.
**Статус:** должен быть `active`.
**Поведение:**
- Переводит `amount` с treasury → buyer
- Статус → refunded
- Лог: `refunded: <id>`
```bash
client call-contract --method refund --arg "deal-001" \
--contract $ESC_ID --key /tmp/seller.json \
--gas 20000 --node http://node1:8080
```
---
### `dispute`
Открыть спор. Может вызвать buyer или seller.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `id` | string |
**Статус:** должен быть `active`.
**Поведение:**
- Статус → disputed
- Лог: `disputed: <id>`
- Средства остаются заблокированы в treasury
```bash
client call-contract --method dispute --arg "deal-001" \
--contract $ESC_ID --key /tmp/buyer.json \
--gas 10000 --node http://node1:8080
```
---
### `resolve`
Admin разрешает спор.
**Аргументы:**
| # | Имя | Тип | Значения |
|---|-----|-----|---------|
| 0 | `id` | string | ID эскроу |
| 1 | `winner` | string | `buyer` или `seller` |
**Права:** только admin.
**Статус:** должен быть `disputed`.
**Поведение:**
- `winner=buyer` → treasury → buyer, статус → refunded
- `winner=seller` → treasury → seller, статус → released
- Лог: `resolved: <id>`
```bash
# Admin решает в пользу buyer
client call-contract --method resolve \
--arg "deal-001" --arg buyer \
--contract $ESC_ID --key /keys/node1.json \
--gas 20000 --node http://node1:8080
# Admin решает в пользу seller
client call-contract --method resolve \
--arg "deal-001" --arg seller \
--contract $ESC_ID --key /keys/node1.json \
--gas 20000 --node http://node1:8080
```
---
### `info`
Запросить состояние эскроу.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `id` | string |
**Логи:**
```
buyer: <pubkey>
seller: <pubkey>
amount: <µT>
status: active|released|refunded|disputed
```
```bash
client call-contract --method info --arg "deal-001" \
--contract $ESC_ID --key /keys/node1.json \
--gas 5000 --node http://node1:8080
```
---
## State Layout
```
cstate:<contractID>:admin → admin pubkey
cstate:<contractID>:e:<id>:b → buyer pubkey
cstate:<contractID>:e:<id>:s → seller pubkey
cstate:<contractID>:e:<id>:a → amount (uint64 big-endian)
cstate:<contractID>:e:<id>:x → status byte ('a'=active, 'd'=disputed, 'r'=released, 'f'=refunded)
```
## Полный сценарий с dispute
```bash
# Параметры
BUYER_KEY=/tmp/buyer.json
SELLER_KEY=/tmp/seller.json
ADMIN_KEY=/keys/node1.json
ID="escrow-001"
# 1. Buyer создаёт эскроу на 5 T
docker exec node1 client call-contract \
--key $BUYER_KEY --contract $ESC_ID \
--method create \
--arg $ID --arg $SELLER_PUB --arg64 5000000 \
--gas 30000 --node http://node1:8080
# 2. Проверить статус
docker exec node1 client call-contract \
--key $BUYER_KEY --contract $ESC_ID \
--method info --arg $ID \
--gas 5000 --node http://node1:8080
# status: active
# 3. Buyer не доволен — открывает спор
docker exec node1 client call-contract \
--key $BUYER_KEY --contract $ESC_ID \
--method dispute --arg $ID \
--gas 10000 --node http://node1:8080
# 4. Admin рассматривает дело и решает в пользу seller
docker exec node1 client call-contract \
--key $ADMIN_KEY --contract $ESC_ID \
--method resolve --arg $ID --arg seller \
--gas 20000 --node http://node1:8080
# Лог: resolved: escrow-001
# Seller получает 5 T
```

View File

@@ -0,0 +1,203 @@
# governance
On-chain governance контракт для управления параметрами сети. Deployer становится admin. Любой может предложить изменение параметра; admin принимает или отклоняет.
## Концепция
```
Участник Admin
│ │
│── propose ──▶ │ state[prop:<key>] = value
│ │
│ │── approve ──▶ state[param:<key>] = value (live)
│ │ удаляет prop:<key>
│ │
│ │── reject ──▶ удаляет prop:<key>
```
После approve параметр становится **live** и читается нодой при следующем вызове контракта.
## Методы
### `init`
Инициализировать контракт, установить caller как admin.
**Аргументы:** нет
**Вызывается:** один раз после деплоя (deploy-скрипт делает это автоматически).
**Поведение:**
- Устанавливает `state[admin] = caller_pubkey`
- Идемпотентен: повторный вызов не меняет admin если уже установлен
```bash
client call-contract --method init --contract $GOV_ID \
--key /keys/node1.json --gas 5000 --node http://node1:8080
```
---
### `propose`
Предложить новое значение параметра. Доступно всем.
**Аргументы:**
| # | Имя | Тип | Ограничение |
|---|-----|-----|------------|
| 0 | `key` | string | max 64 байта |
| 1 | `value` | string | max 256 байт |
**Поведение:**
- Сохраняет `state[prop:<key>] = value`
- Лог: `proposed: <key>=<value>`
- Если pending-предложение уже есть → перезаписывает
```bash
# Предложить новую цену газа 5 µT/gas
client call-contract --method propose \
--arg gas_price --arg 5 \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
---
### `approve`
Admin принимает pending-предложение. Параметр становится live.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `key` | string |
**Права:** только admin.
**Поведение:**
- Копирует `state[prop:<key>]``state[param:<key>]`
- Удаляет `state[prop:<key>]`
- Лог: `approved: <key>`
- Если нет pending → лог `no pending: <key>`
```bash
client call-contract --method approve --arg gas_price \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
---
### `reject`
Admin отклоняет pending-предложение.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `key` | string |
**Права:** только admin.
```bash
client call-contract --method reject --arg gas_price \
--contract $GOV_ID --key /keys/node1.json \
--gas 5000 --node http://node1:8080
```
---
### `get`
Прочитать текущее live-значение параметра.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `key` | string |
**Лог:**
- `value: <value>` — параметр установлен
- `not set: <key>` — параметр не установлен
```bash
client call-contract --method get --arg gas_price \
--contract $GOV_ID --key /keys/node1.json \
--gas 3000 --node http://node1:8080
```
Через REST (без транзакции):
```bash
curl http://localhost:8081/api/contracts/$GOV_ID/state/param:gas_price
```
---
### `get_pending`
Прочитать pending-предложение.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `key` | string |
**Лог:**
- `pending: <value>`
- `no pending: <key>`
---
### `set_admin`
Передать роль admin другому адресу.
**Аргументы:**
| # | Имя | Тип |
|---|-----|-----|
| 0 | `new_admin` | string (hex pubkey) |
**Права:** только текущий admin.
```bash
client call-contract --method set_admin --arg <NEW_ADMIN_PUBKEY> \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
---
## Управляемые параметры
Governance хранит произвольные string-параметры. Нода читает следующие:
| Ключ | Тип | Описание | По умолчанию |
|------|-----|---------|-------------|
| `gas_price` | uint64 (строка) | µT за 1 gas unit | 1 µT |
> Нода читает `gas_price` при каждом `CALL_CONTRACT` через `chain.GetEffectiveGasPrice()`.
> Изменение вступает в силу немедленно после `approve` — без перезапуска.
Можно хранить любые параметры приложения (messenger_entry_fee, relay_fee, etc.) и читать их из других контрактов через межконтрактные вызовы.
## State Layout
```
cstate:<contractID>:admin → admin pubkey (hex string bytes)
cstate:<contractID>:param:<key> → live value
cstate:<contractID>:prop:<key> → pending proposal
```
## Интеграция с нодой
После деплоя governance необходимо его **привязать** к ноде:
```bash
# Автоматически через deploy-скрипт
# Или вручную:
curl -X POST http://localhost:8081/api/governance/link \
-H "Content-Type: application/json" \
-d "{\"governance\": \"$GOV_ID\"}"
```
Подробнее: [Governance интеграция](../node/governance.md)

View File

@@ -0,0 +1,133 @@
# username_registry
Привязка читаемых `@username` к адресам (Ed25519 pubkey). Forward
(`name → addr`) и reverse (`addr → name`) lookup, передача ownership,
освобождение имени.
**Реализация: native Go** (`blockchain/native_username.go`). Работает
без WASM VM — нулевая латентность, не может повесить AddBlock. Старая
WASM-версия заменена; клиенты получают канонический ID через
`/api/well-known-contracts`.
## Ключевые свойства
| | |
|---|---|
| **Contract ID** | `native:username_registry` |
| **Version** | `2.1.0-native` |
| **Регистрация** | Flat fee 10 000 µT (0.01 T), **burn** — никому не идёт |
| **Ограничения имени** | 432 символа, `a-z 0-9 _ -`, первый символ — буква |
| **Зарезервированные** | `system`, `admin`, `root`, `dchain`, `null`, `none` |
| **Один адрес — одно имя** | Чтобы сменить, нужно `release` + `register` |
Комиссия 10 000 µT выбрана как баланс: достаточно дёшево для реальных
пользователей, достаточно дорого чтобы один ID-squatter не забил тысячу
имён из скучающего бота.
## Оплата
Плата передаётся в `tx.Amount` (видна в истории транзакций), контракт
проверяет её точное значение и вычитает из баланса отправителя. Не
направляется никуда — токены сгорают.
Дополнительно платится `tx.Fee = 1 000 µT` за саму транзакцию — это
network fee валидатору, стандартный `MinFee` для любой tx.
Итого: `register(name)` стоит **11 000 µT ≈ 0.011 T**.
## Методы
### `register(name)` — payable 10000
Зарегистрировать имя для `tx.From`.
**Проверки** (в порядке):
1. `name` валиден (длина, символы, не reserved)
2. `tx.Amount == 10 000` (ошибка `register requires tx.amount = 10000 µT`)
3. Имя не занято
4. `tx.From` не владеет другим именем
5. Баланс отправителя достаточен для `amount + fee + gas`
**Успех:**
- Списывает 10 000 µT с баланса (burn)
- `cstate[name:<name>] = tx.From`
- `cstate[addr:<tx.From>] = name`
- Лог `registered: <name> → <pubkey>`
**CLI-пример:**
```bash
client call-contract \
--key my.json \
--contract native:username_registry \
--method register \
--args '["alice"]' \
--amount 10000 \
--node http://localhost:8081
```
**Клиент-приложение** делает это автоматически через Settings →
«Купить никнейм».
### `resolve(name)` — free
Прочитать владельца имени.
Логирует `owner: <pubkey>` или `not found: <name>`. Клиент в HTTP-API
обычно использует прямое чтение state:
```
GET /api/contracts/native:username_registry/state/name:alice
→ { value_hex: "<hex of owner pubkey ASCII>" }
```
### `lookup(address)` — free
Обратный lookup. Логирует `name: <name>` или `no name: <address>`.
Клиент использует `GET .../state/addr:<pubkey>`.
### `transfer(name, new_owner)` — free
Передать `name` другому адресу. Только текущий владелец может вызвать.
Новый владелец не должен уже иметь имя.
### `release(name)` — free
Удалить привязку. Только текущий владелец. Освобождает `name` и
`addr:<caller>` для будущих регистраций.
## State layout
Все ключи записываются в общий namespace `cstate:native:username_registry:`:
| Key | Value |
|-----|-------|
| `name:<name>` | Hex pubkey владельца (64 символа) |
| `addr:<pubkey>` | ASCII-строка с именем |
Значения хранятся как байты; HTTP endpoint
`/api/contracts/:id/state/:key` возвращает их в `value_hex`, клиент
делает `hex → UTF-8` перед показом.
## Клиентская интеграция
- `useWellKnownContracts` хук авто-синхронизирует `settings.contractId =
native:username_registry`
- `resolveUsername(contractId, name)` и `reverseResolve(contractId, addr)`
в `client-app/lib/api.ts` делают HTTP-запрос + hex-decode
- Settings экран покупает никнейм через `buildCallContractTx({amount:
10_000, ...})` + автоматический `reverseResolve` polling после submit
для показа `@name` в профиле за ~1 блок
## Отличия от предыдущей WASM-версии
| | WASM v1 | Native v2 |
|---|---------|-----------|
| **Fee** | 10^(7-len), tiered | Flat 10 000 µT |
| **Fee destination** | Contract treasury | Burn |
| **Min length** | 1 | **4** |
| **Payment point** | Debited inside contract, invisible in tx | `tx.Amount`, visible in history |
| **Latency** | ~10 ms (wazero startup) | ~50 µs (direct Go call) |
| **VM hang risk** | Да (требует timeout) | Нет |
Зарегистрированные в старой WASM-версии имена не мигрируются
автоматически — операторам нужно пере-зарегистрировать после reset
chain (см. `CHANGELOG.md` «Compatibility notes»).

103
docs/development/README.md Normal file
View File

@@ -0,0 +1,103 @@
# Разработка контрактов
DChain поддерживает два способа написания WASM-контрактов.
## Выбор подхода
| | TinyGo SDK | Бинарный WASM |
|-|-----------|--------------|
| **Язык** | Go | Go (кодогенератор) |
| **Инструменты** | TinyGo 0.30+ | Стандартный Go |
| **Сложность** | Низкая | Высокая |
| **Размер .wasm** | ~30100 KB | 5002000 байт |
| **Отладка** | Стандартная | Сложная |
| **Рекомендуется** | Новые контракты | Минимальные/системные |
## TinyGo SDK (рекомендуется)
Пишите контракты как обычный Go-код:
```go
package main
import dc "go-blockchain/contracts/sdk"
//export increment
func increment() {
v := dc.GetU64("counter")
dc.PutU64("counter", v+1)
dc.Log("incremented")
}
func main() {}
```
Подробное руководство: [TinyGo SDK](tinygo.md)
## Бинарный WASM
Генераторы в `contracts/*/gen/main.go` создают минимальные WASM-модули вручную через LEB128-кодирование. Размер получается ~500 байт, но написание сложное.
Подробнее: [Бинарный WASM](binary-wasm.md)
## Документация по разделам
| Документ | Содержание |
|---------|-----------|
| [TinyGo SDK](tinygo.md) | Установка, SDK API, сборка, деплой, пример |
| [Host functions](host-functions.md) | Полный справочник 14 host-функций |
| [Межконтрактные вызовы](inter-contract.md) | call_contract, composability, глубина |
| [Бинарный WASM](binary-wasm.md) | LEB128, секции, кодогенератор |
| [Gas и Treasury](gas-model.md) | Gas модель, treasury, governance |
## Общая структура контракта
```
contracts/
mycontract/
main.go # TinyGo источник
mycontract_abi.json # ABI (описание методов)
mycontract.wasm # Скомпилированный бинарник
```
## ABI формат
```json
{
"contract": "mycontract",
"version": "1.0.0",
"description": "...",
"methods": [
{
"name": "method_name",
"description": "...",
"args": [
{"name": "param1", "type": "string"},
{"name": "amount", "type": "uint64"}
]
}
]
}
```
Поддерживаемые типы: `string`, `uint64`, `bytes`.
## Деплой контракта
```bash
# Локально
client deploy-contract \
--key key.json \
--wasm mycontract.wasm \
--abi mycontract_abi.json \
--node http://localhost:8081
# В Docker
docker exec node1 client deploy-contract \
--key /keys/node1.json \
--wasm /path/to/mycontract.wasm \
--abi /path/to/mycontract_abi.json \
--node http://node1:8080
```
Успешный деплой логирует `contract_id: <hex16>`.

View File

@@ -0,0 +1,249 @@
# Бинарный WASM
Альтернативный способ написания контрактов — генерация WASM-байткода вручную. Используется для системных/минимальных контрактов где критичен размер (~5002000 байт вместо 30100 KB TinyGo).
> **Рекомендуется TinyGo** для новых контрактов. Бинарный WASM используется для встроенных контрактов в этом проекте.
## Структура генератора
```
contracts/
mycontract/
gen/
main.go # Go-программа, печатает WASM байты
mycontract.wasm # Результат: go run gen/main.go > mycontract.wasm
mycontract_abi.json
```
Генератор запускается стандартным Go (`go run gen/main.go`) и выводит бинарный WASM в stdout.
## Анатомия WASM модуля
```
Magic + Version : \0asm\x01\x00\x00\x00
Секции (по порядку):
1. Type — сигнатуры функций
2. Import — импортируемые host-функции ("env" модуль)
3. Function — индексы типов для локальных функций
4. Export — экспортируемые функции (методы контракта)
5. Code — тела функций
6. Data — статические строки в памяти
7. Memory — объявление памяти (мин. 1 страница = 64 KB)
```
## LEB128 кодирование
Целые числа в WASM кодируются в LEB128 (variable-length encoding).
```go
// Unsigned LEB128
func u(v uint64) []byte {
var out []byte
for {
b := byte(v & 0x7f)
v >>= 7
if v != 0 {
b |= 0x80
}
out = append(out, b)
if v == 0 {
break
}
}
return out
}
// Signed LEB128 (для i32/i64 константы)
func s(v int64) []byte {
var out []byte
for {
b := byte(v & 0x7f)
v >>= 7
if (v == 0 && b&0x40 == 0) || (v == -1 && b&0x40 != 0) {
out = append(out, b)
break
}
out = append(out, b|0x80)
}
return out
}
```
## Вспомогательные функции
```go
// Секция с длиной-префиксом
func section(id byte, content []byte) []byte {
return append(append([]byte{id}, u(uint64(len(content)))...), content...)
}
// Вектор (count + элементы)
func vec(items ...[]byte) []byte {
out := u(uint64(len(items)))
for _, item := range items {
out = append(out, item...)
}
return out
}
// Строка с длиной-префиксом
func str(s string) []byte {
return append(u(uint64(len(s))), []byte(s)...)
}
```
## WASM инструкции
Наиболее используемые опкоды:
| Инструкция | Байт | Описание |
|-----------|------|---------|
| `local.get` | `0x20 + leb(idx)` | Прочитать локальную переменную |
| `local.set` | `0x21 + leb(idx)` | Записать локальную переменную |
| `local.tee` | `0x22 + leb(idx)` | Записать и оставить на стеке |
| `i32.const` | `0x41 + sleb(val)` | Константа i32 |
| `i64.const` | `0x42 + sleb(val)` | Константа i64 |
| `i32.load` | `0x28 0x02 leb(offset)` | Загрузить 4 байта |
| `i32.store` | `0x36 0x02 leb(offset)` | Сохранить 4 байта |
| `i64.load` | `0x29 0x03 leb(offset)` | Загрузить 8 байт |
| `i64.store` | `0x37 0x03 leb(offset)` | Сохранить 8 байт |
| `i32.add` | `0x6A` | Сложение i32 |
| `i32.sub` | `0x6B` | Вычитание i32 |
| `i32.eq` | `0x46` | Равенство i32 |
| `i64.eq` | `0x51` | Равенство i64 |
| `if` | `0x04 0x40` | Ветвление (void) |
| `else` | `0x05` | — |
| `end` | `0x0B` | Конец блока/функции |
| `return` | `0x0F` | Возврат из функции |
| `call` | `0x10 + leb(funcIdx)` | Вызов функции |
| `drop` | `0x1A` | Убрать верхний элемент стека |
## Шаблон генератора
```go
package main
import (
"os"
)
// Импорты host-функций
const (
fnGetArgStr = 0 // индекс в таблице импортов
fnSetState = 1
fnGetState = 2
fnGetStateLen = 3
fnGetCaller = 4
fnLog = 5
)
// Смещения в памяти
const (
pKey = 0 // буфер для ключей state
pVal = 128 // буфер для значений
pCaller = 256 // буфер для caller
pArg0 = 320 // буфер для аргумента 0
pStatic = 400 // статические строки (из Data секции)
)
func main() {
// 1. Magic + version
wasm := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}
// 2. Type секция — сигнатуры функций
// ...
// 3. Import секция — host функции
// ...
// 4. Function + Memory + Export + Code + Data
// ...
os.Stdout.Write(wasm)
}
```
## Пример: функция increment
Аналог Go-кода:
```go
func increment() {
count := getU64("counter")
putU64("counter", count+1)
log("incremented")
}
```
В бинарном WASM:
```go
// В Data секции: "counter\x00" @ offset 0, "incremented\x00" @ offset 8
// Функция increment:
func buildIncrement() []byte {
// locals: none
body := []byte{0x00} // local decl count = 0
// load key "counter" (ptr=0, len=7) → call get_u64 → result on stack
body = append(body, 0x41) // i32.const
body = append(body, s(0)...) // ptr = 0 (offset of "counter" in data)
body = append(body, 0x41)
body = append(body, s(7)...) // len = 7
body = append(body, 0x10) // call
body = append(body, u(fnGetU64)...)
// add 1
body = append(body, 0x42, 0x01) // i64.const 1
body = append(body, 0x7C) // i64.add
// put_u64("counter", result)
body = append(body, 0x21, 0x00) // local.set 0 (tmp)
body = append(body, 0x41)
body = append(body, s(0)...)
body = append(body, 0x41)
body = append(body, s(7)...)
body = append(body, 0x20, 0x00) // local.get 0
body = append(body, 0x10)
body = append(body, u(fnPutU64)...)
// log("incremented")
body = append(body, 0x41)
body = append(body, s(8)...) // ptr = 8 (offset of "incremented")
body = append(body, 0x41)
body = append(body, s(11)...) // len = 11
body = append(body, 0x10)
body = append(body, u(fnLog)...)
body = append(body, 0x0B) // end
return append(u(uint64(len(body))), body...)
}
```
## Сборка и деплой
```bash
# Генерация
go run contracts/mycontract/gen/main.go > contracts/mycontract/mycontract.wasm
# Проверка (wasm-objdump из wabt)
wasm-objdump -d contracts/mycontract/mycontract.wasm
# Деплой
client deploy-contract \
--key key.json \
--wasm contracts/mycontract/mycontract.wasm \
--abi contracts/mycontract/mycontract_abi.json \
--node http://localhost:8081
```
## Встроенные контракты в проекте
| Контракт | Генератор | Размер .wasm |
|---------|----------|-------------|
| counter | contracts/counter/gen/main.go | ~500 байт |
| governance | contracts/governance/gen/main.go | ~800 байт |
| name_registry | contracts/name_registry/gen/main.go | ~1.2 KB |
| username_registry | contracts/username_registry/gen/main.go | ~1.5 KB |
| escrow | contracts/escrow/gen/main.go | ~2 KB |
| auction | contracts/auction/gen/main.go | ~2.5 KB |
Все генераторы используют одни и те же LEB128-утилиты и паттерн `vec/section/str`.

View File

@@ -0,0 +1,193 @@
# Gas и Treasury
## Gas модель
Каждый вызов контракта (`CALL_CONTRACT` транзакция) тратит gas. Gas конвертируется в µT и списывается со счёта отправителя.
### Формула
```
fee = gas_used × gas_price
```
| Параметр | Значение | Откуда |
|---------|---------|-------|
| `gas_used` | ≤ `--gas` лимит | считается VM |
| `gas_price` | 1 µT/gas по умолчанию | governance или константа |
### Лимит gas
Задаётся в транзакции флагом `--gas`:
```bash
client call-contract --method increment \
--gas 5000 \
--contract $CONTRACT_ID --key key.json --node http://localhost:8081
```
Если VM израсходует весь лимит до конца исполнения — транзакция откатывается, gas не возвращается.
### Что стоит gas
| Операция | Стоимость |
|---------|---------|
| Итерация цикла (`gas_tick`) | 1 unit |
| Внешний вызов (`call_contract`) | gas подвызова |
| Прочие инструкции | 0 (не инструментированы) |
> TinyGo автоматически вставляет `gas_tick` в циклы. Бинарные WASM-контракты вызывают `gas_tick` вручную.
### Межконтрактный gas
При вызове `dc.CallContract(...)`, подвызов получает budget = `Remaining()` родителя.
После возврата, `gasUsed` подвызова списывается с родительского счётчика.
```
Родитель: limit=5000, used=200
→ подвызов budget = 4800
→ подвызов использовал 300
Родитель: used = 200 + 300 = 500
```
## Treasury
Каждый контракт имеет **treasury** — специальный баланс, привязанный к контракту.
### Получить адрес treasury
```go
treasury := dc.Treasury() // hex pubkey, 64 символа
```
### Переводы через treasury
Treasury используется как промежуточный эскроу:
```go
// Принять деньги от caller → treasury
dc.Transfer(dc.Caller(), dc.Treasury(), amount)
// Отправить деньги из treasury → получателю
dc.Transfer(dc.Treasury(), recipient, amount)
```
**Ограничение:** в `dc.Transfer(from, ...)`, `from` может быть только:
1. `dc.Caller()` — списать со счёта вызывающего
2. `dc.Treasury()` — списать с treasury контракта
Контракт **не может** списывать деньги с произвольных адресов.
### Проверить баланс treasury
```go
treasuryBal := dc.Balance(dc.Treasury())
```
### Паттерны использования treasury
**1. Fee collector:**
```go
const fee = 1000 // µT
dc.Transfer(dc.Caller(), dc.Treasury(), fee)
// Деньги остаются в treasury навсегда (или до явного вывода)
```
**2. Эскроу (lock → release):**
```go
// lock: buyer → treasury
dc.Transfer(buyer, dc.Treasury(), amount)
// release: treasury → seller
dc.Transfer(dc.Treasury(), seller, amount)
// refund: treasury → buyer
dc.Transfer(dc.Treasury(), buyer, amount)
```
**3. Prize pool (аукцион):**
```go
// Принять ставку
dc.Transfer(bidder, dc.Treasury(), bid)
// Возврат предыдущей ставки
dc.Transfer(dc.Treasury(), prevBidder, prevBid)
// Финал: перевод победителю
dc.Transfer(dc.Treasury(), seller, topBid)
```
## Governance и динамический gas_price
По умолчанию `gas_price = 1 µT/gas`. Это значение можно изменить через governance-контракт.
### Как нода читает gas_price
```go
// blockchain/chain.go
func (c *Chain) GetEffectiveGasPrice() uint64 {
val, ok := c.GetGovParam("gas_price")
if !ok { return GasPrice } // константа по умолчанию
price, err := strconv.ParseUint(val, 10, 64)
if err != nil { return GasPrice }
return price
}
```
`GetGovParam` читает `cstate:<govID>:param:gas_price` напрямую из BadgerDB без VM-вызова.
### Изменить gas_price через governance
```bash
# Предложить новое значение (5 µT/gas)
client call-contract --method propose \
--arg gas_price --arg 5 \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
# Admin утверждает
client call-contract --method approve \
--arg gas_price \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
После approve все последующие `CALL_CONTRACT` транзакции будут использовать новую цену.
### Проверить текущий gas_price
```bash
# Через контракт
client call-contract --method get \
--arg gas_price \
--contract $GOV_ID --key key.json \
--gas 3000 --node http://localhost:8081
# Через REST (без транзакции)
curl http://localhost:8081/api/contracts/$GOV_ID/state/param:gas_price
```
## Константы
```go
// blockchain/chain.go
const (
GasPrice = uint64(1) // µT за 1 gas unit (default)
)
// vm/host.go
const (
maxContractCallDepth = 8
)
```
## Рекомендации по выбору --gas
| Операция | Рекомендуемый gas |
|---------|-----------------|
| Простое чтение/запись state | 3 000 |
| Регистрация / короткий метод | 5 000 |
| Метод с несколькими state операциями | 10 000 |
| Создание эскроу/аукциона | 20 000 30 000 |
| Метод с межконтрактным вызовом | 30 000 50 000 |
Gas, который не был потрачён, **не возвращается**. Завышенный лимит безопасен, но лишних денег не списывается больше `gas_used × gas_price`.

View File

@@ -0,0 +1,274 @@
# Host Functions
Полный справочник 14 host-функций, которые DChain экспортирует в WASM-контракты через модуль `env`.
Все функции регистрируются в `vm/host.go`. Контракт импортирует их через `//go:wasmimport env <name>` (TinyGo) или напрямую в секции `imports` WASM-модуля.
---
## Аргументы
### `get_args`
```
get_args(dstPtr i32, dstLen i32) → written i32
```
Читает весь JSON-массив аргументов транзакции в буфер `[dstPtr, dstPtr+dstLen)`.
Возвращает количество записанных байт (0 если аргументов нет).
---
### `get_arg_str`
```
get_arg_str(idx i32, dstPtr i32, dstLen i32) → written i32
```
Читает аргумент с индексом `idx` как строку (без кавычек JSON).
`dstLen` — максимальная длина буфера.
Возвращает 0 если аргумент не существует или тип не строка.
**SDK:** `dc.ArgStr(idx, maxLen)`
---
### `get_arg_u64`
```
get_arg_u64(idx i32) → val i64
```
Читает аргумент с индексом `idx` как беззнаковое 64-битное число.
Возвращает 0 если индекс вне диапазона или тип не число.
**SDK:** `dc.ArgU64(idx)`
---
## State
### `get_state_len`
```
get_state_len(keyPtr i32, keyLen i32) → valLen i32
```
Возвращает размер значения по ключу (0 если ключ не найден).
Используется перед `get_state` для выделения буфера нужного размера.
---
### `get_state`
```
get_state(keyPtr i32, keyLen i32, dstPtr i32, dstLen i32) → written i32
```
Читает `min(len(value), dstLen)` байт по ключу в буфер.
Возвращает фактически записанное количество байт.
**SDK:** `dc.GetState(key)` (выделяет буфер автоматически через `get_state_len`)
---
### `set_state`
```
set_state(keyPtr i32, keyLen i32, valPtr i32, valLen i32)
```
Записывает значение по ключу. Если `valLen == 0` — удаляет ключ.
**SDK:** `dc.SetState(key, value)`, `dc.SetStateStr(key, s)`, `dc.PutU64(key, v)`
---
### `put_u64`
```
put_u64(keyPtr i32, keyLen i32, val i64)
```
Записывает `val` как 8 байт big-endian. Эквивалентно `set_state` с 8-байтным big-endian значением, но компактнее в вызове.
**SDK:** `dc.PutU64(key, v)`
---
### `get_u64`
```
get_u64(keyPtr i32, keyLen i32) → val i64
```
Читает 8 байт big-endian по ключу. Возвращает 0 если ключ не найден.
**SDK:** `dc.GetU64(key)`
---
## Идентификация
### `get_caller`
```
get_caller(bufPtr i32, bufLen i32) → written i32
```
Записывает hex-pubkey вызывающего аккаунта (64 символа ASCII).
Если `bufLen < 64`, записывает частично.
**SDK:** `dc.Caller()`
---
### `get_block_height`
```
get_block_height() → height i64
```
Возвращает высоту текущего блока (в котором исполняется транзакция).
**SDK:** `dc.BlockHeight()`
---
### `get_contract_treasury`
```
get_contract_treasury(bufPtr i32, bufLen i32) → written i32
```
Записывает hex-pubkey treasury контракта (64 символа).
Treasury — специальный счёт, привязанный к контракту. Контракт может переводить с treasury как `from` без ограничений.
**SDK:** `dc.Treasury()`
---
## Токены
### `get_balance`
```
get_balance(pubPtr i32, pubLen i32) → balance i64
```
Возвращает баланс адреса в µT (микро-токены).
**SDK:** `dc.Balance(pubKeyHex)`
---
### `transfer`
```
transfer(fromPtr i32, fromLen i32, toPtr i32, toLen i32, amount i64) → errCode i32
```
Переводит `amount` µT с `from` на `to`.
Возвращает 0 при успехе, 1 при ошибке (недостаточно средств, неверный адрес).
**Ограничения:**
- `from` должен быть либо `dc.Caller()`, либо `dc.Treasury()`.
- Контракт не может тратить чужие деньги (только caller'а или свой treasury).
**SDK:** `dc.Transfer(from, to, amount) bool`
---
## Межконтрактные вызовы
### `call_contract`
```
call_contract(cidPtr i32, cidLen i32, mthPtr i32, mthLen i32, argPtr i32, argLen i32) → errCode i32
```
Вызывает метод другого контракта.
| Параметр | Тип | Описание |
|---------|-----|---------|
| `cidPtr/cidLen` | i32 | hex-ID целевого контракта (16 символов) |
| `mthPtr/mthLen` | i32 | имя метода |
| `argPtr/argLen` | i32 | JSON-массив аргументов |
**Возврат:** 0 = успех, 1 = ошибка (превышена глубина, контракт не найден, и т.д.)
**Gas:** подвызов получает `gc.Remaining()` от родительского счётчика. Потраченный gas вычитается из родителя.
**State:** все вызовы в цепочке разделяют одну `badger.Txn`. Если подвызов не падает, его изменения видны родителю.
**Подробнее:** [Межконтрактные вызовы](inter-contract.md)
**SDK:** `dc.CallContract(contractID, method, argsJSON) bool`
---
## Логирование
### `log`
```
log(msgPtr i32, msgLen i32)
```
Записывает строку в лог контракта. Логи привязаны к транзакции и видны в Explorer → вкладка Logs.
`fmt.Println` и `log.Printf` через WASI stdout **не** попадают в блокчейн-логи. Используйте только `log`.
**SDK:** `dc.Log(msg)`
---
## Gas
### `gas_tick`
```
gas_tick()
```
Вызывается автоматически при каждой итерации цикла в бинарных WASM-контрактах (инструментация вручную). Списывает 1 unit gas.
Для TinyGo-контрактов TinyGo генерирует собственную инструментацию.
---
## Таблица функций
| # | Имя | Сигнатура | SDK |
|---|-----|----------|-----|
| 1 | `get_args` | `(i32,i32)→i32` | — |
| 2 | `get_arg_str` | `(i32,i32,i32)→i32` | `ArgStr` |
| 3 | `get_arg_u64` | `(i32)→i64` | `ArgU64` |
| 4 | `get_state_len` | `(i32,i32)→i32` | внутри `GetState` |
| 5 | `get_state` | `(i32,i32,i32,i32)→i32` | `GetState` |
| 6 | `set_state` | `(i32,i32,i32,i32)` | `SetState` |
| 7 | `put_u64` | `(i32,i32,i64)` | `PutU64` |
| 8 | `get_u64` | `(i32,i32)→i64` | `GetU64` |
| 9 | `get_caller` | `(i32,i32)→i32` | `Caller` |
| 10 | `get_block_height` | `()→i64` | `BlockHeight` |
| 11 | `get_contract_treasury` | `(i32,i32)→i32` | `Treasury` |
| 12 | `get_balance` | `(i32,i32)→i64` | `Balance` |
| 13 | `transfer` | `(i32,i32,i32,i32,i64)→i32` | `Transfer` |
| 14 | `call_contract` | `(i32,i32,i32,i32,i32,i32)→i32` | `CallContract` |
| 15 | `log` | `(i32,i32)` | `Log` |
| 16 | `gas_tick` | `()` | — |
---
## Паттерн прямого импорта (без SDK)
Если вы пишете бинарный WASM вручную (без TinyGo), функции импортируются в секции `imports`:
```
(import "env" "get_state_len" (func (param i32 i32) (result i32)))
(import "env" "get_state" (func (param i32 i32 i32 i32) (result i32)))
(import "env" "set_state" (func (param i32 i32 i32 i32)))
...
```
Подробнее: [Бинарный WASM](binary-wasm.md)

View File

@@ -0,0 +1,129 @@
# Межконтрактные вызовы
DChain поддерживает вызовы одного контракта из другого через host-функцию `call_contract`. Все вызовы в цепочке исполняются в рамках одной транзакции и одного `badger.Txn`.
## Синтаксис (TinyGo SDK)
```go
import dc "go-blockchain/contracts/sdk"
// Вызвать метод с аргументами
ok := dc.CallContract(contractID, "method_name", `["arg1","arg2"]`)
// Без аргументов
ok := dc.CallContract(contractID, "get_price", "[]")
// С числовым аргументом
ok := dc.CallContract(contractID, "increment", `[42]`)
```
`contractID` — 16-символьный hex-ID контракта (как в Explorer).
## Как это работает
```
tx CALL_CONTRACT → Contract A
│ call_contract(B, "method", args)
Contract B
│ call_contract(C, "method", args)
Contract C
└── [возврат gasUsed]
[gasUsed заряжается на B]
[возврат gasUsed]
[gasUsed заряжается на A]
```
### Атомарность
Все изменения состояния в цепочке вызовов используют **одну** `badger.Txn`. Если родительский вызов завершился ошибкой, изменения всех подвызовов откатываются вместе с ним. Если подвызов вернул 1 (ошибку), родитель должен сам решить: паниковать или продолжать.
### Gas
Gas-бюджет подвызова = `gc.Remaining()` родителя (оставшийся gas на момент вызова). После возврата, `gasUsed` подвызова списывается с родительского счётчика. Таким образом, весь gas всей цепочки вычитается из единственного `--gas` лимита транзакции.
### Caller
В подвызове `dc.Caller()` возвращает contractID **вызывающего контракта** (не исходного пользователя).
```
user → Contract A → Contract B
dc.Caller() == Contract A's ID
```
### Глубина
Максимальная глубина вложенности: **8**.
`A → B → C → ... → H` (8 уровней) — допустимо.
Попытка вызвать 9-й уровень вернёт ошибку, транзакция откатится.
## Пример: price oracle
```go
// contracts/mycontract/main.go
package main
import dc "go-blockchain/contracts/sdk"
const priceOracleID = "abcd1234abcd1234" // ID oracle-контракта
//export buy
func buy() {
amount := dc.ArgU64(0)
// Узнать цену у другого контракта
if !dc.CallContract(priceOracleID, "get_price", "[]") {
dc.Log("oracle unavailable")
return
}
// После вызова цена может быть записана в shared state под известным ключом,
// или oracle мог сделать transfer напрямую.
dc.Log("buy executed")
}
func main() {}
```
## Пример: ping (hello_go)
```go
//export ping
func ping() {
target := dc.ArgStr(0, 64)
if target == "" {
dc.Log("no target")
return
}
ok := dc.CallContract(target, "get", "[]")
if ok {
dc.Log("ping ok")
} else {
dc.Log("ping failed")
}
}
```
## Ограничения
| Параметр | Значение |
|---------|---------|
| Максимальная глубина | 8 |
| Разделяемый state | одна `badger.Txn` |
| Caller в подвызове | contractID родителя |
| Gas бюджет подвызова | `Remaining()` родителя |
| Возврат данных | только через state (нет return value) |
> **Нет возврата значений.** `call_contract` возвращает только 0/1 (успех/ошибка). Для передачи данных из подвызова обратно: запишите в state под известным ключом или используйте промежуточный общий контракт.
## Безопасность
- Не доверяйте `dc.Caller()` из подвызванного контракта — он будет contractID инициатора, который может быть любым задеплоенным контрактом.
- Проверяйте contractID перед вызовом, если вам важно с кем вы говорите.
- Помните что подвызов может изменять общий state — не вызывайте непроверенные контракты.

287
docs/development/tinygo.md Normal file
View File

@@ -0,0 +1,287 @@
# TinyGo SDK
Руководство по написанию DChain smart contracts на Go с использованием TinyGo.
## Установка TinyGo
```bash
# macOS
brew install tinygo
# Linux (deb)
wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.deb
sudo dpkg -i tinygo_0.32.0_amd64.deb
# Windows
# Скачать installer с https://github.com/tinygo-org/tinygo/releases
# Проверка
tinygo version
```
Требуется TinyGo **0.30+** (поддержка `//go:wasmimport` + target wasip1).
## Структура контракта
```
contracts/
mycontract/
main.go # контракт
mycontract_abi.json # ABI
mycontract.wasm # собранный (gitignore или коммитим?)
```
Минимальный контракт:
```go
package main
import dc "go-blockchain/contracts/sdk"
//export my_method
func myMethod() {
dc.Log("hello from Go contract!")
}
// main обязателен для TinyGo, но не вызывается.
func main() {}
```
Каждая `//export <name>` функция становится вызываемым методом контракта.
## SDK API
Импорт: `import dc "go-blockchain/contracts/sdk"`
### Аргументы
```go
// Строковый аргумент по индексу (maxLen — максимальная длина буфера)
name := dc.ArgStr(0, 64) // args[0] as string
addr := dc.ArgStr(1, 128) // args[1] as string
// Числовой аргумент (uint64)
amount := dc.ArgU64(0) // args[0] as uint64
```
### State
```go
// Читать/писать произвольные байты
data := dc.GetState("key") // []byte или nil
dc.SetState("key", []byte("value")) // записать
dc.SetState("key", nil) // удалить
// Удобные string обёртки
s := dc.GetStateStr("owner") // string (пустой если нет)
dc.SetStateStr("owner", caller) // записать string
// uint64 (хранится как 8-byte big-endian)
count := dc.GetU64("counter") // uint64
dc.PutU64("counter", count+1) // записать
```
### Идентификация
```go
caller := dc.Caller() // hex pubkey вызывающего (64 символа)
height := dc.BlockHeight() // uint64, текущий блок
treasury := dc.Treasury() // hex адрес treasury контракта
```
### Токены
```go
// Баланс адреса в µT
bal := dc.Balance(pubkey) // uint64
// Перевод µT
ok := dc.Transfer(from, to, amount) // bool: true=success
```
`Transfer` в контексте контракта: `from` должен быть либо caller'ом, либо `dc.Treasury()` — только так контракт может тратить чужие деньги.
### Межконтрактные вызовы
```go
// Вызвать метод другого контракта
ok := dc.CallContract(contractID, "method", `["arg1","arg2"]`)
// Без аргументов
ok := dc.CallContract(contractID, "get_price", "[]")
```
Подробнее: [Межконтрактные вызовы](inter-contract.md)
### Логирование
```go
dc.Log("message") // строка видна в Explorer → Logs
dc.Log("value: " + strconv.FormatUint(v, 10))
```
## Паттерны
### Owner-только метод
```go
//export admin_action
func adminAction() {
owner := dc.GetStateStr("owner")
if owner == "" {
// первый вызов — устанавливаем owner
dc.SetStateStr("owner", dc.Caller())
dc.Log("initialized")
return
}
if dc.Caller() != owner {
dc.Log("unauthorized")
return
}
// ... логика ...
}
```
### Платный метод (fee в treasury)
```go
//export register
func register() {
name := dc.ArgStr(0, 64)
if name == "" { return }
const fee = 1000 // µT
caller := dc.Caller()
treasury := dc.Treasury()
// Снять fee с caller и положить в treasury
if !dc.Transfer(caller, treasury, fee) {
dc.Log("insufficient balance")
return
}
dc.SetStateStr("reg:"+name, caller)
dc.Log("registered: " + name)
}
```
### Эскроу с release
```go
//export lock
func lock() {
id := dc.ArgStr(0, 64)
amount := dc.ArgU64(1)
buyer := dc.Caller()
treasury := dc.Treasury()
if !dc.Transfer(buyer, treasury, amount) {
dc.Log("transfer failed")
return
}
dc.SetStateStr("buyer:"+id, buyer)
dc.PutU64("amount:"+id, amount)
dc.Log("locked: " + id)
}
//export release
func release() {
id := dc.ArgStr(0, 64)
buyer := dc.GetStateStr("buyer:" + id)
if dc.Caller() != buyer {
dc.Log("unauthorized")
return
}
seller := dc.ArgStr(1, 128)
amount := dc.GetU64("amount:" + id)
dc.Transfer(dc.Treasury(), seller, amount)
dc.Log("released: " + id)
}
```
## Сборка
```bash
cd contracts/mycontract
# Собрать WASM
tinygo build -o mycontract.wasm -target wasip1 -no-debug .
# Проверить размер
ls -lh mycontract.wasm
# Опционально: wasm-strip для уменьшения размера
wasm-strip mycontract.wasm
```
Флаги:
- `-target wasip1` — WASI Preview 1 (единственный поддерживаемый target)
- `-no-debug` — убрать debug info из WASM (уменьшает размер)
- `-opt=2` — агрессивная оптимизация (по умолчанию в release)
## Деплой
```bash
CONTRACT_ID=$(client deploy-contract \
--key /keys/node1.json \
--wasm mycontract.wasm \
--abi mycontract_abi.json \
--node http://localhost:8081 | grep contract_id | awk '{print $2}')
echo "Contract: $CONTRACT_ID"
```
## Проверка WASM
Перед деплоем можно проверить что модуль валиден:
```go
// В Go тесте
ctx := context.Background()
v := vm.NewVM(ctx)
defer v.Close(ctx)
wasmBytes, _ := os.ReadFile("mycontract.wasm")
if err := v.Validate(ctx, wasmBytes); err != nil {
log.Fatal(err)
}
```
## Пример: hello_go
Полный пример TinyGo-контракта с комментариями находится в [`contracts/hello_go/main.go`](../../contracts/hello_go/main.go).
Демонстрирует:
- `increment` / `get` / `reset` — счётчик с owner ACL
- `greet` — строковые аргументы
- `ping` — межконтрактный вызов
```bash
# Собрать
cd contracts/hello_go
tinygo build -o hello_go.wasm -target wasip1 -no-debug .
# Задеплоить
docker exec node1 client deploy-contract \
--key /keys/node1.json \
--wasm hello_go.wasm \
--abi hello_go_abi.json \
--node http://node1:8080
# Вызвать
docker exec node1 client call-contract \
--key /keys/node1.json --contract $HELLO_ID \
--method increment --gas 5000 --node http://node1:8080
docker exec node1 client call-contract \
--key /keys/node1.json --contract $HELLO_ID \
--method greet --arg "DChain" --gas 5000 --node http://node1:8080
```
## Ограничения TinyGo в контекcте WASM
- Нет `goroutine` в WASM (нет многопоточности)
- Нет `net`, `os`, `syscall` — контракт изолирован
- Нет доступа к файловой системе и сети
- `fmt.Println` и `log.Printf` работают через WASI stdout — **но не логируются в blockchain**.
Используйте только `dc.Log()` для логов видимых в Explorer.
- Размер памяти по умолчанию: 64 KB на stack + heap управляется TinyGo GC

219
docs/node/README.md Normal file
View File

@@ -0,0 +1,219 @@
# Запуск ноды
## Быстрый старт (Docker)
```bash
git clone https://github.com/your/go-blockchain
cd go-blockchain
docker compose up -d
```
Запускает 3 ноды: `node1` (8081), `node2` (8082), `node3` (8083).
Деплой контрактов:
```bash
docker exec node1 /scripts/deploy_contracts.sh
```
Explorer: http://localhost:8081
---
## Запуск вручную
### Требования
- Go 1.21+
- BadgerDB (встроен)
- libp2p (встроен)
### Сборка
```bash
cd go-blockchain
go build ./cmd/node/
go build ./cmd/client/
```
### Genesis нода
```bash
# Первый запуск — создать genesis
./node \
--key node1.json \
--genesis \
--validators "$(cat node1.json | jq -r .pub_key),$(cat node2.json | jq -r .pub_key),$(cat node3.json | jq -r .pub_key)" \
--stats-addr :8081 \
--listen /ip4/0.0.0.0/tcp/4001 \
--db ./data/node1
```
### Peer нода
```bash
./node \
--key node2.json \
--peers /ip4/127.0.0.1/tcp/4001/p2p/<node1-peer-id> \
--stats-addr :8082 \
--listen /ip4/0.0.0.0/tcp/4002 \
--db ./data/node2
```
---
## Флаги командной строки
| Флаг | По умолчанию | Описание |
|------|------------|---------|
| `--key` | `node.json` | Файл Ed25519 + X25519 идентичности |
| `--db` | `chaindata` | Директория BadgerDB |
| `--listen` | `/ip4/0.0.0.0/tcp/4001` | libp2p адрес |
| `--peers` | — | Bootstrap peer multiaddrs (через запятую) |
| `--validators` | — | Pubkeys валидаторов (через запятую, только для `--genesis`) |
| `--genesis` | false | Создать genesis блок при первом старте |
| `--stats-addr` | `:8080` | HTTP API/Explorer порт |
| `--wallet` | — | Payout кошелёк (hex pubkey) |
| `--wallet-pass` | — | Пароль к кошельку |
| `--heartbeat` | false | Отправлять heartbeat каждые 60 минут |
| `--register-relay` | false | Зарегистрироваться как relay-провайдер |
| `--relay-fee` | 0 | Fee за relay сообщение в µT |
| `--relay-key` | `relay.json` | X25519 ключ для relay шифрования |
| `--mailbox-db` | — | Директория для relay mailbox |
| `--governance-contract` | — | ID governance контракта для динамических параметров |
---
## Docker Compose
### docker-compose.yml структура
```yaml
services:
node1:
build: .
ports:
- "8081:8080" # HTTP API
- "4001:4001" # libp2p
volumes:
- node1_data:/chaindata
- ./keys:/keys:ro
command: >
node
--key /keys/node1.json
--genesis
--validators "$VALIDATORS"
--stats-addr :8080
--listen /ip4/0.0.0.0/tcp/4001
--governance-contract "$GOV_ID"
environment:
- VALIDATORS=03...,04...,05...
node2:
build: .
ports:
- "8082:8080"
command: >
node
--key /keys/node2.json
--peers /dns4/node1/tcp/4001/p2p/$NODE1_PEER_ID
--stats-addr :8080
--listen /ip4/0.0.0.0/tcp/4001
```
### Управление
```bash
# Запустить
docker compose up -d
# Логи
docker compose logs -f node1
# Остановить
docker compose down
# Сбросить данные
docker compose down -v
```
---
## Файл ключа
Каждая нода использует один файл с обоими ключами:
```json
{
"pub_key": "03abcd...", // Ed25519 pubkey (hex, 66 символов)
"priv_key": "...", // Ed25519 privkey (hex)
"x25519_pub": "...", // X25519 pubkey для E2E relay (hex, 64 символа)
"x25519_priv": "..." // X25519 privkey
}
```
Генерация:
```bash
client keygen --out node1.json
```
---
## Мониторинг
### HTTP healthcheck
```bash
curl http://localhost:8081/api/netstats
```
### Logs
```bash
# Docker
docker compose logs -f node1 | grep -E "block|error|warn"
# Прямой запуск
./node ... 2>&1 | tee node.log
```
### Метрики производительности
| Показатель | Норма |
|-----------|------|
| Время блока | ~3 с |
| Блоков в минуту | ~20 |
| PBFT фазы | prepare → commit → finalize |
---
## Структура данных BadgerDB
```
balance:<pubkey> → uint64 (µT)
identity:<pubkey> → JSON RegisterKeyPayload
stake:<pubkey> → uint64 (µT)
block:<index> → JSON Block
tx:<txid> → JSON TxRecord
txidx:<pubkey>:<block>:<seq> → txid (индекс по адресу)
contract:<id> → JSON ContractRecord
cstate:<id>:<key> → []byte (state контракта)
clog:<id>:<seq> → JSON ContractLogEntry
relay:<pubkey> → JSON RegisteredRelayInfo
```
---
## Сброс и восстановление
```bash
# Полный сброс
docker compose down -v
docker compose up -d
# Только данные одной ноды
docker compose stop node1
docker volume rm go-blockchain_node1_data
docker compose up -d node1
```
После сброса нужно заново задеплоить контракты и залинковать governance.

118
docs/node/governance.md Normal file
View File

@@ -0,0 +1,118 @@
# Governance интеграция
Нода может использовать governance-контракт для динамического управления параметрами (gas_price и другими) без перезапуска.
## Принцип работы
```
Governance contract (on-chain)
state: param:gas_price = "5"
param:relay_fee = "200"
...
Node (blockchain/chain.go)
GetEffectiveGasPrice() → читает BadgerDB напрямую
GetGovParam(key) → cstate:<govID>:param:<key>
```
Нода читает параметры **напрямую из BadgerDB** без вызова VM — это быстро и дёшево.
## Привязка governance
### При запуске ноды (флаг)
```bash
./node \
--key node1.json \
--governance-contract a1b2c3d4e5f60001 \
...
```
### В рантайме (без перезапуска)
```bash
curl -X POST http://localhost:8081/api/governance/link \
-H "Content-Type: application/json" \
-d '{"governance": "a1b2c3d4e5f60001"}'
```
**Response:**
```json
{"status": "ok", "governance": "a1b2c3d4e5f60001"}
```
Изменение вступает в силу немедленно — следующий `CALL_CONTRACT` уже будет использовать новый governance.
### Через deploy-скрипт (автоматически)
`scripts/deploy_contracts.sh` автоматически вызывает `link_governance` на всех нодах после деплоя:
```bash
link_governance() {
for NODE in http://node1:8080 http://node2:8080 http://node3:8080; do
curl -sX POST "$NODE/api/governance/link" \
-H "Content-Type: application/json" \
-d "{\"governance\":\"$GOV_ID\"}"
done
}
```
## Управляемые параметры
| Ключ | Тип | По умолчанию | Описание |
|------|-----|-------------|---------|
| `gas_price` | uint64 (строка) | 1 | µT за 1 gas unit |
Дополнительные параметры можно хранить в governance для читающих их контрактов (например, `messenger_entry_fee`, `relay_fee_override`, etc.) — нода их не читает, но они доступны через inter-contract вызовы.
## Изменение gas_price
```bash
# 1. Любой предлагает новое значение
client call-contract --method propose \
--arg gas_price --arg 5 \
--contract $GOV_ID --key key.json \
--gas 10000 --node http://node1:8080
# 2. Admin утверждает
client call-contract --method approve \
--arg gas_price \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
# 3. Проверить текущее значение
curl "http://localhost:8081/api/contracts/$GOV_ID/state/param:gas_price"
```
После approve — изменение вступает в силу немедленно на всех нодах которые привязали этот governance.
## Передача роли admin
```bash
# Передать admin другому валидатору
client call-contract --method set_admin \
--arg $NEW_ADMIN_PUBKEY \
--contract $GOV_ID --key /keys/node1.json \
--gas 10000 --node http://node1:8080
```
## Отвязать governance
Если нужно вернуться к значениям по умолчанию:
```bash
curl -X POST http://localhost:8081/api/governance/link \
-H "Content-Type: application/json" \
-d '{"governance": ""}'
```
После этого нода будет использовать встроенные константы (`GasPrice = 1 µT/gas`).
## Проверка привязки
Прямого API для проверки текущего govContractID нет. Косвенно: если gas_price изменился через governance и применился — привязка работает.
```bash
# Установить gas_price = 1 через governance
# Затем вызвать контракт с --gas 1000 и проверить fee = 1000 µT
```

291
docs/node/multi-server.md Normal file
View File

@@ -0,0 +1,291 @@
# Деплой на реальные серверы
Руководство по запуску трёх полноценных нод на разных VPS/серверах в интернете.
## Концепция
```
Server A (1.2.3.11) Server B (1.2.3.12) Server C (1.2.3.13)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ node1 │ │ node2 │ │ node3 │
│ validator │◀────────▶│ validator │◀────────▶│ validator │
│ relay (2000µT) │ │ relay (1500µT) │ │ relay (1000µT) │
│ :4001 :8080 │ │ :4001 :8080 │ │ :4001 :8080 │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└────────────────────────────┴─────────────────────────────┘
libp2p P2P
gossipsub: tx + blocks
streams: PBFT consensus, sync
```
Каждая нода — полноценный **валидатор** (участвует в PBFT-консенсусе) и **relay-провайдер** (хранит и доставляет зашифрованные сообщения).
## Требования
- Ubuntu 22.04 / Debian 12 (или любой Linux)
- Открытый TCP-порт `4001` (P2P) и опционально `8080` (HTTP API)
- Go 1.21+ **только для сборки** (на сервере нужен только бинарник)
## 1. Подготовка ключей
На любой машине (ноутбук / CI):
```bash
# Сгенерировать 3 ключа
./client keygen --out keys/node1.json
./client keygen --out keys/node2.json
./client keygen --out keys/node3.json
# Получить pubkey и peer ID для каждого
./peerid --key keys/node1.json --ip <SERVER_A_IP> --port 4001
./peerid --key keys/node2.json --ip <SERVER_B_IP> --port 4001
./peerid --key keys/node3.json --ip <SERVER_C_IP> --port 4001
```
Вывод `peerid`:
```
pub_key: 26018d40...
peer_id: 12D3KooW...
multiaddr: /ip4/1.2.3.11/tcp/4001/p2p/12D3KooW...
```
Запишите `pub_key` и `peer_id` для каждой ноды — они нужны в конфигурации.
## 2. Сборка бинарника
```bash
git clone https://github.com/your/go-blockchain
cd go-blockchain
CGO_ENABLED=0 GOOS=linux go build -trimpath -o node ./cmd/node
CGO_ENABLED=0 GOOS=linux go build -trimpath -o client ./cmd/client
# Скопировать на серверы
scp node client root@1.2.3.11:/usr/local/bin/
scp node client root@1.2.3.12:/usr/local/bin/
scp node client root@1.2.3.13:/usr/local/bin/
```
## 3. Копирование ключей
```bash
# Каждый сервер получает ТОЛЬКО свой ключ
scp keys/node1.json root@1.2.3.11:/etc/dchain/node.json
scp keys/node2.json root@1.2.3.12:/etc/dchain/node.json
scp keys/node3.json root@1.2.3.13:/etc/dchain/node.json
chmod 600 /etc/dchain/node.json # на каждом сервере
```
## 4. Переменные конфигурации
Подставьте реальные значения из шага 1:
```bash
# Validators — все три pubkey через запятую
VALIDATORS="<NODE1_PUB>,<NODE2_PUB>,<NODE3_PUB>"
# Bootstrap multiaddrs для node2 и node3
NODE1_PEER="/ip4/1.2.3.11/tcp/4001/p2p/<NODE1_PEER_ID>"
NODE2_PEER="/ip4/1.2.3.12/tcp/4001/p2p/<NODE2_PEER_ID>"
```
## 5. Запуск нод
### Server A — node1 (genesis + validator + relay)
```bash
node \
--genesis \
--key /etc/dchain/node.json \
--db /var/lib/dchain/chain \
--mailbox-db /var/lib/dchain/mailbox \
--listen /ip4/0.0.0.0/tcp/4001 \
--announce /ip4/1.2.3.11/tcp/4001 \
--stats-addr :8080 \
--validators "$VALIDATORS" \
--heartbeat=true \
--register-relay \
--relay-fee 2000
```
### Server B — node2 (validator + relay)
```bash
node \
--key /etc/dchain/node.json \
--db /var/lib/dchain/chain \
--mailbox-db /var/lib/dchain/mailbox \
--listen /ip4/0.0.0.0/tcp/4001 \
--announce /ip4/1.2.3.12/tcp/4001 \
--stats-addr :8080 \
--validators "$VALIDATORS" \
--peers "$NODE1_PEER" \
--heartbeat=true \
--register-relay \
--relay-fee 1500
```
### Server C — node3 (validator + relay)
```bash
node \
--key /etc/dchain/node.json \
--db /var/lib/dchain/chain \
--mailbox-db /var/lib/dchain/mailbox \
--listen /ip4/0.0.0.0/tcp/4001 \
--announce /ip4/1.2.3.13/tcp/4001 \
--stats-addr :8080 \
--validators "$VALIDATORS" \
--peers "$NODE1_PEER,$NODE2_PEER" \
--heartbeat=true \
--register-relay \
--relay-fee 1000
```
## 6. systemd unit
Создайте `/etc/systemd/system/dchain.service` на каждом сервере:
```ini
[Unit]
Description=DChain Node
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dchain
ExecStart=/usr/local/bin/node \
--key /etc/dchain/node.json \
--db /var/lib/dchain/chain \
--mailbox-db /var/lib/dchain/mailbox \
--listen /ip4/0.0.0.0/tcp/4001 \
--announce /ip4/YOUR_PUBLIC_IP/tcp/4001 \
--stats-addr :8080 \
--validators "V1_PUB,V2_PUB,V3_PUB" \
--peers "/ip4/SEED_IP/tcp/4001/p2p/SEED_PEER_ID" \
--heartbeat=true \
--register-relay \
--relay-fee 1000
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
```
```bash
# Применить
sudo systemctl daemon-reload
sudo systemctl enable --now dchain
# Логи
sudo journalctl -u dchain -f
```
## 7. Деплой контрактов
После запуска всех трёх нод:
```bash
# С любой машины имеющей client и ключ node1
export NODE_URL=http://1.2.3.11:8080
export KEY=/etc/dchain/node.json # или локальная копия
# Задеплоить username_registry
CONTRACT_ID=$(client deploy-contract \
--key $KEY \
--wasm contracts/username_registry/username_registry.wasm \
--abi contracts/username_registry/username_registry_abi.json \
--node $NODE_URL | grep contract_id | awk '{print $2}')
echo "username_registry: $CONTRACT_ID"
# Залинковать governance на всех трёх нодах
for NODE in http://1.2.3.11:8080 http://1.2.3.12:8080 http://1.2.3.13:8080; do
curl -sX POST "$NODE/api/governance/link" \
-H "Content-Type: application/json" \
-d "{\"governance\":\"$GOV_ID\"}"
done
```
## 8. Как работает сеть
### Bootstrap и discovery
```
node2 старт:
1. --peers → connectWithRetry("/ip4/1.2.3.11/tcp/4001/p2p/...")
Подключается к node1 (бесконечный retry с backoff 1→30s)
2. При connect → syncOnConnect()
Запрашивает все блоки которых нет у node2
3. Kademlia DHT bootstrap (через node1)
DHT узнаёт о node3 → DiscoverPeers() подключается
4. mDNS (только LAN/Docker) — игнорируется в интернете
5. connectWithRetry keep-alive: каждые 30s проверяет связь,
автоматически переподключает при обрыве
```
### Консенсус
PBFT 3-of-3, fault tolerance f=1:
- Для коммита блока нужны подписи **2 из 3** валидаторов
- Если одна нода упала → сеть продолжает работать
- Если упали две → сеть ждёт (не производит блоки)
### --announce и адреса
Без `--announce` libp2p рекламирует **все** адреса интерфейсов:
- `0.0.0.0` → раскрывается в loopback и внутренние адреса
- Другие ноды получают `127.0.0.1:4001` → не могут подключиться
С `--announce /ip4/1.2.3.11/tcp/4001`:
- Единственный рекламируемый адрес — публичный IP сервера
- `AddrStrings()` возвращает только этот адрес
- DHT propagates этот адрес другим нодам в сети
## 9. Firewall
```bash
# UFW
ufw allow 4001/tcp # P2P — обязательно
ufw allow 8080/tcp # HTTP API — опционально (для Explorer, деплоя контрактов)
# iptables
iptables -A INPUT -p tcp --dport 4001 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
```
## 10. Добавление новой ноды в сеть
Новый участник (не валидатор, только синхронизация и relay):
```bash
node \
--key /etc/dchain/node.json \
--db /var/lib/dchain/chain \
--mailbox-db /var/lib/dchain/mailbox \
--listen /ip4/0.0.0.0/tcp/4001 \
--announce /ip4/YOUR_IP/tcp/4001 \
--stats-addr :8080 \
--validators "$VALIDATORS" \
--peers "$NODE1_PEER" \
--heartbeat=false \
--register-relay \
--relay-fee 500
```
Нода автоматически:
1. Подключится к node1 через `--peers`
2. Синхронизирует всю историю блоков
3. Через DHT найдёт node2 и node3
4. Начнёт получать и пересылать relay-сообщения
Чтобы сделать её валидатором — нужна `ADD_VALIDATOR` транзакция от существующего валидатора.

177
docs/quickstart.md Normal file
View File

@@ -0,0 +1,177 @@
# Быстрый старт
## Требования
- Docker Desktop (или Docker Engine + Compose v2)
- 4 GB RAM, 2 CPU
Для разработки контрактов дополнительно:
- Go 1.21+
- TinyGo 0.30+ (только для TinyGo-контрактов)
---
## 1. Запустить сеть
```bash
git clone <repo>
cd go-blockchain
docker compose up --build -d
```
Запускается три ноды:
| Контейнер | Роль | Explorer |
|-----------|------|---------|
| node1 | genesis + validator + relay | http://localhost:8081 |
| node2 | validator + relay | http://localhost:8082 |
| node3 | relay-only observer | http://localhost:8083 |
Дождитесь пока в Explorer появятся блоки (~10 секунд).
Swagger: http://localhost:8081/swagger
---
## 2. Задеплоить контракты
```bash
docker compose --profile deploy run --rm deploy
```
Скрипт:
1. Ждёт готовности node1
2. Деплоит 4 контракта из genesis-ключа `/keys/node1.json`
3. Вызывает `init` на governance и escrow
4. Привязывает governance к нодам через `/api/governance/link`
5. Выводит contract ID и сохраняет в `/tmp/contracts.env`
Пример вывода:
```
══════════════════════════════════════════════════
DChain — деплой production-контрактов
══════════════════════════════════════════════════
▶ Деплой username_registry
✓ username_registry contract_id: a1b2c3d4e5f60718
▶ Деплой governance
✓ governance contract_id: 9f8e7d6c5b4a3210
▶ Деплой auction
✓ auction contract_id: 1a2b3c4d5e6f7089
▶ Деплой escrow
✓ escrow contract_id: fedcba9876543210
✓ username_registry : a1b2c3d4e5f60718
✓ governance : 9f8e7d6c5b4a3210
✓ auction : 1a2b3c4d5e6f7089
✓ escrow : fedcba9876543210
```
Сохраните ID для последующего использования:
```bash
# Запомнить ID
export UR_ID=a1b2c3d4e5f60718
export GOV_ID=9f8e7d6c5b4a3210
export AUC_ID=1a2b3c4d5e6f7089
export ESC_ID=fedcba9876543210
```
---
## 3. Первые операции
### Проверить баланс genesis-кошелька
```bash
docker exec node1 client balance \
--key /keys/node1.json \
--node http://node1:8080
```
### Создать новый кошелёк
```bash
docker exec node1 wallet keygen --out /tmp/alice.json
docker exec node1 client balance \
--key /tmp/alice.json \
--node http://node1:8080
```
### Перевести токены
```bash
# Получить pubkey Alice
ALICE_PUB=$(docker exec node1 sh -c 'cat /tmp/alice.json | grep pub_key' | grep -oP '"pub_key":\s*"\K[^"]+')
docker exec node1 client transfer \
--key /keys/node1.json \
--to $ALICE_PUB \
--amount 1000000 \
--node http://node1:8080
```
### Зарегистрировать username
```bash
docker exec node1 client call-contract \
--key /keys/node1.json \
--contract $UR_ID \
--method register \
--arg alice \
--gas 20000 \
--node http://node1:8080
```
### Отправить сообщение (по username)
```bash
docker exec node1 client send-msg \
--key /keys/node1.json \
--to @alice \
--registry $UR_ID \
--msg "Привет!" \
--node http://node1:8080
```
---
## 4. Explorer
После деплоя контракты видны в Explorer:
```
http://localhost:8081/contracts — все контракты
http://localhost:8081/contract?id=$UR_ID — username_registry
http://localhost:8081/contract?id=$GOV_ID — governance
http://localhost:8081/contract?id=$AUC_ID — auction
http://localhost:8081/contract?id=$ESC_ID — escrow
```
Вкладки в Explorer на странице контракта:
- **Overview** — метаданные, ABI-методы
- **State** — query state по ключу
- **Logs** — history вызовов с логами
- **Raw** — сырой JSON ContractRecord
---
## 5. Полный сброс
```bash
docker compose down -v && docker compose up --build -d
```
Флаг `-v` удаляет тома BadgerDB. После пересборки сеть стартует с чистого genesis.
---
## Следующие шаги
- [Контракты](contracts/README.md) — использование всех 4 контрактов
- [Разработка контрактов](development/README.md) — написать свой контракт
- [CLI](cli/README.md) — все команды клиента
- [API](api/README.md) — REST-интерфейс