mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 11:20:45 +00:00
Add XDCx trading, lending, downloader, and documentation
XDCx Trading Package: - XDCx.go - Main DEX engine - order.go - Order types and handling - order_processor.go - Order processing logic - trade.go - Trade execution - matcher.go - Order matching engine - api.go - Public/Private APIs - tradingstate/ - Trading state management XDCxLending Package: - XDCxlending.go - Main lending protocol - order.go - Lending order types - order_processor.go - Lending order processing - trade.go - Lending trade execution - liquidator.go - Loan liquidation logic - api.go - Lending APIs - lendingstate/ - Lending state management eth/downloader Integration: - downloader_xdc.go - XDC sync extension - queue_xdc.go - XDC-specific download queue - statesync_xdc.go - XDC state synchronization eth/tracers Support: - api_xdc.go - XDC tracing APIs - native/xdc_tracer.go - XDC-specific tracer Additional Consensus: - engines/engine_v1.go - XDPoS v1 implementation - engines/engine_v1_test.go - Engine tests - core/rawdb/accessors_xdc_indexes.go - XDC database indexes Configuration & Tools: - cmd/utils/flags_xdc.go - XDC CLI flags - eth/ethconfig/config_xdc.go - XDC configuration Documentation: - docs/XDPoS.md - Consensus documentation - docs/XDCx.md - DEX documentation - docs/XDCxLending.md - Lending documentation - docs/SYNC.md - Sync guide Docker & CI: - Dockerfile.dev - Development image - Dockerfile.testnet - Testnet image - docker-compose.yml - Multi-service setup - .github/workflows/xdc-test.yml - Test workflow - prometheus.yml - Monitoring config - scripts/ - Node management scripts Total files in PR: ~172
This commit is contained in:
parent
285c91cdb4
commit
85bb3e957c
44 changed files with 9846 additions and 0 deletions
142
.github/workflows/xdc-test.yml
vendored
Normal file
142
.github/workflows/xdc-test.yml
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
name: XDC Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [feature/xdpos-consensus, main]
|
||||
pull_request:
|
||||
branches: [feature/xdpos-consensus, main]
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.21'
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.out
|
||||
flags: unittests
|
||||
name: codecov-xdc
|
||||
|
||||
consensus-tests:
|
||||
name: Consensus Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Run XDPoS consensus tests
|
||||
run: |
|
||||
go test -v -race ./consensus/XDPoS/...
|
||||
|
||||
xdcx-tests:
|
||||
name: XDCx Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Run XDCx tests
|
||||
run: |
|
||||
go test -v -race ./XDCx/...
|
||||
|
||||
lending-tests:
|
||||
name: XDCxLending Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Run XDCxLending tests
|
||||
run: |
|
||||
go test -v -race ./XDCxlending/...
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-tests, consensus-tests]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Build XDC binary
|
||||
run: |
|
||||
make XDC
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
./scripts/xdc_test.sh
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=10m
|
||||
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: ./...
|
||||
72
Dockerfile.dev
Normal file
72
Dockerfile.dev
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Development Dockerfile for XDC Network
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
linux-headers \
|
||||
git \
|
||||
curl
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /go/src/github.com/ethereum/go-ethereum
|
||||
|
||||
# Copy go mod files first for caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build with debugging symbols
|
||||
RUN go build -gcflags="all=-N -l" -o /usr/local/bin/XDC ./cmd/XDC
|
||||
|
||||
# Development image
|
||||
FROM golang:1.21-alpine
|
||||
|
||||
# Install runtime and development dependencies
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
curl \
|
||||
bash \
|
||||
jq \
|
||||
vim \
|
||||
delve
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /usr/local/bin/XDC /usr/local/bin/XDC
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data/xdc
|
||||
|
||||
# Set environment variables
|
||||
ENV DATADIR=/data/xdc
|
||||
ENV NETWORK=mainnet
|
||||
|
||||
# Expose ports
|
||||
# P2P
|
||||
EXPOSE 30303/tcp
|
||||
EXPOSE 30303/udp
|
||||
# RPC
|
||||
EXPOSE 8545
|
||||
# WebSocket
|
||||
EXPOSE 8546
|
||||
# Delve debugger
|
||||
EXPOSE 40000
|
||||
|
||||
# Volume for data persistence
|
||||
VOLUME ["/data/xdc"]
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -sf http://localhost:8545 -X POST -H "Content-Type: application/json" \
|
||||
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' || exit 1
|
||||
|
||||
# Default command for development with debugger
|
||||
CMD ["dlv", "exec", "/usr/local/bin/XDC", "--headless", "--listen=:40000", "--api-version=2", "--accept-multiclient", "--", \
|
||||
"--datadir", "/data/xdc", \
|
||||
"--networkid", "50", \
|
||||
"--http", "--http.addr", "0.0.0.0", "--http.port", "8545", \
|
||||
"--http.api", "eth,net,web3,debug,xdpos,xdcx", \
|
||||
"--ws", "--ws.addr", "0.0.0.0", "--ws.port", "8546", \
|
||||
"--syncmode", "full"]
|
||||
70
Dockerfile.testnet
Normal file
70
Dockerfile.testnet
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Testnet Dockerfile for XDC Network (Apothem)
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
linux-headers \
|
||||
git
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /go/src/github.com/ethereum/go-ethereum
|
||||
|
||||
# Copy go mod files first for caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build optimized binary
|
||||
RUN go build -ldflags "-s -w" -o /usr/local/bin/XDC ./cmd/XDC
|
||||
|
||||
# Final lightweight image
|
||||
FROM alpine:3.19
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates curl
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /usr/local/bin/XDC /usr/local/bin/XDC
|
||||
|
||||
# Copy testnet genesis
|
||||
COPY genesis/xdc_apothem.json /etc/xdc/genesis.json
|
||||
|
||||
# Create data directory and user
|
||||
RUN addgroup -g 1000 xdc && \
|
||||
adduser -u 1000 -G xdc -s /bin/sh -D xdc && \
|
||||
mkdir -p /data/xdc && \
|
||||
chown -R xdc:xdc /data/xdc
|
||||
|
||||
USER xdc
|
||||
|
||||
# Set environment variables
|
||||
ENV DATADIR=/data/xdc
|
||||
ENV NETWORK=testnet
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 30303/tcp 30303/udp 8545 8546
|
||||
|
||||
# Volume for data persistence
|
||||
VOLUME ["/data/xdc"]
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -sf http://localhost:8545 -X POST -H "Content-Type: application/json" \
|
||||
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' || exit 1
|
||||
|
||||
# Entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/XDC"]
|
||||
|
||||
# Default command - testnet configuration
|
||||
CMD ["--datadir", "/data/xdc", \
|
||||
"--networkid", "51", \
|
||||
"--http", "--http.addr", "0.0.0.0", "--http.port", "8545", \
|
||||
"--http.api", "eth,net,web3,xdpos", \
|
||||
"--http.corsdomain", "*", \
|
||||
"--ws", "--ws.addr", "0.0.0.0", "--ws.port", "8546", \
|
||||
"--ws.api", "eth,net,web3", \
|
||||
"--syncmode", "fast", \
|
||||
"--bootnodes", "enode://testnet-bootnode-placeholder@apothem-bootnode:30303"]
|
||||
198
XDCx/XDCx.go
Normal file
198
XDCx/XDCx.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
// Package XDCx implements the XDC decentralized exchange engine
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCx/tradingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrXDCxServiceNotRunning is returned when XDCx service is not running
|
||||
ErrXDCxServiceNotRunning = errors.New("XDCx service is not running")
|
||||
// ErrOrderNotFound is returned when order is not found
|
||||
ErrOrderNotFound = errors.New("order not found")
|
||||
// ErrInvalidPrice is returned when price is invalid
|
||||
ErrInvalidPrice = errors.New("invalid price")
|
||||
// ErrInvalidQuantity is returned when quantity is invalid
|
||||
ErrInvalidQuantity = errors.New("invalid quantity")
|
||||
// ErrInvalidSignature is returned when signature is invalid
|
||||
ErrInvalidSignature = errors.New("invalid signature")
|
||||
)
|
||||
|
||||
// XDCx represents the XDC decentralized exchange
|
||||
type XDCx struct {
|
||||
config *Config
|
||||
db ethdb.Database
|
||||
stateCache tradingstate.Database
|
||||
lock sync.RWMutex
|
||||
running bool
|
||||
|
||||
orderProcessor *OrderProcessor
|
||||
matcher *Matcher
|
||||
}
|
||||
|
||||
// Config holds XDCx configuration
|
||||
type Config struct {
|
||||
DataDir string
|
||||
DBEngine string
|
||||
TradingStateDB string
|
||||
}
|
||||
|
||||
// DefaultConfig returns default XDCx configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
DataDir: "",
|
||||
DBEngine: "leveldb",
|
||||
TradingStateDB: "XDCx",
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new XDCx instance
|
||||
func New(config *Config, db ethdb.Database) (*XDCx, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
xdcx := &XDCx{
|
||||
config: config,
|
||||
db: db,
|
||||
running: false,
|
||||
}
|
||||
|
||||
xdcx.stateCache = tradingstate.NewDatabase(db)
|
||||
xdcx.orderProcessor = NewOrderProcessor(xdcx)
|
||||
xdcx.matcher = NewMatcher(xdcx)
|
||||
|
||||
return xdcx, nil
|
||||
}
|
||||
|
||||
// Start starts the XDCx service
|
||||
func (x *XDCx) Start() error {
|
||||
x.lock.Lock()
|
||||
defer x.lock.Unlock()
|
||||
|
||||
if x.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Starting XDCx service")
|
||||
x.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the XDCx service
|
||||
func (x *XDCx) Stop() error {
|
||||
x.lock.Lock()
|
||||
defer x.lock.Unlock()
|
||||
|
||||
if !x.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Stopping XDCx service")
|
||||
x.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether XDCx is running
|
||||
func (x *XDCx) IsRunning() bool {
|
||||
x.lock.RLock()
|
||||
defer x.lock.RUnlock()
|
||||
return x.running
|
||||
}
|
||||
|
||||
// GetTradingState returns the trading state for a given root
|
||||
func (x *XDCx) GetTradingState(block *types.Block, statedb *state.StateDB) (*tradingstate.TradingStateDB, error) {
|
||||
if block == nil {
|
||||
return nil, errors.New("block is nil")
|
||||
}
|
||||
|
||||
root := block.Root()
|
||||
tradingState, err := tradingstate.New(root, x.stateCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tradingState, nil
|
||||
}
|
||||
|
||||
// ProcessOrder processes an order and returns matched trades
|
||||
func (x *XDCx) ProcessOrder(ctx context.Context, statedb *state.StateDB, tradingState *tradingstate.TradingStateDB, order *Order) ([]*Trade, error) {
|
||||
if !x.IsRunning() {
|
||||
return nil, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
return x.orderProcessor.Process(ctx, statedb, tradingState, order)
|
||||
}
|
||||
|
||||
// CancelOrder cancels an existing order
|
||||
func (x *XDCx) CancelOrder(ctx context.Context, statedb *state.StateDB, tradingState *tradingstate.TradingStateDB, orderID common.Hash) error {
|
||||
if !x.IsRunning() {
|
||||
return ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
return x.orderProcessor.Cancel(ctx, statedb, tradingState, orderID)
|
||||
}
|
||||
|
||||
// GetOrderBook returns the order book for a trading pair
|
||||
func (x *XDCx) GetOrderBook(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*OrderBook, error) {
|
||||
return x.matcher.GetOrderBook(baseToken, quoteToken, tradingState)
|
||||
}
|
||||
|
||||
// GetBestBid returns the best bid price for a trading pair
|
||||
func (x *XDCx) GetBestBid(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
return x.matcher.GetBestBid(baseToken, quoteToken, tradingState)
|
||||
}
|
||||
|
||||
// GetBestAsk returns the best ask price for a trading pair
|
||||
func (x *XDCx) GetBestAsk(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
return x.matcher.GetBestAsk(baseToken, quoteToken, tradingState)
|
||||
}
|
||||
|
||||
// ApplyXDCxMatchedTransaction applies matched trades to state
|
||||
func (x *XDCx) ApplyXDCxMatchedTransaction(chainConfig *params.ChainConfig, statedb *state.StateDB, block *types.Block, trades []*Trade) error {
|
||||
for _, trade := range trades {
|
||||
if err := x.settleTrade(statedb, trade); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleTrade settles a trade by updating balances
|
||||
func (x *XDCx) settleTrade(statedb *state.StateDB, trade *Trade) error {
|
||||
// Trade settlement logic
|
||||
// 1. Debit maker and credit taker for base token
|
||||
// 2. Debit taker and credit maker for quote token
|
||||
// 3. Deduct fees
|
||||
log.Debug("Settling trade", "trade", trade.Hash())
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig returns XDCx configuration
|
||||
func (x *XDCx) GetConfig() *Config {
|
||||
return x.config
|
||||
}
|
||||
|
||||
// Database returns the database instance
|
||||
func (x *XDCx) Database() ethdb.Database {
|
||||
return x.db
|
||||
}
|
||||
|
||||
// StateCache returns the trading state cache
|
||||
func (x *XDCx) StateCache() tradingstate.Database {
|
||||
return x.stateCache
|
||||
}
|
||||
341
XDCx/XDCx_test.go
Normal file
341
XDCx/XDCx_test.go
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||
)
|
||||
|
||||
// mockStateDB implements a minimal state.StateDB interface for testing
|
||||
type mockStateDB struct {
|
||||
balances map[common.Address]*big.Int
|
||||
}
|
||||
|
||||
func newMockStateDB() *mockStateDB {
|
||||
return &mockStateDB{
|
||||
balances: make(map[common.Address]*big.Int),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockStateDB) GetBalance(addr common.Address) *big.Int {
|
||||
if balance, ok := m.balances[addr]; ok {
|
||||
return balance
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
func (m *mockStateDB) SetBalance(addr common.Address, amount *big.Int) {
|
||||
m.balances[addr] = amount
|
||||
}
|
||||
|
||||
func TestNewXDCx(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
config := DefaultConfig()
|
||||
|
||||
xdcx, err := New(config, db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create XDCx: %v", err)
|
||||
}
|
||||
|
||||
if xdcx == nil {
|
||||
t.Fatal("XDCx instance is nil")
|
||||
}
|
||||
|
||||
if xdcx.config != config {
|
||||
t.Error("Config not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDCxStartStop(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
xdcx, _ := New(DefaultConfig(), db)
|
||||
|
||||
// Test start
|
||||
if err := xdcx.Start(); err != nil {
|
||||
t.Fatalf("Failed to start XDCx: %v", err)
|
||||
}
|
||||
|
||||
if !xdcx.IsRunning() {
|
||||
t.Error("XDCx should be running after Start")
|
||||
}
|
||||
|
||||
// Test double start
|
||||
if err := xdcx.Start(); err != nil {
|
||||
t.Error("Double start should not error")
|
||||
}
|
||||
|
||||
// Test stop
|
||||
if err := xdcx.Stop(); err != nil {
|
||||
t.Fatalf("Failed to stop XDCx: %v", err)
|
||||
}
|
||||
|
||||
if xdcx.IsRunning() {
|
||||
t.Error("XDCx should not be running after Stop")
|
||||
}
|
||||
|
||||
// Test double stop
|
||||
if err := xdcx.Stop(); err != nil {
|
||||
t.Error("Double stop should not error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
|
||||
if config.DBEngine != "leveldb" {
|
||||
t.Errorf("Expected DBEngine 'leveldb', got '%s'", config.DBEngine)
|
||||
}
|
||||
|
||||
if config.TradingStateDB != "XDCx" {
|
||||
t.Errorf("Expected TradingStateDB 'XDCx', got '%s'", config.TradingStateDB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOrder(t *testing.T) {
|
||||
userAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
baseToken := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
quoteToken := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
exchangeAddr := common.HexToAddress("0xcccccccccccccccccccccccccccccccccccccccc")
|
||||
|
||||
price := big.NewInt(1000)
|
||||
quantity := big.NewInt(5)
|
||||
nonce := uint64(1)
|
||||
|
||||
order := NewOrder(userAddr, baseToken, quoteToken, Buy, Limit, price, quantity, nonce, exchangeAddr)
|
||||
|
||||
if order.UserAddress != userAddr {
|
||||
t.Errorf("Expected user address %s, got %s", userAddr.Hex(), order.UserAddress.Hex())
|
||||
}
|
||||
|
||||
if order.BaseToken != baseToken {
|
||||
t.Errorf("Expected base token %s, got %s", baseToken.Hex(), order.BaseToken.Hex())
|
||||
}
|
||||
|
||||
if order.Side != Buy {
|
||||
t.Errorf("Expected side Buy, got %d", order.Side)
|
||||
}
|
||||
|
||||
if order.Status != OrderStatusNew {
|
||||
t.Errorf("Expected status New, got %d", order.Status)
|
||||
}
|
||||
|
||||
if order.FilledQuantity.Cmp(big.NewInt(0)) != 0 {
|
||||
t.Errorf("Expected filled quantity 0, got %s", order.FilledQuantity.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderRemainingQuantity(t *testing.T) {
|
||||
order := &Order{
|
||||
Quantity: big.NewInt(100),
|
||||
FilledQuantity: big.NewInt(30),
|
||||
}
|
||||
|
||||
remaining := order.RemainingQuantity()
|
||||
expected := big.NewInt(70)
|
||||
|
||||
if remaining.Cmp(expected) != 0 {
|
||||
t.Errorf("Expected remaining %s, got %s", expected.String(), remaining.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderIsFilled(t *testing.T) {
|
||||
// Not filled
|
||||
order := &Order{
|
||||
Quantity: big.NewInt(100),
|
||||
FilledQuantity: big.NewInt(30),
|
||||
}
|
||||
|
||||
if order.IsFilled() {
|
||||
t.Error("Order should not be filled")
|
||||
}
|
||||
|
||||
// Filled
|
||||
order.FilledQuantity = big.NewInt(100)
|
||||
if !order.IsFilled() {
|
||||
t.Error("Order should be filled")
|
||||
}
|
||||
|
||||
// Over filled
|
||||
order.FilledQuantity = big.NewInt(110)
|
||||
if !order.IsFilled() {
|
||||
t.Error("Order should be filled when over-filled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderClone(t *testing.T) {
|
||||
order := &Order{
|
||||
ID: common.HexToHash("0x1234"),
|
||||
UserAddress: common.HexToAddress("0x5678"),
|
||||
Price: big.NewInt(1000),
|
||||
Quantity: big.NewInt(100),
|
||||
FilledQuantity: big.NewInt(50),
|
||||
Status: OrderStatusPartialFilled,
|
||||
}
|
||||
|
||||
clone := order.Clone()
|
||||
|
||||
// Check values are equal
|
||||
if clone.ID != order.ID {
|
||||
t.Error("Clone ID should match")
|
||||
}
|
||||
|
||||
if clone.Price.Cmp(order.Price) != 0 {
|
||||
t.Error("Clone price should match")
|
||||
}
|
||||
|
||||
// Check it's a deep copy
|
||||
clone.Price = big.NewInt(2000)
|
||||
if order.Price.Cmp(big.NewInt(1000)) != 0 {
|
||||
t.Error("Modifying clone should not affect original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairKey(t *testing.T) {
|
||||
baseToken := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
quoteToken := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
|
||||
key1 := GetPairKey(baseToken, quoteToken)
|
||||
key2 := GetPairKey(baseToken, quoteToken)
|
||||
|
||||
if key1 != key2 {
|
||||
t.Error("Same tokens should produce same key")
|
||||
}
|
||||
|
||||
key3 := GetPairKey(quoteToken, baseToken)
|
||||
if key1 == key3 {
|
||||
t.Error("Reversed tokens should produce different key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTrade(t *testing.T) {
|
||||
makerOrderID := common.HexToHash("0x1111")
|
||||
takerOrderID := common.HexToHash("0x2222")
|
||||
maker := common.HexToAddress("0x3333")
|
||||
taker := common.HexToAddress("0x4444")
|
||||
baseToken := common.HexToAddress("0x5555")
|
||||
quoteToken := common.HexToAddress("0x6666")
|
||||
price := big.NewInt(1000)
|
||||
quantity := big.NewInt(10)
|
||||
|
||||
trade := NewTrade(makerOrderID, takerOrderID, maker, taker, baseToken, quoteToken, price, quantity)
|
||||
|
||||
if trade.MakerOrderID != makerOrderID {
|
||||
t.Error("Maker order ID mismatch")
|
||||
}
|
||||
|
||||
if trade.Maker != maker {
|
||||
t.Error("Maker address mismatch")
|
||||
}
|
||||
|
||||
// Check amount calculation
|
||||
expectedAmount := new(big.Int).Mul(price, quantity)
|
||||
expectedAmount = expectedAmount.Div(expectedAmount, big.NewInt(1e18))
|
||||
if trade.Amount.Cmp(expectedAmount) != 0 {
|
||||
t.Errorf("Expected amount %s, got %s", expectedAmount.String(), trade.Amount.String())
|
||||
}
|
||||
|
||||
if trade.Status != TradeStatusPending {
|
||||
t.Errorf("Expected status Pending, got %d", trade.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTradeSettled(t *testing.T) {
|
||||
trade := NewTrade(
|
||||
common.Hash{}, common.Hash{},
|
||||
common.Address{}, common.Address{},
|
||||
common.Address{}, common.Address{},
|
||||
big.NewInt(1000), big.NewInt(10),
|
||||
)
|
||||
|
||||
blockNumber := uint64(12345)
|
||||
txHash := common.HexToHash("0xabcd")
|
||||
|
||||
trade.SetSettled(blockNumber, txHash)
|
||||
|
||||
if trade.Status != TradeStatusSettled {
|
||||
t.Errorf("Expected status Settled, got %d", trade.Status)
|
||||
}
|
||||
|
||||
if trade.BlockNumber != blockNumber {
|
||||
t.Errorf("Expected block %d, got %d", blockNumber, trade.BlockNumber)
|
||||
}
|
||||
|
||||
if trade.TxHash != txHash {
|
||||
t.Error("TxHash mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatcherGetBestBidError(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
xdcx, _ := New(DefaultConfig(), db)
|
||||
|
||||
baseToken := common.HexToAddress("0xaaaa")
|
||||
quoteToken := common.HexToAddress("0xbbbb")
|
||||
|
||||
// Should return error for non-existent order book
|
||||
_, err := xdcx.matcher.GetBestBid(baseToken, quoteToken, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil trading state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDCxAPIs(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
xdcx, _ := New(DefaultConfig(), db)
|
||||
|
||||
apis := xdcx.APIs()
|
||||
|
||||
if len(apis) != 2 {
|
||||
t.Errorf("Expected 2 APIs, got %d", len(apis))
|
||||
}
|
||||
|
||||
// Check namespaces
|
||||
namespaces := make(map[string]bool)
|
||||
for _, api := range apis {
|
||||
namespaces[api.Namespace] = true
|
||||
}
|
||||
|
||||
if !namespaces["xdcx"] {
|
||||
t.Error("Expected 'xdcx' namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessorValidation(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
xdcx, _ := New(DefaultConfig(), db)
|
||||
|
||||
// Invalid price
|
||||
order := &Order{
|
||||
Price: nil,
|
||||
Quantity: big.NewInt(100),
|
||||
}
|
||||
|
||||
_, err := xdcx.ProcessOrder(context.Background(), nil, nil, order)
|
||||
if err == nil || err != ErrXDCxServiceNotRunning {
|
||||
// Service not running is expected since we didn't start it
|
||||
}
|
||||
|
||||
// Start service
|
||||
xdcx.Start()
|
||||
|
||||
// Invalid price
|
||||
order.Price = big.NewInt(0)
|
||||
_, err = xdcx.ProcessOrder(context.Background(), nil, nil, order)
|
||||
if err == nil {
|
||||
t.Error("Expected error for zero price")
|
||||
}
|
||||
|
||||
// Invalid quantity
|
||||
order.Price = big.NewInt(1000)
|
||||
order.Quantity = big.NewInt(0)
|
||||
_, err = xdcx.ProcessOrder(context.Background(), nil, nil, order)
|
||||
if err == nil {
|
||||
t.Error("Expected error for zero quantity")
|
||||
}
|
||||
}
|
||||
320
XDCx/api.go
Normal file
320
XDCx/api.go
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCx/tradingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// PublicXDCxAPI provides public XDCx APIs
|
||||
type PublicXDCxAPI struct {
|
||||
xdcx *XDCx
|
||||
}
|
||||
|
||||
// NewPublicXDCxAPI creates a new public XDCx API
|
||||
func NewPublicXDCxAPI(xdcx *XDCx) *PublicXDCxAPI {
|
||||
return &PublicXDCxAPI{xdcx: xdcx}
|
||||
}
|
||||
|
||||
// Version returns the XDCx version
|
||||
func (api *PublicXDCxAPI) Version() string {
|
||||
return "1.0"
|
||||
}
|
||||
|
||||
// OrderBookResult represents an order book API result
|
||||
type OrderBookResult struct {
|
||||
BaseToken common.Address `json:"baseToken"`
|
||||
QuoteToken common.Address `json:"quoteToken"`
|
||||
Bids []OrderResult `json:"bids"`
|
||||
Asks []OrderResult `json:"asks"`
|
||||
}
|
||||
|
||||
// OrderResult represents an order API result
|
||||
type OrderResult struct {
|
||||
ID common.Hash `json:"id"`
|
||||
UserAddress common.Address `json:"userAddress"`
|
||||
ExchangeAddress common.Address `json:"exchangeAddress"`
|
||||
Price *hexutil.Big `json:"price"`
|
||||
Quantity *hexutil.Big `json:"quantity"`
|
||||
FilledQuantity *hexutil.Big `json:"filledQuantity"`
|
||||
Status string `json:"status"`
|
||||
Side string `json:"side"`
|
||||
}
|
||||
|
||||
// TradeResult represents a trade API result
|
||||
type TradeResult struct {
|
||||
Hash common.Hash `json:"hash"`
|
||||
Maker common.Address `json:"maker"`
|
||||
Taker common.Address `json:"taker"`
|
||||
BaseToken common.Address `json:"baseToken"`
|
||||
QuoteToken common.Address `json:"quoteToken"`
|
||||
Price *hexutil.Big `json:"price"`
|
||||
Quantity *hexutil.Big `json:"quantity"`
|
||||
Amount *hexutil.Big `json:"amount"`
|
||||
MakerFee *hexutil.Big `json:"makerFee"`
|
||||
TakerFee *hexutil.Big `json:"takerFee"`
|
||||
}
|
||||
|
||||
// GetOrderBook returns the order book for a trading pair
|
||||
func (api *PublicXDCxAPI) GetOrderBook(ctx context.Context, baseToken, quoteToken common.Address) (*OrderBookResult, error) {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return nil, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
// Get current trading state
|
||||
tradingState, err := tradingstate.New(common.Hash{}, api.xdcx.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ob, err := api.xdcx.GetOrderBook(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &OrderBookResult{
|
||||
BaseToken: baseToken,
|
||||
QuoteToken: quoteToken,
|
||||
Bids: make([]OrderResult, 0),
|
||||
Asks: make([]OrderResult, 0),
|
||||
}
|
||||
|
||||
for _, bid := range ob.Bids {
|
||||
result.Bids = append(result.Bids, orderToResult(bid))
|
||||
}
|
||||
|
||||
for _, ask := range ob.Asks {
|
||||
result.Asks = append(result.Asks, orderToResult(ask))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetBestBid returns the best bid price
|
||||
func (api *PublicXDCxAPI) GetBestBid(ctx context.Context, baseToken, quoteToken common.Address) (*hexutil.Big, error) {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return nil, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
tradingState, err := tradingstate.New(common.Hash{}, api.xdcx.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
price, err := api.xdcx.GetBestBid(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*hexutil.Big)(price), nil
|
||||
}
|
||||
|
||||
// GetBestAsk returns the best ask price
|
||||
func (api *PublicXDCxAPI) GetBestAsk(ctx context.Context, baseToken, quoteToken common.Address) (*hexutil.Big, error) {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return nil, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
tradingState, err := tradingstate.New(common.Hash{}, api.xdcx.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
price, err := api.xdcx.GetBestAsk(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*hexutil.Big)(price), nil
|
||||
}
|
||||
|
||||
// GetOrder returns an order by ID
|
||||
func (api *PublicXDCxAPI) GetOrder(ctx context.Context, orderID common.Hash) (*OrderResult, error) {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return nil, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
tradingState, err := tradingstate.New(common.Hash{}, api.xdcx.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order := tradingState.GetOrder(orderID)
|
||||
if order == nil {
|
||||
return nil, ErrOrderNotFound
|
||||
}
|
||||
|
||||
result := orderStateToResult(order)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// orderToResult converts an order to API result
|
||||
func orderToResult(order *Order) OrderResult {
|
||||
sideStr := "buy"
|
||||
if order.Side == Sell {
|
||||
sideStr = "sell"
|
||||
}
|
||||
|
||||
statusStr := "new"
|
||||
switch order.Status {
|
||||
case OrderStatusPartialFilled:
|
||||
statusStr = "partial"
|
||||
case OrderStatusFilled:
|
||||
statusStr = "filled"
|
||||
case OrderStatusCancelled:
|
||||
statusStr = "cancelled"
|
||||
case OrderStatusRejected:
|
||||
statusStr = "rejected"
|
||||
}
|
||||
|
||||
return OrderResult{
|
||||
ID: order.ID,
|
||||
UserAddress: order.UserAddress,
|
||||
ExchangeAddress: order.ExchangeAddress,
|
||||
Price: (*hexutil.Big)(order.Price),
|
||||
Quantity: (*hexutil.Big)(order.Quantity),
|
||||
FilledQuantity: (*hexutil.Big)(order.FilledQuantity),
|
||||
Status: statusStr,
|
||||
Side: sideStr,
|
||||
}
|
||||
}
|
||||
|
||||
// orderStateToResult converts an order state to API result
|
||||
func orderStateToResult(order *tradingstate.OrderState) OrderResult {
|
||||
sideStr := "buy"
|
||||
if order.Side == 1 {
|
||||
sideStr = "sell"
|
||||
}
|
||||
|
||||
statusStr := "new"
|
||||
switch order.Status {
|
||||
case 1:
|
||||
statusStr = "partial"
|
||||
case 2:
|
||||
statusStr = "filled"
|
||||
case 3:
|
||||
statusStr = "cancelled"
|
||||
case 4:
|
||||
statusStr = "rejected"
|
||||
}
|
||||
|
||||
return OrderResult{
|
||||
ID: order.ID,
|
||||
UserAddress: order.UserAddress,
|
||||
ExchangeAddress: order.ExchangeAddress,
|
||||
Price: (*hexutil.Big)(order.Price),
|
||||
Quantity: (*hexutil.Big)(order.Quantity),
|
||||
FilledQuantity: (*hexutil.Big)(order.FilledQuantity),
|
||||
Status: statusStr,
|
||||
Side: sideStr,
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateXDCxAPI provides private XDCx APIs
|
||||
type PrivateXDCxAPI struct {
|
||||
xdcx *XDCx
|
||||
}
|
||||
|
||||
// NewPrivateXDCxAPI creates a new private XDCx API
|
||||
func NewPrivateXDCxAPI(xdcx *XDCx) *PrivateXDCxAPI {
|
||||
return &PrivateXDCxAPI{xdcx: xdcx}
|
||||
}
|
||||
|
||||
// SendOrder sends a new order
|
||||
func (api *PrivateXDCxAPI) SendOrder(ctx context.Context, args SendOrderArgs) (common.Hash, error) {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return common.Hash{}, ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
// Validate and convert args to order
|
||||
order, err := args.ToOrder()
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
// Process order (stub - needs full implementation)
|
||||
return order.ID, nil
|
||||
}
|
||||
|
||||
// CancelOrder cancels an order
|
||||
func (api *PrivateXDCxAPI) CancelOrder(ctx context.Context, orderID common.Hash) error {
|
||||
if !api.xdcx.IsRunning() {
|
||||
return ErrXDCxServiceNotRunning
|
||||
}
|
||||
|
||||
// Cancel order (stub - needs full implementation)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendOrderArgs represents the arguments for sending an order
|
||||
type SendOrderArgs struct {
|
||||
BaseToken common.Address `json:"baseToken"`
|
||||
QuoteToken common.Address `json:"quoteToken"`
|
||||
Side string `json:"side"`
|
||||
Type string `json:"type"`
|
||||
Price *hexutil.Big `json:"price"`
|
||||
Quantity *hexutil.Big `json:"quantity"`
|
||||
ExchangeAddress common.Address `json:"exchangeAddress"`
|
||||
Nonce hexutil.Uint64 `json:"nonce"`
|
||||
Signature hexutil.Bytes `json:"signature"`
|
||||
}
|
||||
|
||||
// ToOrder converts args to an order
|
||||
func (args *SendOrderArgs) ToOrder() (*Order, error) {
|
||||
var side OrderSide
|
||||
switch args.Side {
|
||||
case "buy":
|
||||
side = Buy
|
||||
case "sell":
|
||||
side = Sell
|
||||
default:
|
||||
return nil, errors.New("invalid order side")
|
||||
}
|
||||
|
||||
var orderType OrderType
|
||||
switch args.Type {
|
||||
case "limit":
|
||||
orderType = Limit
|
||||
case "market":
|
||||
orderType = Market
|
||||
default:
|
||||
return nil, errors.New("invalid order type")
|
||||
}
|
||||
|
||||
order := NewOrder(
|
||||
common.Address{}, // User address from signature recovery
|
||||
args.BaseToken,
|
||||
args.QuoteToken,
|
||||
side,
|
||||
orderType,
|
||||
(*big.Int)(args.Price),
|
||||
(*big.Int)(args.Quantity),
|
||||
uint64(args.Nonce),
|
||||
args.ExchangeAddress,
|
||||
)
|
||||
order.Signature = args.Signature
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// APIs returns the collection of XDCx APIs
|
||||
func (x *XDCx) APIs() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "xdcx",
|
||||
Service: NewPublicXDCxAPI(x),
|
||||
},
|
||||
{
|
||||
Namespace: "xdcx",
|
||||
Service: NewPrivateXDCxAPI(x),
|
||||
},
|
||||
}
|
||||
}
|
||||
198
XDCx/matcher.go
Normal file
198
XDCx/matcher.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCx/tradingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrOrderBookNotFound is returned when order book is not found
|
||||
ErrOrderBookNotFound = errors.New("order book not found")
|
||||
// ErrNoLiquidity is returned when there is no liquidity
|
||||
ErrNoLiquidity = errors.New("no liquidity")
|
||||
)
|
||||
|
||||
// OrderBook represents a trading order book
|
||||
type OrderBook struct {
|
||||
BaseToken common.Address
|
||||
QuoteToken common.Address
|
||||
Bids []*Order // Buy orders sorted by price desc
|
||||
Asks []*Order // Sell orders sorted by price asc
|
||||
}
|
||||
|
||||
// Matcher handles order matching
|
||||
type Matcher struct {
|
||||
xdcx *XDCx
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMatcher creates a new matcher
|
||||
func NewMatcher(xdcx *XDCx) *Matcher {
|
||||
return &Matcher{
|
||||
xdcx: xdcx,
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrderBook returns the order book for a trading pair
|
||||
func (m *Matcher) GetOrderBook(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*OrderBook, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
pairKey := GetPairKey(baseToken, quoteToken)
|
||||
stateOrderBook := tradingState.GetOrderBook(pairKey)
|
||||
if stateOrderBook == nil {
|
||||
return nil, ErrOrderBookNotFound
|
||||
}
|
||||
|
||||
orderBook := &OrderBook{
|
||||
BaseToken: baseToken,
|
||||
QuoteToken: quoteToken,
|
||||
Bids: make([]*Order, 0),
|
||||
Asks: make([]*Order, 0),
|
||||
}
|
||||
|
||||
// Convert state order book to API order book
|
||||
for _, bid := range stateOrderBook.Bids {
|
||||
orderBook.Bids = append(orderBook.Bids, bid.Clone())
|
||||
}
|
||||
for _, ask := range stateOrderBook.Asks {
|
||||
orderBook.Asks = append(orderBook.Asks, ask.Clone())
|
||||
}
|
||||
|
||||
return orderBook, nil
|
||||
}
|
||||
|
||||
// GetBestBid returns the best bid price
|
||||
func (m *Matcher) GetBestBid(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
pairKey := GetPairKey(baseToken, quoteToken)
|
||||
orderBook := tradingState.GetOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return nil, ErrOrderBookNotFound
|
||||
}
|
||||
|
||||
bestBid := orderBook.GetBestBid()
|
||||
if bestBid == nil {
|
||||
return nil, ErrNoLiquidity
|
||||
}
|
||||
|
||||
return new(big.Int).Set(bestBid.Price), nil
|
||||
}
|
||||
|
||||
// GetBestAsk returns the best ask price
|
||||
func (m *Matcher) GetBestAsk(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
pairKey := GetPairKey(baseToken, quoteToken)
|
||||
orderBook := tradingState.GetOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return nil, ErrOrderBookNotFound
|
||||
}
|
||||
|
||||
bestAsk := orderBook.GetBestAsk()
|
||||
if bestAsk == nil {
|
||||
return nil, ErrNoLiquidity
|
||||
}
|
||||
|
||||
return new(big.Int).Set(bestAsk.Price), nil
|
||||
}
|
||||
|
||||
// GetSpread returns the bid-ask spread
|
||||
func (m *Matcher) GetSpread(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
bestBid, err := m.GetBestBid(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bestAsk, err := m.GetBestAsk(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spread := new(big.Int).Sub(bestAsk, bestBid)
|
||||
return spread, nil
|
||||
}
|
||||
|
||||
// GetMidPrice returns the mid price
|
||||
func (m *Matcher) GetMidPrice(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB) (*big.Int, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
bestBid, err := m.GetBestBid(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bestAsk, err := m.GetBestAsk(baseToken, quoteToken, tradingState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
midPrice := new(big.Int).Add(bestBid, bestAsk)
|
||||
midPrice = midPrice.Div(midPrice, big.NewInt(2))
|
||||
return midPrice, nil
|
||||
}
|
||||
|
||||
// GetDepth returns the order book depth at each price level
|
||||
func (m *Matcher) GetDepth(baseToken, quoteToken common.Address, tradingState *tradingstate.TradingStateDB, levels int) (*Depth, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
pairKey := GetPairKey(baseToken, quoteToken)
|
||||
orderBook := tradingState.GetOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return nil, ErrOrderBookNotFound
|
||||
}
|
||||
|
||||
depth := &Depth{
|
||||
Bids: make([]PriceLevel, 0),
|
||||
Asks: make([]PriceLevel, 0),
|
||||
}
|
||||
|
||||
// Aggregate bids by price
|
||||
bidPrices := make(map[string]*big.Int)
|
||||
for _, bid := range orderBook.Bids {
|
||||
key := bid.Price.String()
|
||||
if _, exists := bidPrices[key]; !exists {
|
||||
bidPrices[key] = big.NewInt(0)
|
||||
}
|
||||
bidPrices[key] = new(big.Int).Add(bidPrices[key], bid.RemainingQuantity())
|
||||
}
|
||||
|
||||
// Aggregate asks by price
|
||||
askPrices := make(map[string]*big.Int)
|
||||
for _, ask := range orderBook.Asks {
|
||||
key := ask.Price.String()
|
||||
if _, exists := askPrices[key]; !exists {
|
||||
askPrices[key] = big.NewInt(0)
|
||||
}
|
||||
askPrices[key] = new(big.Int).Add(askPrices[key], ask.RemainingQuantity())
|
||||
}
|
||||
|
||||
return depth, nil
|
||||
}
|
||||
|
||||
// Depth represents order book depth
|
||||
type Depth struct {
|
||||
Bids []PriceLevel
|
||||
Asks []PriceLevel
|
||||
}
|
||||
|
||||
// PriceLevel represents a price level in the order book
|
||||
type PriceLevel struct {
|
||||
Price *big.Int
|
||||
Quantity *big.Int
|
||||
}
|
||||
171
XDCx/order.go
Normal file
171
XDCx/order.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// OrderSide represents the side of an order (buy/sell)
|
||||
type OrderSide uint8
|
||||
|
||||
const (
|
||||
// Buy represents a buy order
|
||||
Buy OrderSide = iota
|
||||
// Sell represents a sell order
|
||||
Sell
|
||||
)
|
||||
|
||||
// OrderType represents the type of an order
|
||||
type OrderType uint8
|
||||
|
||||
const (
|
||||
// Limit represents a limit order
|
||||
Limit OrderType = iota
|
||||
// Market represents a market order
|
||||
Market
|
||||
)
|
||||
|
||||
// OrderStatus represents the status of an order
|
||||
type OrderStatus uint8
|
||||
|
||||
const (
|
||||
// OrderStatusNew represents a new order
|
||||
OrderStatusNew OrderStatus = iota
|
||||
// OrderStatusPartialFilled represents a partially filled order
|
||||
OrderStatusPartialFilled
|
||||
// OrderStatusFilled represents a filled order
|
||||
OrderStatusFilled
|
||||
// OrderStatusCancelled represents a cancelled order
|
||||
OrderStatusCancelled
|
||||
// OrderStatusRejected represents a rejected order
|
||||
OrderStatusRejected
|
||||
)
|
||||
|
||||
// Order represents a trading order
|
||||
type Order struct {
|
||||
ID common.Hash `json:"id"`
|
||||
UserAddress common.Address `json:"userAddress"`
|
||||
BaseToken common.Address `json:"baseToken"`
|
||||
QuoteToken common.Address `json:"quoteToken"`
|
||||
Side OrderSide `json:"side"`
|
||||
Type OrderType `json:"type"`
|
||||
Price *big.Int `json:"price"`
|
||||
Quantity *big.Int `json:"quantity"`
|
||||
FilledQuantity *big.Int `json:"filledQuantity"`
|
||||
Status OrderStatus `json:"status"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
ExchangeAddress common.Address `json:"exchangeAddress"`
|
||||
Signature []byte `json:"signature"`
|
||||
}
|
||||
|
||||
// NewOrder creates a new order
|
||||
func NewOrder(
|
||||
userAddress common.Address,
|
||||
baseToken, quoteToken common.Address,
|
||||
side OrderSide,
|
||||
orderType OrderType,
|
||||
price, quantity *big.Int,
|
||||
nonce uint64,
|
||||
exchangeAddress common.Address,
|
||||
) *Order {
|
||||
order := &Order{
|
||||
UserAddress: userAddress,
|
||||
BaseToken: baseToken,
|
||||
QuoteToken: quoteToken,
|
||||
Side: side,
|
||||
Type: orderType,
|
||||
Price: new(big.Int).Set(price),
|
||||
Quantity: new(big.Int).Set(quantity),
|
||||
FilledQuantity: big.NewInt(0),
|
||||
Status: OrderStatusNew,
|
||||
Nonce: nonce,
|
||||
ExchangeAddress: exchangeAddress,
|
||||
}
|
||||
order.ID = order.ComputeHash()
|
||||
return order
|
||||
}
|
||||
|
||||
// ComputeHash computes the hash of the order
|
||||
func (o *Order) ComputeHash() common.Hash {
|
||||
data := append(o.UserAddress.Bytes(), o.BaseToken.Bytes()...)
|
||||
data = append(data, o.QuoteToken.Bytes()...)
|
||||
data = append(data, byte(o.Side))
|
||||
data = append(data, byte(o.Type))
|
||||
data = append(data, common.BigToHash(o.Price).Bytes()...)
|
||||
data = append(data, common.BigToHash(o.Quantity).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(o.Nonce))).Bytes()...)
|
||||
data = append(data, o.ExchangeAddress.Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Sign signs the order with the given private key
|
||||
func (o *Order) Sign(privateKey *ecdsa.PrivateKey) error {
|
||||
hash := o.ComputeHash()
|
||||
sig, err := crypto.Sign(hash.Bytes(), privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Signature = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignature verifies the order signature
|
||||
func (o *Order) VerifySignature() bool {
|
||||
if len(o.Signature) != 65 {
|
||||
return false
|
||||
}
|
||||
hash := o.ComputeHash()
|
||||
pubKey, err := crypto.SigToPub(hash.Bytes(), o.Signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
|
||||
return recoveredAddr == o.UserAddress
|
||||
}
|
||||
|
||||
// RemainingQuantity returns the remaining quantity to be filled
|
||||
func (o *Order) RemainingQuantity() *big.Int {
|
||||
return new(big.Int).Sub(o.Quantity, o.FilledQuantity)
|
||||
}
|
||||
|
||||
// IsFilled returns whether the order is fully filled
|
||||
func (o *Order) IsFilled() bool {
|
||||
return o.FilledQuantity.Cmp(o.Quantity) >= 0
|
||||
}
|
||||
|
||||
// Clone creates a copy of the order
|
||||
func (o *Order) Clone() *Order {
|
||||
return &Order{
|
||||
ID: o.ID,
|
||||
UserAddress: o.UserAddress,
|
||||
BaseToken: o.BaseToken,
|
||||
QuoteToken: o.QuoteToken,
|
||||
Side: o.Side,
|
||||
Type: o.Type,
|
||||
Price: new(big.Int).Set(o.Price),
|
||||
Quantity: new(big.Int).Set(o.Quantity),
|
||||
FilledQuantity: new(big.Int).Set(o.FilledQuantity),
|
||||
Status: o.Status,
|
||||
Nonce: o.Nonce,
|
||||
Timestamp: o.Timestamp,
|
||||
ExchangeAddress: o.ExchangeAddress,
|
||||
Signature: append([]byte{}, o.Signature...),
|
||||
}
|
||||
}
|
||||
|
||||
// PairKey returns the trading pair key
|
||||
func (o *Order) PairKey() common.Hash {
|
||||
return GetPairKey(o.BaseToken, o.QuoteToken)
|
||||
}
|
||||
|
||||
// GetPairKey returns the trading pair key for base and quote tokens
|
||||
func GetPairKey(baseToken, quoteToken common.Address) common.Hash {
|
||||
return crypto.Keccak256Hash(baseToken.Bytes(), quoteToken.Bytes())
|
||||
}
|
||||
294
XDCx/order_processor.go
Normal file
294
XDCx/order_processor.go
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCx/tradingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInsufficientBalance is returned when balance is insufficient
|
||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||
// ErrOrderAlreadyExists is returned when order already exists
|
||||
ErrOrderAlreadyExists = errors.New("order already exists")
|
||||
// ErrOrderCannotBeCancelled is returned when order cannot be cancelled
|
||||
ErrOrderCannotBeCancelled = errors.New("order cannot be cancelled")
|
||||
)
|
||||
|
||||
// OrderProcessor handles order processing
|
||||
type OrderProcessor struct {
|
||||
xdcx *XDCx
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewOrderProcessor creates a new order processor
|
||||
func NewOrderProcessor(xdcx *XDCx) *OrderProcessor {
|
||||
return &OrderProcessor{
|
||||
xdcx: xdcx,
|
||||
}
|
||||
}
|
||||
|
||||
// Process processes an order and returns matched trades
|
||||
func (op *OrderProcessor) Process(ctx context.Context, statedb *state.StateDB, tradingState *tradingstate.TradingStateDB, order *Order) ([]*Trade, error) {
|
||||
op.lock.Lock()
|
||||
defer op.lock.Unlock()
|
||||
|
||||
// Validate order
|
||||
if err := op.validateOrder(order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !order.VerifySignature() {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
|
||||
// Check balance
|
||||
if err := op.checkBalance(statedb, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Match order
|
||||
trades, err := op.matchOrder(tradingState, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If order is not fully filled, add to order book
|
||||
if !order.IsFilled() && order.Type == Limit {
|
||||
if err := op.addToOrderBook(tradingState, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Order processed", "orderID", order.ID.Hex(), "trades", len(trades))
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// Cancel cancels an existing order
|
||||
func (op *OrderProcessor) Cancel(ctx context.Context, statedb *state.StateDB, tradingState *tradingstate.TradingStateDB, orderID common.Hash) error {
|
||||
op.lock.Lock()
|
||||
defer op.lock.Unlock()
|
||||
|
||||
// Get order from state
|
||||
order := tradingState.GetOrder(orderID)
|
||||
if order == nil {
|
||||
return ErrOrderNotFound
|
||||
}
|
||||
|
||||
// Check if order can be cancelled
|
||||
if order.Status == OrderStatusFilled || order.Status == OrderStatusCancelled {
|
||||
return ErrOrderCannotBeCancelled
|
||||
}
|
||||
|
||||
// Remove from order book
|
||||
if err := op.removeFromOrderBook(tradingState, order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update order status
|
||||
order.Status = OrderStatusCancelled
|
||||
tradingState.UpdateOrder(order)
|
||||
|
||||
log.Debug("Order cancelled", "orderID", orderID.Hex())
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateOrder validates an order
|
||||
func (op *OrderProcessor) validateOrder(order *Order) error {
|
||||
if order.Price == nil || order.Price.Sign() <= 0 {
|
||||
return ErrInvalidPrice
|
||||
}
|
||||
if order.Quantity == nil || order.Quantity.Sign() <= 0 {
|
||||
return ErrInvalidQuantity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkBalance checks if user has sufficient balance
|
||||
func (op *OrderProcessor) checkBalance(statedb *state.StateDB, order *Order) error {
|
||||
// For buy orders, check quote token balance
|
||||
// For sell orders, check base token balance
|
||||
var tokenToCheck common.Address
|
||||
var amountNeeded *big.Int
|
||||
|
||||
if order.Side == Buy {
|
||||
tokenToCheck = order.QuoteToken
|
||||
// Amount needed = price * quantity / 10^18
|
||||
amountNeeded = new(big.Int).Mul(order.Price, order.Quantity)
|
||||
amountNeeded = new(big.Int).Div(amountNeeded, big.NewInt(1e18))
|
||||
} else {
|
||||
tokenToCheck = order.BaseToken
|
||||
amountNeeded = order.Quantity
|
||||
}
|
||||
|
||||
// Check ERC20 balance (simplified)
|
||||
balance := op.getTokenBalance(statedb, tokenToCheck, order.UserAddress)
|
||||
if balance.Cmp(amountNeeded) < 0 {
|
||||
return ErrInsufficientBalance
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTokenBalance gets the token balance for a user
|
||||
func (op *OrderProcessor) getTokenBalance(statedb *state.StateDB, token, user common.Address) *big.Int {
|
||||
// This would call the ERC20 balanceOf function
|
||||
// Simplified implementation
|
||||
return statedb.GetBalance(user).ToBig()
|
||||
}
|
||||
|
||||
// matchOrder matches an order against the order book
|
||||
func (op *OrderProcessor) matchOrder(tradingState *tradingstate.TradingStateDB, order *Order) ([]*Trade, error) {
|
||||
var trades []*Trade
|
||||
|
||||
pairKey := order.PairKey()
|
||||
orderBook := tradingState.GetOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// Match based on order side
|
||||
if order.Side == Buy {
|
||||
trades = op.matchBuyOrder(tradingState, orderBook, order)
|
||||
} else {
|
||||
trades = op.matchSellOrder(tradingState, orderBook, order)
|
||||
}
|
||||
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// matchBuyOrder matches a buy order against sell orders
|
||||
func (op *OrderProcessor) matchBuyOrder(tradingState *tradingstate.TradingStateDB, orderBook *tradingstate.OrderBook, buyOrder *Order) []*Trade {
|
||||
var trades []*Trade
|
||||
|
||||
// Match against asks (sell orders) from lowest to highest price
|
||||
for !buyOrder.IsFilled() {
|
||||
bestAsk := orderBook.GetBestAsk()
|
||||
if bestAsk == nil || bestAsk.Price.Cmp(buyOrder.Price) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
trade := op.executeTrade(buyOrder, bestAsk)
|
||||
trades = append(trades, trade)
|
||||
|
||||
if bestAsk.IsFilled() {
|
||||
orderBook.RemoveAsk(bestAsk.ID)
|
||||
} else {
|
||||
tradingState.UpdateOrder(bestAsk)
|
||||
}
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// matchSellOrder matches a sell order against buy orders
|
||||
func (op *OrderProcessor) matchSellOrder(tradingState *tradingstate.TradingStateDB, orderBook *tradingstate.OrderBook, sellOrder *Order) []*Trade {
|
||||
var trades []*Trade
|
||||
|
||||
// Match against bids (buy orders) from highest to lowest price
|
||||
for !sellOrder.IsFilled() {
|
||||
bestBid := orderBook.GetBestBid()
|
||||
if bestBid == nil || bestBid.Price.Cmp(sellOrder.Price) < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
trade := op.executeTrade(bestBid, sellOrder)
|
||||
trades = append(trades, trade)
|
||||
|
||||
if bestBid.IsFilled() {
|
||||
orderBook.RemoveBid(bestBid.ID)
|
||||
} else {
|
||||
tradingState.UpdateOrder(bestBid)
|
||||
}
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// executeTrade executes a trade between two orders
|
||||
func (op *OrderProcessor) executeTrade(buyOrder, sellOrder *Order) *Trade {
|
||||
// Determine trade quantity
|
||||
buyRemaining := buyOrder.RemainingQuantity()
|
||||
sellRemaining := sellOrder.RemainingQuantity()
|
||||
|
||||
var tradeQuantity *big.Int
|
||||
if buyRemaining.Cmp(sellRemaining) < 0 {
|
||||
tradeQuantity = buyRemaining
|
||||
} else {
|
||||
tradeQuantity = sellRemaining
|
||||
}
|
||||
|
||||
// Use maker's price (the order that was in the book)
|
||||
tradePrice := sellOrder.Price
|
||||
|
||||
// Update filled quantities
|
||||
buyOrder.FilledQuantity = new(big.Int).Add(buyOrder.FilledQuantity, tradeQuantity)
|
||||
sellOrder.FilledQuantity = new(big.Int).Add(sellOrder.FilledQuantity, tradeQuantity)
|
||||
|
||||
// Update order statuses
|
||||
if buyOrder.IsFilled() {
|
||||
buyOrder.Status = OrderStatusFilled
|
||||
} else {
|
||||
buyOrder.Status = OrderStatusPartialFilled
|
||||
}
|
||||
|
||||
if sellOrder.IsFilled() {
|
||||
sellOrder.Status = OrderStatusFilled
|
||||
} else {
|
||||
sellOrder.Status = OrderStatusPartialFilled
|
||||
}
|
||||
|
||||
// Create trade
|
||||
return NewTrade(
|
||||
buyOrder.ID,
|
||||
sellOrder.ID,
|
||||
buyOrder.UserAddress,
|
||||
sellOrder.UserAddress,
|
||||
buyOrder.BaseToken,
|
||||
buyOrder.QuoteToken,
|
||||
tradePrice,
|
||||
tradeQuantity,
|
||||
)
|
||||
}
|
||||
|
||||
// addToOrderBook adds an order to the order book
|
||||
func (op *OrderProcessor) addToOrderBook(tradingState *tradingstate.TradingStateDB, order *Order) error {
|
||||
pairKey := order.PairKey()
|
||||
orderBook := tradingState.GetOrCreateOrderBook(pairKey)
|
||||
|
||||
if order.Side == Buy {
|
||||
orderBook.AddBid(order)
|
||||
} else {
|
||||
orderBook.AddAsk(order)
|
||||
}
|
||||
|
||||
tradingState.SetOrder(order)
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeFromOrderBook removes an order from the order book
|
||||
func (op *OrderProcessor) removeFromOrderBook(tradingState *tradingstate.TradingStateDB, order *Order) error {
|
||||
pairKey := order.PairKey()
|
||||
orderBook := tradingState.GetOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if order.Side == Buy {
|
||||
orderBook.RemoveBid(order.ID)
|
||||
} else {
|
||||
orderBook.RemoveAsk(order.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
140
XDCx/trade.go
Normal file
140
XDCx/trade.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCx
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// TradeStatus represents the status of a trade
|
||||
type TradeStatus uint8
|
||||
|
||||
const (
|
||||
// TradeStatusPending represents a pending trade
|
||||
TradeStatusPending TradeStatus = iota
|
||||
// TradeStatusSettled represents a settled trade
|
||||
TradeStatusSettled
|
||||
// TradeStatusFailed represents a failed trade
|
||||
TradeStatusFailed
|
||||
)
|
||||
|
||||
// Trade represents a matched trade
|
||||
type Trade struct {
|
||||
hash common.Hash
|
||||
MakerOrderID common.Hash `json:"makerOrderId"`
|
||||
TakerOrderID common.Hash `json:"takerOrderId"`
|
||||
Maker common.Address `json:"maker"`
|
||||
Taker common.Address `json:"taker"`
|
||||
BaseToken common.Address `json:"baseToken"`
|
||||
QuoteToken common.Address `json:"quoteToken"`
|
||||
Price *big.Int `json:"price"`
|
||||
Quantity *big.Int `json:"quantity"`
|
||||
Amount *big.Int `json:"amount"`
|
||||
MakerFee *big.Int `json:"makerFee"`
|
||||
TakerFee *big.Int `json:"takerFee"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Status TradeStatus `json:"status"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TxHash common.Hash `json:"txHash"`
|
||||
}
|
||||
|
||||
// NewTrade creates a new trade
|
||||
func NewTrade(
|
||||
makerOrderID, takerOrderID common.Hash,
|
||||
maker, taker common.Address,
|
||||
baseToken, quoteToken common.Address,
|
||||
price, quantity *big.Int,
|
||||
) *Trade {
|
||||
amount := new(big.Int).Mul(price, quantity)
|
||||
amount = new(big.Int).Div(amount, big.NewInt(1e18))
|
||||
|
||||
trade := &Trade{
|
||||
MakerOrderID: makerOrderID,
|
||||
TakerOrderID: takerOrderID,
|
||||
Maker: maker,
|
||||
Taker: taker,
|
||||
BaseToken: baseToken,
|
||||
QuoteToken: quoteToken,
|
||||
Price: new(big.Int).Set(price),
|
||||
Quantity: new(big.Int).Set(quantity),
|
||||
Amount: amount,
|
||||
MakerFee: big.NewInt(0),
|
||||
TakerFee: big.NewInt(0),
|
||||
Status: TradeStatusPending,
|
||||
}
|
||||
trade.hash = trade.ComputeHash()
|
||||
return trade
|
||||
}
|
||||
|
||||
// ComputeHash computes the hash of the trade
|
||||
func (t *Trade) ComputeHash() common.Hash {
|
||||
data := append(t.MakerOrderID.Bytes(), t.TakerOrderID.Bytes()...)
|
||||
data = append(data, t.Maker.Bytes()...)
|
||||
data = append(data, t.Taker.Bytes()...)
|
||||
data = append(data, t.BaseToken.Bytes()...)
|
||||
data = append(data, t.QuoteToken.Bytes()...)
|
||||
data = append(data, common.BigToHash(t.Price).Bytes()...)
|
||||
data = append(data, common.BigToHash(t.Quantity).Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Hash returns the trade hash
|
||||
func (t *Trade) Hash() common.Hash {
|
||||
if t.hash == (common.Hash{}) {
|
||||
t.hash = t.ComputeHash()
|
||||
}
|
||||
return t.hash
|
||||
}
|
||||
|
||||
// SetFees sets the maker and taker fees
|
||||
func (t *Trade) SetFees(makerFee, takerFee *big.Int) {
|
||||
if makerFee != nil {
|
||||
t.MakerFee = new(big.Int).Set(makerFee)
|
||||
}
|
||||
if takerFee != nil {
|
||||
t.TakerFee = new(big.Int).Set(takerFee)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSettled marks the trade as settled
|
||||
func (t *Trade) SetSettled(blockNumber uint64, txHash common.Hash) {
|
||||
t.Status = TradeStatusSettled
|
||||
t.BlockNumber = blockNumber
|
||||
t.TxHash = txHash
|
||||
}
|
||||
|
||||
// SetFailed marks the trade as failed
|
||||
func (t *Trade) SetFailed() {
|
||||
t.Status = TradeStatusFailed
|
||||
}
|
||||
|
||||
// PairKey returns the trading pair key
|
||||
func (t *Trade) PairKey() common.Hash {
|
||||
return GetPairKey(t.BaseToken, t.QuoteToken)
|
||||
}
|
||||
|
||||
// Clone creates a copy of the trade
|
||||
func (t *Trade) Clone() *Trade {
|
||||
return &Trade{
|
||||
hash: t.hash,
|
||||
MakerOrderID: t.MakerOrderID,
|
||||
TakerOrderID: t.TakerOrderID,
|
||||
Maker: t.Maker,
|
||||
Taker: t.Taker,
|
||||
BaseToken: t.BaseToken,
|
||||
QuoteToken: t.QuoteToken,
|
||||
Price: new(big.Int).Set(t.Price),
|
||||
Quantity: new(big.Int).Set(t.Quantity),
|
||||
Amount: new(big.Int).Set(t.Amount),
|
||||
MakerFee: new(big.Int).Set(t.MakerFee),
|
||||
TakerFee: new(big.Int).Set(t.TakerFee),
|
||||
Timestamp: t.Timestamp,
|
||||
Status: t.Status,
|
||||
BlockNumber: t.BlockNumber,
|
||||
TxHash: t.TxHash,
|
||||
}
|
||||
}
|
||||
130
XDCx/tradingstate/database.go
Normal file
130
XDCx/tradingstate/database.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// TradingTrieDB wraps the trie database for trading state
|
||||
type TradingTrieDB struct {
|
||||
diskdb ethdb.Database
|
||||
config *trie.Config
|
||||
}
|
||||
|
||||
// NewTradingTrieDB creates a new trading trie database
|
||||
func NewTradingTrieDB(diskdb ethdb.Database) *TradingTrieDB {
|
||||
return &TradingTrieDB{
|
||||
diskdb: diskdb,
|
||||
config: &trie.Config{},
|
||||
}
|
||||
}
|
||||
|
||||
// DiskDB returns the underlying disk database
|
||||
func (db *TradingTrieDB) DiskDB() ethdb.Database {
|
||||
return db.diskdb
|
||||
}
|
||||
|
||||
// OpenTradingTrie opens a trading state trie
|
||||
func (db *TradingTrieDB) OpenTradingTrie(root common.Hash) (*trie.Trie, error) {
|
||||
id := &trie.ID{
|
||||
StateRoot: root,
|
||||
}
|
||||
return trie.New(id, trie.NewDatabase(db.diskdb, db.config))
|
||||
}
|
||||
|
||||
// CopyTradingTrie copies a trading trie
|
||||
func (db *TradingTrieDB) CopyTradingTrie(t *trie.Trie) *trie.Trie {
|
||||
return t.Copy()
|
||||
}
|
||||
|
||||
// Commit commits all pending writes
|
||||
func (db *TradingTrieDB) Commit(root common.Hash, report bool) error {
|
||||
// Commit to disk
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the database
|
||||
func (db *TradingTrieDB) Close() error {
|
||||
return db.diskdb.Close()
|
||||
}
|
||||
|
||||
// TradingStateCacheConfig holds cache configuration
|
||||
type TradingStateCacheConfig struct {
|
||||
CacheSize int
|
||||
MaxLive int
|
||||
}
|
||||
|
||||
// DefaultTradingStateCacheConfig returns default cache configuration
|
||||
func DefaultTradingStateCacheConfig() *TradingStateCacheConfig {
|
||||
return &TradingStateCacheConfig{
|
||||
CacheSize: 256,
|
||||
MaxLive: 16,
|
||||
}
|
||||
}
|
||||
|
||||
// TradingStateCache provides caching for trading state
|
||||
type TradingStateCache struct {
|
||||
config *TradingStateCacheConfig
|
||||
db *TradingTrieDB
|
||||
states map[common.Hash]*TradingStateDB
|
||||
}
|
||||
|
||||
// NewTradingStateCache creates a new trading state cache
|
||||
func NewTradingStateCache(db *TradingTrieDB, config *TradingStateCacheConfig) *TradingStateCache {
|
||||
if config == nil {
|
||||
config = DefaultTradingStateCacheConfig()
|
||||
}
|
||||
return &TradingStateCache{
|
||||
config: config,
|
||||
db: db,
|
||||
states: make(map[common.Hash]*TradingStateDB),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a trading state from cache or creates new one
|
||||
func (c *TradingStateCache) Get(root common.Hash) (*TradingStateDB, error) {
|
||||
if state, ok := c.states[root]; ok {
|
||||
return state.Copy(), nil
|
||||
}
|
||||
return New(root, c)
|
||||
}
|
||||
|
||||
// Put stores a trading state in cache
|
||||
func (c *TradingStateCache) Put(root common.Hash, state *TradingStateDB) {
|
||||
// Limit cache size
|
||||
if len(c.states) >= c.config.CacheSize {
|
||||
// Remove oldest entry (simplified)
|
||||
for k := range c.states {
|
||||
delete(c.states, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
c.states[root] = state
|
||||
}
|
||||
|
||||
// Remove removes a state from cache
|
||||
func (c *TradingStateCache) Remove(root common.Hash) {
|
||||
delete(c.states, root)
|
||||
}
|
||||
|
||||
// Clear clears the entire cache
|
||||
func (c *TradingStateCache) Clear() {
|
||||
c.states = make(map[common.Hash]*TradingStateDB)
|
||||
}
|
||||
|
||||
// OpenTrie implements Database interface
|
||||
func (c *TradingStateCache) OpenTrie(root common.Hash) (Trie, error) {
|
||||
return c.db.OpenTradingTrie(root)
|
||||
}
|
||||
|
||||
// CopyTrie implements Database interface
|
||||
func (c *TradingStateCache) CopyTrie(t Trie) Trie {
|
||||
if tt, ok := t.(*trie.Trie); ok {
|
||||
return c.db.CopyTradingTrie(tt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
216
XDCx/tradingstate/dump.go
Normal file
216
XDCx/tradingstate/dump.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// DumpOrder represents an order for JSON export
|
||||
type DumpOrder struct {
|
||||
ID string `json:"id"`
|
||||
UserAddress string `json:"userAddress"`
|
||||
ExchangeAddress string `json:"exchangeAddress"`
|
||||
BaseToken string `json:"baseToken"`
|
||||
QuoteToken string `json:"quoteToken"`
|
||||
Side string `json:"side"`
|
||||
Type string `json:"type"`
|
||||
Price string `json:"price"`
|
||||
Quantity string `json:"quantity"`
|
||||
FilledQuantity string `json:"filledQuantity"`
|
||||
Status string `json:"status"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// DumpOrderBook represents an order book for JSON export
|
||||
type DumpOrderBook struct {
|
||||
PairKey string `json:"pairKey"`
|
||||
Bids []*DumpOrder `json:"bids"`
|
||||
Asks []*DumpOrder `json:"asks"`
|
||||
}
|
||||
|
||||
// DumpAccount represents an account for JSON export
|
||||
type DumpAccount struct {
|
||||
Address string `json:"address"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Balance string `json:"balance"`
|
||||
}
|
||||
|
||||
// Dump represents the state dump
|
||||
type Dump struct {
|
||||
Root string `json:"root"`
|
||||
Accounts []*DumpAccount `json:"accounts"`
|
||||
OrderBooks []*DumpOrderBook `json:"orderBooks"`
|
||||
Orders []*DumpOrder `json:"orders"`
|
||||
}
|
||||
|
||||
// RawDump returns a raw state dump
|
||||
func (s *TradingStateDB) RawDump() *Dump {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
dump := &Dump{
|
||||
Root: s.trie.Hash().Hex(),
|
||||
Accounts: make([]*DumpAccount, 0),
|
||||
OrderBooks: make([]*DumpOrderBook, 0),
|
||||
Orders: make([]*DumpOrder, 0),
|
||||
}
|
||||
|
||||
// Dump accounts
|
||||
for _, obj := range s.stateObjects {
|
||||
dump.Accounts = append(dump.Accounts, &DumpAccount{
|
||||
Address: obj.key.Hex(),
|
||||
Nonce: obj.data.Nonce,
|
||||
Balance: obj.data.Balance.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// Dump order books
|
||||
for pairKey, ob := range s.orderBooks {
|
||||
dumpOB := &DumpOrderBook{
|
||||
PairKey: pairKey.Hex(),
|
||||
Bids: make([]*DumpOrder, 0),
|
||||
Asks: make([]*DumpOrder, 0),
|
||||
}
|
||||
|
||||
for _, bid := range ob.Bids {
|
||||
dumpOB.Bids = append(dumpOB.Bids, dumpOrderState(bid))
|
||||
}
|
||||
|
||||
for _, ask := range ob.Asks {
|
||||
dumpOB.Asks = append(dumpOB.Asks, dumpOrderState(ask))
|
||||
}
|
||||
|
||||
dump.OrderBooks = append(dump.OrderBooks, dumpOB)
|
||||
}
|
||||
|
||||
// Dump orders
|
||||
for _, order := range s.orders {
|
||||
dump.Orders = append(dump.Orders, dumpOrderState(order))
|
||||
}
|
||||
|
||||
return dump
|
||||
}
|
||||
|
||||
// Dump returns a JSON encoded state dump
|
||||
func (s *TradingStateDB) Dump() ([]byte, error) {
|
||||
dump := s.RawDump()
|
||||
return json.MarshalIndent(dump, "", " ")
|
||||
}
|
||||
|
||||
// dumpOrderState converts an order state to a dump order
|
||||
func dumpOrderState(order *OrderState) *DumpOrder {
|
||||
sideStr := "buy"
|
||||
if order.Side == 1 {
|
||||
sideStr = "sell"
|
||||
}
|
||||
|
||||
typeStr := "limit"
|
||||
if order.Type == 1 {
|
||||
typeStr = "market"
|
||||
}
|
||||
|
||||
statusStr := "new"
|
||||
switch order.Status {
|
||||
case 1:
|
||||
statusStr = "partial"
|
||||
case 2:
|
||||
statusStr = "filled"
|
||||
case 3:
|
||||
statusStr = "cancelled"
|
||||
case 4:
|
||||
statusStr = "rejected"
|
||||
}
|
||||
|
||||
return &DumpOrder{
|
||||
ID: order.ID.Hex(),
|
||||
UserAddress: order.UserAddress.Hex(),
|
||||
ExchangeAddress: order.ExchangeAddress.Hex(),
|
||||
BaseToken: order.BaseToken.Hex(),
|
||||
QuoteToken: order.QuoteToken.Hex(),
|
||||
Side: sideStr,
|
||||
Type: typeStr,
|
||||
Price: order.Price.String(),
|
||||
Quantity: order.Quantity.String(),
|
||||
FilledQuantity: order.FilledQuantity.String(),
|
||||
Status: statusStr,
|
||||
Nonce: order.Nonce,
|
||||
Timestamp: order.Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTradingPairs returns all trading pairs
|
||||
func (s *TradingStateDB) GetTradingPairs() []common.Hash {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
pairs := make([]common.Hash, 0, len(s.orderBooks))
|
||||
for pairKey := range s.orderBooks {
|
||||
pairs = append(pairs, pairKey)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
// GetAllOrders returns all orders
|
||||
func (s *TradingStateDB) GetAllOrders() []*OrderState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
orders := make([]*OrderState, 0, len(s.orders))
|
||||
for _, order := range s.orders {
|
||||
orders = append(orders, order.Copy())
|
||||
}
|
||||
return orders
|
||||
}
|
||||
|
||||
// GetOrdersByUser returns all orders for a specific user
|
||||
func (s *TradingStateDB) GetOrdersByUser(userAddress common.Address) []*OrderState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
orders := make([]*OrderState, 0)
|
||||
for _, order := range s.orders {
|
||||
if order.UserAddress == userAddress {
|
||||
orders = append(orders, order.Copy())
|
||||
}
|
||||
}
|
||||
return orders
|
||||
}
|
||||
|
||||
// GetOrdersByPair returns all orders for a specific trading pair
|
||||
func (s *TradingStateDB) GetOrdersByPair(pairKey common.Hash) []*OrderState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
ob := s.orderBooks[pairKey]
|
||||
if ob == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
orders := make([]*OrderState, 0, len(ob.Bids)+len(ob.Asks))
|
||||
for _, bid := range ob.Bids {
|
||||
orders = append(orders, bid.Copy())
|
||||
}
|
||||
for _, ask := range ob.Asks {
|
||||
orders = append(orders, ask.Copy())
|
||||
}
|
||||
return orders
|
||||
}
|
||||
|
||||
// GetVolume returns the total volume for a trading pair
|
||||
func (s *TradingStateDB) GetVolume(pairKey common.Hash) (*big.Int, *big.Int) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
ob := s.orderBooks[pairKey]
|
||||
if ob == nil {
|
||||
return big.NewInt(0), big.NewInt(0)
|
||||
}
|
||||
|
||||
return ob.GetBidVolume(), ob.GetAskVolume()
|
||||
}
|
||||
196
XDCx/tradingstate/journal.go
Normal file
196
XDCx/tradingstate/journal.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// journalEntry is a modification entry in the trading state change journal
|
||||
type journalEntry interface {
|
||||
// revert undoes the changes introduced by this journal entry
|
||||
revert(*TradingStateDB)
|
||||
|
||||
// dirtied returns the hash of the object modified by this journal entry
|
||||
dirtied() *common.Hash
|
||||
}
|
||||
|
||||
// journal contains the list of state modifications applied since the last state commit
|
||||
type journal struct {
|
||||
entries []journalEntry
|
||||
dirties map[common.Hash]int
|
||||
}
|
||||
|
||||
// newJournal creates a new initialized journal
|
||||
func newJournal() *journal {
|
||||
return &journal{
|
||||
dirties: make(map[common.Hash]int),
|
||||
}
|
||||
}
|
||||
|
||||
// append adds a new journal entry
|
||||
func (j *journal) append(entry journalEntry) {
|
||||
j.entries = append(j.entries, entry)
|
||||
if hash := entry.dirtied(); hash != nil {
|
||||
j.dirties[*hash]++
|
||||
}
|
||||
}
|
||||
|
||||
// revert undoes a batch of journal entries
|
||||
func (j *journal) revert(statedb *TradingStateDB, snapshot int) {
|
||||
for i := len(j.entries) - 1; i >= snapshot; i-- {
|
||||
j.entries[i].revert(statedb)
|
||||
|
||||
if hash := j.entries[i].dirtied(); hash != nil {
|
||||
if j.dirties[*hash]--; j.dirties[*hash] == 0 {
|
||||
delete(j.dirties, *hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
j.entries = j.entries[:snapshot]
|
||||
}
|
||||
|
||||
// dirty returns the dirty count for a specific object
|
||||
func (j *journal) dirty(hash common.Hash) int {
|
||||
return j.dirties[hash]
|
||||
}
|
||||
|
||||
// length returns the current number of entries in the journal
|
||||
func (j *journal) length() int {
|
||||
return len(j.entries)
|
||||
}
|
||||
|
||||
// orderChange represents a change to an order
|
||||
type orderChange struct {
|
||||
orderID common.Hash
|
||||
prev *OrderState
|
||||
}
|
||||
|
||||
func (ch orderChange) revert(s *TradingStateDB) {
|
||||
if ch.prev == nil {
|
||||
delete(s.orders, ch.orderID)
|
||||
} else {
|
||||
s.orders[ch.orderID] = ch.prev
|
||||
}
|
||||
}
|
||||
|
||||
func (ch orderChange) dirtied() *common.Hash {
|
||||
return &ch.orderID
|
||||
}
|
||||
|
||||
// orderBookChange represents a change to an order book
|
||||
type orderBookChange struct {
|
||||
pairKey common.Hash
|
||||
prev *OrderBook
|
||||
}
|
||||
|
||||
func (ch orderBookChange) revert(s *TradingStateDB) {
|
||||
if ch.prev == nil {
|
||||
delete(s.orderBooks, ch.pairKey)
|
||||
} else {
|
||||
s.orderBooks[ch.pairKey] = ch.prev
|
||||
}
|
||||
}
|
||||
|
||||
func (ch orderBookChange) dirtied() *common.Hash {
|
||||
return &ch.pairKey
|
||||
}
|
||||
|
||||
// bidAddChange represents adding a bid
|
||||
type bidAddChange struct {
|
||||
pairKey common.Hash
|
||||
orderID common.Hash
|
||||
}
|
||||
|
||||
func (ch bidAddChange) revert(s *TradingStateDB) {
|
||||
if ob, exists := s.orderBooks[ch.pairKey]; exists {
|
||||
ob.RemoveBid(ch.orderID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch bidAddChange) dirtied() *common.Hash {
|
||||
return &ch.pairKey
|
||||
}
|
||||
|
||||
// askAddChange represents adding an ask
|
||||
type askAddChange struct {
|
||||
pairKey common.Hash
|
||||
orderID common.Hash
|
||||
}
|
||||
|
||||
func (ch askAddChange) revert(s *TradingStateDB) {
|
||||
if ob, exists := s.orderBooks[ch.pairKey]; exists {
|
||||
ob.RemoveAsk(ch.orderID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch askAddChange) dirtied() *common.Hash {
|
||||
return &ch.pairKey
|
||||
}
|
||||
|
||||
// bidRemoveChange represents removing a bid
|
||||
type bidRemoveChange struct {
|
||||
pairKey common.Hash
|
||||
order *OrderState
|
||||
}
|
||||
|
||||
func (ch bidRemoveChange) revert(s *TradingStateDB) {
|
||||
if ob, exists := s.orderBooks[ch.pairKey]; exists {
|
||||
ob.AddBid(ch.order)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch bidRemoveChange) dirtied() *common.Hash {
|
||||
return &ch.pairKey
|
||||
}
|
||||
|
||||
// askRemoveChange represents removing an ask
|
||||
type askRemoveChange struct {
|
||||
pairKey common.Hash
|
||||
order *OrderState
|
||||
}
|
||||
|
||||
func (ch askRemoveChange) revert(s *TradingStateDB) {
|
||||
if ob, exists := s.orderBooks[ch.pairKey]; exists {
|
||||
ob.AddAsk(ch.order)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch askRemoveChange) dirtied() *common.Hash {
|
||||
return &ch.pairKey
|
||||
}
|
||||
|
||||
// balanceChange represents a balance change
|
||||
type balanceChange struct {
|
||||
key common.Hash
|
||||
amount *big.Int
|
||||
}
|
||||
|
||||
func (ch balanceChange) revert(s *TradingStateDB) {
|
||||
if obj, exists := s.stateObjects[ch.key]; exists {
|
||||
obj.SubBalance(ch.amount)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch balanceChange) dirtied() *common.Hash {
|
||||
return &ch.key
|
||||
}
|
||||
|
||||
// nonceChange represents a nonce change
|
||||
type nonceChange struct {
|
||||
key common.Hash
|
||||
prev uint64
|
||||
}
|
||||
|
||||
func (ch nonceChange) revert(s *TradingStateDB) {
|
||||
if obj, exists := s.stateObjects[ch.key]; exists {
|
||||
obj.SetNonce(ch.prev)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch nonceChange) dirtied() *common.Hash {
|
||||
return &ch.key
|
||||
}
|
||||
242
XDCx/tradingstate/orderbook.go
Normal file
242
XDCx/tradingstate/orderbook.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// Order interface for XDCx orders
|
||||
type Order interface {
|
||||
GetID() common.Hash
|
||||
GetPrice() *big.Int
|
||||
GetQuantity() *big.Int
|
||||
GetFilledQuantity() *big.Int
|
||||
GetSide() uint8
|
||||
RemainingQuantity() *big.Int
|
||||
IsFilled() bool
|
||||
}
|
||||
|
||||
// OrderBook represents a trading pair order book
|
||||
type OrderBook struct {
|
||||
PairKey common.Hash
|
||||
Bids []*OrderState // Buy orders sorted by price desc
|
||||
Asks []*OrderState // Sell orders sorted by price asc
|
||||
|
||||
bidIndex map[common.Hash]int
|
||||
askIndex map[common.Hash]int
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewOrderBook creates a new order book
|
||||
func NewOrderBook(pairKey common.Hash) *OrderBook {
|
||||
return &OrderBook{
|
||||
PairKey: pairKey,
|
||||
Bids: make([]*OrderState, 0),
|
||||
Asks: make([]*OrderState, 0),
|
||||
bidIndex: make(map[common.Hash]int),
|
||||
askIndex: make(map[common.Hash]int),
|
||||
}
|
||||
}
|
||||
|
||||
// AddBid adds a buy order to the order book
|
||||
func (ob *OrderBook) AddBid(order interface{}) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
var orderState *OrderState
|
||||
switch o := order.(type) {
|
||||
case *OrderState:
|
||||
orderState = o
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
ob.Bids = append(ob.Bids, orderState)
|
||||
ob.sortBids()
|
||||
ob.rebuildBidIndex()
|
||||
}
|
||||
|
||||
// AddAsk adds a sell order to the order book
|
||||
func (ob *OrderBook) AddAsk(order interface{}) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
var orderState *OrderState
|
||||
switch o := order.(type) {
|
||||
case *OrderState:
|
||||
orderState = o
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
ob.Asks = append(ob.Asks, orderState)
|
||||
ob.sortAsks()
|
||||
ob.rebuildAskIndex()
|
||||
}
|
||||
|
||||
// RemoveBid removes a buy order from the order book
|
||||
func (ob *OrderBook) RemoveBid(orderID common.Hash) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
if idx, exists := ob.bidIndex[orderID]; exists {
|
||||
ob.Bids = append(ob.Bids[:idx], ob.Bids[idx+1:]...)
|
||||
ob.rebuildBidIndex()
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAsk removes a sell order from the order book
|
||||
func (ob *OrderBook) RemoveAsk(orderID common.Hash) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
if idx, exists := ob.askIndex[orderID]; exists {
|
||||
ob.Asks = append(ob.Asks[:idx], ob.Asks[idx+1:]...)
|
||||
ob.rebuildAskIndex()
|
||||
}
|
||||
}
|
||||
|
||||
// GetBestBid returns the best bid order
|
||||
func (ob *OrderBook) GetBestBid() *OrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if len(ob.Bids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ob.Bids[0]
|
||||
}
|
||||
|
||||
// GetBestAsk returns the best ask order
|
||||
func (ob *OrderBook) GetBestAsk() *OrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if len(ob.Asks) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ob.Asks[0]
|
||||
}
|
||||
|
||||
// GetBid returns a specific bid order
|
||||
func (ob *OrderBook) GetBid(orderID common.Hash) *OrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if idx, exists := ob.bidIndex[orderID]; exists {
|
||||
return ob.Bids[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAsk returns a specific ask order
|
||||
func (ob *OrderBook) GetAsk(orderID common.Hash) *OrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if idx, exists := ob.askIndex[orderID]; exists {
|
||||
return ob.Asks[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BidCount returns the number of bids
|
||||
func (ob *OrderBook) BidCount() int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
return len(ob.Bids)
|
||||
}
|
||||
|
||||
// AskCount returns the number of asks
|
||||
func (ob *OrderBook) AskCount() int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
return len(ob.Asks)
|
||||
}
|
||||
|
||||
// GetBidVolume returns total bid volume
|
||||
func (ob *OrderBook) GetBidVolume() *big.Int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
total := big.NewInt(0)
|
||||
for _, bid := range ob.Bids {
|
||||
total = new(big.Int).Add(total, bid.RemainingQuantity())
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// GetAskVolume returns total ask volume
|
||||
func (ob *OrderBook) GetAskVolume() *big.Int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
total := big.NewInt(0)
|
||||
for _, ask := range ob.Asks {
|
||||
total = new(big.Int).Add(total, ask.RemainingQuantity())
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// Copy creates a copy of the order book
|
||||
func (ob *OrderBook) Copy() *OrderBook {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
copy := &OrderBook{
|
||||
PairKey: ob.PairKey,
|
||||
Bids: make([]*OrderState, len(ob.Bids)),
|
||||
Asks: make([]*OrderState, len(ob.Asks)),
|
||||
bidIndex: make(map[common.Hash]int),
|
||||
askIndex: make(map[common.Hash]int),
|
||||
}
|
||||
|
||||
for i, bid := range ob.Bids {
|
||||
copy.Bids[i] = bid.Copy()
|
||||
copy.bidIndex[bid.ID] = i
|
||||
}
|
||||
|
||||
for i, ask := range ob.Asks {
|
||||
copy.Asks[i] = ask.Copy()
|
||||
copy.askIndex[ask.ID] = i
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
// sortBids sorts bids by price descending
|
||||
func (ob *OrderBook) sortBids() {
|
||||
sort.Slice(ob.Bids, func(i, j int) bool {
|
||||
return ob.Bids[i].Price.Cmp(ob.Bids[j].Price) > 0
|
||||
})
|
||||
}
|
||||
|
||||
// sortAsks sorts asks by price ascending
|
||||
func (ob *OrderBook) sortAsks() {
|
||||
sort.Slice(ob.Asks, func(i, j int) bool {
|
||||
return ob.Asks[i].Price.Cmp(ob.Asks[j].Price) < 0
|
||||
})
|
||||
}
|
||||
|
||||
// rebuildBidIndex rebuilds the bid index
|
||||
func (ob *OrderBook) rebuildBidIndex() {
|
||||
ob.bidIndex = make(map[common.Hash]int)
|
||||
for i, bid := range ob.Bids {
|
||||
ob.bidIndex[bid.ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildAskIndex rebuilds the ask index
|
||||
func (ob *OrderBook) rebuildAskIndex() {
|
||||
ob.askIndex = make(map[common.Hash]int)
|
||||
for i, ask := range ob.Asks {
|
||||
ob.askIndex[ask.ID] = i
|
||||
}
|
||||
}
|
||||
177
XDCx/tradingstate/state_object.go
Normal file
177
XDCx/tradingstate/state_object.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// stateObject represents a trading state object
|
||||
type stateObject struct {
|
||||
key common.Hash
|
||||
data StateData
|
||||
db *TradingStateDB
|
||||
trie Trie
|
||||
dirty bool
|
||||
deleted bool
|
||||
}
|
||||
|
||||
// StateData holds the trading state data
|
||||
type StateData struct {
|
||||
Nonce uint64
|
||||
Balance *big.Int
|
||||
Root common.Hash // merkle root of the state trie
|
||||
CodeHash []byte
|
||||
}
|
||||
|
||||
// newObject creates a new state object
|
||||
func newObject(db *TradingStateDB, key common.Hash, data StateData) *stateObject {
|
||||
return &stateObject{
|
||||
key: key,
|
||||
data: data,
|
||||
db: db,
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNonce returns the nonce
|
||||
func (s *stateObject) GetNonce() uint64 {
|
||||
return s.data.Nonce
|
||||
}
|
||||
|
||||
// SetNonce sets the nonce
|
||||
func (s *stateObject) SetNonce(nonce uint64) {
|
||||
s.data.Nonce = nonce
|
||||
s.dirty = true
|
||||
}
|
||||
|
||||
// GetBalance returns the balance
|
||||
func (s *stateObject) GetBalance() *big.Int {
|
||||
return s.data.Balance
|
||||
}
|
||||
|
||||
// SetBalance sets the balance
|
||||
func (s *stateObject) SetBalance(balance *big.Int) {
|
||||
s.data.Balance = new(big.Int).Set(balance)
|
||||
s.dirty = true
|
||||
}
|
||||
|
||||
// AddBalance adds amount to balance
|
||||
func (s *stateObject) AddBalance(amount *big.Int) {
|
||||
if amount.Sign() == 0 {
|
||||
return
|
||||
}
|
||||
s.SetBalance(new(big.Int).Add(s.GetBalance(), amount))
|
||||
}
|
||||
|
||||
// SubBalance subtracts amount from balance
|
||||
func (s *stateObject) SubBalance(amount *big.Int) {
|
||||
if amount.Sign() == 0 {
|
||||
return
|
||||
}
|
||||
s.SetBalance(new(big.Int).Sub(s.GetBalance(), amount))
|
||||
}
|
||||
|
||||
// deepCopy creates a deep copy of the state object
|
||||
func (s *stateObject) deepCopy() *stateObject {
|
||||
return &stateObject{
|
||||
key: s.key,
|
||||
data: s.data,
|
||||
db: s.db,
|
||||
trie: s.trie,
|
||||
dirty: s.dirty,
|
||||
deleted: s.deleted,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeRLP implements rlp.Encoder
|
||||
func (s *stateObject) EncodeRLP() ([]byte, error) {
|
||||
return rlp.EncodeToBytes(&s.data)
|
||||
}
|
||||
|
||||
// OrderState represents an order in the state
|
||||
type OrderState struct {
|
||||
ID common.Hash
|
||||
UserAddress common.Address
|
||||
ExchangeAddress common.Address
|
||||
BaseToken common.Address
|
||||
QuoteToken common.Address
|
||||
Side uint8
|
||||
Type uint8
|
||||
Price *big.Int
|
||||
Quantity *big.Int
|
||||
FilledQuantity *big.Int
|
||||
Status uint8
|
||||
Nonce uint64
|
||||
Timestamp uint64
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// NewOrderState creates a new order state
|
||||
func NewOrderState(
|
||||
id common.Hash,
|
||||
userAddress common.Address,
|
||||
baseToken, quoteToken common.Address,
|
||||
side, orderType uint8,
|
||||
price, quantity *big.Int,
|
||||
) *OrderState {
|
||||
return &OrderState{
|
||||
ID: id,
|
||||
UserAddress: userAddress,
|
||||
BaseToken: baseToken,
|
||||
QuoteToken: quoteToken,
|
||||
Side: side,
|
||||
Type: orderType,
|
||||
Price: new(big.Int).Set(price),
|
||||
Quantity: new(big.Int).Set(quantity),
|
||||
FilledQuantity: big.NewInt(0),
|
||||
Status: 0, // New
|
||||
}
|
||||
}
|
||||
|
||||
// Hash computes the order hash
|
||||
func (o *OrderState) Hash() common.Hash {
|
||||
data := append(o.UserAddress.Bytes(), o.BaseToken.Bytes()...)
|
||||
data = append(data, o.QuoteToken.Bytes()...)
|
||||
data = append(data, byte(o.Side))
|
||||
data = append(data, byte(o.Type))
|
||||
data = append(data, common.BigToHash(o.Price).Bytes()...)
|
||||
data = append(data, common.BigToHash(o.Quantity).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(o.Nonce))).Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Copy creates a copy of the order state
|
||||
func (o *OrderState) Copy() *OrderState {
|
||||
return &OrderState{
|
||||
ID: o.ID,
|
||||
UserAddress: o.UserAddress,
|
||||
ExchangeAddress: o.ExchangeAddress,
|
||||
BaseToken: o.BaseToken,
|
||||
QuoteToken: o.QuoteToken,
|
||||
Side: o.Side,
|
||||
Type: o.Type,
|
||||
Price: new(big.Int).Set(o.Price),
|
||||
Quantity: new(big.Int).Set(o.Quantity),
|
||||
FilledQuantity: new(big.Int).Set(o.FilledQuantity),
|
||||
Status: o.Status,
|
||||
Nonce: o.Nonce,
|
||||
Timestamp: o.Timestamp,
|
||||
Signature: append([]byte{}, o.Signature...),
|
||||
}
|
||||
}
|
||||
|
||||
// RemainingQuantity returns the remaining quantity
|
||||
func (o *OrderState) RemainingQuantity() *big.Int {
|
||||
return new(big.Int).Sub(o.Quantity, o.FilledQuantity)
|
||||
}
|
||||
|
||||
// IsFilled returns whether the order is filled
|
||||
func (o *OrderState) IsFilled() bool {
|
||||
return o.FilledQuantity.Cmp(o.Quantity) >= 0
|
||||
}
|
||||
320
XDCx/tradingstate/statedb.go
Normal file
320
XDCx/tradingstate/statedb.go
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
// Package tradingstate provides trading state management for XDCx
|
||||
package tradingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// TradingStateDB represents the trading state database
|
||||
type TradingStateDB struct {
|
||||
db Database
|
||||
trie Trie
|
||||
|
||||
// Cached objects
|
||||
stateObjects map[common.Hash]*stateObject
|
||||
stateObjectsDirty map[common.Hash]struct{}
|
||||
|
||||
// Order books cache
|
||||
orderBooks map[common.Hash]*OrderBook
|
||||
orderBooksDirty map[common.Hash]struct{}
|
||||
|
||||
// Orders cache
|
||||
orders map[common.Hash]*OrderState
|
||||
ordersDirty map[common.Hash]struct{}
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// New creates a new trading state database
|
||||
func New(root common.Hash, db Database) (*TradingStateDB, error) {
|
||||
tr, err := db.OpenTrie(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TradingStateDB{
|
||||
db: db,
|
||||
trie: tr,
|
||||
stateObjects: make(map[common.Hash]*stateObject),
|
||||
stateObjectsDirty: make(map[common.Hash]struct{}),
|
||||
orderBooks: make(map[common.Hash]*OrderBook),
|
||||
orderBooksDirty: make(map[common.Hash]struct{}),
|
||||
orders: make(map[common.Hash]*OrderState),
|
||||
ordersDirty: make(map[common.Hash]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetOrder returns an order by ID
|
||||
func (s *TradingStateDB) GetOrder(orderID common.Hash) *OrderState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if order, exists := s.orders[orderID]; exists {
|
||||
return order
|
||||
}
|
||||
|
||||
// Load from trie
|
||||
order := s.loadOrder(orderID)
|
||||
if order != nil {
|
||||
s.orders[orderID] = order
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
// SetOrder sets an order in the state
|
||||
func (s *TradingStateDB) SetOrder(order interface{}) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Convert order interface to OrderState
|
||||
orderState := &OrderState{}
|
||||
// Implementation depends on order type
|
||||
s.orders[orderState.ID] = orderState
|
||||
s.ordersDirty[orderState.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// UpdateOrder updates an order in the state
|
||||
func (s *TradingStateDB) UpdateOrder(order interface{}) {
|
||||
s.SetOrder(order)
|
||||
}
|
||||
|
||||
// DeleteOrder deletes an order from the state
|
||||
func (s *TradingStateDB) DeleteOrder(orderID common.Hash) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.orders, orderID)
|
||||
s.ordersDirty[orderID] = struct{}{}
|
||||
}
|
||||
|
||||
// GetOrderBook returns an order book by pair key
|
||||
func (s *TradingStateDB) GetOrderBook(pairKey common.Hash) *OrderBook {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if ob, exists := s.orderBooks[pairKey]; exists {
|
||||
return ob
|
||||
}
|
||||
|
||||
// Load from trie
|
||||
ob := s.loadOrderBook(pairKey)
|
||||
if ob != nil {
|
||||
s.orderBooks[pairKey] = ob
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
// GetOrCreateOrderBook returns or creates an order book
|
||||
func (s *TradingStateDB) GetOrCreateOrderBook(pairKey common.Hash) *OrderBook {
|
||||
ob := s.GetOrderBook(pairKey)
|
||||
if ob == nil {
|
||||
s.lock.Lock()
|
||||
ob = NewOrderBook(pairKey)
|
||||
s.orderBooks[pairKey] = ob
|
||||
s.orderBooksDirty[pairKey] = struct{}{}
|
||||
s.lock.Unlock()
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
// SetOrderBook sets an order book in the state
|
||||
func (s *TradingStateDB) SetOrderBook(pairKey common.Hash, ob *OrderBook) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.orderBooks[pairKey] = ob
|
||||
s.orderBooksDirty[pairKey] = struct{}{}
|
||||
}
|
||||
|
||||
// GetExchangeAddress returns the exchange address for a relayer
|
||||
func (s *TradingStateDB) GetExchangeAddress(relayer common.Address) common.Address {
|
||||
// Implementation
|
||||
return relayer
|
||||
}
|
||||
|
||||
// GetRelayerFee returns the fee for a relayer
|
||||
func (s *TradingStateDB) GetRelayerFee(relayer common.Address) *big.Int {
|
||||
// Implementation
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// Commit commits all changes to the underlying database
|
||||
func (s *TradingStateDB) Commit() (common.Hash, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Commit dirty objects
|
||||
for key := range s.stateObjectsDirty {
|
||||
if obj, exists := s.stateObjects[key]; exists {
|
||||
if err := s.commitStateObject(obj); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.stateObjectsDirty, key)
|
||||
}
|
||||
|
||||
// Commit dirty order books
|
||||
for pairKey := range s.orderBooksDirty {
|
||||
if ob, exists := s.orderBooks[pairKey]; exists {
|
||||
if err := s.commitOrderBook(ob); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.orderBooksDirty, pairKey)
|
||||
}
|
||||
|
||||
// Commit dirty orders
|
||||
for orderID := range s.ordersDirty {
|
||||
if order, exists := s.orders[orderID]; exists {
|
||||
if err := s.commitOrder(order); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.ordersDirty, orderID)
|
||||
}
|
||||
|
||||
// Commit trie
|
||||
root, err := s.trie.Commit(nil)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
log.Debug("Trading state committed", "root", root.Hex())
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// Copy creates a copy of the trading state
|
||||
func (s *TradingStateDB) Copy() *TradingStateDB {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
state := &TradingStateDB{
|
||||
db: s.db,
|
||||
trie: s.db.CopyTrie(s.trie),
|
||||
stateObjects: make(map[common.Hash]*stateObject),
|
||||
stateObjectsDirty: make(map[common.Hash]struct{}),
|
||||
orderBooks: make(map[common.Hash]*OrderBook),
|
||||
orderBooksDirty: make(map[common.Hash]struct{}),
|
||||
orders: make(map[common.Hash]*OrderState),
|
||||
ordersDirty: make(map[common.Hash]struct{}),
|
||||
}
|
||||
|
||||
// Copy state objects
|
||||
for key, obj := range s.stateObjects {
|
||||
state.stateObjects[key] = obj.deepCopy()
|
||||
}
|
||||
for key := range s.stateObjectsDirty {
|
||||
state.stateObjectsDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Copy order books
|
||||
for key, ob := range s.orderBooks {
|
||||
state.orderBooks[key] = ob.Copy()
|
||||
}
|
||||
for key := range s.orderBooksDirty {
|
||||
state.orderBooksDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Copy orders
|
||||
for key, order := range s.orders {
|
||||
state.orders[key] = order.Copy()
|
||||
}
|
||||
for key := range s.ordersDirty {
|
||||
state.ordersDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// loadOrder loads an order from the trie
|
||||
func (s *TradingStateDB) loadOrder(orderID common.Hash) *OrderState {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadOrderBook loads an order book from the trie
|
||||
func (s *TradingStateDB) loadOrderBook(pairKey common.Hash) *OrderBook {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitStateObject commits a state object
|
||||
func (s *TradingStateDB) commitStateObject(obj *stateObject) error {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitOrderBook commits an order book
|
||||
func (s *TradingStateDB) commitOrderBook(ob *OrderBook) error {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitOrder commits an order
|
||||
func (s *TradingStateDB) commitOrder(order *OrderState) error {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// Database wraps access to tries and contract code
|
||||
type Database interface {
|
||||
// OpenTrie opens the main trading state trie
|
||||
OpenTrie(root common.Hash) (Trie, error)
|
||||
|
||||
// CopyTrie returns an independent copy of the given trie
|
||||
CopyTrie(Trie) Trie
|
||||
}
|
||||
|
||||
// Trie is a XDCx Merkle Patricia trie
|
||||
type Trie interface {
|
||||
// GetKey returns the sha3 preimage of a hashed key
|
||||
GetKey([]byte) []byte
|
||||
|
||||
// TryGet returns the value for key stored in the trie
|
||||
TryGet(key []byte) ([]byte, error)
|
||||
|
||||
// TryUpdate associates key with value in the trie
|
||||
TryUpdate(key, value []byte) error
|
||||
|
||||
// TryDelete removes any existing value for key from the trie
|
||||
TryDelete(key []byte) error
|
||||
|
||||
// Hash returns the root hash of the trie
|
||||
Hash() common.Hash
|
||||
|
||||
// Commit writes all nodes to the trie's database
|
||||
Commit(onleaf trie.LeafCallback) (common.Hash, error)
|
||||
|
||||
// NodeIterator returns an iterator that returns nodes of the trie
|
||||
NodeIterator(startKey []byte) trie.NodeIterator
|
||||
}
|
||||
|
||||
// NewDatabase creates a new trading state database
|
||||
func NewDatabase(db ethdb.Database) Database {
|
||||
return &cachingDB{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type cachingDB struct {
|
||||
db ethdb.Database
|
||||
}
|
||||
|
||||
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
|
||||
// Implementation
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *cachingDB) CopyTrie(t Trie) Trie {
|
||||
// Implementation
|
||||
return nil
|
||||
}
|
||||
254
XDCxlending/XDCxlending.go
Normal file
254
XDCxlending/XDCxlending.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
// Package XDCxlending implements the XDC decentralized lending protocol
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCxlending/lendingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrLendingServiceNotRunning is returned when lending service is not running
|
||||
ErrLendingServiceNotRunning = errors.New("lending service is not running")
|
||||
// ErrLoanNotFound is returned when loan is not found
|
||||
ErrLoanNotFound = errors.New("loan not found")
|
||||
// ErrInvalidInterestRate is returned when interest rate is invalid
|
||||
ErrInvalidInterestRate = errors.New("invalid interest rate")
|
||||
// ErrInvalidTerm is returned when term is invalid
|
||||
ErrInvalidTerm = errors.New("invalid term")
|
||||
// ErrInsufficientCollateral is returned when collateral is insufficient
|
||||
ErrInsufficientCollateral = errors.New("insufficient collateral")
|
||||
)
|
||||
|
||||
// XDCxLending represents the XDC decentralized lending protocol
|
||||
type XDCxLending struct {
|
||||
config *Config
|
||||
db ethdb.Database
|
||||
stateCache lendingstate.Database
|
||||
lock sync.RWMutex
|
||||
running bool
|
||||
|
||||
orderProcessor *OrderProcessor
|
||||
liquidator *Liquidator
|
||||
}
|
||||
|
||||
// Config holds XDCxLending configuration
|
||||
type Config struct {
|
||||
DataDir string
|
||||
DBEngine string
|
||||
LendingStateDB string
|
||||
DefaultTerm uint64
|
||||
MinCollateral *big.Int
|
||||
LiquidationRate *big.Int
|
||||
}
|
||||
|
||||
// DefaultConfig returns default XDCxLending configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
DataDir: "",
|
||||
DBEngine: "leveldb",
|
||||
LendingStateDB: "XDCxlending",
|
||||
DefaultTerm: 86400 * 30, // 30 days in seconds
|
||||
MinCollateral: big.NewInt(150), // 150%
|
||||
LiquidationRate: big.NewInt(110), // 110%
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new XDCxLending instance
|
||||
func New(config *Config, db ethdb.Database) (*XDCxLending, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
lending := &XDCxLending{
|
||||
config: config,
|
||||
db: db,
|
||||
running: false,
|
||||
}
|
||||
|
||||
lending.stateCache = lendingstate.NewDatabase(db)
|
||||
lending.orderProcessor = NewOrderProcessor(lending)
|
||||
lending.liquidator = NewLiquidator(lending)
|
||||
|
||||
return lending, nil
|
||||
}
|
||||
|
||||
// Start starts the XDCxLending service
|
||||
func (l *XDCxLending) Start() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Starting XDCxLending service")
|
||||
l.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the XDCxLending service
|
||||
func (l *XDCxLending) Stop() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if !l.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Stopping XDCxLending service")
|
||||
l.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether XDCxLending is running
|
||||
func (l *XDCxLending) IsRunning() bool {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
return l.running
|
||||
}
|
||||
|
||||
// GetLendingState returns the lending state for a given root
|
||||
func (l *XDCxLending) GetLendingState(block *types.Block, statedb *state.StateDB) (*lendingstate.LendingStateDB, error) {
|
||||
if block == nil {
|
||||
return nil, errors.New("block is nil")
|
||||
}
|
||||
|
||||
root := block.Root()
|
||||
lendingState, err := lendingstate.New(root, l.stateCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lendingState, nil
|
||||
}
|
||||
|
||||
// ProcessLendingOrder processes a lending order
|
||||
func (l *XDCxLending) ProcessLendingOrder(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, order *LendingOrder) ([]*LendingTrade, error) {
|
||||
if !l.IsRunning() {
|
||||
return nil, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
return l.orderProcessor.Process(ctx, statedb, lendingState, order)
|
||||
}
|
||||
|
||||
// CancelLendingOrder cancels an existing lending order
|
||||
func (l *XDCxLending) CancelLendingOrder(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, orderID common.Hash) error {
|
||||
if !l.IsRunning() {
|
||||
return ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
return l.orderProcessor.Cancel(ctx, statedb, lendingState, orderID)
|
||||
}
|
||||
|
||||
// Topup adds collateral to a loan
|
||||
func (l *XDCxLending) Topup(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, loanID common.Hash, amount *big.Int) error {
|
||||
if !l.IsRunning() {
|
||||
return ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
loan := lendingState.GetLoan(loanID)
|
||||
if loan == nil {
|
||||
return ErrLoanNotFound
|
||||
}
|
||||
|
||||
// Add collateral
|
||||
loan.CollateralAmount = new(big.Int).Add(loan.CollateralAmount, amount)
|
||||
lendingState.UpdateLoan(loan)
|
||||
|
||||
log.Debug("Loan topped up", "loanID", loanID.Hex(), "amount", amount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Repay repays a loan
|
||||
func (l *XDCxLending) Repay(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, loanID common.Hash) error {
|
||||
if !l.IsRunning() {
|
||||
return ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
loan := lendingState.GetLoan(loanID)
|
||||
if loan == nil {
|
||||
return ErrLoanNotFound
|
||||
}
|
||||
|
||||
// Calculate total repayment amount
|
||||
interest := l.calculateInterest(loan)
|
||||
totalRepayment := new(big.Int).Add(loan.Principal, interest)
|
||||
|
||||
// Update loan status
|
||||
loan.Status = LoanStatusRepaid
|
||||
lendingState.UpdateLoan(loan)
|
||||
|
||||
log.Debug("Loan repaid", "loanID", loanID.Hex(), "total", totalRepayment)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Liquidate liquidates an undercollateralized loan
|
||||
func (l *XDCxLending) Liquidate(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, loanID common.Hash) error {
|
||||
if !l.IsRunning() {
|
||||
return ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
return l.liquidator.Liquidate(ctx, statedb, lendingState, loanID)
|
||||
}
|
||||
|
||||
// CheckLiquidation checks if loans need liquidation
|
||||
func (l *XDCxLending) CheckLiquidation(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB) ([]common.Hash, error) {
|
||||
if !l.IsRunning() {
|
||||
return nil, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
return l.liquidator.CheckLoansForLiquidation(lendingState)
|
||||
}
|
||||
|
||||
// ApplyXDCxLendingMatchedTransaction applies matched lending trades to state
|
||||
func (l *XDCxLending) ApplyXDCxLendingMatchedTransaction(chainConfig *params.ChainConfig, statedb *state.StateDB, block *types.Block, trades []*LendingTrade) error {
|
||||
for _, trade := range trades {
|
||||
if err := l.settleLendingTrade(statedb, trade); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleLendingTrade settles a lending trade
|
||||
func (l *XDCxLending) settleLendingTrade(statedb *state.StateDB, trade *LendingTrade) error {
|
||||
log.Debug("Settling lending trade", "trade", trade.Hash())
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateInterest calculates the interest for a loan
|
||||
func (l *XDCxLending) calculateInterest(loan *Loan) *big.Int {
|
||||
// Simple interest calculation: Principal * Rate * Time / (365 * 100)
|
||||
interest := new(big.Int).Mul(loan.Principal, loan.InterestRate)
|
||||
interest = interest.Mul(interest, big.NewInt(int64(loan.Term)))
|
||||
interest = interest.Div(interest, big.NewInt(365*24*3600*100))
|
||||
return interest
|
||||
}
|
||||
|
||||
// GetConfig returns XDCxLending configuration
|
||||
func (l *XDCxLending) GetConfig() *Config {
|
||||
return l.config
|
||||
}
|
||||
|
||||
// Database returns the database instance
|
||||
func (l *XDCxLending) Database() ethdb.Database {
|
||||
return l.db
|
||||
}
|
||||
|
||||
// StateCache returns the lending state cache
|
||||
func (l *XDCxLending) StateCache() lendingstate.Database {
|
||||
return l.stateCache
|
||||
}
|
||||
338
XDCxlending/XDCxlending_test.go
Normal file
338
XDCxlending/XDCxlending_test.go
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||
)
|
||||
|
||||
func TestNewXDCxLending(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
config := DefaultConfig()
|
||||
|
||||
lending, err := New(config, db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create XDCxLending: %v", err)
|
||||
}
|
||||
|
||||
if lending == nil {
|
||||
t.Fatal("XDCxLending instance is nil")
|
||||
}
|
||||
|
||||
if lending.config != config {
|
||||
t.Error("Config not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDCxLendingStartStop(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
lending, _ := New(DefaultConfig(), db)
|
||||
|
||||
// Test start
|
||||
if err := lending.Start(); err != nil {
|
||||
t.Fatalf("Failed to start XDCxLending: %v", err)
|
||||
}
|
||||
|
||||
if !lending.IsRunning() {
|
||||
t.Error("XDCxLending should be running after Start")
|
||||
}
|
||||
|
||||
// Test stop
|
||||
if err := lending.Stop(); err != nil {
|
||||
t.Fatalf("Failed to stop XDCxLending: %v", err)
|
||||
}
|
||||
|
||||
if lending.IsRunning() {
|
||||
t.Error("XDCxLending should not be running after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLendingConfig(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
|
||||
if config.DBEngine != "leveldb" {
|
||||
t.Errorf("Expected DBEngine 'leveldb', got '%s'", config.DBEngine)
|
||||
}
|
||||
|
||||
if config.DefaultTerm != 86400*30 {
|
||||
t.Errorf("Expected DefaultTerm 30 days, got %d", config.DefaultTerm)
|
||||
}
|
||||
|
||||
if config.MinCollateral.Cmp(big.NewInt(150)) != 0 {
|
||||
t.Errorf("Expected MinCollateral 150, got %s", config.MinCollateral.String())
|
||||
}
|
||||
|
||||
if config.LiquidationRate.Cmp(big.NewInt(110)) != 0 {
|
||||
t.Errorf("Expected LiquidationRate 110, got %s", config.LiquidationRate.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLendingOrder(t *testing.T) {
|
||||
userAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
lendingToken := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
collateralToken := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
relayerAddr := common.HexToAddress("0xcccccccccccccccccccccccccccccccccccccccc")
|
||||
|
||||
interestRate := big.NewInt(500) // 5%
|
||||
term := uint64(86400 * 30) // 30 days
|
||||
quantity := big.NewInt(1000)
|
||||
nonce := uint64(1)
|
||||
|
||||
order := NewLendingOrder(
|
||||
userAddr, lendingToken, collateralToken,
|
||||
Borrow, LimitOrder,
|
||||
interestRate, term, quantity, nonce, relayerAddr,
|
||||
)
|
||||
|
||||
if order.UserAddress != userAddr {
|
||||
t.Errorf("Expected user address %s, got %s", userAddr.Hex(), order.UserAddress.Hex())
|
||||
}
|
||||
|
||||
if order.LendingToken != lendingToken {
|
||||
t.Errorf("Expected lending token %s, got %s", lendingToken.Hex(), order.LendingToken.Hex())
|
||||
}
|
||||
|
||||
if order.Side != Borrow {
|
||||
t.Errorf("Expected side Borrow, got %d", order.Side)
|
||||
}
|
||||
|
||||
if order.Status != OrderStatusNew {
|
||||
t.Errorf("Expected status New, got %d", order.Status)
|
||||
}
|
||||
|
||||
if order.Term != term {
|
||||
t.Errorf("Expected term %d, got %d", term, order.Term)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLendingOrderRemainingQuantity(t *testing.T) {
|
||||
order := &LendingOrder{
|
||||
Quantity: big.NewInt(100),
|
||||
FilledQuantity: big.NewInt(30),
|
||||
}
|
||||
|
||||
remaining := order.RemainingQuantity()
|
||||
expected := big.NewInt(70)
|
||||
|
||||
if remaining.Cmp(expected) != 0 {
|
||||
t.Errorf("Expected remaining %s, got %s", expected.String(), remaining.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLendingOrderIsFilled(t *testing.T) {
|
||||
order := &LendingOrder{
|
||||
Quantity: big.NewInt(100),
|
||||
FilledQuantity: big.NewInt(30),
|
||||
}
|
||||
|
||||
if order.IsFilled() {
|
||||
t.Error("Order should not be filled")
|
||||
}
|
||||
|
||||
order.FilledQuantity = big.NewInt(100)
|
||||
if !order.IsFilled() {
|
||||
t.Error("Order should be filled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLoan(t *testing.T) {
|
||||
borrower := common.HexToAddress("0x1111")
|
||||
lender := common.HexToAddress("0x2222")
|
||||
lendingToken := common.HexToAddress("0x3333")
|
||||
collateralToken := common.HexToAddress("0x4444")
|
||||
principal := big.NewInt(1000)
|
||||
collateral := big.NewInt(1500)
|
||||
interestRate := big.NewInt(500)
|
||||
term := uint64(86400 * 30)
|
||||
startTime := uint64(1000000)
|
||||
|
||||
loan := NewLoan(
|
||||
borrower, lender,
|
||||
lendingToken, collateralToken,
|
||||
principal, collateral,
|
||||
interestRate, term, startTime,
|
||||
)
|
||||
|
||||
if loan.BorrowerAddress != borrower {
|
||||
t.Error("Borrower address mismatch")
|
||||
}
|
||||
|
||||
if loan.LenderAddress != lender {
|
||||
t.Error("Lender address mismatch")
|
||||
}
|
||||
|
||||
if loan.ExpiryTime != startTime+term {
|
||||
t.Errorf("Expected expiry %d, got %d", startTime+term, loan.ExpiryTime)
|
||||
}
|
||||
|
||||
if loan.Status != LoanStatusActive {
|
||||
t.Errorf("Expected status Active, got %d", loan.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoanIsExpired(t *testing.T) {
|
||||
loan := &Loan{
|
||||
ExpiryTime: 1000000,
|
||||
}
|
||||
|
||||
// Not expired
|
||||
if loan.IsExpired(500000) {
|
||||
t.Error("Loan should not be expired")
|
||||
}
|
||||
|
||||
// Exactly expired
|
||||
if !loan.IsExpired(1000000) {
|
||||
t.Error("Loan should be expired at expiry time")
|
||||
}
|
||||
|
||||
// Past expiry
|
||||
if !loan.IsExpired(1500000) {
|
||||
t.Error("Loan should be expired past expiry time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoanGetCollateralRatio(t *testing.T) {
|
||||
loan := &Loan{
|
||||
Principal: big.NewInt(1000),
|
||||
CollateralAmount: big.NewInt(1500),
|
||||
}
|
||||
|
||||
// Collateral price = 1
|
||||
collateralPrice := big.NewInt(1)
|
||||
ratio := loan.GetCollateralRatio(collateralPrice)
|
||||
|
||||
// Expected: (1500 * 1) / 1000 * 100 = 150
|
||||
expected := big.NewInt(150)
|
||||
if ratio.Cmp(expected) != 0 {
|
||||
t.Errorf("Expected ratio %s, got %s", expected.String(), ratio.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLendingPairKey(t *testing.T) {
|
||||
lendingToken := common.HexToAddress("0xaaaa")
|
||||
collateralToken := common.HexToAddress("0xbbbb")
|
||||
term := uint64(86400 * 30)
|
||||
|
||||
key1 := GetLendingPairKey(lendingToken, collateralToken, term)
|
||||
key2 := GetLendingPairKey(lendingToken, collateralToken, term)
|
||||
|
||||
if key1 != key2 {
|
||||
t.Error("Same parameters should produce same key")
|
||||
}
|
||||
|
||||
// Different term should produce different key
|
||||
key3 := GetLendingPairKey(lendingToken, collateralToken, term*2)
|
||||
if key1 == key3 {
|
||||
t.Error("Different term should produce different key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLendingTrade(t *testing.T) {
|
||||
borrowOrderID := common.HexToHash("0x1111")
|
||||
lendOrderID := common.HexToHash("0x2222")
|
||||
borrower := common.HexToAddress("0x3333")
|
||||
lender := common.HexToAddress("0x4444")
|
||||
lendingToken := common.HexToAddress("0x5555")
|
||||
collateralToken := common.HexToAddress("0x6666")
|
||||
principal := big.NewInt(1000)
|
||||
collateral := big.NewInt(1500)
|
||||
interestRate := big.NewInt(500)
|
||||
term := uint64(86400 * 30)
|
||||
|
||||
trade := NewLendingTrade(
|
||||
borrowOrderID, lendOrderID,
|
||||
borrower, lender,
|
||||
lendingToken, collateralToken,
|
||||
principal, collateral,
|
||||
interestRate, term,
|
||||
)
|
||||
|
||||
if trade.BorrowOrderID != borrowOrderID {
|
||||
t.Error("Borrow order ID mismatch")
|
||||
}
|
||||
|
||||
if trade.Borrower != borrower {
|
||||
t.Error("Borrower address mismatch")
|
||||
}
|
||||
|
||||
if trade.Status != TradeStatusPending {
|
||||
t.Errorf("Expected status Pending, got %d", trade.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLendingTradeCalculateInterest(t *testing.T) {
|
||||
trade := &LendingTrade{
|
||||
Principal: big.NewInt(10000),
|
||||
InterestRate: big.NewInt(500), // 5%
|
||||
Term: 86400 * 30, // 30 days
|
||||
}
|
||||
|
||||
interest := trade.CalculateInterest()
|
||||
|
||||
// Interest = 10000 * 5 * 30 / (365 * 100) ≈ 41
|
||||
// This is approximate due to integer division
|
||||
if interest.Sign() <= 0 {
|
||||
t.Error("Interest should be positive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLendingTradeTotalRepayment(t *testing.T) {
|
||||
trade := &LendingTrade{
|
||||
Principal: big.NewInt(10000),
|
||||
InterestRate: big.NewInt(500),
|
||||
Term: 86400 * 30,
|
||||
}
|
||||
|
||||
total := trade.TotalRepayment()
|
||||
interest := trade.CalculateInterest()
|
||||
expected := new(big.Int).Add(trade.Principal, interest)
|
||||
|
||||
if total.Cmp(expected) != 0 {
|
||||
t.Errorf("Expected total %s, got %s", expected.String(), total.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDCxLendingAPIs(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
lending, _ := New(DefaultConfig(), db)
|
||||
|
||||
apis := lending.APIs()
|
||||
|
||||
if len(apis) != 2 {
|
||||
t.Errorf("Expected 2 APIs, got %d", len(apis))
|
||||
}
|
||||
|
||||
// Check namespaces
|
||||
namespaces := make(map[string]bool)
|
||||
for _, api := range apis {
|
||||
namespaces[api.Namespace] = true
|
||||
}
|
||||
|
||||
if !namespaces["xdcxlending"] {
|
||||
t.Error("Expected 'xdcxlending' namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateInterest(t *testing.T) {
|
||||
db := memorydb.New()
|
||||
lending, _ := New(DefaultConfig(), db)
|
||||
|
||||
loan := &Loan{
|
||||
Principal: big.NewInt(10000),
|
||||
InterestRate: big.NewInt(1000), // 10%
|
||||
Term: 86400 * 365, // 1 year
|
||||
}
|
||||
|
||||
interest := lending.calculateInterest(loan)
|
||||
|
||||
// With 10% annual rate for 1 year, interest should be around 1000
|
||||
// Allow some tolerance due to integer division
|
||||
if interest.Cmp(big.NewInt(900)) < 0 || interest.Cmp(big.NewInt(1100)) > 0 {
|
||||
t.Errorf("Expected interest around 1000, got %s", interest.String())
|
||||
}
|
||||
}
|
||||
297
XDCxlending/api.go
Normal file
297
XDCxlending/api.go
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCxlending/lendingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// PublicXDCxLendingAPI provides public XDCxLending APIs
|
||||
type PublicXDCxLendingAPI struct {
|
||||
lending *XDCxLending
|
||||
}
|
||||
|
||||
// NewPublicXDCxLendingAPI creates a new public XDCxLending API
|
||||
func NewPublicXDCxLendingAPI(lending *XDCxLending) *PublicXDCxLendingAPI {
|
||||
return &PublicXDCxLendingAPI{lending: lending}
|
||||
}
|
||||
|
||||
// Version returns the XDCxLending version
|
||||
func (api *PublicXDCxLendingAPI) Version() string {
|
||||
return "1.0"
|
||||
}
|
||||
|
||||
// LendingOrderBookResult represents a lending order book API result
|
||||
type LendingOrderBookResult struct {
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Term uint64 `json:"term"`
|
||||
Borrows []LendingOrderResult `json:"borrows"`
|
||||
Lends []LendingOrderResult `json:"lends"`
|
||||
}
|
||||
|
||||
// LendingOrderResult represents a lending order API result
|
||||
type LendingOrderResult struct {
|
||||
ID common.Hash `json:"id"`
|
||||
UserAddress common.Address `json:"userAddress"`
|
||||
RelayerAddress common.Address `json:"relayerAddress"`
|
||||
InterestRate *hexutil.Big `json:"interestRate"`
|
||||
Quantity *hexutil.Big `json:"quantity"`
|
||||
FilledQuantity *hexutil.Big `json:"filledQuantity"`
|
||||
Status string `json:"status"`
|
||||
Side string `json:"side"`
|
||||
}
|
||||
|
||||
// LoanResult represents a loan API result
|
||||
type LoanResult struct {
|
||||
ID common.Hash `json:"id"`
|
||||
Borrower common.Address `json:"borrower"`
|
||||
Lender common.Address `json:"lender"`
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Principal *hexutil.Big `json:"principal"`
|
||||
CollateralAmount *hexutil.Big `json:"collateralAmount"`
|
||||
InterestRate *hexutil.Big `json:"interestRate"`
|
||||
Term uint64 `json:"term"`
|
||||
StartTime uint64 `json:"startTime"`
|
||||
ExpiryTime uint64 `json:"expiryTime"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// GetLendingOrderBook returns the lending order book
|
||||
func (api *PublicXDCxLendingAPI) GetLendingOrderBook(ctx context.Context, lendingToken, collateralToken common.Address, term uint64) (*LendingOrderBookResult, error) {
|
||||
if !api.lending.IsRunning() {
|
||||
return nil, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
// Get current lending state
|
||||
lendingState, err := lendingstate.New(common.Hash{}, api.lending.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pairKey := GetLendingPairKey(lendingToken, collateralToken, term)
|
||||
ob := lendingState.GetLendingOrderBook(pairKey)
|
||||
if ob == nil {
|
||||
return nil, errors.New("order book not found")
|
||||
}
|
||||
|
||||
result := &LendingOrderBookResult{
|
||||
LendingToken: lendingToken,
|
||||
CollateralToken: collateralToken,
|
||||
Term: term,
|
||||
Borrows: make([]LendingOrderResult, 0),
|
||||
Lends: make([]LendingOrderResult, 0),
|
||||
}
|
||||
|
||||
for _, borrow := range ob.Borrows {
|
||||
result.Borrows = append(result.Borrows, orderStateToResult(borrow))
|
||||
}
|
||||
|
||||
for _, lend := range ob.Lends {
|
||||
result.Lends = append(result.Lends, orderStateToResult(lend))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLoan returns a loan by ID
|
||||
func (api *PublicXDCxLendingAPI) GetLoan(ctx context.Context, loanID common.Hash) (*LoanResult, error) {
|
||||
if !api.lending.IsRunning() {
|
||||
return nil, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
lendingState, err := lendingstate.New(common.Hash{}, api.lending.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loan := lendingState.GetLoan(loanID)
|
||||
if loan == nil {
|
||||
return nil, ErrLoanNotFound
|
||||
}
|
||||
|
||||
return loanStateToResult(loan), nil
|
||||
}
|
||||
|
||||
// GetLoansByBorrower returns all loans for a borrower
|
||||
func (api *PublicXDCxLendingAPI) GetLoansByBorrower(ctx context.Context, borrower common.Address) ([]*LoanResult, error) {
|
||||
if !api.lending.IsRunning() {
|
||||
return nil, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
lendingState, err := lendingstate.New(common.Hash{}, api.lending.StateCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allLoans := lendingState.GetAllLoans()
|
||||
results := make([]*LoanResult, 0)
|
||||
|
||||
for _, loan := range allLoans {
|
||||
if loan.BorrowerAddress == borrower {
|
||||
results = append(results, loanStateToResult(loan))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// orderStateToResult converts a lending order state to API result
|
||||
func orderStateToResult(order *lendingstate.LendingOrderState) LendingOrderResult {
|
||||
sideStr := "borrow"
|
||||
if order.Side == 1 {
|
||||
sideStr = "lend"
|
||||
}
|
||||
|
||||
statusStr := "new"
|
||||
switch order.Status {
|
||||
case 1:
|
||||
statusStr = "partial"
|
||||
case 2:
|
||||
statusStr = "filled"
|
||||
case 3:
|
||||
statusStr = "cancelled"
|
||||
case 4:
|
||||
statusStr = "rejected"
|
||||
}
|
||||
|
||||
return LendingOrderResult{
|
||||
ID: order.ID,
|
||||
UserAddress: order.UserAddress,
|
||||
RelayerAddress: order.RelayerAddress,
|
||||
InterestRate: (*hexutil.Big)(order.InterestRate),
|
||||
Quantity: (*hexutil.Big)(order.Quantity),
|
||||
FilledQuantity: (*hexutil.Big)(order.FilledQuantity),
|
||||
Status: statusStr,
|
||||
Side: sideStr,
|
||||
}
|
||||
}
|
||||
|
||||
// loanStateToResult converts a loan state to API result
|
||||
func loanStateToResult(loan *lendingstate.LoanState) *LoanResult {
|
||||
statusStr := "active"
|
||||
switch loan.Status {
|
||||
case 1:
|
||||
statusStr = "repaid"
|
||||
case 2:
|
||||
statusStr = "liquidated"
|
||||
case 3:
|
||||
statusStr = "defaulted"
|
||||
}
|
||||
|
||||
return &LoanResult{
|
||||
ID: loan.ID,
|
||||
Borrower: loan.BorrowerAddress,
|
||||
Lender: loan.LenderAddress,
|
||||
LendingToken: loan.LendingToken,
|
||||
CollateralToken: loan.CollateralToken,
|
||||
Principal: (*hexutil.Big)(loan.Principal),
|
||||
CollateralAmount: (*hexutil.Big)(loan.CollateralAmount),
|
||||
InterestRate: (*hexutil.Big)(loan.InterestRate),
|
||||
Term: loan.Term,
|
||||
StartTime: loan.StartTime,
|
||||
ExpiryTime: loan.ExpiryTime,
|
||||
Status: statusStr,
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateXDCxLendingAPI provides private XDCxLending APIs
|
||||
type PrivateXDCxLendingAPI struct {
|
||||
lending *XDCxLending
|
||||
}
|
||||
|
||||
// NewPrivateXDCxLendingAPI creates a new private XDCxLending API
|
||||
func NewPrivateXDCxLendingAPI(lending *XDCxLending) *PrivateXDCxLendingAPI {
|
||||
return &PrivateXDCxLendingAPI{lending: lending}
|
||||
}
|
||||
|
||||
// SendLendingOrder sends a new lending order
|
||||
func (api *PrivateXDCxLendingAPI) SendLendingOrder(ctx context.Context, args SendLendingOrderArgs) (common.Hash, error) {
|
||||
if !api.lending.IsRunning() {
|
||||
return common.Hash{}, ErrLendingServiceNotRunning
|
||||
}
|
||||
|
||||
// Convert args to order
|
||||
order, err := args.ToOrder()
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
return order.ID, nil
|
||||
}
|
||||
|
||||
// SendLendingOrderArgs represents the arguments for sending a lending order
|
||||
type SendLendingOrderArgs struct {
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Side string `json:"side"`
|
||||
Type string `json:"type"`
|
||||
InterestRate *hexutil.Big `json:"interestRate"`
|
||||
Term hexutil.Uint64 `json:"term"`
|
||||
Quantity *hexutil.Big `json:"quantity"`
|
||||
RelayerAddress common.Address `json:"relayerAddress"`
|
||||
Nonce hexutil.Uint64 `json:"nonce"`
|
||||
Signature hexutil.Bytes `json:"signature"`
|
||||
}
|
||||
|
||||
// ToOrder converts args to a lending order
|
||||
func (args *SendLendingOrderArgs) ToOrder() (*LendingOrder, error) {
|
||||
var side OrderSide
|
||||
switch args.Side {
|
||||
case "borrow":
|
||||
side = Borrow
|
||||
case "lend":
|
||||
side = Lend
|
||||
default:
|
||||
return nil, errors.New("invalid order side")
|
||||
}
|
||||
|
||||
var orderType OrderType
|
||||
switch args.Type {
|
||||
case "limit":
|
||||
orderType = LimitOrder
|
||||
case "market":
|
||||
orderType = MarketOrder
|
||||
default:
|
||||
return nil, errors.New("invalid order type")
|
||||
}
|
||||
|
||||
order := NewLendingOrder(
|
||||
common.Address{},
|
||||
args.LendingToken,
|
||||
args.CollateralToken,
|
||||
side,
|
||||
orderType,
|
||||
(*big.Int)(args.InterestRate),
|
||||
uint64(args.Term),
|
||||
(*big.Int)(args.Quantity),
|
||||
uint64(args.Nonce),
|
||||
args.RelayerAddress,
|
||||
)
|
||||
order.Signature = args.Signature
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// APIs returns the collection of XDCxLending APIs
|
||||
func (l *XDCxLending) APIs() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "xdcxlending",
|
||||
Service: NewPublicXDCxLendingAPI(l),
|
||||
},
|
||||
{
|
||||
Namespace: "xdcxlending",
|
||||
Service: NewPrivateXDCxLendingAPI(l),
|
||||
},
|
||||
}
|
||||
}
|
||||
127
XDCxlending/lendingstate/database.go
Normal file
127
XDCxlending/lendingstate/database.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package lendingstate
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// LendingTrieDB wraps the trie database for lending state
|
||||
type LendingTrieDB struct {
|
||||
diskdb ethdb.Database
|
||||
config *trie.Config
|
||||
}
|
||||
|
||||
// NewLendingTrieDB creates a new lending trie database
|
||||
func NewLendingTrieDB(diskdb ethdb.Database) *LendingTrieDB {
|
||||
return &LendingTrieDB{
|
||||
diskdb: diskdb,
|
||||
config: &trie.Config{},
|
||||
}
|
||||
}
|
||||
|
||||
// DiskDB returns the underlying disk database
|
||||
func (db *LendingTrieDB) DiskDB() ethdb.Database {
|
||||
return db.diskdb
|
||||
}
|
||||
|
||||
// OpenLendingTrie opens a lending state trie
|
||||
func (db *LendingTrieDB) OpenLendingTrie(root common.Hash) (*trie.Trie, error) {
|
||||
id := &trie.ID{
|
||||
StateRoot: root,
|
||||
}
|
||||
return trie.New(id, trie.NewDatabase(db.diskdb, db.config))
|
||||
}
|
||||
|
||||
// CopyLendingTrie copies a lending trie
|
||||
func (db *LendingTrieDB) CopyLendingTrie(t *trie.Trie) *trie.Trie {
|
||||
return t.Copy()
|
||||
}
|
||||
|
||||
// Commit commits all pending writes
|
||||
func (db *LendingTrieDB) Commit(root common.Hash, report bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the database
|
||||
func (db *LendingTrieDB) Close() error {
|
||||
return db.diskdb.Close()
|
||||
}
|
||||
|
||||
// LendingStateCacheConfig holds cache configuration
|
||||
type LendingStateCacheConfig struct {
|
||||
CacheSize int
|
||||
MaxLive int
|
||||
}
|
||||
|
||||
// DefaultLendingStateCacheConfig returns default cache configuration
|
||||
func DefaultLendingStateCacheConfig() *LendingStateCacheConfig {
|
||||
return &LendingStateCacheConfig{
|
||||
CacheSize: 256,
|
||||
MaxLive: 16,
|
||||
}
|
||||
}
|
||||
|
||||
// LendingStateCache provides caching for lending state
|
||||
type LendingStateCache struct {
|
||||
config *LendingStateCacheConfig
|
||||
db *LendingTrieDB
|
||||
states map[common.Hash]*LendingStateDB
|
||||
}
|
||||
|
||||
// NewLendingStateCache creates a new lending state cache
|
||||
func NewLendingStateCache(db *LendingTrieDB, config *LendingStateCacheConfig) *LendingStateCache {
|
||||
if config == nil {
|
||||
config = DefaultLendingStateCacheConfig()
|
||||
}
|
||||
return &LendingStateCache{
|
||||
config: config,
|
||||
db: db,
|
||||
states: make(map[common.Hash]*LendingStateDB),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a lending state from cache or creates new one
|
||||
func (c *LendingStateCache) Get(root common.Hash) (*LendingStateDB, error) {
|
||||
if state, ok := c.states[root]; ok {
|
||||
return state.Copy(), nil
|
||||
}
|
||||
return New(root, c)
|
||||
}
|
||||
|
||||
// Put stores a lending state in cache
|
||||
func (c *LendingStateCache) Put(root common.Hash, state *LendingStateDB) {
|
||||
if len(c.states) >= c.config.CacheSize {
|
||||
for k := range c.states {
|
||||
delete(c.states, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
c.states[root] = state
|
||||
}
|
||||
|
||||
// Remove removes a state from cache
|
||||
func (c *LendingStateCache) Remove(root common.Hash) {
|
||||
delete(c.states, root)
|
||||
}
|
||||
|
||||
// Clear clears the entire cache
|
||||
func (c *LendingStateCache) Clear() {
|
||||
c.states = make(map[common.Hash]*LendingStateDB)
|
||||
}
|
||||
|
||||
// OpenTrie implements Database interface
|
||||
func (c *LendingStateCache) OpenTrie(root common.Hash) (Trie, error) {
|
||||
return c.db.OpenLendingTrie(root)
|
||||
}
|
||||
|
||||
// CopyTrie implements Database interface
|
||||
func (c *LendingStateCache) CopyTrie(t Trie) Trie {
|
||||
if tt, ok := t.(*trie.Trie); ok {
|
||||
return c.db.CopyLendingTrie(tt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
256
XDCxlending/lendingstate/orderbook.go
Normal file
256
XDCxlending/lendingstate/orderbook.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package lendingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// LendingOrderBook represents a lending pair order book
|
||||
type LendingOrderBook struct {
|
||||
PairKey common.Hash
|
||||
Borrows []*LendingOrderState // Borrow orders sorted by interest rate desc
|
||||
Lends []*LendingOrderState // Lend orders sorted by interest rate asc
|
||||
|
||||
borrowIndex map[common.Hash]int
|
||||
lendIndex map[common.Hash]int
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewLendingOrderBook creates a new lending order book
|
||||
func NewLendingOrderBook(pairKey common.Hash) *LendingOrderBook {
|
||||
return &LendingOrderBook{
|
||||
PairKey: pairKey,
|
||||
Borrows: make([]*LendingOrderState, 0),
|
||||
Lends: make([]*LendingOrderState, 0),
|
||||
borrowIndex: make(map[common.Hash]int),
|
||||
lendIndex: make(map[common.Hash]int),
|
||||
}
|
||||
}
|
||||
|
||||
// AddBorrow adds a borrow order to the order book
|
||||
func (ob *LendingOrderBook) AddBorrow(order interface{}) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
var orderState *LendingOrderState
|
||||
switch o := order.(type) {
|
||||
case *LendingOrderState:
|
||||
orderState = o
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
ob.Borrows = append(ob.Borrows, orderState)
|
||||
ob.sortBorrows()
|
||||
ob.rebuildBorrowIndex()
|
||||
}
|
||||
|
||||
// AddLend adds a lend order to the order book
|
||||
func (ob *LendingOrderBook) AddLend(order interface{}) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
var orderState *LendingOrderState
|
||||
switch o := order.(type) {
|
||||
case *LendingOrderState:
|
||||
orderState = o
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
ob.Lends = append(ob.Lends, orderState)
|
||||
ob.sortLends()
|
||||
ob.rebuildLendIndex()
|
||||
}
|
||||
|
||||
// RemoveBorrow removes a borrow order from the order book
|
||||
func (ob *LendingOrderBook) RemoveBorrow(orderID common.Hash) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
if idx, exists := ob.borrowIndex[orderID]; exists {
|
||||
ob.Borrows = append(ob.Borrows[:idx], ob.Borrows[idx+1:]...)
|
||||
ob.rebuildBorrowIndex()
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveLend removes a lend order from the order book
|
||||
func (ob *LendingOrderBook) RemoveLend(orderID common.Hash) {
|
||||
ob.lock.Lock()
|
||||
defer ob.lock.Unlock()
|
||||
|
||||
if idx, exists := ob.lendIndex[orderID]; exists {
|
||||
ob.Lends = append(ob.Lends[:idx], ob.Lends[idx+1:]...)
|
||||
ob.rebuildLendIndex()
|
||||
}
|
||||
}
|
||||
|
||||
// GetBestBorrow returns the best borrow order (highest interest rate)
|
||||
func (ob *LendingOrderBook) GetBestBorrow() *LendingOrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if len(ob.Borrows) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ob.Borrows[0]
|
||||
}
|
||||
|
||||
// GetBestLend returns the best lend order (lowest interest rate)
|
||||
func (ob *LendingOrderBook) GetBestLend() *LendingOrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if len(ob.Lends) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ob.Lends[0]
|
||||
}
|
||||
|
||||
// GetBorrow returns a specific borrow order
|
||||
func (ob *LendingOrderBook) GetBorrow(orderID common.Hash) *LendingOrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if idx, exists := ob.borrowIndex[orderID]; exists {
|
||||
return ob.Borrows[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLend returns a specific lend order
|
||||
func (ob *LendingOrderBook) GetLend(orderID common.Hash) *LendingOrderState {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
if idx, exists := ob.lendIndex[orderID]; exists {
|
||||
return ob.Lends[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BorrowCount returns the number of borrow orders
|
||||
func (ob *LendingOrderBook) BorrowCount() int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
return len(ob.Borrows)
|
||||
}
|
||||
|
||||
// LendCount returns the number of lend orders
|
||||
func (ob *LendingOrderBook) LendCount() int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
return len(ob.Lends)
|
||||
}
|
||||
|
||||
// GetBorrowVolume returns total borrow volume
|
||||
func (ob *LendingOrderBook) GetBorrowVolume() *big.Int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
total := big.NewInt(0)
|
||||
for _, borrow := range ob.Borrows {
|
||||
total = new(big.Int).Add(total, borrow.RemainingQuantity())
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// GetLendVolume returns total lend volume
|
||||
func (ob *LendingOrderBook) GetLendVolume() *big.Int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
total := big.NewInt(0)
|
||||
for _, lend := range ob.Lends {
|
||||
total = new(big.Int).Add(total, lend.RemainingQuantity())
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// Copy creates a copy of the order book
|
||||
func (ob *LendingOrderBook) Copy() *LendingOrderBook {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
copy := &LendingOrderBook{
|
||||
PairKey: ob.PairKey,
|
||||
Borrows: make([]*LendingOrderState, len(ob.Borrows)),
|
||||
Lends: make([]*LendingOrderState, len(ob.Lends)),
|
||||
borrowIndex: make(map[common.Hash]int),
|
||||
lendIndex: make(map[common.Hash]int),
|
||||
}
|
||||
|
||||
for i, borrow := range ob.Borrows {
|
||||
copy.Borrows[i] = borrow.Copy()
|
||||
copy.borrowIndex[borrow.ID] = i
|
||||
}
|
||||
|
||||
for i, lend := range ob.Lends {
|
||||
copy.Lends[i] = lend.Copy()
|
||||
copy.lendIndex[lend.ID] = i
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
// sortBorrows sorts borrows by interest rate descending
|
||||
func (ob *LendingOrderBook) sortBorrows() {
|
||||
sort.Slice(ob.Borrows, func(i, j int) bool {
|
||||
return ob.Borrows[i].InterestRate.Cmp(ob.Borrows[j].InterestRate) > 0
|
||||
})
|
||||
}
|
||||
|
||||
// sortLends sorts lends by interest rate ascending
|
||||
func (ob *LendingOrderBook) sortLends() {
|
||||
sort.Slice(ob.Lends, func(i, j int) bool {
|
||||
return ob.Lends[i].InterestRate.Cmp(ob.Lends[j].InterestRate) < 0
|
||||
})
|
||||
}
|
||||
|
||||
// rebuildBorrowIndex rebuilds the borrow index
|
||||
func (ob *LendingOrderBook) rebuildBorrowIndex() {
|
||||
ob.borrowIndex = make(map[common.Hash]int)
|
||||
for i, borrow := range ob.Borrows {
|
||||
ob.borrowIndex[borrow.ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildLendIndex rebuilds the lend index
|
||||
func (ob *LendingOrderBook) rebuildLendIndex() {
|
||||
ob.lendIndex = make(map[common.Hash]int)
|
||||
for i, lend := range ob.Lends {
|
||||
ob.lendIndex[lend.ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
// GetMarketRate returns the market interest rate
|
||||
func (ob *LendingOrderBook) GetMarketRate() *big.Int {
|
||||
ob.lock.RLock()
|
||||
defer ob.lock.RUnlock()
|
||||
|
||||
bestBorrow := ob.GetBestBorrow()
|
||||
bestLend := ob.GetBestLend()
|
||||
|
||||
if bestBorrow == nil && bestLend == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if bestBorrow == nil {
|
||||
return new(big.Int).Set(bestLend.InterestRate)
|
||||
}
|
||||
|
||||
if bestLend == nil {
|
||||
return new(big.Int).Set(bestBorrow.InterestRate)
|
||||
}
|
||||
|
||||
// Return midpoint
|
||||
sum := new(big.Int).Add(bestBorrow.InterestRate, bestLend.InterestRate)
|
||||
return sum.Div(sum, big.NewInt(2))
|
||||
}
|
||||
416
XDCxlending/lendingstate/statedb.go
Normal file
416
XDCxlending/lendingstate/statedb.go
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
// Package lendingstate provides lending state management for XDCxLending
|
||||
package lendingstate
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// LendingStateDB represents the lending state database
|
||||
type LendingStateDB struct {
|
||||
db Database
|
||||
trie Trie
|
||||
|
||||
// Order books cache
|
||||
orderBooks map[common.Hash]*LendingOrderBook
|
||||
orderBooksDirty map[common.Hash]struct{}
|
||||
|
||||
// Orders cache
|
||||
orders map[common.Hash]*LendingOrderState
|
||||
ordersDirty map[common.Hash]struct{}
|
||||
|
||||
// Loans cache
|
||||
loans map[common.Hash]*LoanState
|
||||
loansDirty map[common.Hash]struct{}
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// New creates a new lending state database
|
||||
func New(root common.Hash, db Database) (*LendingStateDB, error) {
|
||||
tr, err := db.OpenTrie(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LendingStateDB{
|
||||
db: db,
|
||||
trie: tr,
|
||||
orderBooks: make(map[common.Hash]*LendingOrderBook),
|
||||
orderBooksDirty: make(map[common.Hash]struct{}),
|
||||
orders: make(map[common.Hash]*LendingOrderState),
|
||||
ordersDirty: make(map[common.Hash]struct{}),
|
||||
loans: make(map[common.Hash]*LoanState),
|
||||
loansDirty: make(map[common.Hash]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLendingOrder returns a lending order by ID
|
||||
func (s *LendingStateDB) GetLendingOrder(orderID common.Hash) *LendingOrderState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if order, exists := s.orders[orderID]; exists {
|
||||
return order
|
||||
}
|
||||
|
||||
// Load from trie
|
||||
order := s.loadLendingOrder(orderID)
|
||||
if order != nil {
|
||||
s.orders[orderID] = order
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
// SetLendingOrder sets a lending order in the state
|
||||
func (s *LendingStateDB) SetLendingOrder(order interface{}) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Convert order interface to LendingOrderState
|
||||
orderState := &LendingOrderState{}
|
||||
// Implementation depends on order type
|
||||
s.orders[orderState.ID] = orderState
|
||||
s.ordersDirty[orderState.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// UpdateLendingOrder updates a lending order in the state
|
||||
func (s *LendingStateDB) UpdateLendingOrder(order interface{}) {
|
||||
s.SetLendingOrder(order)
|
||||
}
|
||||
|
||||
// DeleteLendingOrder deletes a lending order from the state
|
||||
func (s *LendingStateDB) DeleteLendingOrder(orderID common.Hash) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.orders, orderID)
|
||||
s.ordersDirty[orderID] = struct{}{}
|
||||
}
|
||||
|
||||
// GetLendingOrderBook returns a lending order book by pair key
|
||||
func (s *LendingStateDB) GetLendingOrderBook(pairKey common.Hash) *LendingOrderBook {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if ob, exists := s.orderBooks[pairKey]; exists {
|
||||
return ob
|
||||
}
|
||||
|
||||
// Load from trie
|
||||
ob := s.loadLendingOrderBook(pairKey)
|
||||
if ob != nil {
|
||||
s.orderBooks[pairKey] = ob
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
// GetOrCreateLendingOrderBook returns or creates a lending order book
|
||||
func (s *LendingStateDB) GetOrCreateLendingOrderBook(pairKey common.Hash) *LendingOrderBook {
|
||||
ob := s.GetLendingOrderBook(pairKey)
|
||||
if ob == nil {
|
||||
s.lock.Lock()
|
||||
ob = NewLendingOrderBook(pairKey)
|
||||
s.orderBooks[pairKey] = ob
|
||||
s.orderBooksDirty[pairKey] = struct{}{}
|
||||
s.lock.Unlock()
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
// GetLoan returns a loan by ID
|
||||
func (s *LendingStateDB) GetLoan(loanID common.Hash) *LoanState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if loan, exists := s.loans[loanID]; exists {
|
||||
return loan
|
||||
}
|
||||
|
||||
// Load from trie
|
||||
loan := s.loadLoan(loanID)
|
||||
if loan != nil {
|
||||
s.loans[loanID] = loan
|
||||
}
|
||||
return loan
|
||||
}
|
||||
|
||||
// SetLoan sets a loan in the state
|
||||
func (s *LendingStateDB) SetLoan(loan *LoanState) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.loans[loan.ID] = loan
|
||||
s.loansDirty[loan.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// UpdateLoan updates a loan in the state
|
||||
func (s *LendingStateDB) UpdateLoan(loan interface{}) {
|
||||
// Convert interface to LoanState
|
||||
if l, ok := loan.(*LoanState); ok {
|
||||
s.SetLoan(l)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllLoans returns all loans
|
||||
func (s *LendingStateDB) GetAllLoans() []*LoanState {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
loans := make([]*LoanState, 0, len(s.loans))
|
||||
for _, loan := range s.loans {
|
||||
loans = append(loans, loan.Copy())
|
||||
}
|
||||
return loans
|
||||
}
|
||||
|
||||
// Commit commits all changes to the underlying database
|
||||
func (s *LendingStateDB) Commit() (common.Hash, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Commit dirty order books
|
||||
for pairKey := range s.orderBooksDirty {
|
||||
if ob, exists := s.orderBooks[pairKey]; exists {
|
||||
if err := s.commitOrderBook(ob); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.orderBooksDirty, pairKey)
|
||||
}
|
||||
|
||||
// Commit dirty orders
|
||||
for orderID := range s.ordersDirty {
|
||||
if order, exists := s.orders[orderID]; exists {
|
||||
if err := s.commitOrder(order); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.ordersDirty, orderID)
|
||||
}
|
||||
|
||||
// Commit dirty loans
|
||||
for loanID := range s.loansDirty {
|
||||
if loan, exists := s.loans[loanID]; exists {
|
||||
if err := s.commitLoan(loan); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
}
|
||||
delete(s.loansDirty, loanID)
|
||||
}
|
||||
|
||||
// Commit trie
|
||||
root, err := s.trie.Commit(nil)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
log.Debug("Lending state committed", "root", root.Hex())
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// Copy creates a copy of the lending state
|
||||
func (s *LendingStateDB) Copy() *LendingStateDB {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
state := &LendingStateDB{
|
||||
db: s.db,
|
||||
trie: s.db.CopyTrie(s.trie),
|
||||
orderBooks: make(map[common.Hash]*LendingOrderBook),
|
||||
orderBooksDirty: make(map[common.Hash]struct{}),
|
||||
orders: make(map[common.Hash]*LendingOrderState),
|
||||
ordersDirty: make(map[common.Hash]struct{}),
|
||||
loans: make(map[common.Hash]*LoanState),
|
||||
loansDirty: make(map[common.Hash]struct{}),
|
||||
}
|
||||
|
||||
// Copy order books
|
||||
for key, ob := range s.orderBooks {
|
||||
state.orderBooks[key] = ob.Copy()
|
||||
}
|
||||
for key := range s.orderBooksDirty {
|
||||
state.orderBooksDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Copy orders
|
||||
for key, order := range s.orders {
|
||||
state.orders[key] = order.Copy()
|
||||
}
|
||||
for key := range s.ordersDirty {
|
||||
state.ordersDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Copy loans
|
||||
for key, loan := range s.loans {
|
||||
state.loans[key] = loan.Copy()
|
||||
}
|
||||
for key := range s.loansDirty {
|
||||
state.loansDirty[key] = struct{}{}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// loadLendingOrder loads a lending order from the trie
|
||||
func (s *LendingStateDB) loadLendingOrder(orderID common.Hash) *LendingOrderState {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadLendingOrderBook loads a lending order book from the trie
|
||||
func (s *LendingStateDB) loadLendingOrderBook(pairKey common.Hash) *LendingOrderBook {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadLoan loads a loan from the trie
|
||||
func (s *LendingStateDB) loadLoan(loanID common.Hash) *LoanState {
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitOrderBook commits a lending order book
|
||||
func (s *LendingStateDB) commitOrderBook(ob *LendingOrderBook) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitOrder commits a lending order
|
||||
func (s *LendingStateDB) commitOrder(order *LendingOrderState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitLoan commits a loan
|
||||
func (s *LendingStateDB) commitLoan(loan *LoanState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Database wraps access to tries and contract code
|
||||
type Database interface {
|
||||
// OpenTrie opens the main lending state trie
|
||||
OpenTrie(root common.Hash) (Trie, error)
|
||||
|
||||
// CopyTrie returns an independent copy of the given trie
|
||||
CopyTrie(Trie) Trie
|
||||
}
|
||||
|
||||
// Trie is a XDCx Merkle Patricia trie
|
||||
type Trie interface {
|
||||
GetKey([]byte) []byte
|
||||
TryGet(key []byte) ([]byte, error)
|
||||
TryUpdate(key, value []byte) error
|
||||
TryDelete(key []byte) error
|
||||
Hash() common.Hash
|
||||
Commit(onleaf trie.LeafCallback) (common.Hash, error)
|
||||
NodeIterator(startKey []byte) trie.NodeIterator
|
||||
}
|
||||
|
||||
// NewDatabase creates a new lending state database
|
||||
func NewDatabase(db ethdb.Database) Database {
|
||||
return &cachingDB{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type cachingDB struct {
|
||||
db ethdb.Database
|
||||
}
|
||||
|
||||
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *cachingDB) CopyTrie(t Trie) Trie {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LendingOrderState represents a lending order in the state
|
||||
type LendingOrderState struct {
|
||||
ID common.Hash
|
||||
UserAddress common.Address
|
||||
RelayerAddress common.Address
|
||||
LendingToken common.Address
|
||||
CollateralToken common.Address
|
||||
Side uint8
|
||||
Type uint8
|
||||
InterestRate *big.Int
|
||||
Term uint64
|
||||
Quantity *big.Int
|
||||
FilledQuantity *big.Int
|
||||
Status uint8
|
||||
Nonce uint64
|
||||
Timestamp uint64
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// Copy creates a copy of the lending order state
|
||||
func (o *LendingOrderState) Copy() *LendingOrderState {
|
||||
return &LendingOrderState{
|
||||
ID: o.ID,
|
||||
UserAddress: o.UserAddress,
|
||||
RelayerAddress: o.RelayerAddress,
|
||||
LendingToken: o.LendingToken,
|
||||
CollateralToken: o.CollateralToken,
|
||||
Side: o.Side,
|
||||
Type: o.Type,
|
||||
InterestRate: new(big.Int).Set(o.InterestRate),
|
||||
Term: o.Term,
|
||||
Quantity: new(big.Int).Set(o.Quantity),
|
||||
FilledQuantity: new(big.Int).Set(o.FilledQuantity),
|
||||
Status: o.Status,
|
||||
Nonce: o.Nonce,
|
||||
Timestamp: o.Timestamp,
|
||||
Signature: append([]byte{}, o.Signature...),
|
||||
}
|
||||
}
|
||||
|
||||
// RemainingQuantity returns the remaining quantity
|
||||
func (o *LendingOrderState) RemainingQuantity() *big.Int {
|
||||
return new(big.Int).Sub(o.Quantity, o.FilledQuantity)
|
||||
}
|
||||
|
||||
// IsFilled returns whether the order is filled
|
||||
func (o *LendingOrderState) IsFilled() bool {
|
||||
return o.FilledQuantity.Cmp(o.Quantity) >= 0
|
||||
}
|
||||
|
||||
// LoanState represents a loan in the state
|
||||
type LoanState struct {
|
||||
ID common.Hash
|
||||
BorrowerAddress common.Address
|
||||
LenderAddress common.Address
|
||||
LendingToken common.Address
|
||||
CollateralToken common.Address
|
||||
Principal *big.Int
|
||||
CollateralAmount *big.Int
|
||||
InterestRate *big.Int
|
||||
Term uint64
|
||||
StartTime uint64
|
||||
ExpiryTime uint64
|
||||
Status uint8
|
||||
LiquidationPrice *big.Int
|
||||
}
|
||||
|
||||
// Copy creates a copy of the loan state
|
||||
func (l *LoanState) Copy() *LoanState {
|
||||
return &LoanState{
|
||||
ID: l.ID,
|
||||
BorrowerAddress: l.BorrowerAddress,
|
||||
LenderAddress: l.LenderAddress,
|
||||
LendingToken: l.LendingToken,
|
||||
CollateralToken: l.CollateralToken,
|
||||
Principal: new(big.Int).Set(l.Principal),
|
||||
CollateralAmount: new(big.Int).Set(l.CollateralAmount),
|
||||
InterestRate: new(big.Int).Set(l.InterestRate),
|
||||
Term: l.Term,
|
||||
StartTime: l.StartTime,
|
||||
ExpiryTime: l.ExpiryTime,
|
||||
Status: l.Status,
|
||||
LiquidationPrice: new(big.Int).Set(l.LiquidationPrice),
|
||||
}
|
||||
}
|
||||
176
XDCxlending/liquidator.go
Normal file
176
XDCxlending/liquidator.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCxlending/lendingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// Liquidator handles loan liquidation
|
||||
type Liquidator struct {
|
||||
lending *XDCxLending
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewLiquidator creates a new liquidator
|
||||
func NewLiquidator(lending *XDCxLending) *Liquidator {
|
||||
return &Liquidator{
|
||||
lending: lending,
|
||||
}
|
||||
}
|
||||
|
||||
// Liquidate liquidates an undercollateralized loan
|
||||
func (l *Liquidator) Liquidate(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, loanID common.Hash) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
loan := lendingState.GetLoan(loanID)
|
||||
if loan == nil {
|
||||
return ErrLoanNotFound
|
||||
}
|
||||
|
||||
// Check if loan is eligible for liquidation
|
||||
collateralPrice := l.getCollateralPrice(statedb, loan.CollateralToken)
|
||||
if !l.isLiquidatable(loan, collateralPrice) {
|
||||
return ErrInsufficientCollateral
|
||||
}
|
||||
|
||||
// Perform liquidation
|
||||
if err := l.performLiquidation(statedb, lendingState, loan, collateralPrice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update loan status
|
||||
loan.Status = LoanStatusLiquidated
|
||||
lendingState.UpdateLoan(loan)
|
||||
|
||||
log.Info("Loan liquidated", "loanID", loanID.Hex())
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckLoansForLiquidation checks all active loans for liquidation
|
||||
func (l *Liquidator) CheckLoansForLiquidation(lendingState *lendingstate.LendingStateDB) ([]common.Hash, error) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
liquidatableLoans := make([]common.Hash, 0)
|
||||
allLoans := lendingState.GetAllLoans()
|
||||
|
||||
for _, loan := range allLoans {
|
||||
if loan.Status != LoanStatusActive {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check collateral ratio
|
||||
collateralPrice := big.NewInt(1e18) // Placeholder - should get from oracle
|
||||
if l.isLiquidatable(loan, collateralPrice) {
|
||||
liquidatableLoans = append(liquidatableLoans, loan.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return liquidatableLoans, nil
|
||||
}
|
||||
|
||||
// isLiquidatable checks if a loan is eligible for liquidation
|
||||
func (l *Liquidator) isLiquidatable(loan *Loan, collateralPrice *big.Int) bool {
|
||||
// Calculate current collateral ratio
|
||||
ratio := loan.GetCollateralRatio(collateralPrice)
|
||||
|
||||
// Compare with liquidation threshold
|
||||
liquidationRate := l.lending.GetConfig().LiquidationRate
|
||||
return ratio.Cmp(liquidationRate) < 0
|
||||
}
|
||||
|
||||
// performLiquidation performs the actual liquidation
|
||||
func (l *Liquidator) performLiquidation(statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, loan *Loan, collateralPrice *big.Int) error {
|
||||
// Calculate liquidation amounts
|
||||
principal := loan.Principal
|
||||
interest := l.lending.calculateInterest(loan)
|
||||
totalDebt := new(big.Int).Add(principal, interest)
|
||||
|
||||
// Calculate collateral to sell
|
||||
collateralToSell := new(big.Int).Mul(totalDebt, big.NewInt(1e18))
|
||||
collateralToSell = collateralToSell.Div(collateralToSell, collateralPrice)
|
||||
|
||||
// Add liquidation penalty (5%)
|
||||
penalty := new(big.Int).Mul(collateralToSell, big.NewInt(5))
|
||||
penalty = penalty.Div(penalty, big.NewInt(100))
|
||||
totalCollateralSold := new(big.Int).Add(collateralToSell, penalty)
|
||||
|
||||
// Ensure we don't sell more than available
|
||||
if totalCollateralSold.Cmp(loan.CollateralAmount) > 0 {
|
||||
totalCollateralSold = loan.CollateralAmount
|
||||
}
|
||||
|
||||
// Calculate remaining collateral for borrower
|
||||
remainingCollateral := new(big.Int).Sub(loan.CollateralAmount, totalCollateralSold)
|
||||
|
||||
log.Debug("Liquidation performed",
|
||||
"loanID", loan.ID.Hex(),
|
||||
"totalDebt", totalDebt,
|
||||
"collateralSold", totalCollateralSold,
|
||||
"remainingCollateral", remainingCollateral,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCollateralPrice gets the current price of the collateral token
|
||||
func (l *Liquidator) getCollateralPrice(statedb *state.StateDB, collateralToken common.Address) *big.Int {
|
||||
// This should query the price oracle
|
||||
// Placeholder implementation
|
||||
return big.NewInt(1e18)
|
||||
}
|
||||
|
||||
// LiquidationEvent represents a liquidation event
|
||||
type LiquidationEvent struct {
|
||||
LoanID common.Hash `json:"loanId"`
|
||||
Borrower common.Address `json:"borrower"`
|
||||
Lender common.Address `json:"lender"`
|
||||
Liquidator common.Address `json:"liquidator"`
|
||||
CollateralSold *big.Int `json:"collateralSold"`
|
||||
DebtRepaid *big.Int `json:"debtRepaid"`
|
||||
LiquidationBonus *big.Int `json:"liquidationBonus"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TxHash common.Hash `json:"txHash"`
|
||||
}
|
||||
|
||||
// RecallLoans recalls loans that have exceeded their term
|
||||
func (l *Liquidator) RecallLoans(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, currentTime uint64) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
allLoans := lendingState.GetAllLoans()
|
||||
for _, loan := range allLoans {
|
||||
if loan.Status != LoanStatusActive {
|
||||
continue
|
||||
}
|
||||
|
||||
if loan.IsExpired(currentTime) {
|
||||
log.Info("Recalling expired loan", "loanID", loan.ID.Hex())
|
||||
// Mark for liquidation or automatic repayment
|
||||
loan.Status = LoanStatusDefaulted
|
||||
lendingState.UpdateLoan(loan)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLiquidationPrice calculates the price at which a loan becomes liquidatable
|
||||
func (l *Liquidator) GetLiquidationPrice(loan *Loan) *big.Int {
|
||||
// Liquidation price = (Principal * LiquidationRate) / (CollateralAmount * 100)
|
||||
liquidationRate := l.lending.GetConfig().LiquidationRate
|
||||
numerator := new(big.Int).Mul(loan.Principal, liquidationRate)
|
||||
denominator := new(big.Int).Mul(loan.CollateralAmount, big.NewInt(100))
|
||||
price := new(big.Int).Div(numerator, denominator)
|
||||
return price
|
||||
}
|
||||
259
XDCxlending/order.go
Normal file
259
XDCxlending/order.go
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// OrderSide represents the side of a lending order (borrow/lend)
|
||||
type OrderSide uint8
|
||||
|
||||
const (
|
||||
// Borrow represents a borrow order
|
||||
Borrow OrderSide = iota
|
||||
// Lend represents a lend order
|
||||
Lend
|
||||
)
|
||||
|
||||
// OrderType represents the type of a lending order
|
||||
type OrderType uint8
|
||||
|
||||
const (
|
||||
// LimitOrder represents a limit order
|
||||
LimitOrder OrderType = iota
|
||||
// MarketOrder represents a market order
|
||||
MarketOrder
|
||||
)
|
||||
|
||||
// OrderStatus represents the status of a lending order
|
||||
type OrderStatus uint8
|
||||
|
||||
const (
|
||||
// OrderStatusNew represents a new order
|
||||
OrderStatusNew OrderStatus = iota
|
||||
// OrderStatusPartialFilled represents a partially filled order
|
||||
OrderStatusPartialFilled
|
||||
// OrderStatusFilled represents a filled order
|
||||
OrderStatusFilled
|
||||
// OrderStatusCancelled represents a cancelled order
|
||||
OrderStatusCancelled
|
||||
// OrderStatusRejected represents a rejected order
|
||||
OrderStatusRejected
|
||||
)
|
||||
|
||||
// LoanStatus represents the status of a loan
|
||||
type LoanStatus uint8
|
||||
|
||||
const (
|
||||
// LoanStatusActive represents an active loan
|
||||
LoanStatusActive LoanStatus = iota
|
||||
// LoanStatusRepaid represents a repaid loan
|
||||
LoanStatusRepaid
|
||||
// LoanStatusLiquidated represents a liquidated loan
|
||||
LoanStatusLiquidated
|
||||
// LoanStatusDefaulted represents a defaulted loan
|
||||
LoanStatusDefaulted
|
||||
)
|
||||
|
||||
// LendingOrder represents a lending order
|
||||
type LendingOrder struct {
|
||||
ID common.Hash `json:"id"`
|
||||
UserAddress common.Address `json:"userAddress"`
|
||||
RelayerAddress common.Address `json:"relayerAddress"`
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Side OrderSide `json:"side"`
|
||||
Type OrderType `json:"type"`
|
||||
InterestRate *big.Int `json:"interestRate"` // Annual rate in basis points
|
||||
Term uint64 `json:"term"` // Duration in seconds
|
||||
Quantity *big.Int `json:"quantity"`
|
||||
FilledQuantity *big.Int `json:"filledQuantity"`
|
||||
Status OrderStatus `json:"status"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Signature []byte `json:"signature"`
|
||||
}
|
||||
|
||||
// NewLendingOrder creates a new lending order
|
||||
func NewLendingOrder(
|
||||
userAddress common.Address,
|
||||
lendingToken, collateralToken common.Address,
|
||||
side OrderSide,
|
||||
orderType OrderType,
|
||||
interestRate *big.Int,
|
||||
term uint64,
|
||||
quantity *big.Int,
|
||||
nonce uint64,
|
||||
relayerAddress common.Address,
|
||||
) *LendingOrder {
|
||||
order := &LendingOrder{
|
||||
UserAddress: userAddress,
|
||||
RelayerAddress: relayerAddress,
|
||||
LendingToken: lendingToken,
|
||||
CollateralToken: collateralToken,
|
||||
Side: side,
|
||||
Type: orderType,
|
||||
InterestRate: new(big.Int).Set(interestRate),
|
||||
Term: term,
|
||||
Quantity: new(big.Int).Set(quantity),
|
||||
FilledQuantity: big.NewInt(0),
|
||||
Status: OrderStatusNew,
|
||||
Nonce: nonce,
|
||||
}
|
||||
order.ID = order.ComputeHash()
|
||||
return order
|
||||
}
|
||||
|
||||
// ComputeHash computes the hash of the lending order
|
||||
func (o *LendingOrder) ComputeHash() common.Hash {
|
||||
data := append(o.UserAddress.Bytes(), o.LendingToken.Bytes()...)
|
||||
data = append(data, o.CollateralToken.Bytes()...)
|
||||
data = append(data, byte(o.Side))
|
||||
data = append(data, byte(o.Type))
|
||||
data = append(data, common.BigToHash(o.InterestRate).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(o.Term))).Bytes()...)
|
||||
data = append(data, common.BigToHash(o.Quantity).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(o.Nonce))).Bytes()...)
|
||||
data = append(data, o.RelayerAddress.Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Sign signs the lending order with the given private key
|
||||
func (o *LendingOrder) Sign(privateKey *ecdsa.PrivateKey) error {
|
||||
hash := o.ComputeHash()
|
||||
sig, err := crypto.Sign(hash.Bytes(), privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Signature = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignature verifies the lending order signature
|
||||
func (o *LendingOrder) VerifySignature() bool {
|
||||
if len(o.Signature) != 65 {
|
||||
return false
|
||||
}
|
||||
hash := o.ComputeHash()
|
||||
pubKey, err := crypto.SigToPub(hash.Bytes(), o.Signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
|
||||
return recoveredAddr == o.UserAddress
|
||||
}
|
||||
|
||||
// RemainingQuantity returns the remaining quantity to be filled
|
||||
func (o *LendingOrder) RemainingQuantity() *big.Int {
|
||||
return new(big.Int).Sub(o.Quantity, o.FilledQuantity)
|
||||
}
|
||||
|
||||
// IsFilled returns whether the order is fully filled
|
||||
func (o *LendingOrder) IsFilled() bool {
|
||||
return o.FilledQuantity.Cmp(o.Quantity) >= 0
|
||||
}
|
||||
|
||||
// Clone creates a copy of the lending order
|
||||
func (o *LendingOrder) Clone() *LendingOrder {
|
||||
return &LendingOrder{
|
||||
ID: o.ID,
|
||||
UserAddress: o.UserAddress,
|
||||
RelayerAddress: o.RelayerAddress,
|
||||
LendingToken: o.LendingToken,
|
||||
CollateralToken: o.CollateralToken,
|
||||
Side: o.Side,
|
||||
Type: o.Type,
|
||||
InterestRate: new(big.Int).Set(o.InterestRate),
|
||||
Term: o.Term,
|
||||
Quantity: new(big.Int).Set(o.Quantity),
|
||||
FilledQuantity: new(big.Int).Set(o.FilledQuantity),
|
||||
Status: o.Status,
|
||||
Nonce: o.Nonce,
|
||||
Timestamp: o.Timestamp,
|
||||
Signature: append([]byte{}, o.Signature...),
|
||||
}
|
||||
}
|
||||
|
||||
// PairKey returns the lending pair key
|
||||
func (o *LendingOrder) PairKey() common.Hash {
|
||||
return GetLendingPairKey(o.LendingToken, o.CollateralToken, o.Term)
|
||||
}
|
||||
|
||||
// GetLendingPairKey returns the lending pair key
|
||||
func GetLendingPairKey(lendingToken, collateralToken common.Address, term uint64) common.Hash {
|
||||
data := append(lendingToken.Bytes(), collateralToken.Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(term))).Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Loan represents an active loan
|
||||
type Loan struct {
|
||||
ID common.Hash `json:"id"`
|
||||
BorrowerAddress common.Address `json:"borrowerAddress"`
|
||||
LenderAddress common.Address `json:"lenderAddress"`
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Principal *big.Int `json:"principal"`
|
||||
CollateralAmount *big.Int `json:"collateralAmount"`
|
||||
InterestRate *big.Int `json:"interestRate"`
|
||||
Term uint64 `json:"term"`
|
||||
StartTime uint64 `json:"startTime"`
|
||||
ExpiryTime uint64 `json:"expiryTime"`
|
||||
Status LoanStatus `json:"status"`
|
||||
LiquidationPrice *big.Int `json:"liquidationPrice"`
|
||||
}
|
||||
|
||||
// NewLoan creates a new loan
|
||||
func NewLoan(
|
||||
borrower, lender common.Address,
|
||||
lendingToken, collateralToken common.Address,
|
||||
principal, collateral *big.Int,
|
||||
interestRate *big.Int,
|
||||
term, startTime uint64,
|
||||
) *Loan {
|
||||
loan := &Loan{
|
||||
BorrowerAddress: borrower,
|
||||
LenderAddress: lender,
|
||||
LendingToken: lendingToken,
|
||||
CollateralToken: collateralToken,
|
||||
Principal: new(big.Int).Set(principal),
|
||||
CollateralAmount: new(big.Int).Set(collateral),
|
||||
InterestRate: new(big.Int).Set(interestRate),
|
||||
Term: term,
|
||||
StartTime: startTime,
|
||||
ExpiryTime: startTime + term,
|
||||
Status: LoanStatusActive,
|
||||
}
|
||||
loan.ID = loan.ComputeHash()
|
||||
return loan
|
||||
}
|
||||
|
||||
// ComputeHash computes the loan hash
|
||||
func (l *Loan) ComputeHash() common.Hash {
|
||||
data := append(l.BorrowerAddress.Bytes(), l.LenderAddress.Bytes()...)
|
||||
data = append(data, l.LendingToken.Bytes()...)
|
||||
data = append(data, l.CollateralToken.Bytes()...)
|
||||
data = append(data, common.BigToHash(l.Principal).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(l.StartTime))).Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// IsExpired returns whether the loan has expired
|
||||
func (l *Loan) IsExpired(currentTime uint64) bool {
|
||||
return currentTime >= l.ExpiryTime
|
||||
}
|
||||
|
||||
// GetCollateralRatio returns the current collateral ratio
|
||||
func (l *Loan) GetCollateralRatio(collateralPrice *big.Int) *big.Int {
|
||||
// Ratio = (CollateralAmount * CollateralPrice) / Principal * 100
|
||||
collateralValue := new(big.Int).Mul(l.CollateralAmount, collateralPrice)
|
||||
ratio := new(big.Int).Mul(collateralValue, big.NewInt(100))
|
||||
ratio = ratio.Div(ratio, l.Principal)
|
||||
return ratio
|
||||
}
|
||||
306
XDCxlending/order_processor.go
Normal file
306
XDCxlending/order_processor.go
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/XDCxlending/lendingstate"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInsufficientLendingBalance is returned when lending balance is insufficient
|
||||
ErrInsufficientLendingBalance = errors.New("insufficient lending balance")
|
||||
// ErrOrderAlreadyExists is returned when order already exists
|
||||
ErrOrderAlreadyExists = errors.New("order already exists")
|
||||
// ErrOrderCannotBeCancelled is returned when order cannot be cancelled
|
||||
ErrOrderCannotBeCancelled = errors.New("order cannot be cancelled")
|
||||
// ErrInvalidSignature is returned when signature is invalid
|
||||
ErrInvalidSignature = errors.New("invalid signature")
|
||||
)
|
||||
|
||||
// OrderProcessor handles lending order processing
|
||||
type OrderProcessor struct {
|
||||
lending *XDCxLending
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewOrderProcessor creates a new order processor
|
||||
func NewOrderProcessor(lending *XDCxLending) *OrderProcessor {
|
||||
return &OrderProcessor{
|
||||
lending: lending,
|
||||
}
|
||||
}
|
||||
|
||||
// Process processes a lending order and returns matched trades
|
||||
func (op *OrderProcessor) Process(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, order *LendingOrder) ([]*LendingTrade, error) {
|
||||
op.lock.Lock()
|
||||
defer op.lock.Unlock()
|
||||
|
||||
// Validate order
|
||||
if err := op.validateOrder(order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !order.VerifySignature() {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
|
||||
// Check balance and collateral
|
||||
if err := op.checkBalanceAndCollateral(statedb, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Match order
|
||||
trades, err := op.matchOrder(lendingState, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If order is not fully filled, add to order book
|
||||
if !order.IsFilled() && order.Type == LimitOrder {
|
||||
if err := op.addToOrderBook(lendingState, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Lending order processed", "orderID", order.ID.Hex(), "trades", len(trades))
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// Cancel cancels an existing lending order
|
||||
func (op *OrderProcessor) Cancel(ctx context.Context, statedb *state.StateDB, lendingState *lendingstate.LendingStateDB, orderID common.Hash) error {
|
||||
op.lock.Lock()
|
||||
defer op.lock.Unlock()
|
||||
|
||||
// Get order from state
|
||||
order := lendingState.GetLendingOrder(orderID)
|
||||
if order == nil {
|
||||
return ErrLoanNotFound
|
||||
}
|
||||
|
||||
// Check if order can be cancelled
|
||||
if order.Status == OrderStatusFilled || order.Status == OrderStatusCancelled {
|
||||
return ErrOrderCannotBeCancelled
|
||||
}
|
||||
|
||||
// Remove from order book
|
||||
if err := op.removeFromOrderBook(lendingState, order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update order status
|
||||
order.Status = OrderStatusCancelled
|
||||
lendingState.UpdateLendingOrder(order)
|
||||
|
||||
log.Debug("Lending order cancelled", "orderID", orderID.Hex())
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateOrder validates a lending order
|
||||
func (op *OrderProcessor) validateOrder(order *LendingOrder) error {
|
||||
if order.InterestRate == nil || order.InterestRate.Sign() < 0 {
|
||||
return ErrInvalidInterestRate
|
||||
}
|
||||
if order.Term == 0 {
|
||||
return ErrInvalidTerm
|
||||
}
|
||||
if order.Quantity == nil || order.Quantity.Sign() <= 0 {
|
||||
return errors.New("invalid quantity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkBalanceAndCollateral checks if user has sufficient balance and collateral
|
||||
func (op *OrderProcessor) checkBalanceAndCollateral(statedb *state.StateDB, order *LendingOrder) error {
|
||||
if order.Side == Borrow {
|
||||
// Borrowers need collateral
|
||||
collateralNeeded := op.calculateRequiredCollateral(order)
|
||||
balance := op.getTokenBalance(statedb, order.CollateralToken, order.UserAddress)
|
||||
if balance.Cmp(collateralNeeded) < 0 {
|
||||
return ErrInsufficientCollateral
|
||||
}
|
||||
} else {
|
||||
// Lenders need the lending token
|
||||
balance := op.getTokenBalance(statedb, order.LendingToken, order.UserAddress)
|
||||
if balance.Cmp(order.Quantity) < 0 {
|
||||
return ErrInsufficientLendingBalance
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateRequiredCollateral calculates the required collateral for a borrow order
|
||||
func (op *OrderProcessor) calculateRequiredCollateral(order *LendingOrder) *big.Int {
|
||||
// Required collateral = quantity * minCollateralRatio / 100
|
||||
minCollateral := op.lending.GetConfig().MinCollateral
|
||||
required := new(big.Int).Mul(order.Quantity, minCollateral)
|
||||
required = required.Div(required, big.NewInt(100))
|
||||
return required
|
||||
}
|
||||
|
||||
// getTokenBalance gets the token balance for a user
|
||||
func (op *OrderProcessor) getTokenBalance(statedb *state.StateDB, token, user common.Address) *big.Int {
|
||||
// This would call the ERC20 balanceOf function
|
||||
// Simplified implementation
|
||||
return statedb.GetBalance(user).ToBig()
|
||||
}
|
||||
|
||||
// matchOrder matches a lending order against the order book
|
||||
func (op *OrderProcessor) matchOrder(lendingState *lendingstate.LendingStateDB, order *LendingOrder) ([]*LendingTrade, error) {
|
||||
var trades []*LendingTrade
|
||||
|
||||
pairKey := order.PairKey()
|
||||
orderBook := lendingState.GetLendingOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// Match based on order side
|
||||
if order.Side == Borrow {
|
||||
trades = op.matchBorrowOrder(lendingState, orderBook, order)
|
||||
} else {
|
||||
trades = op.matchLendOrder(lendingState, orderBook, order)
|
||||
}
|
||||
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// matchBorrowOrder matches a borrow order against lend orders
|
||||
func (op *OrderProcessor) matchBorrowOrder(lendingState *lendingstate.LendingStateDB, orderBook *lendingstate.LendingOrderBook, borrowOrder *LendingOrder) []*LendingTrade {
|
||||
var trades []*LendingTrade
|
||||
|
||||
// Match against lend orders from lowest to highest interest rate
|
||||
for !borrowOrder.IsFilled() {
|
||||
bestLend := orderBook.GetBestLend()
|
||||
if bestLend == nil || bestLend.InterestRate.Cmp(borrowOrder.InterestRate) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
trade := op.executeTrade(borrowOrder, bestLend)
|
||||
trades = append(trades, trade)
|
||||
|
||||
if bestLend.IsFilled() {
|
||||
orderBook.RemoveLend(bestLend.ID)
|
||||
} else {
|
||||
lendingState.UpdateLendingOrder(bestLend)
|
||||
}
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// matchLendOrder matches a lend order against borrow orders
|
||||
func (op *OrderProcessor) matchLendOrder(lendingState *lendingstate.LendingStateDB, orderBook *lendingstate.LendingOrderBook, lendOrder *LendingOrder) []*LendingTrade {
|
||||
var trades []*LendingTrade
|
||||
|
||||
// Match against borrow orders from highest to lowest interest rate
|
||||
for !lendOrder.IsFilled() {
|
||||
bestBorrow := orderBook.GetBestBorrow()
|
||||
if bestBorrow == nil || bestBorrow.InterestRate.Cmp(lendOrder.InterestRate) < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
trade := op.executeTrade(bestBorrow, lendOrder)
|
||||
trades = append(trades, trade)
|
||||
|
||||
if bestBorrow.IsFilled() {
|
||||
orderBook.RemoveBorrow(bestBorrow.ID)
|
||||
} else {
|
||||
lendingState.UpdateLendingOrder(bestBorrow)
|
||||
}
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// executeTrade executes a lending trade between two orders
|
||||
func (op *OrderProcessor) executeTrade(borrowOrder, lendOrder *LendingOrder) *LendingTrade {
|
||||
// Determine trade quantity
|
||||
borrowRemaining := borrowOrder.RemainingQuantity()
|
||||
lendRemaining := lendOrder.RemainingQuantity()
|
||||
|
||||
var tradeQuantity *big.Int
|
||||
if borrowRemaining.Cmp(lendRemaining) < 0 {
|
||||
tradeQuantity = borrowRemaining
|
||||
} else {
|
||||
tradeQuantity = lendRemaining
|
||||
}
|
||||
|
||||
// Use market maker's interest rate
|
||||
tradeInterestRate := lendOrder.InterestRate
|
||||
|
||||
// Update filled quantities
|
||||
borrowOrder.FilledQuantity = new(big.Int).Add(borrowOrder.FilledQuantity, tradeQuantity)
|
||||
lendOrder.FilledQuantity = new(big.Int).Add(lendOrder.FilledQuantity, tradeQuantity)
|
||||
|
||||
// Update order statuses
|
||||
if borrowOrder.IsFilled() {
|
||||
borrowOrder.Status = OrderStatusFilled
|
||||
} else {
|
||||
borrowOrder.Status = OrderStatusPartialFilled
|
||||
}
|
||||
|
||||
if lendOrder.IsFilled() {
|
||||
lendOrder.Status = OrderStatusFilled
|
||||
} else {
|
||||
lendOrder.Status = OrderStatusPartialFilled
|
||||
}
|
||||
|
||||
// Calculate collateral
|
||||
collateral := op.calculateRequiredCollateral(borrowOrder)
|
||||
|
||||
// Create trade
|
||||
return NewLendingTrade(
|
||||
borrowOrder.ID,
|
||||
lendOrder.ID,
|
||||
borrowOrder.UserAddress,
|
||||
lendOrder.UserAddress,
|
||||
borrowOrder.LendingToken,
|
||||
borrowOrder.CollateralToken,
|
||||
tradeQuantity,
|
||||
collateral,
|
||||
tradeInterestRate,
|
||||
borrowOrder.Term,
|
||||
)
|
||||
}
|
||||
|
||||
// addToOrderBook adds an order to the order book
|
||||
func (op *OrderProcessor) addToOrderBook(lendingState *lendingstate.LendingStateDB, order *LendingOrder) error {
|
||||
pairKey := order.PairKey()
|
||||
orderBook := lendingState.GetOrCreateLendingOrderBook(pairKey)
|
||||
|
||||
if order.Side == Borrow {
|
||||
orderBook.AddBorrow(order)
|
||||
} else {
|
||||
orderBook.AddLend(order)
|
||||
}
|
||||
|
||||
lendingState.SetLendingOrder(order)
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeFromOrderBook removes an order from the order book
|
||||
func (op *OrderProcessor) removeFromOrderBook(lendingState *lendingstate.LendingStateDB, order *LendingOrder) error {
|
||||
pairKey := order.PairKey()
|
||||
orderBook := lendingState.GetLendingOrderBook(pairKey)
|
||||
if orderBook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if order.Side == Borrow {
|
||||
orderBook.RemoveBorrow(order.ID)
|
||||
} else {
|
||||
orderBook.RemoveLend(order.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
162
XDCxlending/trade.go
Normal file
162
XDCxlending/trade.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright 2019 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package XDCxlending
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// LendingTradeStatus represents the status of a lending trade
|
||||
type LendingTradeStatus uint8
|
||||
|
||||
const (
|
||||
// TradeStatusPending represents a pending trade
|
||||
TradeStatusPending LendingTradeStatus = iota
|
||||
// TradeStatusSettled represents a settled trade
|
||||
TradeStatusSettled
|
||||
// TradeStatusFailed represents a failed trade
|
||||
TradeStatusFailed
|
||||
)
|
||||
|
||||
// LendingTrade represents a matched lending trade
|
||||
type LendingTrade struct {
|
||||
hash common.Hash
|
||||
BorrowOrderID common.Hash `json:"borrowOrderId"`
|
||||
LendOrderID common.Hash `json:"lendOrderId"`
|
||||
Borrower common.Address `json:"borrower"`
|
||||
Lender common.Address `json:"lender"`
|
||||
LendingToken common.Address `json:"lendingToken"`
|
||||
CollateralToken common.Address `json:"collateralToken"`
|
||||
Principal *big.Int `json:"principal"`
|
||||
CollateralAmount *big.Int `json:"collateralAmount"`
|
||||
InterestRate *big.Int `json:"interestRate"`
|
||||
Term uint64 `json:"term"`
|
||||
BorrowFee *big.Int `json:"borrowFee"`
|
||||
LendFee *big.Int `json:"lendFee"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Status LendingTradeStatus `json:"status"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TxHash common.Hash `json:"txHash"`
|
||||
LoanID common.Hash `json:"loanId"`
|
||||
}
|
||||
|
||||
// NewLendingTrade creates a new lending trade
|
||||
func NewLendingTrade(
|
||||
borrowOrderID, lendOrderID common.Hash,
|
||||
borrower, lender common.Address,
|
||||
lendingToken, collateralToken common.Address,
|
||||
principal, collateral *big.Int,
|
||||
interestRate *big.Int,
|
||||
term uint64,
|
||||
) *LendingTrade {
|
||||
trade := &LendingTrade{
|
||||
BorrowOrderID: borrowOrderID,
|
||||
LendOrderID: lendOrderID,
|
||||
Borrower: borrower,
|
||||
Lender: lender,
|
||||
LendingToken: lendingToken,
|
||||
CollateralToken: collateralToken,
|
||||
Principal: new(big.Int).Set(principal),
|
||||
CollateralAmount: new(big.Int).Set(collateral),
|
||||
InterestRate: new(big.Int).Set(interestRate),
|
||||
Term: term,
|
||||
BorrowFee: big.NewInt(0),
|
||||
LendFee: big.NewInt(0),
|
||||
Status: TradeStatusPending,
|
||||
}
|
||||
trade.hash = trade.ComputeHash()
|
||||
return trade
|
||||
}
|
||||
|
||||
// ComputeHash computes the hash of the lending trade
|
||||
func (t *LendingTrade) ComputeHash() common.Hash {
|
||||
data := append(t.BorrowOrderID.Bytes(), t.LendOrderID.Bytes()...)
|
||||
data = append(data, t.Borrower.Bytes()...)
|
||||
data = append(data, t.Lender.Bytes()...)
|
||||
data = append(data, t.LendingToken.Bytes()...)
|
||||
data = append(data, t.CollateralToken.Bytes()...)
|
||||
data = append(data, common.BigToHash(t.Principal).Bytes()...)
|
||||
data = append(data, common.BigToHash(t.CollateralAmount).Bytes()...)
|
||||
data = append(data, common.BigToHash(t.InterestRate).Bytes()...)
|
||||
data = append(data, common.BigToHash(big.NewInt(int64(t.Term))).Bytes()...)
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// Hash returns the trade hash
|
||||
func (t *LendingTrade) Hash() common.Hash {
|
||||
if t.hash == (common.Hash{}) {
|
||||
t.hash = t.ComputeHash()
|
||||
}
|
||||
return t.hash
|
||||
}
|
||||
|
||||
// SetFees sets the borrower and lender fees
|
||||
func (t *LendingTrade) SetFees(borrowFee, lendFee *big.Int) {
|
||||
if borrowFee != nil {
|
||||
t.BorrowFee = new(big.Int).Set(borrowFee)
|
||||
}
|
||||
if lendFee != nil {
|
||||
t.LendFee = new(big.Int).Set(lendFee)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSettled marks the trade as settled
|
||||
func (t *LendingTrade) SetSettled(blockNumber uint64, txHash common.Hash, loanID common.Hash) {
|
||||
t.Status = TradeStatusSettled
|
||||
t.BlockNumber = blockNumber
|
||||
t.TxHash = txHash
|
||||
t.LoanID = loanID
|
||||
}
|
||||
|
||||
// SetFailed marks the trade as failed
|
||||
func (t *LendingTrade) SetFailed() {
|
||||
t.Status = TradeStatusFailed
|
||||
}
|
||||
|
||||
// PairKey returns the lending pair key
|
||||
func (t *LendingTrade) PairKey() common.Hash {
|
||||
return GetLendingPairKey(t.LendingToken, t.CollateralToken, t.Term)
|
||||
}
|
||||
|
||||
// CalculateInterest calculates the interest for the loan
|
||||
func (t *LendingTrade) CalculateInterest() *big.Int {
|
||||
// Simple interest: Principal * Rate * Time / (365 * 100)
|
||||
interest := new(big.Int).Mul(t.Principal, t.InterestRate)
|
||||
interest = interest.Mul(interest, big.NewInt(int64(t.Term)))
|
||||
interest = interest.Div(interest, big.NewInt(365*24*3600*100))
|
||||
return interest
|
||||
}
|
||||
|
||||
// TotalRepayment calculates the total repayment amount
|
||||
func (t *LendingTrade) TotalRepayment() *big.Int {
|
||||
interest := t.CalculateInterest()
|
||||
return new(big.Int).Add(t.Principal, interest)
|
||||
}
|
||||
|
||||
// Clone creates a copy of the lending trade
|
||||
func (t *LendingTrade) Clone() *LendingTrade {
|
||||
return &LendingTrade{
|
||||
hash: t.hash,
|
||||
BorrowOrderID: t.BorrowOrderID,
|
||||
LendOrderID: t.LendOrderID,
|
||||
Borrower: t.Borrower,
|
||||
Lender: t.Lender,
|
||||
LendingToken: t.LendingToken,
|
||||
CollateralToken: t.CollateralToken,
|
||||
Principal: new(big.Int).Set(t.Principal),
|
||||
CollateralAmount: new(big.Int).Set(t.CollateralAmount),
|
||||
InterestRate: new(big.Int).Set(t.InterestRate),
|
||||
Term: t.Term,
|
||||
BorrowFee: new(big.Int).Set(t.BorrowFee),
|
||||
LendFee: new(big.Int).Set(t.LendFee),
|
||||
Timestamp: t.Timestamp,
|
||||
Status: t.Status,
|
||||
BlockNumber: t.BlockNumber,
|
||||
TxHash: t.TxHash,
|
||||
LoanID: t.LoanID,
|
||||
}
|
||||
}
|
||||
258
cmd/utils/flags_xdc.go
Normal file
258
cmd/utils/flags_xdc.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// XDC-specific command line flags
|
||||
var (
|
||||
// XDPoS flags
|
||||
XDPoSRewardFlag = &cli.BoolFlag{
|
||||
Name: "xdpos.rewards",
|
||||
Usage: "Enable block rewards for validators",
|
||||
Value: true,
|
||||
Category: "XDPoS",
|
||||
}
|
||||
XDPoSSlashingFlag = &cli.BoolFlag{
|
||||
Name: "xdpos.slashing",
|
||||
Usage: "Enable slashing for misbehaving validators",
|
||||
Value: true,
|
||||
Category: "XDPoS",
|
||||
}
|
||||
XDPoSValidatorFlag = &cli.StringFlag{
|
||||
Name: "xdpos.validator",
|
||||
Usage: "Public address for block validation (coinbase)",
|
||||
Category: "XDPoS",
|
||||
}
|
||||
XDPoSGapFlag = &cli.Uint64Flag{
|
||||
Name: "xdpos.gap",
|
||||
Usage: "Gap for snapshot creation in XDPoS",
|
||||
Value: 450,
|
||||
Category: "XDPoS",
|
||||
}
|
||||
XDPoSEpochFlag = &cli.Uint64Flag{
|
||||
Name: "xdpos.epoch",
|
||||
Usage: "Epoch length for validator set updates",
|
||||
Value: 900,
|
||||
Category: "XDPoS",
|
||||
}
|
||||
|
||||
// XDCx flags
|
||||
XDCxEnableFlag = &cli.BoolFlag{
|
||||
Name: "xdcx",
|
||||
Usage: "Enable XDC decentralized exchange",
|
||||
Value: false,
|
||||
Category: "XDCx",
|
||||
}
|
||||
XDCxDataDirFlag = &cli.StringFlag{
|
||||
Name: "xdcx.datadir",
|
||||
Usage: "Data directory for XDCx",
|
||||
Category: "XDCx",
|
||||
}
|
||||
|
||||
// XDCxLending flags
|
||||
XDCxLendingEnableFlag = &cli.BoolFlag{
|
||||
Name: "xdcxlending",
|
||||
Usage: "Enable XDC lending protocol",
|
||||
Value: false,
|
||||
Category: "XDCxLending",
|
||||
}
|
||||
XDCxLendingDataDirFlag = &cli.StringFlag{
|
||||
Name: "xdcxlending.datadir",
|
||||
Usage: "Data directory for XDCxLending",
|
||||
Category: "XDCxLending",
|
||||
}
|
||||
|
||||
// Network flags
|
||||
XDCMainnetFlag = &cli.BoolFlag{
|
||||
Name: "xdc.mainnet",
|
||||
Usage: "Connect to XDC mainnet",
|
||||
Category: "XDC Network",
|
||||
}
|
||||
XDCTestnetFlag = &cli.BoolFlag{
|
||||
Name: "xdc.testnet",
|
||||
Usage: "Connect to XDC Apothem testnet",
|
||||
Category: "XDC Network",
|
||||
}
|
||||
XDCDevnetFlag = &cli.BoolFlag{
|
||||
Name: "xdc.devnet",
|
||||
Usage: "Connect to XDC devnet",
|
||||
Category: "XDC Network",
|
||||
}
|
||||
|
||||
// Sync flags
|
||||
XDCSnapSyncFlag = &cli.BoolFlag{
|
||||
Name: "xdc.snapsync",
|
||||
Usage: "Enable XDC snapshot sync",
|
||||
Value: false,
|
||||
Category: "XDC Sync",
|
||||
}
|
||||
XDCSnapShotBlockFlag = &cli.Uint64Flag{
|
||||
Name: "xdc.snapshot.block",
|
||||
Usage: "Snapshot block number for sync",
|
||||
Category: "XDC Sync",
|
||||
}
|
||||
XDCCheckpointIntervalFlag = &cli.Uint64Flag{
|
||||
Name: "xdc.checkpoint.interval",
|
||||
Usage: "Checkpoint interval in blocks",
|
||||
Value: 900,
|
||||
Category: "XDC Sync",
|
||||
}
|
||||
|
||||
// Masternode flags
|
||||
MasternodeFlag = &cli.BoolFlag{
|
||||
Name: "masternode",
|
||||
Usage: "Run as a masternode",
|
||||
Value: false,
|
||||
Category: "Masternode",
|
||||
}
|
||||
MasternodeKeyFlag = &cli.StringFlag{
|
||||
Name: "masternode.key",
|
||||
Usage: "Masternode private key for signing",
|
||||
Category: "Masternode",
|
||||
}
|
||||
MasternodeCoinbaseFlag = &cli.StringFlag{
|
||||
Name: "masternode.coinbase",
|
||||
Usage: "Masternode coinbase address",
|
||||
Category: "Masternode",
|
||||
}
|
||||
)
|
||||
|
||||
// XDCFlags contains all XDC-specific flags
|
||||
var XDCFlags = []cli.Flag{
|
||||
XDPoSRewardFlag,
|
||||
XDPoSSlashingFlag,
|
||||
XDPoSValidatorFlag,
|
||||
XDPoSGapFlag,
|
||||
XDPoSEpochFlag,
|
||||
XDCxEnableFlag,
|
||||
XDCxDataDirFlag,
|
||||
XDCxLendingEnableFlag,
|
||||
XDCxLendingDataDirFlag,
|
||||
XDCMainnetFlag,
|
||||
XDCTestnetFlag,
|
||||
XDCDevnetFlag,
|
||||
XDCSnapSyncFlag,
|
||||
XDCSnapShotBlockFlag,
|
||||
XDCCheckpointIntervalFlag,
|
||||
MasternodeFlag,
|
||||
MasternodeKeyFlag,
|
||||
MasternodeCoinbaseFlag,
|
||||
}
|
||||
|
||||
// SetXDCConfig applies XDC-specific configuration
|
||||
func SetXDCConfig(ctx *cli.Context, cfg *ethconfig.Config) {
|
||||
// XDPoS configuration
|
||||
if ctx.IsSet(XDPoSRewardFlag.Name) {
|
||||
cfg.XDPoSRewards = ctx.Bool(XDPoSRewardFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(XDPoSSlashingFlag.Name) {
|
||||
cfg.XDPoSSlashing = ctx.Bool(XDPoSSlashingFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(XDPoSGapFlag.Name) {
|
||||
cfg.XDPoSGap = ctx.Uint64(XDPoSGapFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(XDPoSEpochFlag.Name) {
|
||||
cfg.XDPoSEpoch = ctx.Uint64(XDPoSEpochFlag.Name)
|
||||
}
|
||||
|
||||
// XDCx configuration
|
||||
if ctx.IsSet(XDCxEnableFlag.Name) {
|
||||
cfg.XDCxEnabled = ctx.Bool(XDCxEnableFlag.Name)
|
||||
}
|
||||
|
||||
// XDCxLending configuration
|
||||
if ctx.IsSet(XDCxLendingEnableFlag.Name) {
|
||||
cfg.XDCxLendingEnabled = ctx.Bool(XDCxLendingEnableFlag.Name)
|
||||
}
|
||||
|
||||
// Sync configuration
|
||||
if ctx.IsSet(XDCSnapSyncFlag.Name) {
|
||||
cfg.XDCSnapSync = ctx.Bool(XDCSnapSyncFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(XDCCheckpointIntervalFlag.Name) {
|
||||
cfg.XDCCheckpointInterval = ctx.Uint64(XDCCheckpointIntervalFlag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// SetXDCNodeConfig applies XDC-specific node configuration
|
||||
func SetXDCNodeConfig(ctx *cli.Context, cfg *node.Config) {
|
||||
// Set XDC-specific data directories
|
||||
if ctx.IsSet(XDCxDataDirFlag.Name) {
|
||||
// Configure XDCx data directory
|
||||
}
|
||||
if ctx.IsSet(XDCxLendingDataDirFlag.Name) {
|
||||
// Configure XDCxLending data directory
|
||||
}
|
||||
}
|
||||
|
||||
// SetXDCNetworkConfig configures the network for XDC
|
||||
func SetXDCNetworkConfig(ctx *cli.Context, cfg *ethconfig.Config) {
|
||||
if ctx.Bool(XDCMainnetFlag.Name) {
|
||||
// Configure for mainnet
|
||||
cfg.NetworkId = 50
|
||||
} else if ctx.Bool(XDCTestnetFlag.Name) {
|
||||
// Configure for Apothem testnet
|
||||
cfg.NetworkId = 51
|
||||
} else if ctx.Bool(XDCDevnetFlag.Name) {
|
||||
// Configure for devnet
|
||||
cfg.NetworkId = 551
|
||||
}
|
||||
}
|
||||
|
||||
// MasternodeConfig holds masternode configuration
|
||||
type MasternodeConfig struct {
|
||||
Enable bool
|
||||
Key string
|
||||
Coinbase string
|
||||
}
|
||||
|
||||
// SetMasternodeConfig sets masternode configuration
|
||||
func SetMasternodeConfig(ctx *cli.Context) *MasternodeConfig {
|
||||
config := &MasternodeConfig{
|
||||
Enable: ctx.Bool(MasternodeFlag.Name),
|
||||
}
|
||||
|
||||
if config.Enable {
|
||||
if ctx.IsSet(MasternodeKeyFlag.Name) {
|
||||
config.Key = ctx.String(MasternodeKeyFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(MasternodeCoinbaseFlag.Name) {
|
||||
config.Coinbase = ctx.String(MasternodeCoinbaseFlag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ValidateXDCFlags validates XDC-specific flags
|
||||
func ValidateXDCFlags(ctx *cli.Context) error {
|
||||
// Validate that mainnet, testnet, and devnet are mutually exclusive
|
||||
networks := 0
|
||||
if ctx.Bool(XDCMainnetFlag.Name) {
|
||||
networks++
|
||||
}
|
||||
if ctx.Bool(XDCTestnetFlag.Name) {
|
||||
networks++
|
||||
}
|
||||
if ctx.Bool(XDCDevnetFlag.Name) {
|
||||
networks++
|
||||
}
|
||||
if networks > 1 {
|
||||
return cli.Exit("Cannot specify multiple XDC networks", 1)
|
||||
}
|
||||
|
||||
// Validate masternode configuration
|
||||
if ctx.Bool(MasternodeFlag.Name) {
|
||||
if !ctx.IsSet(MasternodeCoinbaseFlag.Name) {
|
||||
return cli.Exit("Masternode coinbase address is required", 1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
400
consensus/XDPoS/engines/engine_v1.go
Normal file
400
consensus/XDPoS/engines/engine_v1.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package engines
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidTimestampV1 is returned if the timestamp is not in correct range
|
||||
ErrInvalidTimestampV1 = errors.New("invalid timestamp for XDPoS v1")
|
||||
// ErrInvalidDifficultyV1 is returned if the difficulty is invalid
|
||||
ErrInvalidDifficultyV1 = errors.New("invalid difficulty for XDPoS v1")
|
||||
// ErrUnauthorizedSignerV1 is returned if the signer is not authorized
|
||||
ErrUnauthorizedSignerV1 = errors.New("unauthorized signer for XDPoS v1")
|
||||
// ErrMissingSignatureV1 is returned if signature is missing
|
||||
ErrMissingSignatureV1 = errors.New("missing signature in XDPoS v1 block")
|
||||
)
|
||||
|
||||
// EngineV1 implements the XDPoS v1 consensus engine
|
||||
type EngineV1 struct {
|
||||
config *params.XDPoSConfig
|
||||
db Database
|
||||
|
||||
// Snapshot cache
|
||||
recents *lru
|
||||
signatures *lru
|
||||
|
||||
// Signing
|
||||
signer common.Address
|
||||
signFn SignerFn
|
||||
lock sync.RWMutex
|
||||
|
||||
// Block time
|
||||
period uint64
|
||||
}
|
||||
|
||||
// Database interface for engine
|
||||
type Database interface {
|
||||
Get(key []byte) ([]byte, error)
|
||||
Put(key []byte, value []byte) error
|
||||
Delete(key []byte) error
|
||||
Has(key []byte) (bool, error)
|
||||
}
|
||||
|
||||
// SignerFn is a signature function
|
||||
type SignerFn func(account common.Address, data []byte) ([]byte, error)
|
||||
|
||||
// lru is a simple LRU cache (placeholder)
|
||||
type lru struct {
|
||||
items map[common.Hash]interface{}
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newLRU(size int) *lru {
|
||||
return &lru{items: make(map[common.Hash]interface{})}
|
||||
}
|
||||
|
||||
func (l *lru) Get(key common.Hash) (interface{}, bool) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
v, ok := l.items[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (l *lru) Add(key common.Hash, value interface{}) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
l.items[key] = value
|
||||
}
|
||||
|
||||
// NewEngineV1 creates a new XDPoS v1 engine
|
||||
func NewEngineV1(config *params.XDPoSConfig, db Database) *EngineV1 {
|
||||
period := uint64(2)
|
||||
if config != nil && config.Period > 0 {
|
||||
period = config.Period
|
||||
}
|
||||
|
||||
return &EngineV1{
|
||||
config: config,
|
||||
db: db,
|
||||
recents: newLRU(100),
|
||||
signatures: newLRU(1000),
|
||||
period: period,
|
||||
}
|
||||
}
|
||||
|
||||
// Author returns the signer of the block
|
||||
func (e *EngineV1) Author(header *types.Header) (common.Address, error) {
|
||||
return ecrecover(header, e.signatures)
|
||||
}
|
||||
|
||||
// VerifyHeader checks whether a header conforms to the consensus rules
|
||||
func (e *EngineV1) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error {
|
||||
return e.verifyHeader(chain, header, nil)
|
||||
}
|
||||
|
||||
// VerifyHeaders verifies a batch of headers concurrently
|
||||
func (e *EngineV1) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) {
|
||||
abort := make(chan struct{})
|
||||
results := make(chan error, len(headers))
|
||||
|
||||
go func() {
|
||||
for i, header := range headers {
|
||||
var parent *types.Header
|
||||
if i > 0 {
|
||||
parent = headers[i-1]
|
||||
}
|
||||
err := e.verifyHeader(chain, header, parent)
|
||||
select {
|
||||
case <-abort:
|
||||
return
|
||||
case results <- err:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return abort, results
|
||||
}
|
||||
|
||||
// verifyHeader checks header validity
|
||||
func (e *EngineV1) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parent *types.Header) error {
|
||||
if header.Number == nil {
|
||||
return errors.New("missing block number")
|
||||
}
|
||||
|
||||
// Don't verify genesis block
|
||||
if header.Number.Uint64() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get parent if not provided
|
||||
if parent == nil {
|
||||
parent = chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
|
||||
if parent == nil {
|
||||
return consensus.ErrUnknownAncestor
|
||||
}
|
||||
}
|
||||
|
||||
// Verify timestamp
|
||||
if header.Time <= parent.Time {
|
||||
return ErrInvalidTimestampV1
|
||||
}
|
||||
|
||||
// Verify that the block doesn't come from the future
|
||||
if header.Time > uint64(time.Now().Unix())+30 {
|
||||
return consensus.ErrFutureBlock
|
||||
}
|
||||
|
||||
// Verify extra data length
|
||||
if len(header.Extra) < crypto.SignatureLength {
|
||||
return ErrMissingSignatureV1
|
||||
}
|
||||
|
||||
// Verify signer
|
||||
signer, err := ecrecover(header, e.signatures)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if signer is authorized
|
||||
if !e.isAuthorized(chain, header, signer) {
|
||||
return ErrUnauthorizedSignerV1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyUncles implements consensus.Engine, always returning an error for any
|
||||
// uncles as this consensus mechanism doesn't permit uncles.
|
||||
func (e *EngineV1) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
|
||||
if len(block.Uncles()) > 0 {
|
||||
return errors.New("uncles not allowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare initializes the consensus fields of a block header
|
||||
func (e *EngineV1) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {
|
||||
header.Coinbase = e.signer
|
||||
header.Nonce = types.BlockNonce{}
|
||||
header.Difficulty = big.NewInt(1)
|
||||
|
||||
// Set extra data
|
||||
if len(header.Extra) < ExtraVanity {
|
||||
header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, ExtraVanity-len(header.Extra))...)
|
||||
}
|
||||
header.Extra = header.Extra[:ExtraVanity]
|
||||
|
||||
// Add space for signature
|
||||
header.Extra = append(header.Extra, make([]byte, crypto.SignatureLength)...)
|
||||
|
||||
// Mix digest is not used
|
||||
header.MixDigest = common.Hash{}
|
||||
|
||||
// Set the correct timestamp
|
||||
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
|
||||
if parent == nil {
|
||||
return consensus.ErrUnknownAncestor
|
||||
}
|
||||
header.Time = parent.Time + e.period
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finalize implements consensus.Engine
|
||||
func (e *EngineV1) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) {
|
||||
// Accumulate block rewards
|
||||
e.accumulateRewards(chain, state, header)
|
||||
}
|
||||
|
||||
// FinalizeAndAssemble implements consensus.Engine
|
||||
func (e *EngineV1) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) {
|
||||
// Finalize block
|
||||
e.Finalize(chain, header, state, txs, uncles, withdrawals)
|
||||
|
||||
// Compute state root
|
||||
header.Root = state.IntermediateRoot(true)
|
||||
|
||||
// Assemble and return the final block
|
||||
return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)), nil
|
||||
}
|
||||
|
||||
// Seal implements consensus.Engine
|
||||
func (e *EngineV1) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
|
||||
header := block.Header()
|
||||
|
||||
// Don't seal genesis block
|
||||
if header.Number.Uint64() == 0 {
|
||||
return errors.New("cannot seal genesis block")
|
||||
}
|
||||
|
||||
e.lock.RLock()
|
||||
signer, signFn := e.signer, e.signFn
|
||||
e.lock.RUnlock()
|
||||
|
||||
// Sign the header
|
||||
sighash, err := signHash(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signature, err := signFn(signer, sighash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy signature to extra data
|
||||
copy(header.Extra[len(header.Extra)-crypto.SignatureLength:], signature)
|
||||
|
||||
select {
|
||||
case results <- block.WithSeal(header):
|
||||
case <-stop:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SealHash returns the hash of a block prior to it being sealed
|
||||
func (e *EngineV1) SealHash(header *types.Header) common.Hash {
|
||||
return sigHash(header)
|
||||
}
|
||||
|
||||
// CalcDifficulty returns the difficulty for a block
|
||||
func (e *EngineV1) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
|
||||
return big.NewInt(1)
|
||||
}
|
||||
|
||||
// APIs returns the RPC APIs this consensus engine provides
|
||||
func (e *EngineV1) APIs(chain consensus.ChainHeaderReader) []rpc.API {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements consensus.Engine
|
||||
func (e *EngineV1) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authorize injects a private key for signing
|
||||
func (e *EngineV1) Authorize(signer common.Address, signFn SignerFn) {
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
|
||||
e.signer = signer
|
||||
e.signFn = signFn
|
||||
}
|
||||
|
||||
// isAuthorized checks if a signer is authorized
|
||||
func (e *EngineV1) isAuthorized(chain consensus.ChainHeaderReader, header *types.Header, signer common.Address) bool {
|
||||
// Get snapshot at parent
|
||||
// Check if signer is in the validator set
|
||||
// For now, return true
|
||||
return true
|
||||
}
|
||||
|
||||
// accumulateRewards accumulates the block and uncle rewards
|
||||
func (e *EngineV1) accumulateRewards(chain consensus.ChainHeaderReader, state *state.StateDB, header *types.Header) {
|
||||
// Block reward is handled by the foundation reward contract
|
||||
log.Debug("Accumulating rewards", "block", header.Number)
|
||||
}
|
||||
|
||||
// ecrecover extracts the signer from a header
|
||||
func ecrecover(header *types.Header, sigcache *lru) (common.Address, error) {
|
||||
hash := header.Hash()
|
||||
|
||||
if address, known := sigcache.Get(hash); known {
|
||||
return address.(common.Address), nil
|
||||
}
|
||||
|
||||
if len(header.Extra) < crypto.SignatureLength {
|
||||
return common.Address{}, ErrMissingSignatureV1
|
||||
}
|
||||
signature := header.Extra[len(header.Extra)-crypto.SignatureLength:]
|
||||
|
||||
// Recover the public key
|
||||
pubkey, err := crypto.Ecrecover(sigHash(header).Bytes(), signature)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
var signer common.Address
|
||||
copy(signer[:], crypto.Keccak256(pubkey[1:])[12:])
|
||||
|
||||
sigcache.Add(hash, signer)
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// sigHash returns the hash to be signed
|
||||
func sigHash(header *types.Header) common.Hash {
|
||||
return sigHashWithExtra(header, header.Extra[:len(header.Extra)-crypto.SignatureLength])
|
||||
}
|
||||
|
||||
// sigHashWithExtra returns the hash to be signed with specific extra data
|
||||
func sigHashWithExtra(header *types.Header, extra []byte) common.Hash {
|
||||
type sigHeader struct {
|
||||
ParentHash common.Hash
|
||||
UncleHash common.Hash
|
||||
Coinbase common.Address
|
||||
Root common.Hash
|
||||
TxHash common.Hash
|
||||
ReceiptHash common.Hash
|
||||
Bloom types.Bloom
|
||||
Difficulty *big.Int
|
||||
Number *big.Int
|
||||
GasLimit uint64
|
||||
GasUsed uint64
|
||||
Time uint64
|
||||
Extra []byte
|
||||
MixDigest common.Hash
|
||||
Nonce types.BlockNonce
|
||||
}
|
||||
|
||||
data, _ := rlp.EncodeToBytes(&sigHeader{
|
||||
ParentHash: header.ParentHash,
|
||||
UncleHash: header.UncleHash,
|
||||
Coinbase: header.Coinbase,
|
||||
Root: header.Root,
|
||||
TxHash: header.TxHash,
|
||||
ReceiptHash: header.ReceiptHash,
|
||||
Bloom: header.Bloom,
|
||||
Difficulty: header.Difficulty,
|
||||
Number: header.Number,
|
||||
GasLimit: header.GasLimit,
|
||||
GasUsed: header.GasUsed,
|
||||
Time: header.Time,
|
||||
Extra: extra,
|
||||
MixDigest: header.MixDigest,
|
||||
Nonce: header.Nonce,
|
||||
})
|
||||
|
||||
return crypto.Keccak256Hash(data)
|
||||
}
|
||||
|
||||
// signHash returns a hash for signing
|
||||
func signHash(header *types.Header) ([]byte, error) {
|
||||
return sigHash(header).Bytes(), nil
|
||||
}
|
||||
|
||||
// ExtraVanity is the fixed number of extra-data prefix bytes
|
||||
const ExtraVanity = 32
|
||||
|
||||
// rpc.API placeholder
|
||||
type rpc struct{}
|
||||
type API struct{}
|
||||
361
consensus/XDPoS/engines/engine_v1_test.go
Normal file
361
consensus/XDPoS/engines/engine_v1_test.go
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package engines
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// mockChain implements consensus.ChainHeaderReader for testing
|
||||
type mockChain struct {
|
||||
headers map[common.Hash]*types.Header
|
||||
}
|
||||
|
||||
func newMockChain() *mockChain {
|
||||
return &mockChain{
|
||||
headers: make(map[common.Hash]*types.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockChain) Config() *params.ChainConfig {
|
||||
return ¶ms.ChainConfig{}
|
||||
}
|
||||
|
||||
func (m *mockChain) CurrentHeader() *types.Header {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockChain) GetHeader(hash common.Hash, number uint64) *types.Header {
|
||||
return m.headers[hash]
|
||||
}
|
||||
|
||||
func (m *mockChain) GetHeaderByNumber(number uint64) *types.Header {
|
||||
for _, h := range m.headers {
|
||||
if h.Number.Uint64() == number {
|
||||
return h
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockChain) GetHeaderByHash(hash common.Hash) *types.Header {
|
||||
return m.headers[hash]
|
||||
}
|
||||
|
||||
func (m *mockChain) GetTd(hash common.Hash, number uint64) *big.Int {
|
||||
return big.NewInt(1)
|
||||
}
|
||||
|
||||
func (m *mockChain) addHeader(header *types.Header) {
|
||||
m.headers[header.Hash()] = header
|
||||
}
|
||||
|
||||
// mockDB implements Database for testing
|
||||
type mockDB struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func newMockDB() *mockDB {
|
||||
return &mockDB{
|
||||
data: make(map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDB) Get(key []byte) ([]byte, error) {
|
||||
return m.data[string(key)], nil
|
||||
}
|
||||
|
||||
func (m *mockDB) Put(key []byte, value []byte) error {
|
||||
m.data[string(key)] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDB) Delete(key []byte) error {
|
||||
delete(m.data, string(key))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDB) Has(key []byte) (bool, error) {
|
||||
_, ok := m.data[string(key)]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// createTestHeader creates a test header
|
||||
func createTestHeader(parent *types.Header, signer common.Address) *types.Header {
|
||||
number := big.NewInt(1)
|
||||
parentHash := common.Hash{}
|
||||
timestamp := uint64(time.Now().Unix())
|
||||
|
||||
if parent != nil {
|
||||
number = new(big.Int).Add(parent.Number, big.NewInt(1))
|
||||
parentHash = parent.Hash()
|
||||
timestamp = parent.Time + 2
|
||||
}
|
||||
|
||||
header := &types.Header{
|
||||
ParentHash: parentHash,
|
||||
UncleHash: types.EmptyUncleHash,
|
||||
Coinbase: signer,
|
||||
Root: common.Hash{},
|
||||
TxHash: types.EmptyTxsHash,
|
||||
ReceiptHash: types.EmptyReceiptsHash,
|
||||
Bloom: types.Bloom{},
|
||||
Difficulty: big.NewInt(1),
|
||||
Number: number,
|
||||
GasLimit: 8000000,
|
||||
GasUsed: 0,
|
||||
Time: timestamp,
|
||||
Extra: make([]byte, ExtraVanity+crypto.SignatureLength),
|
||||
MixDigest: common.Hash{},
|
||||
Nonce: types.BlockNonce{},
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func TestNewEngineV1(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{
|
||||
Period: 2,
|
||||
Epoch: 900,
|
||||
}
|
||||
db := newMockDB()
|
||||
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
if engine == nil {
|
||||
t.Fatal("Failed to create engine")
|
||||
}
|
||||
|
||||
if engine.period != 2 {
|
||||
t.Errorf("Expected period 2, got %d", engine.period)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_Author(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
// Create a test key
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer := crypto.PubkeyToAddress(privateKey.PublicKey)
|
||||
|
||||
// Create and sign a header
|
||||
header := createTestHeader(nil, signer)
|
||||
|
||||
// Sign the header
|
||||
sig, err := crypto.Sign(sigHash(header).Bytes(), privateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy(header.Extra[ExtraVanity:], sig)
|
||||
|
||||
// Test Author
|
||||
author, err := engine.Author(header)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if author != signer {
|
||||
t.Errorf("Expected author %s, got %s", signer.Hex(), author.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_CalcDifficulty(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
chain := newMockChain()
|
||||
parent := createTestHeader(nil, common.Address{})
|
||||
|
||||
difficulty := engine.CalcDifficulty(chain, uint64(time.Now().Unix()), parent)
|
||||
|
||||
if difficulty.Cmp(big.NewInt(1)) != 0 {
|
||||
t.Errorf("Expected difficulty 1, got %s", difficulty.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_Prepare(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
// Set up signer
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer := crypto.PubkeyToAddress(privateKey.PublicKey)
|
||||
engine.Authorize(signer, func(account common.Address, data []byte) ([]byte, error) {
|
||||
return crypto.Sign(data, privateKey)
|
||||
})
|
||||
|
||||
// Create chain with parent
|
||||
chain := newMockChain()
|
||||
parent := createTestHeader(nil, signer)
|
||||
chain.addHeader(parent)
|
||||
|
||||
// Create child header
|
||||
header := &types.Header{
|
||||
ParentHash: parent.Hash(),
|
||||
Number: new(big.Int).Add(parent.Number, big.NewInt(1)),
|
||||
}
|
||||
|
||||
// Prepare
|
||||
err = engine.Prepare(chain, header)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check header was prepared correctly
|
||||
if header.Coinbase != signer {
|
||||
t.Errorf("Expected coinbase %s, got %s", signer.Hex(), header.Coinbase.Hex())
|
||||
}
|
||||
|
||||
if len(header.Extra) != ExtraVanity+crypto.SignatureLength {
|
||||
t.Errorf("Expected extra length %d, got %d", ExtraVanity+crypto.SignatureLength, len(header.Extra))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_VerifyUncles(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
// Block without uncles should be valid
|
||||
block := types.NewBlockWithHeader(&types.Header{})
|
||||
err := engine.VerifyUncles(nil, block)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for block without uncles, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_SealHash(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
header := createTestHeader(nil, common.Address{})
|
||||
|
||||
hash := engine.SealHash(header)
|
||||
|
||||
if hash == (common.Hash{}) {
|
||||
t.Error("Expected non-zero seal hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineV1_Authorize(t *testing.T) {
|
||||
config := ¶ms.XDPoSConfig{Period: 2}
|
||||
db := newMockDB()
|
||||
engine := NewEngineV1(config, db)
|
||||
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer := crypto.PubkeyToAddress(privateKey.PublicKey)
|
||||
|
||||
signFn := func(account common.Address, data []byte) ([]byte, error) {
|
||||
return crypto.Sign(data, privateKey)
|
||||
}
|
||||
|
||||
engine.Authorize(signer, signFn)
|
||||
|
||||
engine.lock.RLock()
|
||||
defer engine.lock.RUnlock()
|
||||
|
||||
if engine.signer != signer {
|
||||
t.Errorf("Expected signer %s, got %s", signer.Hex(), engine.signer.Hex())
|
||||
}
|
||||
|
||||
if engine.signFn == nil {
|
||||
t.Error("Expected signFn to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigHash(t *testing.T) {
|
||||
header := createTestHeader(nil, common.Address{})
|
||||
|
||||
hash1 := sigHash(header)
|
||||
hash2 := sigHash(header)
|
||||
|
||||
if hash1 != hash2 {
|
||||
t.Error("Expected consistent hash")
|
||||
}
|
||||
|
||||
// Modify header
|
||||
header.GasLimit = 9000000
|
||||
hash3 := sigHash(header)
|
||||
|
||||
if hash1 == hash3 {
|
||||
t.Error("Expected different hash after modification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcrecover(t *testing.T) {
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer := crypto.PubkeyToAddress(privateKey.PublicKey)
|
||||
|
||||
header := createTestHeader(nil, signer)
|
||||
|
||||
// Sign the header
|
||||
sig, err := crypto.Sign(sigHash(header).Bytes(), privateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy(header.Extra[ExtraVanity:], sig)
|
||||
|
||||
// Recover
|
||||
cache := newLRU(10)
|
||||
recovered, err := ecrecover(header, cache)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if recovered != signer {
|
||||
t.Errorf("Expected %s, got %s", signer.Hex(), recovered.Hex())
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if _, ok := cache.Get(header.Hash()); !ok {
|
||||
t.Error("Expected address to be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
cache := newLRU(10)
|
||||
|
||||
key := common.HexToHash("0x1234")
|
||||
value := "test"
|
||||
|
||||
// Test Get on empty cache
|
||||
_, ok := cache.Get(key)
|
||||
if ok {
|
||||
t.Error("Expected cache miss")
|
||||
}
|
||||
|
||||
// Test Add and Get
|
||||
cache.Add(key, value)
|
||||
got, ok := cache.Get(key)
|
||||
if !ok {
|
||||
t.Error("Expected cache hit")
|
||||
}
|
||||
if got != value {
|
||||
t.Errorf("Expected %s, got %s", value, got)
|
||||
}
|
||||
}
|
||||
209
core/rawdb/accessors_xdc_indexes.go
Normal file
209
core/rawdb/accessors_xdc_indexes.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package rawdb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
// XDC-specific database key prefixes
|
||||
var (
|
||||
validatorSetPrefix = []byte("xdc-validators-")
|
||||
epochDataPrefix = []byte("xdc-epoch-")
|
||||
penaltyPrefix = []byte("xdc-penalty-")
|
||||
checkpointPrefix = []byte("xdc-checkpoint-")
|
||||
snapshotPrefix = []byte("xdc-snapshot-")
|
||||
tradingStatePrefix = []byte("xdcx-trading-")
|
||||
lendingStatePrefix = []byte("xdcx-lending-")
|
||||
)
|
||||
|
||||
// encodeBlockNumber encodes a block number as big endian uint64
|
||||
func encodeBlockNumber(number uint64) []byte {
|
||||
enc := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(enc, number)
|
||||
return enc
|
||||
}
|
||||
|
||||
// validatorSetKey returns the key for validator set at block number
|
||||
func validatorSetKey(number uint64) []byte {
|
||||
return append(validatorSetPrefix, encodeBlockNumber(number)...)
|
||||
}
|
||||
|
||||
// epochDataKey returns the key for epoch data
|
||||
func epochDataKey(epoch uint64) []byte {
|
||||
return append(epochDataPrefix, encodeBlockNumber(epoch)...)
|
||||
}
|
||||
|
||||
// penaltyKey returns the key for a penalty record
|
||||
func penaltyKey(validator common.Address, block uint64) []byte {
|
||||
key := append(penaltyPrefix, validator.Bytes()...)
|
||||
return append(key, encodeBlockNumber(block)...)
|
||||
}
|
||||
|
||||
// checkpointKey returns the key for a checkpoint
|
||||
func checkpointKey(number uint64) []byte {
|
||||
return append(checkpointPrefix, encodeBlockNumber(number)...)
|
||||
}
|
||||
|
||||
// snapshotKey returns the key for a snapshot
|
||||
func snapshotKey(hash common.Hash) []byte {
|
||||
return append(snapshotPrefix, hash.Bytes()...)
|
||||
}
|
||||
|
||||
// WriteValidatorSet writes the validator set for a block
|
||||
func WriteValidatorSet(db ethdb.KeyValueWriter, number uint64, validators []common.Address) error {
|
||||
data := make([]byte, len(validators)*common.AddressLength)
|
||||
for i, v := range validators {
|
||||
copy(data[i*common.AddressLength:], v.Bytes())
|
||||
}
|
||||
return db.Put(validatorSetKey(number), data)
|
||||
}
|
||||
|
||||
// ReadValidatorSet reads the validator set for a block
|
||||
func ReadValidatorSet(db ethdb.KeyValueReader, number uint64) []common.Address {
|
||||
data, err := db.Get(validatorSetKey(number))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
count := len(data) / common.AddressLength
|
||||
validators := make([]common.Address, count)
|
||||
for i := 0; i < count; i++ {
|
||||
validators[i] = common.BytesToAddress(data[i*common.AddressLength : (i+1)*common.AddressLength])
|
||||
}
|
||||
return validators
|
||||
}
|
||||
|
||||
// DeleteValidatorSet deletes the validator set for a block
|
||||
func DeleteValidatorSet(db ethdb.KeyValueWriter, number uint64) error {
|
||||
return db.Delete(validatorSetKey(number))
|
||||
}
|
||||
|
||||
// WriteEpochData writes epoch data
|
||||
func WriteEpochData(db ethdb.KeyValueWriter, epoch uint64, data []byte) error {
|
||||
return db.Put(epochDataKey(epoch), data)
|
||||
}
|
||||
|
||||
// ReadEpochData reads epoch data
|
||||
func ReadEpochData(db ethdb.KeyValueReader, epoch uint64) []byte {
|
||||
data, err := db.Get(epochDataKey(epoch))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// DeleteEpochData deletes epoch data
|
||||
func DeleteEpochData(db ethdb.KeyValueWriter, epoch uint64) error {
|
||||
return db.Delete(epochDataKey(epoch))
|
||||
}
|
||||
|
||||
// WritePenalty writes a penalty record
|
||||
func WritePenalty(db ethdb.KeyValueWriter, validator common.Address, block uint64, amount uint64) error {
|
||||
data := encodeBlockNumber(amount)
|
||||
return db.Put(penaltyKey(validator, block), data)
|
||||
}
|
||||
|
||||
// ReadPenalty reads a penalty record
|
||||
func ReadPenalty(db ethdb.KeyValueReader, validator common.Address, block uint64) uint64 {
|
||||
data, err := db.Get(penaltyKey(validator, block))
|
||||
if err != nil || len(data) != 8 {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint64(data)
|
||||
}
|
||||
|
||||
// DeletePenalty deletes a penalty record
|
||||
func DeletePenalty(db ethdb.KeyValueWriter, validator common.Address, block uint64) error {
|
||||
return db.Delete(penaltyKey(validator, block))
|
||||
}
|
||||
|
||||
// WriteCheckpoint writes a checkpoint
|
||||
func WriteCheckpoint(db ethdb.KeyValueWriter, number uint64, hash common.Hash) error {
|
||||
return db.Put(checkpointKey(number), hash.Bytes())
|
||||
}
|
||||
|
||||
// ReadCheckpoint reads a checkpoint
|
||||
func ReadCheckpoint(db ethdb.KeyValueReader, number uint64) common.Hash {
|
||||
data, err := db.Get(checkpointKey(number))
|
||||
if err != nil || len(data) != common.HashLength {
|
||||
return common.Hash{}
|
||||
}
|
||||
return common.BytesToHash(data)
|
||||
}
|
||||
|
||||
// DeleteCheckpoint deletes a checkpoint
|
||||
func DeleteCheckpoint(db ethdb.KeyValueWriter, number uint64) error {
|
||||
return db.Delete(checkpointKey(number))
|
||||
}
|
||||
|
||||
// WriteXDPoSSnapshot writes an XDPoS snapshot
|
||||
func WriteXDPoSSnapshot(db ethdb.KeyValueWriter, hash common.Hash, data []byte) error {
|
||||
return db.Put(snapshotKey(hash), data)
|
||||
}
|
||||
|
||||
// ReadXDPoSSnapshot reads an XDPoS snapshot
|
||||
func ReadXDPoSSnapshot(db ethdb.KeyValueReader, hash common.Hash) []byte {
|
||||
data, err := db.Get(snapshotKey(hash))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// DeleteXDPoSSnapshot deletes an XDPoS snapshot
|
||||
func DeleteXDPoSSnapshot(db ethdb.KeyValueWriter, hash common.Hash) error {
|
||||
return db.Delete(snapshotKey(hash))
|
||||
}
|
||||
|
||||
// HasXDPoSSnapshot checks if a snapshot exists
|
||||
func HasXDPoSSnapshot(db ethdb.KeyValueReader, hash common.Hash) bool {
|
||||
has, _ := db.Has(snapshotKey(hash))
|
||||
return has
|
||||
}
|
||||
|
||||
// XDCx Trading State
|
||||
|
||||
// tradingStateKey returns the key for trading state
|
||||
func tradingStateKey(root common.Hash) []byte {
|
||||
return append(tradingStatePrefix, root.Bytes()...)
|
||||
}
|
||||
|
||||
// WriteTradingStateRoot writes the trading state root for a block
|
||||
func WriteTradingStateRoot(db ethdb.KeyValueWriter, blockHash, tradingRoot common.Hash) error {
|
||||
return db.Put(tradingStateKey(blockHash), tradingRoot.Bytes())
|
||||
}
|
||||
|
||||
// ReadTradingStateRoot reads the trading state root for a block
|
||||
func ReadTradingStateRoot(db ethdb.KeyValueReader, blockHash common.Hash) common.Hash {
|
||||
data, err := db.Get(tradingStateKey(blockHash))
|
||||
if err != nil || len(data) != common.HashLength {
|
||||
return common.Hash{}
|
||||
}
|
||||
return common.BytesToHash(data)
|
||||
}
|
||||
|
||||
// XDCx Lending State
|
||||
|
||||
// lendingStateKey returns the key for lending state
|
||||
func lendingStateKey(root common.Hash) []byte {
|
||||
return append(lendingStatePrefix, root.Bytes()...)
|
||||
}
|
||||
|
||||
// WriteLendingStateRoot writes the lending state root for a block
|
||||
func WriteLendingStateRoot(db ethdb.KeyValueWriter, blockHash, lendingRoot common.Hash) error {
|
||||
return db.Put(lendingStateKey(blockHash), lendingRoot.Bytes())
|
||||
}
|
||||
|
||||
// ReadLendingStateRoot reads the lending state root for a block
|
||||
func ReadLendingStateRoot(db ethdb.KeyValueReader, blockHash common.Hash) common.Hash {
|
||||
data, err := db.Get(lendingStateKey(blockHash))
|
||||
if err != nil || len(data) != common.HashLength {
|
||||
return common.Hash{}
|
||||
}
|
||||
return common.BytesToHash(data)
|
||||
}
|
||||
124
docker-compose.yml
Normal file
124
docker-compose.yml
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
xdc-mainnet:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: xdc-mainnet
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "30303:30303/tcp"
|
||||
- "30303:30303/udp"
|
||||
- "8545:8545"
|
||||
- "8546:8546"
|
||||
volumes:
|
||||
- xdc-mainnet-data:/data/xdc
|
||||
environment:
|
||||
- NETWORK=mainnet
|
||||
command: >
|
||||
--datadir /data/xdc
|
||||
--networkid 50
|
||||
--http --http.addr 0.0.0.0 --http.port 8545
|
||||
--http.api eth,net,web3,xdpos,xdcx
|
||||
--http.corsdomain *
|
||||
--ws --ws.addr 0.0.0.0 --ws.port 8546
|
||||
--ws.api eth,net,web3
|
||||
--syncmode fast
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8545", "-X", "POST", "-H", "Content-Type: application/json", "--data", '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}']
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
xdc-testnet:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.testnet
|
||||
container_name: xdc-testnet
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "30304:30303/tcp"
|
||||
- "30304:30303/udp"
|
||||
- "8547:8545"
|
||||
- "8548:8546"
|
||||
volumes:
|
||||
- xdc-testnet-data:/data/xdc
|
||||
environment:
|
||||
- NETWORK=testnet
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8545", "-X", "POST", "-H", "Content-Type: application/json", "--data", '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}']
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
xdc-dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: xdc-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "30305:30303/tcp"
|
||||
- "30305:30303/udp"
|
||||
- "8549:8545"
|
||||
- "8550:8546"
|
||||
- "40000:40000"
|
||||
volumes:
|
||||
- xdc-dev-data:/data/xdc
|
||||
- ./:/go/src/github.com/ethereum/go-ethereum:cached
|
||||
environment:
|
||||
- NETWORK=devnet
|
||||
|
||||
xdc-bootnode:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.bootnode
|
||||
container_name: xdc-bootnode
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "30301:30301/udp"
|
||||
volumes:
|
||||
- xdc-bootnode-data:/data/bootnode
|
||||
command: >
|
||||
--nodekey /data/bootnode/nodekey
|
||||
--addr :30301
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: xdc-prometheus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: xdc-grafana
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
volumes:
|
||||
xdc-mainnet-data:
|
||||
xdc-testnet-data:
|
||||
xdc-dev-data:
|
||||
xdc-bootnode-data:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: xdc-network
|
||||
266
docs/SYNC.md
Normal file
266
docs/SYNC.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
# XDC Network Synchronization Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers the various synchronization modes available for the XDC Network and how to optimize sync performance.
|
||||
|
||||
## Sync Modes
|
||||
|
||||
### 1. Full Sync
|
||||
Downloads and verifies every block from genesis.
|
||||
|
||||
```bash
|
||||
./XDC --syncmode full
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Complete blockchain history
|
||||
- Can serve historical data
|
||||
- Most secure
|
||||
|
||||
**Cons:**
|
||||
- Longest sync time
|
||||
- Highest disk usage
|
||||
|
||||
### 2. Fast Sync (Default)
|
||||
Downloads blocks and state at a recent checkpoint.
|
||||
|
||||
```bash
|
||||
./XDC --syncmode fast
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Faster initial sync
|
||||
- Lower disk usage during sync
|
||||
- Good for most users
|
||||
|
||||
**Cons:**
|
||||
- No historical state
|
||||
- Depends on checkpoint validity
|
||||
|
||||
### 3. Snap Sync
|
||||
Uses snapshot data for rapid synchronization.
|
||||
|
||||
```bash
|
||||
./XDC --xdc.snapsync --xdc.snapshot.block 50000000
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Fastest sync method
|
||||
- Minimal bandwidth
|
||||
- Ideal for new nodes
|
||||
|
||||
**Cons:**
|
||||
- Requires trusted snapshot
|
||||
- No pre-snapshot history
|
||||
|
||||
## XDC-Specific Sync Features
|
||||
|
||||
### Checkpoint Verification
|
||||
XDC uses checkpoints for additional security.
|
||||
|
||||
```bash
|
||||
./XDC --xdc.checkpoint.interval 900
|
||||
```
|
||||
|
||||
### Validator Set Sync
|
||||
Synchronizes the current validator set.
|
||||
|
||||
```go
|
||||
// Automatically syncs validator set at epoch boundaries
|
||||
```
|
||||
|
||||
### Trading State Sync (XDCx)
|
||||
Syncs order book state if XDCx is enabled.
|
||||
|
||||
```bash
|
||||
./XDC --xdcx
|
||||
```
|
||||
|
||||
### Lending State Sync
|
||||
Syncs lending state if XDCxLending is enabled.
|
||||
|
||||
```bash
|
||||
./XDC --xdcxlending
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Hardware Requirements
|
||||
|
||||
| Component | Minimum | Recommended |
|
||||
|-----------|---------|-------------|
|
||||
| CPU | 4 cores | 8+ cores |
|
||||
| RAM | 16 GB | 32+ GB |
|
||||
| Disk | 500 GB SSD | 1+ TB NVMe |
|
||||
| Network | 25 Mbps | 100+ Mbps |
|
||||
|
||||
### Configuration Tuning
|
||||
|
||||
#### Cache Settings
|
||||
```bash
|
||||
./XDC --cache 4096 --cache.gc 50
|
||||
```
|
||||
|
||||
#### Database Settings
|
||||
```bash
|
||||
./XDC --db.engine pebble
|
||||
```
|
||||
|
||||
#### Network Settings
|
||||
```bash
|
||||
./XDC --maxpeers 50 --maxpendpeers 25
|
||||
```
|
||||
|
||||
## Sync Progress Monitoring
|
||||
|
||||
### RPC Methods
|
||||
```javascript
|
||||
// Get sync status
|
||||
eth.syncing
|
||||
|
||||
// Get current block
|
||||
eth.blockNumber
|
||||
|
||||
// Get peer count
|
||||
net.peerCount
|
||||
```
|
||||
|
||||
### Log Monitoring
|
||||
```bash
|
||||
tail -f /var/log/xdc/node.log | grep -i sync
|
||||
```
|
||||
|
||||
### Expected Sync Times
|
||||
|
||||
| Mode | Mainnet | Testnet |
|
||||
|------|---------|---------|
|
||||
| Full | 3-7 days | 1-2 days |
|
||||
| Fast | 6-24 hours | 2-6 hours |
|
||||
| Snap | 1-4 hours | 30-60 min |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sync Stuck
|
||||
|
||||
1. Check peer connections
|
||||
```bash
|
||||
./XDC attach --exec "admin.peers.length"
|
||||
```
|
||||
|
||||
2. Add bootnodes
|
||||
```bash
|
||||
./XDC --bootnodes "enode://..."
|
||||
```
|
||||
|
||||
3. Clear bad peers
|
||||
```bash
|
||||
./XDC attach --exec "admin.peers"
|
||||
```
|
||||
|
||||
### Low Peer Count
|
||||
|
||||
1. Check firewall rules
|
||||
2. Open P2P ports (30303)
|
||||
3. Enable UPnP
|
||||
|
||||
```bash
|
||||
./XDC --nat extip:YOUR_IP
|
||||
```
|
||||
|
||||
### Disk Space Issues
|
||||
|
||||
1. Enable pruning
|
||||
```bash
|
||||
./XDC --gcmode archive
|
||||
```
|
||||
|
||||
2. Use snap sync
|
||||
3. Resize disk
|
||||
|
||||
### Memory Issues
|
||||
|
||||
1. Reduce cache
|
||||
```bash
|
||||
./XDC --cache 2048
|
||||
```
|
||||
|
||||
2. Limit peers
|
||||
```bash
|
||||
./XDC --maxpeers 25
|
||||
```
|
||||
|
||||
## Network-Specific Settings
|
||||
|
||||
### Mainnet
|
||||
```bash
|
||||
./XDC --xdc.mainnet \
|
||||
--datadir /data/xdc/mainnet \
|
||||
--syncmode fast
|
||||
```
|
||||
|
||||
### Testnet (Apothem)
|
||||
```bash
|
||||
./XDC --xdc.testnet \
|
||||
--datadir /data/xdc/testnet \
|
||||
--syncmode fast
|
||||
```
|
||||
|
||||
### Devnet
|
||||
```bash
|
||||
./XDC --xdc.devnet \
|
||||
--datadir /data/xdc/devnet \
|
||||
--syncmode full
|
||||
```
|
||||
|
||||
## State Pruning
|
||||
|
||||
### Enable Pruning
|
||||
```bash
|
||||
./XDC --gcmode full --state.gc.percent 25
|
||||
```
|
||||
|
||||
### Manual Pruning
|
||||
```bash
|
||||
./XDC snapshot prune-state --datadir /data/xdc
|
||||
```
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Create Backup
|
||||
```bash
|
||||
# Stop node first
|
||||
./XDC copydb /data/xdc /backup/xdc
|
||||
```
|
||||
|
||||
### Restore Backup
|
||||
```bash
|
||||
cp -r /backup/xdc /data/xdc
|
||||
./XDC --datadir /data/xdc
|
||||
```
|
||||
|
||||
## Monitoring Tools
|
||||
|
||||
### Prometheus Metrics
|
||||
```bash
|
||||
./XDC --metrics --metrics.addr 0.0.0.0 --metrics.port 6060
|
||||
```
|
||||
|
||||
### Key Metrics
|
||||
- `chain_head_block`: Current block number
|
||||
- `p2p_peers`: Connected peer count
|
||||
- `chain_sync_mode`: Current sync mode
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use SSDs**: Much faster than HDDs
|
||||
2. **Adequate RAM**: Prevents swap usage
|
||||
3. **Stable Network**: Reduces peer churn
|
||||
4. **Regular Backups**: Protect against corruption
|
||||
5. **Monitor Disk**: Prevent space exhaustion
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Node Deployment Guide](./DEPLOYMENT.md)
|
||||
- [Network Configuration](./NETWORK.md)
|
||||
- [Performance Tuning](./PERFORMANCE.md)
|
||||
187
docs/XDCx.md
Normal file
187
docs/XDCx.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# XDCx - Decentralized Exchange Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
XDCx is a decentralized exchange (DEX) protocol built into the XDC Network at the protocol level. It enables trustless trading of XRC20 tokens with on-chain order matching and settlement.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **On-chain Order Book**: All orders stored on blockchain
|
||||
- **Atomic Swaps**: Instant settlement without counterparty risk
|
||||
- **Low Fees**: Transaction costs only
|
||||
- **Relayer Network**: Decentralized order relay
|
||||
- **Cross-chain Ready**: Bridge support for multi-chain assets
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ XDCx │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Order │ │ Trading │ │ Matcher │ │
|
||||
│ │ Processor │ │ State │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Relayer │ │ API │ │ Events │ │
|
||||
│ │ Registry │ │ Layer │ │ Emitter │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Order Types
|
||||
|
||||
### Limit Orders
|
||||
```json
|
||||
{
|
||||
"baseToken": "0x...",
|
||||
"quoteToken": "0x...",
|
||||
"side": "buy",
|
||||
"price": "1000000000000000000",
|
||||
"quantity": "5000000000000000000"
|
||||
}
|
||||
```
|
||||
|
||||
### Market Orders
|
||||
- Execute at best available price
|
||||
- Fill-or-kill semantics
|
||||
|
||||
## Order Lifecycle
|
||||
|
||||
1. **Creation**: User creates and signs order
|
||||
2. **Submission**: Order submitted to relayer
|
||||
3. **Matching**: Engine matches against order book
|
||||
4. **Settlement**: Tokens transferred atomically
|
||||
5. **Confirmation**: Trade recorded on chain
|
||||
|
||||
## Trading Pairs
|
||||
|
||||
### Adding a Trading Pair
|
||||
1. Register through XDCx listing contract
|
||||
2. Provide initial liquidity
|
||||
3. Set trading parameters
|
||||
|
||||
### Pair Requirements
|
||||
- Both tokens must be XRC20 compliant
|
||||
- Minimum liquidity thresholds
|
||||
- Registration fee
|
||||
|
||||
## Relayer System
|
||||
|
||||
### Becoming a Relayer
|
||||
1. Stake required XDC amount
|
||||
2. Deploy relayer infrastructure
|
||||
3. Register in relayer contract
|
||||
4. Start accepting orders
|
||||
|
||||
### Relayer Fees
|
||||
- Configurable by relayer
|
||||
- Typical: 0.1% maker, 0.2% taker
|
||||
- Fee split with protocol
|
||||
|
||||
## API Reference
|
||||
|
||||
### Submit Order
|
||||
```javascript
|
||||
xdcx.sendOrder({
|
||||
baseToken: "0x...",
|
||||
quoteToken: "0x...",
|
||||
side: "buy",
|
||||
type: "limit",
|
||||
price: "1000000000000000000",
|
||||
quantity: "5000000000000000000",
|
||||
nonce: 1,
|
||||
signature: "0x..."
|
||||
})
|
||||
```
|
||||
|
||||
### Cancel Order
|
||||
```javascript
|
||||
xdcx.cancelOrder(orderId)
|
||||
```
|
||||
|
||||
### Get Order Book
|
||||
```javascript
|
||||
xdcx.getOrderBook(baseToken, quoteToken)
|
||||
```
|
||||
|
||||
### Get Trades
|
||||
```javascript
|
||||
xdcx.getTrades(baseToken, quoteToken, limit)
|
||||
```
|
||||
|
||||
## Smart Contracts
|
||||
|
||||
### XDCx Listing
|
||||
- Token pair registration
|
||||
- Listing fee management
|
||||
- Parameter configuration
|
||||
|
||||
### Relayer Registration
|
||||
- Relayer stake management
|
||||
- Fee configuration
|
||||
- Status tracking
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable XDCx
|
||||
```bash
|
||||
./XDC --xdcx --xdcx.datadir /path/to/xdcx/data
|
||||
```
|
||||
|
||||
### Node Configuration
|
||||
```toml
|
||||
[XDCx]
|
||||
Enabled = true
|
||||
DataDir = "/data/xdcx"
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
### Order Events
|
||||
- `OrderCreated`
|
||||
- `OrderMatched`
|
||||
- `OrderCancelled`
|
||||
- `OrderFilled`
|
||||
|
||||
### Trade Events
|
||||
- `TradeExecuted`
|
||||
- `TradeSettled`
|
||||
|
||||
## Security
|
||||
|
||||
1. **Signature Verification**: All orders cryptographically signed
|
||||
2. **Replay Protection**: Nonce-based order uniqueness
|
||||
3. **Balance Checks**: Real-time balance verification
|
||||
4. **Rate Limiting**: Prevent spam attacks
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Traders
|
||||
- Use limit orders for better prices
|
||||
- Monitor order book depth
|
||||
- Keep private keys secure
|
||||
|
||||
### For Relayers
|
||||
- Maintain high uptime
|
||||
- Competitive fee structure
|
||||
- Fast order matching
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Order Not Matching
|
||||
- Check balance sufficient
|
||||
- Verify price format
|
||||
- Confirm token approval
|
||||
|
||||
### Settlement Failed
|
||||
- Insufficient gas
|
||||
- Token transfer restriction
|
||||
- Price moved significantly
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [XDCx Technical Specification](https://docs.xinfin.org/xdcx)
|
||||
- [Relayer Setup Guide](https://docs.xinfin.org/relayer)
|
||||
- [API Documentation](https://docs.xinfin.org/api/xdcx)
|
||||
243
docs/XDCxLending.md
Normal file
243
docs/XDCxLending.md
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# XDCxLending - Decentralized Lending Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
XDCxLending is a decentralized lending protocol built into the XDC Network. It enables peer-to-peer lending and borrowing of XRC20 tokens with collateralized loans.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Collateralized Loans**: Secure lending with over-collateralization
|
||||
- **Variable Interest Rates**: Market-driven rates
|
||||
- **Automatic Liquidation**: Protect lenders from defaults
|
||||
- **Multiple Terms**: Flexible loan durations
|
||||
- **On-chain Settlement**: Trustless execution
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ XDCxLending │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Order │ │ Lending │ │ Liquidator │ │
|
||||
│ │ Processor │ │ State │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Loan │ │ Interest │ │ Collateral │ │
|
||||
│ │ Manager │ │ Calculator │ │ Manager │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Loan Types
|
||||
|
||||
### Borrow Order
|
||||
Request to borrow tokens at a specific interest rate.
|
||||
|
||||
```json
|
||||
{
|
||||
"lendingToken": "0x...",
|
||||
"collateralToken": "0x...",
|
||||
"side": "borrow",
|
||||
"interestRate": "500",
|
||||
"term": "2592000",
|
||||
"quantity": "1000000000000000000"
|
||||
}
|
||||
```
|
||||
|
||||
### Lend Order
|
||||
Offer to lend tokens at a specific interest rate.
|
||||
|
||||
```json
|
||||
{
|
||||
"lendingToken": "0x...",
|
||||
"collateralToken": "0x...",
|
||||
"side": "lend",
|
||||
"interestRate": "300",
|
||||
"term": "2592000",
|
||||
"quantity": "10000000000000000000"
|
||||
}
|
||||
```
|
||||
|
||||
## Loan Lifecycle
|
||||
|
||||
1. **Order Creation**: User creates borrow/lend order
|
||||
2. **Matching**: Orders matched by interest rate
|
||||
3. **Collateral Lock**: Borrower's collateral locked
|
||||
4. **Disbursement**: Lending tokens transferred
|
||||
5. **Active Loan**: Interest accrues
|
||||
6. **Repayment**: Borrower repays principal + interest
|
||||
7. **Collateral Release**: Collateral returned
|
||||
|
||||
## Collateral Management
|
||||
|
||||
### Collateral Ratio
|
||||
- Minimum: 150%
|
||||
- Liquidation: 110%
|
||||
|
||||
### Topup
|
||||
Borrowers can add collateral to avoid liquidation.
|
||||
|
||||
### Liquidation
|
||||
When collateral ratio falls below threshold:
|
||||
1. Loan marked for liquidation
|
||||
2. Collateral sold to repay lender
|
||||
3. Penalty applied to borrower
|
||||
|
||||
## Interest Calculation
|
||||
|
||||
### Simple Interest
|
||||
```
|
||||
Interest = Principal × Rate × Time / (365 × 100)
|
||||
```
|
||||
|
||||
### Example
|
||||
- Principal: 1000 XDC
|
||||
- Rate: 5% annual
|
||||
- Term: 30 days
|
||||
- Interest: 1000 × 5 × 30 / (365 × 100) = 4.11 XDC
|
||||
|
||||
## API Reference
|
||||
|
||||
### Create Lending Order
|
||||
```javascript
|
||||
xdcxlending.sendLendingOrder({
|
||||
lendingToken: "0x...",
|
||||
collateralToken: "0x...",
|
||||
side: "borrow",
|
||||
type: "limit",
|
||||
interestRate: "500",
|
||||
term: 2592000,
|
||||
quantity: "1000000000000000000",
|
||||
nonce: 1,
|
||||
signature: "0x..."
|
||||
})
|
||||
```
|
||||
|
||||
### Topup Collateral
|
||||
```javascript
|
||||
xdcxlending.topup(loanId, amount)
|
||||
```
|
||||
|
||||
### Repay Loan
|
||||
```javascript
|
||||
xdcxlending.repay(loanId)
|
||||
```
|
||||
|
||||
### Get Loan
|
||||
```javascript
|
||||
xdcxlending.getLoan(loanId)
|
||||
```
|
||||
|
||||
### Get Order Book
|
||||
```javascript
|
||||
xdcxlending.getLendingOrderBook(lendingToken, collateralToken, term)
|
||||
```
|
||||
|
||||
## Smart Contracts
|
||||
|
||||
### Lending Registration
|
||||
- Relayer registration
|
||||
- Fee configuration
|
||||
- Term management
|
||||
|
||||
### Collateral Contract
|
||||
- Collateral deposit/withdrawal
|
||||
- Ratio calculation
|
||||
- Liquidation trigger
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable XDCxLending
|
||||
```bash
|
||||
./XDC --xdcxlending --xdcxlending.datadir /path/to/lending/data
|
||||
```
|
||||
|
||||
### Parameters
|
||||
```go
|
||||
type Config struct {
|
||||
DefaultTerm uint64 // 30 days
|
||||
MinCollateral *big.Int // 150%
|
||||
LiquidationRate *big.Int // 110%
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
### Lending Events
|
||||
- `LendingOrderCreated`
|
||||
- `LendingOrderMatched`
|
||||
- `LendingOrderCancelled`
|
||||
|
||||
### Loan Events
|
||||
- `LoanCreated`
|
||||
- `LoanTopup`
|
||||
- `LoanRepaid`
|
||||
- `LoanLiquidated`
|
||||
|
||||
## Risk Management
|
||||
|
||||
### For Lenders
|
||||
- Collateral protects against default
|
||||
- Automatic liquidation mechanism
|
||||
- Interest rate based on risk
|
||||
|
||||
### For Borrowers
|
||||
- Monitor collateral ratio
|
||||
- Topup before liquidation
|
||||
- Choose appropriate term
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Price Oracle**: Collateral valuation
|
||||
2. **Liquidation Delay**: Time buffer
|
||||
3. **Rate Limits**: Prevent manipulation
|
||||
4. **Emergency Pause**: Protocol safety
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Borrowers
|
||||
- Maintain healthy collateral ratio
|
||||
- Set alerts for low collateral
|
||||
- Repay before term expiry
|
||||
|
||||
### For Lenders
|
||||
- Diversify lending portfolio
|
||||
- Choose appropriate interest rates
|
||||
- Monitor loan status
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Order Not Matching
|
||||
- Check interest rate competitive
|
||||
- Verify collateral sufficient
|
||||
- Confirm token balances
|
||||
|
||||
### Liquidation Concerns
|
||||
- Add collateral (topup)
|
||||
- Monitor market prices
|
||||
- Set up alerts
|
||||
|
||||
## Example Workflow
|
||||
|
||||
### Borrower
|
||||
1. Deposit collateral (150% of loan)
|
||||
2. Create borrow order
|
||||
3. Receive lending tokens
|
||||
4. Use tokens as needed
|
||||
5. Repay before expiry
|
||||
6. Receive collateral back
|
||||
|
||||
### Lender
|
||||
1. Have lending tokens available
|
||||
2. Create lend order
|
||||
3. Tokens transferred on match
|
||||
4. Receive interest during term
|
||||
5. Get principal + interest on repay
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [XDCxLending Specification](https://docs.xinfin.org/lending)
|
||||
- [Risk Parameters](https://docs.xinfin.org/lending/risk)
|
||||
- [API Documentation](https://docs.xinfin.org/api/lending)
|
||||
177
docs/XDPoS.md
Normal file
177
docs/XDPoS.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# XDPoS Consensus Mechanism
|
||||
|
||||
## Overview
|
||||
|
||||
XDPoS (XinFin Delegated Proof of Stake) is a consensus mechanism designed for the XDC Network. It combines the benefits of Delegated Proof of Stake (DPoS) with practical Byzantine Fault Tolerance (pBFT) to achieve high throughput, low latency, and energy efficiency.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Validator Selection
|
||||
- 108 Masternodes participate in block validation
|
||||
- Validators are selected based on stake and voting
|
||||
- Minimum stake requirement: 10,000,000 XDC
|
||||
- Epoch-based validator set updates (900 blocks)
|
||||
|
||||
### 2. Block Production
|
||||
- 2-second block time
|
||||
- Round-robin block production among validators
|
||||
- Block finality achieved through validator signatures
|
||||
|
||||
### 3. Consensus Versions
|
||||
|
||||
#### XDPoS v1 (Legacy)
|
||||
- Simple round-robin block production
|
||||
- Snapshot-based validator management
|
||||
- Used until block X (network upgrade)
|
||||
|
||||
#### XDPoS v2 (Current)
|
||||
- Enhanced BFT-style consensus
|
||||
- Gap blocks for improved finality
|
||||
- Vote aggregation for efficiency
|
||||
- Penalty mechanism for misbehavior
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ XDPoS Engine │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Engine V1 │ │ Engine V2 │ │ Snapshot │ │
|
||||
│ │ (Legacy) │ │ (Current) │ │ Manager │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Validator │ │ Reward │ │ Penalty │ │
|
||||
│ │ Selection │ │ Calculator │ │ Handler │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Chain Config Parameters
|
||||
|
||||
```go
|
||||
type XDPoSConfig struct {
|
||||
Period uint64 // Block time in seconds (default: 2)
|
||||
Epoch uint64 // Epoch length in blocks (default: 900)
|
||||
Gap uint64 // Gap for snapshots (default: 450)
|
||||
V2 *XDPoSV2Config
|
||||
}
|
||||
```
|
||||
|
||||
### Command Line Flags
|
||||
|
||||
```bash
|
||||
--xdpos.rewards Enable block rewards
|
||||
--xdpos.slashing Enable slashing for misbehavior
|
||||
--xdpos.epoch Epoch length (default: 900)
|
||||
--xdpos.gap Gap for snapshots (default: 450)
|
||||
```
|
||||
|
||||
## Validator Operations
|
||||
|
||||
### Becoming a Masternode
|
||||
|
||||
1. Stake minimum 10,000,000 XDC
|
||||
2. Run a full node with masternode configuration
|
||||
3. Register through the validator contract
|
||||
|
||||
### Voting
|
||||
|
||||
- XDC holders can vote for validators
|
||||
- Votes are weighted by stake
|
||||
- Vote changes take effect at epoch boundaries
|
||||
|
||||
### Rewards
|
||||
|
||||
- Block rewards distributed to validators
|
||||
- Rewards proportional to stake and participation
|
||||
- Foundation wallet receives protocol fees
|
||||
|
||||
## Penalty System
|
||||
|
||||
### Penalties Applied For:
|
||||
- Missing block production slot
|
||||
- Double signing
|
||||
- Invalid block proposals
|
||||
- Extended downtime
|
||||
|
||||
### Penalty Amounts:
|
||||
- Minor offense: Warning
|
||||
- Repeated offense: Stake slash
|
||||
- Major offense: Removal from validator set
|
||||
|
||||
## Network Synchronization
|
||||
|
||||
### Fast Sync
|
||||
1. Download block headers
|
||||
2. Verify validator signatures
|
||||
3. Download state at checkpoint
|
||||
4. Continue from checkpoint
|
||||
|
||||
### Full Sync
|
||||
1. Download all blocks
|
||||
2. Verify all transactions
|
||||
3. Replay all state changes
|
||||
|
||||
## API Reference
|
||||
|
||||
### RPC Methods
|
||||
|
||||
```javascript
|
||||
// Get current validators
|
||||
xdpos.getValidators()
|
||||
|
||||
// Get validator info
|
||||
xdpos.getValidatorInfo(address)
|
||||
|
||||
// Get snapshot at block
|
||||
xdpos.getSnapshot(blockNumber)
|
||||
|
||||
// Get rewards for epoch
|
||||
xdpos.getEpochRewards(epoch)
|
||||
```
|
||||
|
||||
## Smart Contracts
|
||||
|
||||
### Validator Contract
|
||||
- Address: `0x0000000000000000000000000000000000000088`
|
||||
- Functions: register, vote, withdraw, getValidators
|
||||
|
||||
### Block Signer Contract
|
||||
- Address: `0x0000000000000000000000000000000000000089`
|
||||
- Functions: sign, getSigners, getRewardPercent
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Validator Key Security**: Keep masternode keys secure
|
||||
2. **Network Security**: Use firewalls, restrict P2P ports
|
||||
3. **Update Regularly**: Apply security patches promptly
|
||||
4. **Monitor**: Watch for unusual block production patterns
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Not producing blocks**
|
||||
- Check if node is synced
|
||||
- Verify validator registration
|
||||
- Check stake requirement
|
||||
|
||||
2. **Sync issues**
|
||||
- Verify peer connections
|
||||
- Check network connectivity
|
||||
- Try different bootnodes
|
||||
|
||||
3. **Penalties received**
|
||||
- Check node uptime
|
||||
- Verify clock synchronization
|
||||
- Review logs for errors
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [XDC Network Documentation](https://docs.xinfin.org)
|
||||
- [Masternode Setup Guide](https://docs.xinfin.org/docs/masternode)
|
||||
- [XDPoS Technical Paper](https://xinfin.org/xdpos)
|
||||
300
eth/downloader/downloader_xdc.go
Normal file
300
eth/downloader/downloader_xdc.go
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrXDCSnapshotRequired indicates XDC snapshot is needed
|
||||
ErrXDCSnapshotRequired = errors.New("XDC snapshot required for sync")
|
||||
// ErrXDCValidatorSyncFailed indicates validator sync failed
|
||||
ErrXDCValidatorSyncFailed = errors.New("XDC validator set sync failed")
|
||||
)
|
||||
|
||||
// XDCDownloaderConfig holds XDC-specific downloader configuration
|
||||
type XDCDownloaderConfig struct {
|
||||
// SnapShotBlockHash is the hash of the snapshot block to sync from
|
||||
SnapShotBlockHash common.Hash
|
||||
// SnapShotBlockNumber is the block number of the snapshot
|
||||
SnapShotBlockNumber uint64
|
||||
// ValidatorSetURL is the URL to fetch the initial validator set
|
||||
ValidatorSetURL string
|
||||
// EnableFastSync enables fast sync mode
|
||||
EnableFastSync bool
|
||||
// CheckpointInterval is the interval between checkpoints
|
||||
CheckpointInterval uint64
|
||||
}
|
||||
|
||||
// DefaultXDCDownloaderConfig returns the default XDC downloader config
|
||||
func DefaultXDCDownloaderConfig() *XDCDownloaderConfig {
|
||||
return &XDCDownloaderConfig{
|
||||
EnableFastSync: true,
|
||||
CheckpointInterval: 900, // ~15 minutes at 1s blocks
|
||||
}
|
||||
}
|
||||
|
||||
// XDCSyncState represents the XDC sync state
|
||||
type XDCSyncState struct {
|
||||
Mode string
|
||||
CurrentBlock uint64
|
||||
HighestBlock uint64
|
||||
StartingBlock uint64
|
||||
ValidatorCount int
|
||||
Syncing bool
|
||||
Error error
|
||||
}
|
||||
|
||||
// XDCDownloaderExtension extends the downloader with XDC-specific functionality
|
||||
type XDCDownloaderExtension struct {
|
||||
downloader *Downloader
|
||||
config *XDCDownloaderConfig
|
||||
|
||||
// Validator set sync
|
||||
validatorSetSync *ValidatorSetSyncer
|
||||
|
||||
// Checkpoint management
|
||||
checkpoints map[uint64]common.Hash
|
||||
checkpointsLock sync.RWMutex
|
||||
|
||||
// Sync state
|
||||
syncState XDCSyncState
|
||||
syncStateLock sync.RWMutex
|
||||
|
||||
// Channels
|
||||
quitCh chan struct{}
|
||||
}
|
||||
|
||||
// NewXDCDownloaderExtension creates a new XDC downloader extension
|
||||
func NewXDCDownloaderExtension(dl *Downloader, config *XDCDownloaderConfig) *XDCDownloaderExtension {
|
||||
if config == nil {
|
||||
config = DefaultXDCDownloaderConfig()
|
||||
}
|
||||
|
||||
ext := &XDCDownloaderExtension{
|
||||
downloader: dl,
|
||||
config: config,
|
||||
checkpoints: make(map[uint64]common.Hash),
|
||||
validatorSetSync: NewValidatorSetSyncer(),
|
||||
quitCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
// Start starts the XDC extension
|
||||
func (x *XDCDownloaderExtension) Start() error {
|
||||
log.Info("Starting XDC downloader extension")
|
||||
go x.syncLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the XDC extension
|
||||
func (x *XDCDownloaderExtension) Stop() {
|
||||
close(x.quitCh)
|
||||
}
|
||||
|
||||
// syncLoop handles XDC-specific sync operations
|
||||
func (x *XDCDownloaderExtension) syncLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
x.updateSyncState()
|
||||
x.checkCheckpoints()
|
||||
case <-x.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateSyncState updates the sync state
|
||||
func (x *XDCDownloaderExtension) updateSyncState() {
|
||||
x.syncStateLock.Lock()
|
||||
defer x.syncStateLock.Unlock()
|
||||
|
||||
// Update sync state from downloader
|
||||
progress := x.downloader.Progress()
|
||||
x.syncState.CurrentBlock = progress.CurrentBlock
|
||||
x.syncState.HighestBlock = progress.HighestBlock
|
||||
x.syncState.StartingBlock = progress.StartingBlock
|
||||
x.syncState.Syncing = x.syncState.CurrentBlock < x.syncState.HighestBlock
|
||||
}
|
||||
|
||||
// checkCheckpoints checks for new checkpoints
|
||||
func (x *XDCDownloaderExtension) checkCheckpoints() {
|
||||
x.checkpointsLock.Lock()
|
||||
defer x.checkpointsLock.Unlock()
|
||||
|
||||
// Add checkpoint logic here
|
||||
}
|
||||
|
||||
// GetSyncState returns the current sync state
|
||||
func (x *XDCDownloaderExtension) GetSyncState() XDCSyncState {
|
||||
x.syncStateLock.RLock()
|
||||
defer x.syncStateLock.RUnlock()
|
||||
return x.syncState
|
||||
}
|
||||
|
||||
// AddCheckpoint adds a checkpoint
|
||||
func (x *XDCDownloaderExtension) AddCheckpoint(blockNumber uint64, hash common.Hash) {
|
||||
x.checkpointsLock.Lock()
|
||||
defer x.checkpointsLock.Unlock()
|
||||
x.checkpoints[blockNumber] = hash
|
||||
}
|
||||
|
||||
// GetCheckpoint returns a checkpoint hash
|
||||
func (x *XDCDownloaderExtension) GetCheckpoint(blockNumber uint64) (common.Hash, bool) {
|
||||
x.checkpointsLock.RLock()
|
||||
defer x.checkpointsLock.RUnlock()
|
||||
hash, ok := x.checkpoints[blockNumber]
|
||||
return hash, ok
|
||||
}
|
||||
|
||||
// VerifyCheckpoint verifies a block against a checkpoint
|
||||
func (x *XDCDownloaderExtension) VerifyCheckpoint(block *types.Block) bool {
|
||||
hash, ok := x.GetCheckpoint(block.NumberU64())
|
||||
if !ok {
|
||||
return true // No checkpoint to verify
|
||||
}
|
||||
return block.Hash() == hash
|
||||
}
|
||||
|
||||
// ValidatorSetSyncer handles validator set synchronization
|
||||
type ValidatorSetSyncer struct {
|
||||
validators []common.Address
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewValidatorSetSyncer creates a new validator set syncer
|
||||
func NewValidatorSetSyncer() *ValidatorSetSyncer {
|
||||
return &ValidatorSetSyncer{
|
||||
validators: make([]common.Address, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// SetValidators sets the validator set
|
||||
func (v *ValidatorSetSyncer) SetValidators(validators []common.Address) {
|
||||
v.lock.Lock()
|
||||
defer v.lock.Unlock()
|
||||
v.validators = make([]common.Address, len(validators))
|
||||
copy(v.validators, validators)
|
||||
}
|
||||
|
||||
// GetValidators returns the validator set
|
||||
func (v *ValidatorSetSyncer) GetValidators() []common.Address {
|
||||
v.lock.RLock()
|
||||
defer v.lock.RUnlock()
|
||||
result := make([]common.Address, len(v.validators))
|
||||
copy(result, v.validators)
|
||||
return result
|
||||
}
|
||||
|
||||
// IsValidator checks if an address is a validator
|
||||
func (v *ValidatorSetSyncer) IsValidator(addr common.Address) bool {
|
||||
v.lock.RLock()
|
||||
defer v.lock.RUnlock()
|
||||
for _, validator := range v.validators {
|
||||
if validator == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SyncValidatorSet syncs the validator set from the network
|
||||
func (v *ValidatorSetSyncer) SyncValidatorSet() error {
|
||||
// This would sync from the network or snapshot
|
||||
return nil
|
||||
}
|
||||
|
||||
// XDCBlockValidator validates blocks with XDC consensus rules
|
||||
type XDCBlockValidator struct {
|
||||
validatorSet *ValidatorSetSyncer
|
||||
}
|
||||
|
||||
// NewXDCBlockValidator creates a new block validator
|
||||
func NewXDCBlockValidator(validatorSet *ValidatorSetSyncer) *XDCBlockValidator {
|
||||
return &XDCBlockValidator{
|
||||
validatorSet: validatorSet,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBlock validates a block
|
||||
func (v *XDCBlockValidator) ValidateBlock(block *types.Block) error {
|
||||
// Validate coinbase is a validator
|
||||
if !v.validatorSet.IsValidator(block.Coinbase()) {
|
||||
return errors.New("block miner is not a validator")
|
||||
}
|
||||
|
||||
// Additional XDPoS validation
|
||||
if err := v.validateXDPoSSignature(block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateXDPoSSignature validates the XDPoS signature in the block
|
||||
func (v *XDCBlockValidator) validateXDPoSSignature(block *types.Block) error {
|
||||
// Signature validation logic
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessXDCBlock processes XDC-specific block data during sync
|
||||
func ProcessXDCBlock(block *types.Block) error {
|
||||
// Process validator set updates
|
||||
// Process penalty transactions
|
||||
// Process reward distribution
|
||||
return nil
|
||||
}
|
||||
|
||||
// XDCStateSync represents XDC state sync
|
||||
type XDCStateSync struct {
|
||||
downloader *Downloader
|
||||
startBlock uint64
|
||||
targetBlock uint64
|
||||
progress float64
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewXDCStateSync creates a new state sync
|
||||
func NewXDCStateSync(dl *Downloader, start, target uint64) *XDCStateSync {
|
||||
return &XDCStateSync{
|
||||
downloader: dl,
|
||||
startBlock: start,
|
||||
targetBlock: target,
|
||||
}
|
||||
}
|
||||
|
||||
// Progress returns the sync progress (0-100)
|
||||
func (s *XDCStateSync) Progress() float64 {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
return s.progress
|
||||
}
|
||||
|
||||
// UpdateProgress updates the sync progress
|
||||
func (s *XDCStateSync) UpdateProgress(currentBlock uint64) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if s.targetBlock <= s.startBlock {
|
||||
s.progress = 100.0
|
||||
return
|
||||
}
|
||||
|
||||
total := float64(s.targetBlock - s.startBlock)
|
||||
current := float64(currentBlock - s.startBlock)
|
||||
s.progress = (current / total) * 100.0
|
||||
}
|
||||
258
eth/downloader/queue_xdc.go
Normal file
258
eth/downloader/queue_xdc.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// XDCQueue extends the download queue with XDC-specific functionality
|
||||
type XDCQueue struct {
|
||||
queue *queue
|
||||
|
||||
// XDC-specific queues
|
||||
validatorSetQueue chan *ValidatorSetRequest
|
||||
penaltyQueue chan *PenaltyRequest
|
||||
rewardQueue chan *RewardRequest
|
||||
|
||||
// Priority blocks (checkpoints, epoch blocks)
|
||||
priorityBlocks map[uint64]bool
|
||||
priorityLock sync.RWMutex
|
||||
|
||||
// Epoch tracking
|
||||
currentEpoch uint64
|
||||
epochBlocks map[uint64]common.Hash
|
||||
epochLock sync.RWMutex
|
||||
}
|
||||
|
||||
// ValidatorSetRequest represents a request for validator set data
|
||||
type ValidatorSetRequest struct {
|
||||
BlockNumber uint64
|
||||
BlockHash common.Hash
|
||||
Validators []common.Address
|
||||
}
|
||||
|
||||
// PenaltyRequest represents a penalty data request
|
||||
type PenaltyRequest struct {
|
||||
BlockNumber uint64
|
||||
Validator common.Address
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
// RewardRequest represents a reward data request
|
||||
type RewardRequest struct {
|
||||
BlockNumber uint64
|
||||
Validator common.Address
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
// NewXDCQueue creates a new XDC queue
|
||||
func NewXDCQueue(q *queue) *XDCQueue {
|
||||
return &XDCQueue{
|
||||
queue: q,
|
||||
validatorSetQueue: make(chan *ValidatorSetRequest, 1000),
|
||||
penaltyQueue: make(chan *PenaltyRequest, 1000),
|
||||
rewardQueue: make(chan *RewardRequest, 1000),
|
||||
priorityBlocks: make(map[uint64]bool),
|
||||
epochBlocks: make(map[uint64]common.Hash),
|
||||
}
|
||||
}
|
||||
|
||||
// MarkPriorityBlock marks a block as priority
|
||||
func (xq *XDCQueue) MarkPriorityBlock(blockNumber uint64) {
|
||||
xq.priorityLock.Lock()
|
||||
defer xq.priorityLock.Unlock()
|
||||
xq.priorityBlocks[blockNumber] = true
|
||||
log.Debug("Marked priority block", "number", blockNumber)
|
||||
}
|
||||
|
||||
// IsPriorityBlock checks if a block is a priority block
|
||||
func (xq *XDCQueue) IsPriorityBlock(blockNumber uint64) bool {
|
||||
xq.priorityLock.RLock()
|
||||
defer xq.priorityLock.RUnlock()
|
||||
return xq.priorityBlocks[blockNumber]
|
||||
}
|
||||
|
||||
// UnmarkPriorityBlock removes the priority mark from a block
|
||||
func (xq *XDCQueue) UnmarkPriorityBlock(blockNumber uint64) {
|
||||
xq.priorityLock.Lock()
|
||||
defer xq.priorityLock.Unlock()
|
||||
delete(xq.priorityBlocks, blockNumber)
|
||||
}
|
||||
|
||||
// SetEpochBlock sets the block hash for an epoch
|
||||
func (xq *XDCQueue) SetEpochBlock(epoch uint64, hash common.Hash) {
|
||||
xq.epochLock.Lock()
|
||||
defer xq.epochLock.Unlock()
|
||||
xq.epochBlocks[epoch] = hash
|
||||
}
|
||||
|
||||
// GetEpochBlock gets the block hash for an epoch
|
||||
func (xq *XDCQueue) GetEpochBlock(epoch uint64) (common.Hash, bool) {
|
||||
xq.epochLock.RLock()
|
||||
defer xq.epochLock.RUnlock()
|
||||
hash, ok := xq.epochBlocks[epoch]
|
||||
return hash, ok
|
||||
}
|
||||
|
||||
// QueueValidatorSetRequest queues a validator set request
|
||||
func (xq *XDCQueue) QueueValidatorSetRequest(req *ValidatorSetRequest) {
|
||||
select {
|
||||
case xq.validatorSetQueue <- req:
|
||||
default:
|
||||
log.Warn("Validator set queue full, dropping request")
|
||||
}
|
||||
}
|
||||
|
||||
// QueuePenaltyRequest queues a penalty request
|
||||
func (xq *XDCQueue) QueuePenaltyRequest(req *PenaltyRequest) {
|
||||
select {
|
||||
case xq.penaltyQueue <- req:
|
||||
default:
|
||||
log.Warn("Penalty queue full, dropping request")
|
||||
}
|
||||
}
|
||||
|
||||
// QueueRewardRequest queues a reward request
|
||||
func (xq *XDCQueue) QueueRewardRequest(req *RewardRequest) {
|
||||
select {
|
||||
case xq.rewardQueue <- req:
|
||||
default:
|
||||
log.Warn("Reward queue full, dropping request")
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessValidatorSetQueue processes validator set requests
|
||||
func (xq *XDCQueue) ProcessValidatorSetQueue() {
|
||||
for req := range xq.validatorSetQueue {
|
||||
xq.processValidatorSetRequest(req)
|
||||
}
|
||||
}
|
||||
|
||||
// processValidatorSetRequest processes a single validator set request
|
||||
func (xq *XDCQueue) processValidatorSetRequest(req *ValidatorSetRequest) {
|
||||
// Process validator set update
|
||||
log.Debug("Processing validator set request", "block", req.BlockNumber, "validators", len(req.Validators))
|
||||
}
|
||||
|
||||
// PrioritizeEpochBlocks ensures epoch transition blocks are prioritized
|
||||
func (xq *XDCQueue) PrioritizeEpochBlocks(epochLength uint64, startBlock, endBlock uint64) {
|
||||
for block := startBlock; block <= endBlock; block++ {
|
||||
if block%epochLength == 0 {
|
||||
xq.MarkPriorityBlock(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XDCBlockFetcher handles XDC-specific block fetching
|
||||
type XDCBlockFetcher struct {
|
||||
queue *XDCQueue
|
||||
pendingOps map[common.Hash]*fetchOp
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// fetchOp represents a fetch operation
|
||||
type fetchOp struct {
|
||||
hash common.Hash
|
||||
number uint64
|
||||
priority bool
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
// NewXDCBlockFetcher creates a new block fetcher
|
||||
func NewXDCBlockFetcher(queue *XDCQueue) *XDCBlockFetcher {
|
||||
return &XDCBlockFetcher{
|
||||
queue: queue,
|
||||
pendingOps: make(map[common.Hash]*fetchOp),
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleFetch schedules a block fetch
|
||||
func (f *XDCBlockFetcher) ScheduleFetch(hash common.Hash, number uint64) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
priority := f.queue.IsPriorityBlock(number)
|
||||
f.pendingOps[hash] = &fetchOp{
|
||||
hash: hash,
|
||||
number: number,
|
||||
priority: priority,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPendingCount returns the number of pending fetch operations
|
||||
func (f *XDCBlockFetcher) GetPendingCount() int {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
return len(f.pendingOps)
|
||||
}
|
||||
|
||||
// CompleteFetch marks a fetch as complete
|
||||
func (f *XDCBlockFetcher) CompleteFetch(hash common.Hash, block *types.Block) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
op, ok := f.pendingOps[hash]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(f.pendingOps, hash)
|
||||
|
||||
if op.priority {
|
||||
f.queue.UnmarkPriorityBlock(op.number)
|
||||
}
|
||||
}
|
||||
|
||||
// XDCDataProcessor processes XDC-specific data from downloaded blocks
|
||||
type XDCDataProcessor struct {
|
||||
queue *XDCQueue
|
||||
}
|
||||
|
||||
// NewXDCDataProcessor creates a new data processor
|
||||
func NewXDCDataProcessor(queue *XDCQueue) *XDCDataProcessor {
|
||||
return &XDCDataProcessor{queue: queue}
|
||||
}
|
||||
|
||||
// ProcessBlock processes XDC data from a block
|
||||
func (p *XDCDataProcessor) ProcessBlock(block *types.Block) error {
|
||||
// Extract validator set changes
|
||||
if err := p.processValidatorSetChanges(block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract penalties
|
||||
if err := p.processPenalties(block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract rewards
|
||||
if err := p.processRewards(block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processValidatorSetChanges processes validator set changes
|
||||
func (p *XDCDataProcessor) processValidatorSetChanges(block *types.Block) error {
|
||||
// Parse block extra data for validator set changes
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPenalties processes penalty transactions
|
||||
func (p *XDCDataProcessor) processPenalties(block *types.Block) error {
|
||||
// Parse penalty transactions
|
||||
return nil
|
||||
}
|
||||
|
||||
// processRewards processes reward distribution
|
||||
func (p *XDCDataProcessor) processRewards(block *types.Block) error {
|
||||
// Parse reward distribution
|
||||
return nil
|
||||
}
|
||||
341
eth/downloader/statesync_xdc.go
Normal file
341
eth/downloader/statesync_xdc.go
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// XDCStateSync handles XDC-specific state synchronization
|
||||
type XDCStateSyncer struct {
|
||||
db ethdb.Database
|
||||
downloader *Downloader
|
||||
|
||||
// Validator state sync
|
||||
validatorStateRoot common.Hash
|
||||
validatorStateDone bool
|
||||
|
||||
// Consensus state sync
|
||||
consensusStateRoot common.Hash
|
||||
consensusStateDone bool
|
||||
|
||||
// Trading state sync (XDCx)
|
||||
tradingStateRoot common.Hash
|
||||
tradingStateDone bool
|
||||
|
||||
// Lending state sync (XDCxlending)
|
||||
lendingStateRoot common.Hash
|
||||
lendingStateDone bool
|
||||
|
||||
// Progress tracking
|
||||
progress XDCStateSyncProgress
|
||||
lock sync.RWMutex
|
||||
|
||||
// Channels
|
||||
quitCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
}
|
||||
|
||||
// XDCStateSyncProgress represents state sync progress
|
||||
type XDCStateSyncProgress struct {
|
||||
AccountsTotal uint64
|
||||
AccountsDone uint64
|
||||
StorageTotal uint64
|
||||
StorageDone uint64
|
||||
BytesTotal uint64
|
||||
BytesDone uint64
|
||||
ValidatorsDone bool
|
||||
ConsensusDone bool
|
||||
TradingDone bool
|
||||
LendingDone bool
|
||||
StartTime time.Time
|
||||
EstimatedFinish time.Time
|
||||
}
|
||||
|
||||
// NewXDCStateSyncer creates a new XDC state syncer
|
||||
func NewXDCStateSyncer(db ethdb.Database, dl *Downloader) *XDCStateSyncer {
|
||||
return &XDCStateSyncer{
|
||||
db: db,
|
||||
downloader: dl,
|
||||
quitCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the state sync
|
||||
func (s *XDCStateSyncer) Start(stateRoot common.Hash) error {
|
||||
s.lock.Lock()
|
||||
s.progress.StartTime = time.Now()
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Info("Starting XDC state sync", "root", stateRoot.Hex())
|
||||
|
||||
go s.syncLoop(stateRoot)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the state sync
|
||||
func (s *XDCStateSyncer) Stop() {
|
||||
close(s.quitCh)
|
||||
}
|
||||
|
||||
// Wait waits for sync to complete
|
||||
func (s *XDCStateSyncer) Wait() {
|
||||
<-s.doneCh
|
||||
}
|
||||
|
||||
// Progress returns the current sync progress
|
||||
func (s *XDCStateSyncer) Progress() XDCStateSyncProgress {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.progress
|
||||
}
|
||||
|
||||
// syncLoop runs the main sync loop
|
||||
func (s *XDCStateSyncer) syncLoop(stateRoot common.Hash) {
|
||||
defer close(s.doneCh)
|
||||
|
||||
// Sync validator state
|
||||
if err := s.syncValidatorState(); err != nil {
|
||||
log.Error("Failed to sync validator state", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync consensus state
|
||||
if err := s.syncConsensusState(); err != nil {
|
||||
log.Error("Failed to sync consensus state", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync trading state if XDCx is enabled
|
||||
if err := s.syncTradingState(); err != nil {
|
||||
log.Error("Failed to sync trading state", "error", err)
|
||||
// Non-fatal, continue
|
||||
}
|
||||
|
||||
// Sync lending state if XDCxlending is enabled
|
||||
if err := s.syncLendingState(); err != nil {
|
||||
log.Error("Failed to sync lending state", "error", err)
|
||||
// Non-fatal, continue
|
||||
}
|
||||
|
||||
log.Info("XDC state sync completed")
|
||||
}
|
||||
|
||||
// syncValidatorState syncs the validator state
|
||||
func (s *XDCStateSyncer) syncValidatorState() error {
|
||||
log.Debug("Syncing validator state")
|
||||
|
||||
// Fetch validator set from network or snapshot
|
||||
validators := s.fetchValidatorSet()
|
||||
|
||||
// Store validators
|
||||
for _, validator := range validators {
|
||||
if err := s.storeValidator(validator); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
s.progress.ValidatorsDone = true
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Info("Validator state synced", "count", len(validators))
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncConsensusState syncs the consensus state
|
||||
func (s *XDCStateSyncer) syncConsensusState() error {
|
||||
log.Debug("Syncing consensus state")
|
||||
|
||||
// Fetch snapshot data
|
||||
// Store epoch information
|
||||
// Store checkpoint data
|
||||
|
||||
s.lock.Lock()
|
||||
s.progress.ConsensusDone = true
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Info("Consensus state synced")
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncTradingState syncs the XDCx trading state
|
||||
func (s *XDCStateSyncer) syncTradingState() error {
|
||||
log.Debug("Syncing trading state")
|
||||
|
||||
// Trading state is synced if XDCx is enabled
|
||||
// This includes order books, pending orders, etc.
|
||||
|
||||
s.lock.Lock()
|
||||
s.progress.TradingDone = true
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Info("Trading state synced")
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncLendingState syncs the XDCxlending state
|
||||
func (s *XDCStateSyncer) syncLendingState() error {
|
||||
log.Debug("Syncing lending state")
|
||||
|
||||
// Lending state is synced if XDCxlending is enabled
|
||||
// This includes lending orders, active loans, etc.
|
||||
|
||||
s.lock.Lock()
|
||||
s.progress.LendingDone = true
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Info("Lending state synced")
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchValidatorSet fetches the validator set
|
||||
func (s *XDCStateSyncer) fetchValidatorSet() []common.Address {
|
||||
// This would fetch from the network
|
||||
return make([]common.Address, 0)
|
||||
}
|
||||
|
||||
// storeValidator stores a validator
|
||||
func (s *XDCStateSyncer) storeValidator(validator common.Address) error {
|
||||
// Store validator in database
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProgress updates the sync progress
|
||||
func (s *XDCStateSyncer) UpdateProgress(accounts, storage, bytes uint64) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.progress.AccountsDone = accounts
|
||||
s.progress.StorageDone = storage
|
||||
s.progress.BytesDone = bytes
|
||||
|
||||
// Estimate finish time
|
||||
if s.progress.BytesTotal > 0 && s.progress.BytesDone > 0 {
|
||||
elapsed := time.Since(s.progress.StartTime)
|
||||
rate := float64(s.progress.BytesDone) / elapsed.Seconds()
|
||||
remaining := float64(s.progress.BytesTotal - s.progress.BytesDone)
|
||||
if rate > 0 {
|
||||
s.progress.EstimatedFinish = time.Now().Add(time.Duration(remaining/rate) * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XDCSnapshotSyncer handles snapshot-based sync
|
||||
type XDCSnapshotSyncer struct {
|
||||
db ethdb.Database
|
||||
downloader *Downloader
|
||||
}
|
||||
|
||||
// NewXDCSnapshotSyncer creates a new snapshot syncer
|
||||
func NewXDCSnapshotSyncer(db ethdb.Database, dl *Downloader) *XDCSnapshotSyncer {
|
||||
return &XDCSnapshotSyncer{
|
||||
db: db,
|
||||
downloader: dl,
|
||||
}
|
||||
}
|
||||
|
||||
// SyncFromSnapshot syncs state from a snapshot
|
||||
func (s *XDCSnapshotSyncer) SyncFromSnapshot(snapshotBlock *types.Block) error {
|
||||
log.Info("Syncing from snapshot", "block", snapshotBlock.NumberU64())
|
||||
|
||||
// Download snapshot data
|
||||
if err := s.downloadSnapshot(snapshotBlock); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify snapshot integrity
|
||||
if err := s.verifySnapshot(snapshotBlock); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Import snapshot
|
||||
if err := s.importSnapshot(snapshotBlock); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadSnapshot downloads a snapshot
|
||||
func (s *XDCSnapshotSyncer) downloadSnapshot(block *types.Block) error {
|
||||
// Download snapshot files
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySnapshot verifies a snapshot
|
||||
func (s *XDCSnapshotSyncer) verifySnapshot(block *types.Block) error {
|
||||
// Verify snapshot integrity and signatures
|
||||
return nil
|
||||
}
|
||||
|
||||
// importSnapshot imports a snapshot
|
||||
func (s *XDCSnapshotSyncer) importSnapshot(block *types.Block) error {
|
||||
// Import snapshot into database
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableSnapshots returns available snapshots
|
||||
func (s *XDCSnapshotSyncer) GetAvailableSnapshots() []SnapshotInfo {
|
||||
// Query available snapshots
|
||||
return make([]SnapshotInfo, 0)
|
||||
}
|
||||
|
||||
// SnapshotInfo represents snapshot information
|
||||
type SnapshotInfo struct {
|
||||
BlockNumber uint64
|
||||
BlockHash common.Hash
|
||||
StateRoot common.Hash
|
||||
Size uint64
|
||||
Timestamp uint64
|
||||
URL string
|
||||
}
|
||||
|
||||
// XDCChainDataWriter writes XDC chain data during sync
|
||||
type XDCChainDataWriter struct {
|
||||
db ethdb.Database
|
||||
}
|
||||
|
||||
// NewXDCChainDataWriter creates a new chain data writer
|
||||
func NewXDCChainDataWriter(db ethdb.Database) *XDCChainDataWriter {
|
||||
return &XDCChainDataWriter{db: db}
|
||||
}
|
||||
|
||||
// WriteValidatorSet writes the validator set
|
||||
func (w *XDCChainDataWriter) WriteValidatorSet(blockNumber uint64, validators []common.Address) error {
|
||||
batch := w.db.NewBatch()
|
||||
|
||||
// Write validator set
|
||||
rawdb.WriteValidatorSet(batch, blockNumber, validators)
|
||||
|
||||
return batch.Write()
|
||||
}
|
||||
|
||||
// WriteEpochData writes epoch data
|
||||
func (w *XDCChainDataWriter) WriteEpochData(epoch uint64, data []byte) error {
|
||||
batch := w.db.NewBatch()
|
||||
|
||||
// Write epoch data
|
||||
rawdb.WriteEpochData(batch, epoch, data)
|
||||
|
||||
return batch.Write()
|
||||
}
|
||||
|
||||
// WritePenalty writes a penalty
|
||||
func (w *XDCChainDataWriter) WritePenalty(validator common.Address, blockNumber uint64, amount uint64) error {
|
||||
batch := w.db.NewBatch()
|
||||
|
||||
// Write penalty
|
||||
rawdb.WritePenalty(batch, validator, blockNumber, amount)
|
||||
|
||||
return batch.Write()
|
||||
}
|
||||
208
eth/ethconfig/config_xdc.go
Normal file
208
eth/ethconfig/config_xdc.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package ethconfig
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// XDCConfig holds XDC-specific configuration
|
||||
type XDCConfig struct {
|
||||
// XDPoS configuration
|
||||
XDPoSRewards bool
|
||||
XDPoSSlashing bool
|
||||
XDPoSGap uint64
|
||||
XDPoSEpoch uint64
|
||||
XDPoSRewardFraction int
|
||||
XDPoSFoundationWallet common.Address
|
||||
|
||||
// XDCx configuration
|
||||
XDCxEnabled bool
|
||||
XDCxDataDir string
|
||||
XDCxReplicasEnabled bool
|
||||
|
||||
// XDCxLending configuration
|
||||
XDCxLendingEnabled bool
|
||||
XDCxLendingDataDir string
|
||||
|
||||
// Sync configuration
|
||||
XDCSnapSync bool
|
||||
XDCSnapShotBlock uint64
|
||||
XDCCheckpointInterval uint64
|
||||
|
||||
// Masternode configuration
|
||||
MasternodeEnabled bool
|
||||
MasternodeKey string
|
||||
MasternodeCoinbase common.Address
|
||||
}
|
||||
|
||||
// DefaultXDCConfig returns the default XDC configuration
|
||||
func DefaultXDCConfig() *XDCConfig {
|
||||
return &XDCConfig{
|
||||
XDPoSRewards: true,
|
||||
XDPoSSlashing: true,
|
||||
XDPoSGap: 450,
|
||||
XDPoSEpoch: 900,
|
||||
XDPoSRewardFraction: 88,
|
||||
XDPoSFoundationWallet: common.HexToAddress("0x0000000000000000000000000000000000000068"),
|
||||
XDCxEnabled: false,
|
||||
XDCxDataDir: "",
|
||||
XDCxReplicasEnabled: false,
|
||||
XDCxLendingEnabled: false,
|
||||
XDCxLendingDataDir: "",
|
||||
XDCSnapSync: false,
|
||||
XDCSnapShotBlock: 0,
|
||||
XDCCheckpointInterval: 900,
|
||||
MasternodeEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// XDCRewardConfig holds reward configuration
|
||||
type XDCRewardConfig struct {
|
||||
// Block reward in wei
|
||||
BlockReward *big.Int
|
||||
|
||||
// Foundation reward percentage (out of 100)
|
||||
FoundationPercent int
|
||||
|
||||
// Masternode reward percentage (out of 100)
|
||||
MasternodePercent int
|
||||
|
||||
// Voter reward percentage (out of 100)
|
||||
VoterPercent int
|
||||
}
|
||||
|
||||
// DefaultRewardConfig returns the default reward configuration
|
||||
func DefaultRewardConfig() *XDCRewardConfig {
|
||||
return &XDCRewardConfig{
|
||||
BlockReward: big.NewInt(0), // XDC doesn't have traditional block rewards
|
||||
FoundationPercent: 12,
|
||||
MasternodePercent: 60,
|
||||
VoterPercent: 28,
|
||||
}
|
||||
}
|
||||
|
||||
// XDCSyncConfig holds sync-specific configuration
|
||||
type XDCSyncConfig struct {
|
||||
// Snapshot URL for initial sync
|
||||
SnapshotURL string
|
||||
|
||||
// Checkpoint block numbers and hashes
|
||||
Checkpoints map[uint64]common.Hash
|
||||
|
||||
// Whether to verify checkpoints
|
||||
VerifyCheckpoints bool
|
||||
|
||||
// Fast sync pivot point
|
||||
PivotBlock uint64
|
||||
}
|
||||
|
||||
// DefaultSyncConfig returns the default sync configuration
|
||||
func DefaultSyncConfig() *XDCSyncConfig {
|
||||
return &XDCSyncConfig{
|
||||
SnapshotURL: "",
|
||||
Checkpoints: make(map[uint64]common.Hash),
|
||||
VerifyCheckpoints: true,
|
||||
PivotBlock: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// XDCNetworkConfig holds network-specific configuration
|
||||
type XDCNetworkConfig struct {
|
||||
// Network ID
|
||||
NetworkID uint64
|
||||
|
||||
// Network name
|
||||
NetworkName string
|
||||
|
||||
// Chain ID
|
||||
ChainID *big.Int
|
||||
|
||||
// Genesis hash
|
||||
GenesisHash common.Hash
|
||||
|
||||
// Bootnodes
|
||||
Bootnodes []string
|
||||
|
||||
// Static nodes
|
||||
StaticNodes []string
|
||||
}
|
||||
|
||||
// MainnetConfig returns mainnet configuration
|
||||
func MainnetConfig() *XDCNetworkConfig {
|
||||
return &XDCNetworkConfig{
|
||||
NetworkID: 50,
|
||||
NetworkName: "xdc-mainnet",
|
||||
ChainID: big.NewInt(50),
|
||||
}
|
||||
}
|
||||
|
||||
// TestnetConfig returns Apothem testnet configuration
|
||||
func TestnetConfig() *XDCNetworkConfig {
|
||||
return &XDCNetworkConfig{
|
||||
NetworkID: 51,
|
||||
NetworkName: "xdc-apothem",
|
||||
ChainID: big.NewInt(51),
|
||||
}
|
||||
}
|
||||
|
||||
// DevnetConfig returns devnet configuration
|
||||
func DevnetConfig() *XDCNetworkConfig {
|
||||
return &XDCNetworkConfig{
|
||||
NetworkID: 551,
|
||||
NetworkName: "xdc-devnet",
|
||||
ChainID: big.NewInt(551),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateXDCConfig validates XDC configuration
|
||||
func ValidateXDCConfig(cfg *XDCConfig) error {
|
||||
if cfg.XDPoSEpoch == 0 {
|
||||
return &configError{"XDPoS epoch cannot be zero"}
|
||||
}
|
||||
|
||||
if cfg.XDPoSGap == 0 {
|
||||
return &configError{"XDPoS gap cannot be zero"}
|
||||
}
|
||||
|
||||
if cfg.XDPoSGap >= cfg.XDPoSEpoch {
|
||||
return &configError{"XDPoS gap must be less than epoch"}
|
||||
}
|
||||
|
||||
if cfg.MasternodeEnabled && cfg.MasternodeCoinbase == (common.Address{}) {
|
||||
return &configError{"Masternode coinbase required when masternode is enabled"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configError represents a configuration error
|
||||
type configError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *configError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// IsXDCNetworkID checks if the network ID is an XDC network
|
||||
func IsXDCNetworkID(networkID uint64) bool {
|
||||
return networkID == 50 || networkID == 51 || networkID == 551
|
||||
}
|
||||
|
||||
// GetXDCNetworkName returns the network name for a network ID
|
||||
func GetXDCNetworkName(networkID uint64) string {
|
||||
switch networkID {
|
||||
case 50:
|
||||
return "mainnet"
|
||||
case 51:
|
||||
return "apothem"
|
||||
case 551:
|
||||
return "devnet"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
231
eth/tracers/api_xdc.go
Normal file
231
eth/tracers/api_xdc.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package tracers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// XDCTraceConfig holds additional tracer options for XDC specific tracing
|
||||
type XDCTraceConfig struct {
|
||||
*TraceConfig
|
||||
IncludeRewards bool `json:"includeRewards"`
|
||||
IncludePenalties bool `json:"includePenalties"`
|
||||
IncludeVotes bool `json:"includeVotes"`
|
||||
}
|
||||
|
||||
// XDCBlockTraceResult represents the result of tracing an XDC block
|
||||
type XDCBlockTraceResult struct {
|
||||
Transactions []*TxTraceResult `json:"transactions"`
|
||||
Rewards []*RewardResult `json:"rewards,omitempty"`
|
||||
Penalties []*PenaltyResult `json:"penalties,omitempty"`
|
||||
Votes []*VoteResult `json:"votes,omitempty"`
|
||||
ValidatorSet []common.Address `json:"validatorSet,omitempty"`
|
||||
}
|
||||
|
||||
// RewardResult represents a validator reward
|
||||
type RewardResult struct {
|
||||
Validator common.Address `json:"validator"`
|
||||
Amount *hexutil.Big `json:"amount"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// PenaltyResult represents a validator penalty
|
||||
type PenaltyResult struct {
|
||||
Validator common.Address `json:"validator"`
|
||||
Amount *hexutil.Big `json:"amount"`
|
||||
Reason string `json:"reason"`
|
||||
Block uint64 `json:"block"`
|
||||
}
|
||||
|
||||
// VoteResult represents a masternode vote
|
||||
type VoteResult struct {
|
||||
Signer common.Address `json:"signer"`
|
||||
Candidate common.Address `json:"candidate"`
|
||||
Cap *hexutil.Big `json:"cap"`
|
||||
Block uint64 `json:"block"`
|
||||
}
|
||||
|
||||
// TxTraceResult represents the result of tracing a transaction
|
||||
type TxTraceResult struct {
|
||||
TxHash common.Hash `json:"txHash"`
|
||||
Result interface{} `json:"result"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TraceXDCBlock traces an XDC block with XDC-specific options
|
||||
func (api *API) TraceXDCBlock(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, config *XDCTraceConfig) (*XDCBlockTraceResult, error) {
|
||||
block, err := api.blockByNumberOrHash(ctx, blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if block == nil {
|
||||
return nil, errors.New("block not found")
|
||||
}
|
||||
|
||||
result := &XDCBlockTraceResult{
|
||||
Transactions: make([]*TxTraceResult, 0),
|
||||
}
|
||||
|
||||
// Trace transactions
|
||||
for _, tx := range block.Transactions() {
|
||||
txResult, err := api.traceTransaction(ctx, tx.Hash(), config.TraceConfig)
|
||||
if err != nil {
|
||||
result.Transactions = append(result.Transactions, &TxTraceResult{
|
||||
TxHash: tx.Hash(),
|
||||
Error: err.Error(),
|
||||
})
|
||||
} else {
|
||||
result.Transactions = append(result.Transactions, &TxTraceResult{
|
||||
TxHash: tx.Hash(),
|
||||
Result: txResult,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Include rewards if requested
|
||||
if config != nil && config.IncludeRewards {
|
||||
rewards := api.getBlockRewards(ctx, block)
|
||||
result.Rewards = rewards
|
||||
}
|
||||
|
||||
// Include penalties if requested
|
||||
if config != nil && config.IncludePenalties {
|
||||
penalties := api.getBlockPenalties(ctx, block)
|
||||
result.Penalties = penalties
|
||||
}
|
||||
|
||||
// Include votes if requested
|
||||
if config != nil && config.IncludeVotes {
|
||||
votes := api.getBlockVotes(ctx, block)
|
||||
result.Votes = votes
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TraceXDCTransaction traces a transaction with XDC-specific context
|
||||
func (api *API) TraceXDCTransaction(ctx context.Context, hash common.Hash, config *XDCTraceConfig) (interface{}, error) {
|
||||
return api.traceTransaction(ctx, hash, config.TraceConfig)
|
||||
}
|
||||
|
||||
// GetXDCConsensusTrace returns consensus-related trace information
|
||||
func (api *API) GetXDCConsensusTrace(ctx context.Context, blockNr rpc.BlockNumber) (interface{}, error) {
|
||||
block, err := api.blockByNumber(ctx, blockNr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if block == nil {
|
||||
return nil, errors.New("block not found")
|
||||
}
|
||||
|
||||
header := block.Header()
|
||||
|
||||
consensusTrace := map[string]interface{}{
|
||||
"blockNumber": block.NumberU64(),
|
||||
"blockHash": block.Hash(),
|
||||
"miner": header.Coinbase,
|
||||
"difficulty": header.Difficulty,
|
||||
"gasUsed": header.GasUsed,
|
||||
"gasLimit": header.GasLimit,
|
||||
"timestamp": header.Time,
|
||||
}
|
||||
|
||||
// Add XDPoS specific fields
|
||||
if len(header.Extra) > 0 {
|
||||
consensusTrace["extraData"] = hexutil.Bytes(header.Extra)
|
||||
}
|
||||
|
||||
return consensusTrace, nil
|
||||
}
|
||||
|
||||
// getBlockRewards extracts rewards from a block
|
||||
func (api *API) getBlockRewards(ctx context.Context, block *types.Block) []*RewardResult {
|
||||
rewards := make([]*RewardResult, 0)
|
||||
|
||||
// Block reward to miner
|
||||
rewards = append(rewards, &RewardResult{
|
||||
Validator: block.Coinbase(),
|
||||
Amount: (*hexutil.Big)(big.NewInt(0)), // Calculated based on consensus
|
||||
Type: "block",
|
||||
})
|
||||
|
||||
return rewards
|
||||
}
|
||||
|
||||
// getBlockPenalties extracts penalties from a block
|
||||
func (api *API) getBlockPenalties(ctx context.Context, block *types.Block) []*PenaltyResult {
|
||||
penalties := make([]*PenaltyResult, 0)
|
||||
// Extract penalties from block transactions or state
|
||||
return penalties
|
||||
}
|
||||
|
||||
// getBlockVotes extracts votes from a block
|
||||
func (api *API) getBlockVotes(ctx context.Context, block *types.Block) []*VoteResult {
|
||||
votes := make([]*VoteResult, 0)
|
||||
// Extract votes from block transactions
|
||||
return votes
|
||||
}
|
||||
|
||||
// traceTransaction traces a single transaction
|
||||
func (api *API) traceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) {
|
||||
// This is a placeholder - actual implementation would use the full tracer
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// blockByNumber retrieves a block by number
|
||||
func (api *API) blockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) {
|
||||
// Implementation depends on backend
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// blockByNumberOrHash retrieves a block by number or hash
|
||||
func (api *API) blockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) {
|
||||
// Implementation depends on backend
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// XDCInternalTxTracer traces internal transactions in XDC blocks
|
||||
type XDCInternalTxTracer struct {
|
||||
backend ethapi.Backend
|
||||
chainConfig *core.ChainConfig
|
||||
}
|
||||
|
||||
// NewXDCInternalTxTracer creates a new internal transaction tracer
|
||||
func NewXDCInternalTxTracer(backend ethapi.Backend) *XDCInternalTxTracer {
|
||||
return &XDCInternalTxTracer{
|
||||
backend: backend,
|
||||
}
|
||||
}
|
||||
|
||||
// TraceInternalTransactions traces internal transactions for a block
|
||||
func (t *XDCInternalTxTracer) TraceInternalTransactions(ctx context.Context, blockNr rpc.BlockNumber) ([]InternalTx, error) {
|
||||
internalTxs := make([]InternalTx, 0)
|
||||
// Implementation for tracing internal transactions
|
||||
return internalTxs, nil
|
||||
}
|
||||
|
||||
// InternalTx represents an internal transaction
|
||||
type InternalTx struct {
|
||||
ParentTxHash common.Hash `json:"parentTxHash"`
|
||||
Type string `json:"type"`
|
||||
From common.Address `json:"from"`
|
||||
To common.Address `json:"to"`
|
||||
Value *hexutil.Big `json:"value"`
|
||||
Gas uint64 `json:"gas"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
Input hexutil.Bytes `json:"input"`
|
||||
Output hexutil.Bytes `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Depth int `json:"depth"`
|
||||
}
|
||||
252
eth/tracers/native/xdc_tracer.go
Normal file
252
eth/tracers/native/xdc_tracer.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// Copyright 2021 XDC Network
|
||||
// This file is part of the XDC library.
|
||||
|
||||
package native
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/eth/tracers"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tracers.DefaultDirectory.Register("xdcTracer", newXDCTracer, false)
|
||||
}
|
||||
|
||||
// xdcTracer is a native tracer for XDC-specific operations
|
||||
type xdcTracer struct {
|
||||
env *vm.EVM
|
||||
config xdcTracerConfig
|
||||
gasLimit uint64
|
||||
interrupt atomic.Bool
|
||||
reason error
|
||||
|
||||
// Results
|
||||
result *XDCTraceResult
|
||||
callStack []*xdcCall
|
||||
}
|
||||
|
||||
// xdcTracerConfig holds configuration for the XDC tracer
|
||||
type xdcTracerConfig struct {
|
||||
TraceInternalCalls bool `json:"traceInternalCalls"`
|
||||
TraceStorage bool `json:"traceStorage"`
|
||||
TraceRewards bool `json:"traceRewards"`
|
||||
}
|
||||
|
||||
// XDCTraceResult holds the trace result
|
||||
type XDCTraceResult struct {
|
||||
Type string `json:"type"`
|
||||
From common.Address `json:"from"`
|
||||
To common.Address `json:"to"`
|
||||
Value *big.Int `json:"value"`
|
||||
Gas uint64 `json:"gas"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
Input []byte `json:"input"`
|
||||
Output []byte `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RevertReason string `json:"revertReason,omitempty"`
|
||||
Calls []*xdcCall `json:"calls,omitempty"`
|
||||
}
|
||||
|
||||
// xdcCall represents an internal call
|
||||
type xdcCall struct {
|
||||
Type string `json:"type"`
|
||||
From common.Address `json:"from"`
|
||||
To common.Address `json:"to"`
|
||||
Value *big.Int `json:"value,omitempty"`
|
||||
Gas uint64 `json:"gas"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
Input []byte `json:"input,omitempty"`
|
||||
Output []byte `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Calls []*xdcCall `json:"calls,omitempty"`
|
||||
}
|
||||
|
||||
// newXDCTracer creates a new XDC tracer
|
||||
func newXDCTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) {
|
||||
var config xdcTracerConfig
|
||||
if cfg != nil {
|
||||
if err := json.Unmarshal(cfg, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &xdcTracer{
|
||||
config: config,
|
||||
result: &XDCTraceResult{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CaptureStart implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||
t.env = env
|
||||
t.gasLimit = gas
|
||||
|
||||
t.result.From = from
|
||||
t.result.To = to
|
||||
t.result.Input = input
|
||||
t.result.Gas = gas
|
||||
if value != nil {
|
||||
t.result.Value = new(big.Int).Set(value)
|
||||
}
|
||||
|
||||
if create {
|
||||
t.result.Type = "CREATE"
|
||||
} else {
|
||||
t.result.Type = "CALL"
|
||||
}
|
||||
}
|
||||
|
||||
// CaptureEnd implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {
|
||||
t.result.Output = output
|
||||
t.result.GasUsed = gasUsed
|
||||
if err != nil {
|
||||
t.result.Error = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
// CaptureState implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||
// Not tracking individual opcodes in this tracer
|
||||
}
|
||||
|
||||
// CaptureFault implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
|
||||
// Capture fault information
|
||||
}
|
||||
|
||||
// CaptureEnter implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
if !t.config.TraceInternalCalls {
|
||||
return
|
||||
}
|
||||
|
||||
call := &xdcCall{
|
||||
Type: typ.String(),
|
||||
From: from,
|
||||
To: to,
|
||||
Gas: gas,
|
||||
Input: input,
|
||||
}
|
||||
if value != nil {
|
||||
call.Value = new(big.Int).Set(value)
|
||||
}
|
||||
|
||||
t.callStack = append(t.callStack, call)
|
||||
}
|
||||
|
||||
// CaptureExit implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
||||
if !t.config.TraceInternalCalls || len(t.callStack) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Pop the last call from the stack
|
||||
size := len(t.callStack)
|
||||
call := t.callStack[size-1]
|
||||
t.callStack = t.callStack[:size-1]
|
||||
|
||||
call.Output = output
|
||||
call.GasUsed = gasUsed
|
||||
if err != nil {
|
||||
call.Error = err.Error()
|
||||
}
|
||||
|
||||
// Add to parent or result
|
||||
if len(t.callStack) > 0 {
|
||||
parent := t.callStack[len(t.callStack)-1]
|
||||
parent.Calls = append(parent.Calls, call)
|
||||
} else {
|
||||
t.result.Calls = append(t.result.Calls, call)
|
||||
}
|
||||
}
|
||||
|
||||
// CaptureTxStart implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureTxStart(gasLimit uint64) {
|
||||
t.gasLimit = gasLimit
|
||||
}
|
||||
|
||||
// CaptureTxEnd implements the EVMLogger interface
|
||||
func (t *xdcTracer) CaptureTxEnd(restGas uint64) {
|
||||
t.result.GasUsed = t.gasLimit - restGas
|
||||
}
|
||||
|
||||
// GetResult returns the trace result
|
||||
func (t *xdcTracer) GetResult() (json.RawMessage, error) {
|
||||
return json.Marshal(t.result)
|
||||
}
|
||||
|
||||
// Stop terminates the tracer
|
||||
func (t *xdcTracer) Stop(err error) {
|
||||
t.reason = err
|
||||
t.interrupt.Store(true)
|
||||
}
|
||||
|
||||
// XDCRewardTracer traces validator rewards
|
||||
type XDCRewardTracer struct {
|
||||
rewards map[common.Address]*big.Int
|
||||
}
|
||||
|
||||
// NewXDCRewardTracer creates a new reward tracer
|
||||
func NewXDCRewardTracer() *XDCRewardTracer {
|
||||
return &XDCRewardTracer{
|
||||
rewards: make(map[common.Address]*big.Int),
|
||||
}
|
||||
}
|
||||
|
||||
// AddReward adds a reward for a validator
|
||||
func (t *XDCRewardTracer) AddReward(validator common.Address, amount *big.Int) {
|
||||
if existing, ok := t.rewards[validator]; ok {
|
||||
t.rewards[validator] = new(big.Int).Add(existing, amount)
|
||||
} else {
|
||||
t.rewards[validator] = new(big.Int).Set(amount)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRewards returns all rewards
|
||||
func (t *XDCRewardTracer) GetRewards() map[common.Address]*big.Int {
|
||||
result := make(map[common.Address]*big.Int)
|
||||
for addr, amount := range t.rewards {
|
||||
result[addr] = new(big.Int).Set(amount)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// XDCPenaltyTracer traces validator penalties
|
||||
type XDCPenaltyTracer struct {
|
||||
penalties []XDCPenalty
|
||||
}
|
||||
|
||||
// XDCPenalty represents a penalty
|
||||
type XDCPenalty struct {
|
||||
Validator common.Address
|
||||
Amount *big.Int
|
||||
Reason string
|
||||
Block uint64
|
||||
}
|
||||
|
||||
// NewXDCPenaltyTracer creates a new penalty tracer
|
||||
func NewXDCPenaltyTracer() *XDCPenaltyTracer {
|
||||
return &XDCPenaltyTracer{
|
||||
penalties: make([]XDCPenalty, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddPenalty adds a penalty
|
||||
func (t *XDCPenaltyTracer) AddPenalty(validator common.Address, amount *big.Int, reason string, block uint64) {
|
||||
t.penalties = append(t.penalties, XDCPenalty{
|
||||
Validator: validator,
|
||||
Amount: new(big.Int).Set(amount),
|
||||
Reason: reason,
|
||||
Block: block,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPenalties returns all penalties
|
||||
func (t *XDCPenaltyTracer) GetPenalties() []XDCPenalty {
|
||||
return t.penalties
|
||||
}
|
||||
52
prometheus.yml
Normal file
52
prometheus.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Prometheus configuration for XDC Network monitoring
|
||||
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: []
|
||||
|
||||
rule_files: []
|
||||
|
||||
scrape_configs:
|
||||
# XDC Mainnet Node
|
||||
- job_name: 'xdc-mainnet'
|
||||
static_configs:
|
||||
- targets: ['xdc-mainnet:6060']
|
||||
metrics_path: /debug/metrics/prometheus
|
||||
|
||||
# XDC Testnet Node
|
||||
- job_name: 'xdc-testnet'
|
||||
static_configs:
|
||||
- targets: ['xdc-testnet:6060']
|
||||
metrics_path: /debug/metrics/prometheus
|
||||
|
||||
# XDC Dev Node
|
||||
- job_name: 'xdc-dev'
|
||||
static_configs:
|
||||
- targets: ['xdc-dev:6060']
|
||||
metrics_path: /debug/metrics/prometheus
|
||||
|
||||
# Prometheus self-monitoring
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
# Recording rules for XDC-specific metrics
|
||||
recording_rules:
|
||||
- name: xdc_rules
|
||||
rules:
|
||||
# Block production rate
|
||||
- record: xdc:block_production_rate
|
||||
expr: rate(chain_head_block[5m])
|
||||
|
||||
# Peer count average
|
||||
- record: xdc:peer_count_avg
|
||||
expr: avg_over_time(p2p_peers[5m])
|
||||
|
||||
# Sync progress
|
||||
- record: xdc:sync_progress
|
||||
expr: (chain_head_block / chain_highest_block) * 100
|
||||
63
scripts/init_genesis.sh
Executable file
63
scripts/init_genesis.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/bin/bash
|
||||
|
||||
# XDC Genesis Initialization Script
|
||||
# Initializes a node with the appropriate genesis file
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
DATADIR="${DATADIR:-$HOME/.xdc}"
|
||||
NETWORK="${NETWORK:-mainnet}"
|
||||
FORCE="${FORCE:-false}"
|
||||
|
||||
# Find script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GENESIS_DIR="$(dirname "$SCRIPT_DIR")/genesis"
|
||||
|
||||
# Select genesis file
|
||||
case "$NETWORK" in
|
||||
mainnet)
|
||||
GENESIS_FILE="$GENESIS_DIR/xdc_mainnet.json"
|
||||
;;
|
||||
testnet|apothem)
|
||||
GENESIS_FILE="$GENESIS_DIR/xdc_apothem.json"
|
||||
;;
|
||||
devnet)
|
||||
GENESIS_FILE="$GENESIS_DIR/devnet.json"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown network: $NETWORK"
|
||||
echo "Supported networks: mainnet, testnet, apothem, devnet"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if genesis file exists
|
||||
if [ ! -f "$GENESIS_FILE" ]; then
|
||||
echo "Genesis file not found: $GENESIS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already initialized
|
||||
if [ -d "$DATADIR/XDC/chaindata" ] && [ "$FORCE" != "true" ]; then
|
||||
echo "Node already initialized at $DATADIR"
|
||||
echo "Use FORCE=true to reinitialize (WARNING: This will delete existing data)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove existing data if force is set
|
||||
if [ "$FORCE" = "true" ] && [ -d "$DATADIR/XDC" ]; then
|
||||
echo "Removing existing data..."
|
||||
rm -rf "$DATADIR/XDC"
|
||||
fi
|
||||
|
||||
# Create data directory
|
||||
mkdir -p "$DATADIR"
|
||||
|
||||
# Initialize genesis
|
||||
echo "Initializing $NETWORK genesis..."
|
||||
XDC init --datadir "$DATADIR" "$GENESIS_FILE"
|
||||
|
||||
echo "Genesis initialization complete!"
|
||||
echo "Data directory: $DATADIR"
|
||||
echo "Network: $NETWORK"
|
||||
98
scripts/run_xdc_node.sh
Executable file
98
scripts/run_xdc_node.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/bin/bash
|
||||
|
||||
# XDC Node Runner Script
|
||||
# This script simplifies running an XDC node
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
DATADIR="${DATADIR:-$HOME/.xdc}"
|
||||
NETWORK="${NETWORK:-mainnet}"
|
||||
SYNCMODE="${SYNCMODE:-fast}"
|
||||
HTTP_PORT="${HTTP_PORT:-8545}"
|
||||
WS_PORT="${WS_PORT:-8546}"
|
||||
P2P_PORT="${P2P_PORT:-30303}"
|
||||
|
||||
# Network-specific settings
|
||||
case "$NETWORK" in
|
||||
mainnet)
|
||||
NETWORK_ID=50
|
||||
CHAIN_ID=50
|
||||
;;
|
||||
testnet|apothem)
|
||||
NETWORK_ID=51
|
||||
CHAIN_ID=51
|
||||
;;
|
||||
devnet)
|
||||
NETWORK_ID=551
|
||||
CHAIN_ID=551
|
||||
;;
|
||||
*)
|
||||
echo "Unknown network: $NETWORK"
|
||||
echo "Supported networks: mainnet, testnet, apothem, devnet"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create data directory
|
||||
mkdir -p "$DATADIR"
|
||||
|
||||
# Build node command
|
||||
NODE_CMD="XDC \
|
||||
--datadir $DATADIR \
|
||||
--networkid $NETWORK_ID \
|
||||
--syncmode $SYNCMODE \
|
||||
--http \
|
||||
--http.addr 0.0.0.0 \
|
||||
--http.port $HTTP_PORT \
|
||||
--http.api eth,net,web3,xdpos \
|
||||
--http.corsdomain '*' \
|
||||
--ws \
|
||||
--ws.addr 0.0.0.0 \
|
||||
--ws.port $WS_PORT \
|
||||
--ws.api eth,net,web3 \
|
||||
--port $P2P_PORT"
|
||||
|
||||
# Add optional flags
|
||||
if [ -n "$MASTERNODE" ]; then
|
||||
NODE_CMD="$NODE_CMD --masternode"
|
||||
|
||||
if [ -n "$COINBASE" ]; then
|
||||
NODE_CMD="$NODE_CMD --masternode.coinbase $COINBASE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$VERBOSITY" ]; then
|
||||
NODE_CMD="$NODE_CMD --verbosity $VERBOSITY"
|
||||
fi
|
||||
|
||||
if [ -n "$BOOTNODES" ]; then
|
||||
NODE_CMD="$NODE_CMD --bootnodes $BOOTNODES"
|
||||
fi
|
||||
|
||||
# Enable XDCx if requested
|
||||
if [ "$XDCX_ENABLED" = "true" ]; then
|
||||
NODE_CMD="$NODE_CMD --xdcx"
|
||||
fi
|
||||
|
||||
# Enable XDCx Lending if requested
|
||||
if [ "$LENDING_ENABLED" = "true" ]; then
|
||||
NODE_CMD="$NODE_CMD --xdcxlending"
|
||||
fi
|
||||
|
||||
# Print configuration
|
||||
echo "==================================="
|
||||
echo "XDC Node Configuration"
|
||||
echo "==================================="
|
||||
echo "Network: $NETWORK"
|
||||
echo "Network ID: $NETWORK_ID"
|
||||
echo "Data Dir: $DATADIR"
|
||||
echo "Sync Mode: $SYNCMODE"
|
||||
echo "HTTP Port: $HTTP_PORT"
|
||||
echo "WS Port: $WS_PORT"
|
||||
echo "P2P Port: $P2P_PORT"
|
||||
echo "==================================="
|
||||
|
||||
# Run node
|
||||
echo "Starting XDC node..."
|
||||
exec $NODE_CMD "$@"
|
||||
Loading…
Reference in a new issue