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:
45
docs/api/README.md
Normal file
45
docs/api/README.md
Normal 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
314
docs/api/chain.md
Normal 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
285
docs/api/contracts.md
Normal 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
246
docs/api/relay.md
Normal 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
207
docs/architecture.md
Normal 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
411
docs/cli/README.md
Normal 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
59
docs/contracts/README.md
Normal 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
213
docs/contracts/auction.md
Normal 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
267
docs/contracts/escrow.md
Normal 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
|
||||
```
|
||||
203
docs/contracts/governance.md
Normal file
203
docs/contracts/governance.md
Normal 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)
|
||||
133
docs/contracts/username_registry.md
Normal file
133
docs/contracts/username_registry.md
Normal 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** — никому не идёт |
|
||||
| **Ограничения имени** | 4–32 символа, `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
103
docs/development/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Разработка контрактов
|
||||
|
||||
DChain поддерживает два способа написания WASM-контрактов.
|
||||
|
||||
## Выбор подхода
|
||||
|
||||
| | TinyGo SDK | Бинарный WASM |
|
||||
|-|-----------|--------------|
|
||||
| **Язык** | Go | Go (кодогенератор) |
|
||||
| **Инструменты** | TinyGo 0.30+ | Стандартный Go |
|
||||
| **Сложность** | Низкая | Высокая |
|
||||
| **Размер .wasm** | ~30–100 KB | 500–2000 байт |
|
||||
| **Отладка** | Стандартная | Сложная |
|
||||
| **Рекомендуется** | Новые контракты | Минимальные/системные |
|
||||
|
||||
## 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>`.
|
||||
249
docs/development/binary-wasm.md
Normal file
249
docs/development/binary-wasm.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Бинарный WASM
|
||||
|
||||
Альтернативный способ написания контрактов — генерация WASM-байткода вручную. Используется для системных/минимальных контрактов где критичен размер (~500–2000 байт вместо 30–100 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`.
|
||||
193
docs/development/gas-model.md
Normal file
193
docs/development/gas-model.md
Normal 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`.
|
||||
274
docs/development/host-functions.md
Normal file
274
docs/development/host-functions.md
Normal 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)
|
||||
129
docs/development/inter-contract.md
Normal file
129
docs/development/inter-contract.md
Normal 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
287
docs/development/tinygo.md
Normal 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
219
docs/node/README.md
Normal 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
118
docs/node/governance.md
Normal 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
291
docs/node/multi-server.md
Normal 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
177
docs/quickstart.md
Normal 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-интерфейс
|
||||
Reference in New Issue
Block a user