Разработка горячего кошелька биржи
Горячий кошелёк биржи — это онлайн-система хранения криптовалюты, которая автоматически обрабатывает выводы пользователей. Он всегда подключён к интернету, поэтому представляет основной вектор атаки. Компромисс между удобством и безопасностью решается через правильную архитектуру: минимальные остатки на горячем, основной резерв — на холодном.
Архитектура горячего кошелька
HD Wallet для Ethereum и EVM-сетей
Горячий кошелёк — это master hot wallet address, куда консолидируются средства с депозитных адресов. Для Ethereum-based сетей — один адрес на всю биржу или несколько для параллельной обработки выводов.
type HotWallet struct {
address common.Address
keyManager KeyManager // абстракция над HSM или keystore
client *ethclient.Client
nonceTrack *NonceTracker // управление nonce
gasTrack *GasTracker
}
// NonceTracker — критический компонент
// PostgreSQL хранит последний использованный nonce
// При параллельных выводах нужна атомарная выдача nonce
type NonceTracker struct {
db *DB
mu sync.Mutex
pool chan uint64 // pre-fetched nonce pool
}
func (nt *NonceTracker) Next(ctx context.Context) (uint64, error) {
nt.mu.Lock()
defer nt.mu.Unlock()
// Получаем следующий nonce атомарно
var nonce uint64
err := nt.db.QueryRow(ctx,
"UPDATE hot_wallet SET nonce = nonce + 1 RETURNING nonce - 1"
).Scan(&nonce)
return nonce, err
}
Управление nonce — одна из главных технических сложностей горячего кошелька. При параллельных транзакциях нужно гарантировать, что два воркера не получат один nonce. Решение: атомарный инкремент в БД с mutex-защитой.
Мультивалютный горячий кошелёк
Разные активы требуют разных подходов:
| Сеть | Тип кошелька | Особенности |
|---|---|---|
| Ethereum / EVM | EOA или Smart Contract Wallet | ERC-20 через transfer/transferFrom |
| Bitcoin | P2WPKH (SegWit) | UTXO модель, coin selection |
| Tron | TRX/TRC-20 | Bandwidth/Energy модель |
| Solana | ED25519 keypair | SPL tokens, compute units |
| Ripple | Shared address + destination tag | Tag обязателен |
# Bitcoin горячий кошелёк — UTXO coin selection
from bitcoinlib.wallets import Wallet
class BitcoinHotWallet:
def __init__(self, wallet_name: str):
self.wallet = Wallet(wallet_name)
def create_withdrawal(self, to_address: str, amount_sat: int, fee_rate: int):
# Coin selection — выбираем минимальное количество UTXO
# для покрытия суммы + комиссии
utxos = self.wallet.utxos()
selected, change = self.select_coins(utxos, amount_sat, fee_rate)
tx = self.wallet.transaction_create(
outputs=[(to_address, amount_sat)],
utxos=selected,
fee=self.estimate_fee(len(selected), fee_rate),
replace_by_fee=True, # RBF для возможности bump fee
)
return tx
def select_coins(self, utxos, amount, fee_rate):
# Простая стратегия: smallest first (минимизирует UTXO set)
utxos.sort(key=lambda u: u.value)
selected = []
total = 0
for utxo in utxos:
selected.append(utxo)
total += utxo.value
fee = self.estimate_fee(len(selected), fee_rate)
if total >= amount + fee:
change = total - amount - fee
return selected, change
raise InsufficientFunds()
Безопасность горячего кошелька
HSM интеграция
Приватный ключ горячего кошелька никогда не должен быть в plain text в памяти сервера. Уровни защиты:
Уровень 1 (минимальный): Зашифрованный keystore на диске (AES-256), пароль из environment variable или secrets manager (AWS Secrets Manager, HashiCorp Vault). Ключ дешифруется при старте сервиса и хранится в памяти.
Уровень 2 (рекомендуемый): HashiCorp Vault Transit Secrets Engine. Ключ никогда не покидает Vault — сервер посылает данные на подпись, получает подписанную транзакцию обратно.
import vault "github.com/hashicorp/vault/api"
type VaultSigner struct {
client *vault.Client
keyName string
}
func (vs *VaultSigner) SignTransaction(txHash []byte) ([]byte, error) {
// Vault Transit: подпись данных без выгрузки ключа
path := fmt.Sprintf("transit/sign/%s", vs.keyName)
secret, err := vs.client.Logical().Write(path, map[string]interface{}{
"input": base64.StdEncoding.EncodeToString(txHash),
"hash_algorithm": "sha2-256",
"signature_algorithm": "pkcs1v15",
"prehashed": true,
})
// Парсим ECDSA подпись и преобразуем в Ethereum формат
return parseVaultSignature(secret.Data["signature"].(string))
}
Уровень 3 (максимальный): Hardware HSM (Thales Luna, AWS CloudHSM, YubiHSM). Подпись выполняется в аппаратном чипе, приватный ключ физически не извлекаем. Стоимость — от $20,000/год для dedicated HSM. Для большинства бирж Vault — оптимальный компромисс.
Лимиты и контроль
type WithdrawalLimiter struct {
db *DB
cache *redis.Client
}
// Дневной лимит автоматических выводов из горячего кошелька
func (wl *WithdrawalLimiter) CheckAndConsume(amount decimal.Decimal, currency string) error {
key := fmt.Sprintf("hot_wallet:daily_limit:%s:%s", currency, today())
// Атомарный инкремент с проверкой лимита
script := redis.NewScript(`
local current = redis.call('GET', KEYS[1])
if current == false then current = 0 end
local new_val = tonumber(current) + tonumber(ARGV[1])
if new_val > tonumber(ARGV[2]) then
return redis.error_reply("LIMIT_EXCEEDED")
end
redis.call('SETEX', KEYS[1], 86400, new_val)
return new_val
`)
_, err := script.Run(context.Background(), wl.cache,
[]string{key}, amount.String(), DAILY_LIMIT.String()).Result()
return err
}
При превышении лимита — вывод встаёт в очередь и требует ручного апрува оператора (пополнение из warm wallet).
Управление балансом и пополнение
Hot wallet balance monitoring
type BalanceWatcher struct {
hotWallet *HotWallet
threshold decimal.Decimal // минимальный порог
target decimal.Decimal // целевой баланс после пополнения
alerter Alerter
}
func (bw *BalanceWatcher) Watch(ctx context.Context) {
ticker := time.NewTicker(5 * time.Minute)
for {
select {
case <-ticker.C:
balance := bw.hotWallet.GetBalance()
if balance.LessThan(bw.threshold) {
bw.alerter.Alert(Alert{
Level: "CRITICAL",
Message: fmt.Sprintf("Hot wallet balance low: %s. Need topup from warm wallet", balance),
})
// Автоматическое пополнение из warm wallet (если настроено)
bw.requestTopup(bw.target.Sub(balance))
}
case <-ctx.Done():
return
}
}
}
Консолидация ERC-20 токенов
Депозитные адреса накапливают токены. Sweep-процесс их консолидирует:
func (hw *HotWallet) SweepERC20(depositAddr common.Address, token common.Address) error {
tokenContract := NewERC20(token, hw.client)
balance, _ := tokenContract.BalanceOf(depositAddr)
if balance.Cmp(MinSweepAmount) < 0 {
return nil // не стоит газа
}
// Сначала нужен ETH для газа на depositAddr
gasCost := hw.estimateSweepGas(depositAddr, token)
if ethBalance := hw.getETHBalance(depositAddr); ethBalance.Cmp(gasCost) < 0 {
// Отправляем ETH с hot wallet на депозитный адрес
err := hw.sendETH(depositAddr, gasCost)
if err != nil { return err }
// Ждём подтверждения
time.Sleep(15 * time.Second)
}
// Теперь sweep токенов с депозитного адреса на hot wallet
nonce, _ := hw.nonceTrack.NextForAddress(depositAddr)
tx := hw.buildERC20Transfer(depositAddr, hw.address, token, balance, nonce)
signed := hw.keyManager.Sign(depositAddr, tx)
return hw.client.SendTransaction(signed)
}
Transaction management
Stuck transaction handling
Транзакция может зависнуть в mempool при недостаточном gas price. Система должна автоматически бампировать комиссию:
func (hw *HotWallet) BumpFee(txHash common.Hash) error {
// Получаем оригинальную транзакцию
origTx, _, _ := hw.client.TransactionByHash(txHash)
// EIP-1559: увеличиваем maxFeePerGas и maxPriorityFeePerGas на 10%
newMaxFee := new(big.Int).Mul(origTx.GasFeeCap(), big.NewInt(110))
newMaxFee.Div(newMaxFee, big.NewInt(100))
newPriorityFee := new(big.Int).Mul(origTx.GasTipCap(), big.NewInt(110))
newPriorityFee.Div(newPriorityFee, big.NewInt(100))
// Создаём замещающую транзакцию с тем же nonce
replaceTx := types.NewTx(&types.DynamicFeeTx{
Nonce: origTx.Nonce(),
To: origTx.To(),
Value: origTx.Value(),
Data: origTx.Data(),
Gas: origTx.Gas(),
GasFeeCap: newMaxFee,
GasTipCap: newPriorityFee,
})
signed := hw.keyManager.Sign(replaceTx)
return hw.client.SendTransaction(signed)
}
Транзакционный журнал
Каждая транзакция горячего кошелька логируется:
CREATE TABLE hot_wallet_transactions (
id BIGSERIAL PRIMARY KEY,
tx_hash VARCHAR(66),
network VARCHAR(20) NOT NULL,
from_address VARCHAR(42) NOT NULL,
to_address VARCHAR(42) NOT NULL,
token VARCHAR(42),
amount NUMERIC(36,18) NOT NULL,
gas_price NUMERIC(36,0),
gas_used INTEGER,
status VARCHAR(20) NOT NULL, -- pending/confirmed/failed/replaced
withdrawal_id BIGINT REFERENCES withdrawals(id),
nonce INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
confirmed_at TIMESTAMPTZ,
block_number BIGINT
);
Мониторинг
Горячий кошелёк требует 24/7 мониторинга:
- Balanace alerts: при падении ниже порога
- Failed transaction alerts: если транзакция не подтвердилась через N минут
- Nonce gap detection: если в nonce sequence есть пропуск — что-то пошло не так
- Unusual outflow: резкий рост объёма выводов — потенциальная атака
Grafana + Prometheus для метрик, PagerDuty или аналог для on-call алертов.
Сроки разработки
| Компонент | Срок |
|---|---|
| ETH/ERC-20 hot wallet | 3–4 недели |
| Bitcoin UTXO wallet | 3–4 недели |
| HSM/Vault интеграция | 1–2 недели |
| Sweep automation | 2–3 недели |
| Monitoring dashboard | 1–2 недели |
| Тестирование на testnet | 2–3 недели |
Полный мультивалютный горячий кошелёк с HSM — 3–4 месяца.







