chore: initial commit for v0.0.1

DChain single-node blockchain + React Native messenger client.

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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