diff --git a/.github/workflows/xdc-build.yml b/.github/workflows/xdc-build.yml new file mode 100644 index 0000000000..df18b9cdce --- /dev/null +++ b/.github/workflows/xdc-build.yml @@ -0,0 +1,177 @@ +name: XDC Build and Test + +on: + push: + branches: [ master, develop, feature/* ] + pull_request: + branches: [ master, develop ] + +env: + GO_VERSION: '1.21' + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Get dependencies + run: | + go mod download + go mod verify + + - name: Build XDC + run: | + make XDC + ./build/bin/XDC version + + - name: Build all tools + run: make all + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: xdc-binaries + path: build/bin/ + retention-days: 7 + + test: + name: Test + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run unit tests + run: | + go test -v -race -coverprofile=coverage.out ./... + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: ./coverage.out + flags: unittests + name: codecov-xdc + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --timeout=10m + + consensus-test: + name: Consensus Tests + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up 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/... + go test -v -race ./consensus/XDPoS/engines/... + + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Build XDC + run: make XDC + + - name: Run integration tests + run: | + go test -v -tags=integration ./tests/... + timeout-minutes: 30 + + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [build, test] + if: github.event_name == 'push' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + tags: xinfin/xdc:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + benchmark: + name: Benchmarks + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run benchmarks + run: | + go test -bench=. -benchmem ./consensus/XDPoS/... > benchmark.txt + cat benchmark.txt + + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmark.txt diff --git a/accounts/abi/bind/xdc_bind.go b/accounts/abi/bind/xdc_bind.go new file mode 100644 index 0000000000..e8c20e5862 --- /dev/null +++ b/accounts/abi/bind/xdc_bind.go @@ -0,0 +1,153 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package bind contains XDPoS-specific contract bindings. +package bind + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// XDCValidatorContract provides methods for interacting with the validator contract +type XDCValidatorContract struct { + backend ContractBackend + address common.Address +} + +// ValidatorContractAddress is the address of the validator contract +var ValidatorContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000088") + +// NewXDCValidatorContract creates a new validator contract instance +func NewXDCValidatorContract(backend ContractBackend) *XDCValidatorContract { + return &XDCValidatorContract{ + backend: backend, + address: ValidatorContractAddress, + } +} + +// GetCandidates returns the list of candidates +func (c *XDCValidatorContract) GetCandidates(ctx context.Context) ([]common.Address, error) { + // This would call the getCandidates method on the validator contract + // Placeholder implementation + return nil, nil +} + +// GetCandidateCap returns the stake of a candidate +func (c *XDCValidatorContract) GetCandidateCap(ctx context.Context, candidate common.Address) (*big.Int, error) { + // This would call the getCandidateCap method + return big.NewInt(0), nil +} + +// GetCandidateOwner returns the owner of a candidate +func (c *XDCValidatorContract) GetCandidateOwner(ctx context.Context, candidate common.Address) (common.Address, error) { + // This would call the getCandidateOwner method + return common.Address{}, nil +} + +// GetVoterCap returns the stake of a voter for a candidate +func (c *XDCValidatorContract) GetVoterCap(ctx context.Context, candidate, voter common.Address) (*big.Int, error) { + // This would call the getVoterCap method + return big.NewInt(0), nil +} + +// GetVoters returns the list of voters for a candidate +func (c *XDCValidatorContract) GetVoters(ctx context.Context, candidate common.Address) ([]common.Address, error) { + // This would call the getVoters method + return nil, nil +} + +// IsCandidate checks if an address is a candidate +func (c *XDCValidatorContract) IsCandidate(ctx context.Context, addr common.Address) (bool, error) { + // This would call the isCandidate method + return false, nil +} + +// GetWithdrawBlockNumber returns the withdraw block number for an address +func (c *XDCValidatorContract) GetWithdrawBlockNumber(ctx context.Context, addr common.Address) (*big.Int, error) { + return big.NewInt(0), nil +} + +// XDCBlockSignerContract provides methods for interacting with the block signer contract +type XDCBlockSignerContract struct { + backend ContractBackend + address common.Address +} + +// BlockSignerContractAddress is the address of the block signer contract +var BlockSignerContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000089") + +// NewXDCBlockSignerContract creates a new block signer contract instance +func NewXDCBlockSignerContract(backend ContractBackend) *XDCBlockSignerContract { + return &XDCBlockSignerContract{ + backend: backend, + address: BlockSignerContractAddress, + } +} + +// GetSigners returns the signers for a block number +func (c *XDCBlockSignerContract) GetSigners(ctx context.Context, blockNumber *big.Int) ([]common.Address, error) { + // This would call the getSigners method + return nil, nil +} + +// XDCRandomizeContract provides methods for the randomize contract +type XDCRandomizeContract struct { + backend ContractBackend + address common.Address +} + +// RandomizeContractAddress is the address of the randomize contract +var RandomizeContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000090") + +// NewXDCRandomizeContract creates a new randomize contract instance +func NewXDCRandomizeContract(backend ContractBackend) *XDCRandomizeContract { + return &XDCRandomizeContract{ + backend: backend, + address: RandomizeContractAddress, + } +} + +// GetSecret returns the secret for a validator +func (c *XDCRandomizeContract) GetSecret(ctx context.Context, addr common.Address) ([]byte, error) { + return nil, nil +} + +// GetOpening returns the opening for a validator +func (c *XDCRandomizeContract) GetOpening(ctx context.Context, addr common.Address) ([]byte, error) { + return nil, nil +} + +// XDCMasternodeStatus represents the status of a masternode +type XDCMasternodeStatus struct { + IsCandidate bool + IsMasternode bool + Stake *big.Int + Owner common.Address + VoterCount int +} + +// GetMasternodeStatus returns the full status of a masternode +func GetMasternodeStatus(ctx context.Context, backend ContractBackend, addr common.Address) (*XDCMasternodeStatus, error) { + validator := NewXDCValidatorContract(backend) + + isCandidate, _ := validator.IsCandidate(ctx, addr) + stake, _ := validator.GetCandidateCap(ctx, addr) + owner, _ := validator.GetCandidateOwner(ctx, addr) + voters, _ := validator.GetVoters(ctx, addr) + + return &XDCMasternodeStatus{ + IsCandidate: isCandidate, + IsMasternode: false, // Would need to check against current masternode list + Stake: stake, + Owner: owner, + VoterCount: len(voters), + }, nil +} diff --git a/build/ci_xdc.go b/build/ci_xdc.go new file mode 100644 index 0000000000..98a5a6e943 --- /dev/null +++ b/build/ci_xdc.go @@ -0,0 +1,285 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// Build script for XDC Network. This provides XDC-specific build targets +// in addition to the standard go-ethereum build system. + +//go:build ignore +// +build ignore + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +var ( + // Build flags + buildTags string + ldflags string + race bool + verbose bool + crossCompile string + + // XDC specific + enableXDCx bool + enableLending bool + staticLink bool +) + +func init() { + flag.StringVar(&buildTags, "tags", "", "Build tags to use") + flag.StringVar(&ldflags, "ldflags", "", "Additional ldflags") + flag.BoolVar(&race, "race", false, "Enable race detector") + flag.BoolVar(&verbose, "v", false, "Verbose output") + flag.StringVar(&crossCompile, "cross", "", "Cross compile target (e.g., linux/amd64)") + flag.BoolVar(&enableXDCx, "xdcx", true, "Enable XDCx DEX support") + flag.BoolVar(&enableLending, "lending", true, "Enable lending support") + flag.BoolVar(&staticLink, "static", false, "Static linking") +} + +func main() { + flag.Parse() + args := flag.Args() + + if len(args) == 0 { + fmt.Println("Usage: go run ci_xdc.go [flags] ") + fmt.Println("\nTargets:") + fmt.Println(" build - Build XDC binary") + fmt.Println(" test - Run tests") + fmt.Println(" lint - Run linters") + fmt.Println(" docker - Build Docker image") + fmt.Println(" clean - Clean build artifacts") + fmt.Println(" install - Install XDC binary") + os.Exit(1) + } + + target := args[0] + switch target { + case "build": + buildXDC() + case "test": + runTests() + case "lint": + runLint() + case "docker": + buildDocker() + case "clean": + clean() + case "install": + installXDC() + default: + fmt.Printf("Unknown target: %s\n", target) + os.Exit(1) + } +} + +func buildXDC() { + fmt.Println("Building XDC...") + + // Set up environment + env := os.Environ() + if crossCompile != "" { + parts := strings.Split(crossCompile, "/") + if len(parts) == 2 { + env = append(env, "GOOS="+parts[0]) + env = append(env, "GOARCH="+parts[1]) + } + } + + // Build ldflags + ldf := buildLdflags() + + // Build tags + tags := buildTags + if enableXDCx { + if tags != "" { + tags += "," + } + tags += "xdcx" + } + if enableLending { + if tags != "" { + tags += "," + } + tags += "lending" + } + + // Build command + args := []string{"build", "-o", "build/bin/XDC"} + if tags != "" { + args = append(args, "-tags", tags) + } + if ldf != "" { + args = append(args, "-ldflags", ldf) + } + if race { + args = append(args, "-race") + } + if verbose { + args = append(args, "-v") + } + args = append(args, "./cmd/XDC") + + runCommand("go", args, env) + fmt.Println("Build complete: build/bin/XDC") +} + +func buildLdflags() string { + var parts []string + + // Version info + gitCommit := getGitCommit() + gitDate := getGitDate() + buildDate := time.Now().Format(time.RFC3339) + + parts = append(parts, fmt.Sprintf("-X main.gitCommit=%s", gitCommit)) + parts = append(parts, fmt.Sprintf("-X main.gitDate=%s", gitDate)) + parts = append(parts, fmt.Sprintf("-X main.buildDate=%s", buildDate)) + + // Static linking + if staticLink { + parts = append(parts, "-linkmode external -extldflags -static") + } + + if ldflags != "" { + parts = append(parts, ldflags) + } + + return strings.Join(parts, " ") +} + +func runTests() { + fmt.Println("Running tests...") + + args := []string{"test"} + if race { + args = append(args, "-race") + } + if verbose { + args = append(args, "-v") + } + args = append(args, "-coverprofile=coverage.out") + args = append(args, "./...") + + runCommand("go", args, nil) + + // Run XDPoS specific tests + fmt.Println("\nRunning XDPoS consensus tests...") + xdposArgs := []string{"test"} + if verbose { + xdposArgs = append(xdposArgs, "-v") + } + xdposArgs = append(xdposArgs, "./consensus/XDPoS/...") + runCommand("go", xdposArgs, nil) +} + +func runLint() { + fmt.Println("Running linters...") + + // Check if golangci-lint is installed + if _, err := exec.LookPath("golangci-lint"); err != nil { + fmt.Println("Installing golangci-lint...") + runCommand("go", []string{"install", "github.com/golangci/golangci-lint/cmd/golangci-lint@latest"}, nil) + } + + runCommand("golangci-lint", []string{"run", "--timeout", "10m"}, nil) +} + +func buildDocker() { + fmt.Println("Building Docker image...") + + args := []string{"build", "-t", "xinfin/xdc:latest", "."} + runCommand("docker", args, nil) +} + +func clean() { + fmt.Println("Cleaning build artifacts...") + + os.RemoveAll("build/bin") + os.Remove("coverage.out") + + fmt.Println("Clean complete") +} + +func installXDC() { + fmt.Println("Installing XDC...") + + // Build first + buildXDC() + + // Copy to GOPATH/bin + gopath := os.Getenv("GOPATH") + if gopath == "" { + gopath = filepath.Join(os.Getenv("HOME"), "go") + } + + binDir := filepath.Join(gopath, "bin") + os.MkdirAll(binDir, 0755) + + src := "build/bin/XDC" + dst := filepath.Join(binDir, "XDC") + if runtime.GOOS == "windows" { + src += ".exe" + dst += ".exe" + } + + copyFile(src, dst) + fmt.Printf("Installed to %s\n", dst) +} + +func runCommand(name string, args []string, env []string) { + cmd := exec.Command(name, args...) + if env != nil { + cmd.Env = env + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if verbose { + fmt.Printf("Running: %s %s\n", name, strings.Join(args, " ")) + } + + if err := cmd.Run(); err != nil { + fmt.Printf("Command failed: %v\n", err) + os.Exit(1) + } +} + +func getGitCommit() string { + cmd := exec.Command("git", "rev-parse", "HEAD") + out, err := cmd.Output() + if err != nil { + return "unknown" + } + return strings.TrimSpace(string(out))[:8] +} + +func getGitDate() string { + cmd := exec.Command("git", "log", "-1", "--format=%ci") + out, err := cmd.Output() + if err != nil { + return "unknown" + } + return strings.TrimSpace(string(out)) +} + +func copyFile(src, dst string) { + data, err := os.ReadFile(src) + if err != nil { + fmt.Printf("Failed to read %s: %v\n", src, err) + os.Exit(1) + } + + if err := os.WriteFile(dst, data, 0755); err != nil { + fmt.Printf("Failed to write %s: %v\n", dst, err) + os.Exit(1) + } +} diff --git a/consensus/XDPoS/engines/engine_v2.go b/consensus/XDPoS/engines/engine_v2.go new file mode 100644 index 0000000000..9e9909054f --- /dev/null +++ b/consensus/XDPoS/engines/engine_v2.go @@ -0,0 +1,377 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package engines contains XDPoS 2.0 engine implementations. +package engines + +import ( + "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" +) + +var ( + // ErrInvalidRound is returned when round is invalid + ErrInvalidRound = errors.New("invalid round") + + // ErrFutureBlock is returned for future blocks + ErrFutureBlock = errors.New("block in future") + + // ErrInvalidQC is returned for invalid quorum certificate + ErrInvalidQC = errors.New("invalid quorum certificate") + + // ExtraVanity is the fixed number of extra-data prefix bytes reserved for signer vanity + ExtraVanity = 32 + + // ExtraSeal is the fixed number of extra-data suffix bytes reserved for signer seal + ExtraSeal = 65 +) + +// V2Engine implements XDPoS 2.0 consensus +type V2Engine struct { + config *params.XDPoSConfig + db consensus.ChainHeaderReader + lock sync.RWMutex + + // State + round uint64 + epoch uint64 + highQC *types.QuorumCert + + // Signer + signer common.Address + signFn SignerFn + + // Services + xdcxService interface{} + lendingService interface{} +} + +// SignerFn is a callback for signing +type SignerFn func(signer common.Address, data []byte) ([]byte, error) + +// NewV2Engine creates a new V2 engine +func NewV2Engine(config *params.XDPoSConfig, db consensus.ChainHeaderReader) *V2Engine { + return &V2Engine{ + config: config, + db: db, + } +} + +// Author implements consensus.Engine +func (e *V2Engine) Author(header *types.Header) (common.Address, error) { + return header.Coinbase, nil +} + +// VerifyHeader implements consensus.Engine +func (e *V2Engine) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header, seal bool) error { + return e.verifyHeader(chain, header, nil, seal) +} + +// VerifyHeaders implements consensus.Engine +func (e *V2Engine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + go func() { + for i, header := range headers { + err := e.verifyHeader(chain, header, headers[:i], seals[i]) + select { + case <-abort: + return + case results <- err: + } + } + }() + + return abort, results +} + +func (e *V2Engine) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header, seal bool) error { + if header.Number == nil { + return consensus.ErrUnknownAncestor + } + + // Verify future block + if header.Time > uint64(time.Now().Unix()+15) { + return ErrFutureBlock + } + + // Verify extra data length + if len(header.Extra) < ExtraVanity { + return errors.New("missing extra vanity") + } + if len(header.Extra) < ExtraVanity+ExtraSeal { + return errors.New("missing seal") + } + + // Verify difficulty is 1 for V2 + if header.Difficulty.Cmp(big.NewInt(1)) != 0 { + return errors.New("invalid difficulty for V2") + } + + // Verify seal if required + if seal { + return e.verifySeal(chain, header, parents) + } + + return nil +} + +func (e *V2Engine) verifySeal(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // Extract signature + signature := header.Extra[len(header.Extra)-ExtraSeal:] + + // Recover signer + sealHash := e.SealHash(header) + pubkey, err := crypto.SigToPub(sealHash.Bytes(), signature) + if err != nil { + return err + } + + signer := crypto.PubkeyToAddress(*pubkey) + + // Verify signer is authorized (would check masternode list) + log.Debug("Verified block seal", "signer", signer.Hex()) + + return nil +} + +// VerifyUncles implements consensus.Engine +func (e *V2Engine) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + // XDPoS doesn't have uncles + if len(block.Uncles()) > 0 { + return errors.New("uncles not allowed") + } + return nil +} + +// Prepare implements consensus.Engine +func (e *V2Engine) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + // Set difficulty to 1 + header.Difficulty = big.NewInt(1) + + // Prepare extra data + if len(header.Extra) < ExtraVanity { + header.Extra = append(header.Extra, make([]byte, ExtraVanity-len(header.Extra))...) + } + header.Extra = header.Extra[:ExtraVanity] + header.Extra = append(header.Extra, make([]byte, ExtraSeal)...) + + return nil +} + +// Finalize implements consensus.Engine +func (e *V2Engine) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) { + // Add block reward + reward := big.NewInt(0).Mul(big.NewInt(250), big.NewInt(1e18)) + state.AddBalance(header.Coinbase, reward, 0) +} + +// FinalizeAndAssemble implements consensus.Engine +func (e *V2Engine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { + e.Finalize(chain, header, state, body) + return types.NewBlock(header, body, receipts, nil), nil +} + +// Seal implements consensus.Engine +func (e *V2Engine) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + header := block.Header() + + if header.Number.Uint64() == 0 { + return errors.New("cannot seal genesis") + } + + e.lock.RLock() + signer, signFn := e.signer, e.signFn + e.lock.RUnlock() + + if signFn == nil { + return errors.New("sign function not set") + } + + // Sign the seal hash + sealHash := e.SealHash(header) + signature, err := signFn(signer, sealHash.Bytes()) + if err != nil { + return err + } + + copy(header.Extra[len(header.Extra)-ExtraSeal:], signature) + + select { + case results <- block.WithSeal(header): + default: + log.Warn("Sealing result not consumed") + } + + return nil +} + +// SealHash returns the hash that is used for signing +func (e *V2Engine) SealHash(header *types.Header) common.Hash { + return SealHash(header) +} + +// SealHash calculates the seal hash +func SealHash(header *types.Header) common.Hash { + return rlpHash([]interface{}{ + header.ParentHash, + header.UncleHash, + header.Coinbase, + header.Root, + header.TxHash, + header.ReceiptHash, + header.Bloom, + header.Difficulty, + header.Number, + header.GasLimit, + header.GasUsed, + header.Time, + header.Extra[:len(header.Extra)-ExtraSeal], // Exclude seal + header.MixDigest, + header.Nonce, + }) +} + +func rlpHash(x interface{}) common.Hash { + data, _ := rlp.EncodeToBytes(x) + return crypto.Keccak256Hash(data) +} + +// CalcDifficulty implements consensus.Engine +func (e *V2Engine) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return big.NewInt(1) +} + +// APIs implements consensus.Engine +func (e *V2Engine) APIs(chain consensus.ChainHeaderReader) []consensus.API { + return nil +} + +// Close implements consensus.Engine +func (e *V2Engine) Close() error { + return nil +} + +// Authorize sets the signer +func (e *V2Engine) Authorize(signer common.Address, signFn SignerFn) { + e.lock.Lock() + defer e.lock.Unlock() + + e.signer = signer + e.signFn = signFn +} + +// SetXDCxService sets the XDCx service +func (e *V2Engine) SetXDCxService(service interface{}) { + e.lock.Lock() + defer e.lock.Unlock() + e.xdcxService = service +} + +// SetLendingService sets the lending service +func (e *V2Engine) SetLendingService(service interface{}) { + e.lock.Lock() + defer e.lock.Unlock() + e.lendingService = service +} + +// GetXDCXService returns the XDCx service +func (e *V2Engine) GetXDCXService() interface{} { + e.lock.RLock() + defer e.lock.RUnlock() + return e.xdcxService +} + +// GetLendingService returns the lending service +func (e *V2Engine) GetLendingService() interface{} { + e.lock.RLock() + defer e.lock.RUnlock() + return e.lendingService +} + +// HandleVote handles a vote message +func (e *V2Engine) HandleVote(vote *types.Vote) error { + e.lock.Lock() + defer e.lock.Unlock() + + log.Debug("Handling vote", + "block", vote.ProposedBlockInfo.Hash, + "round", vote.ProposedBlockInfo.Round, + ) + + return nil +} + +// HandleTimeout handles a timeout message +func (e *V2Engine) HandleTimeout(timeout *types.Timeout) error { + e.lock.Lock() + defer e.lock.Unlock() + + log.Debug("Handling timeout", "round", timeout.Round) + + return nil +} + +// HandleSyncInfo handles a sync info message +func (e *V2Engine) HandleSyncInfo(syncInfo *types.SyncInfo) error { + e.lock.Lock() + defer e.lock.Unlock() + + // Update high QC if newer + if syncInfo.HighestQC != nil { + if e.highQC == nil || syncInfo.HighestQC.Round > e.highQC.Round { + e.highQC = syncInfo.HighestQC + } + } + + return nil +} + +// HandleProposedBlock handles a proposed block +func (e *V2Engine) HandleProposedBlock(chain consensus.ChainHeaderReader, header *types.Header) error { + return e.VerifyHeader(chain, header, true) +} + +// IsEpochSwitch checks if block is an epoch switch +func (e *V2Engine) IsEpochSwitch(header *types.Header) (bool, uint64, error) { + number := header.Number.Uint64() + if e.config.Epoch == 0 { + return false, 0, nil + } + + isSwitch := number%e.config.Epoch == 0 + epoch := number / e.config.Epoch + + return isSwitch, epoch, nil +} + +// GetCurrentRound returns the current round +func (e *V2Engine) GetCurrentRound() uint64 { + e.lock.RLock() + defer e.lock.RUnlock() + return e.round +} + +// GetCurrentEpoch returns the current epoch +func (e *V2Engine) GetCurrentEpoch() uint64 { + e.lock.RLock() + defer e.lock.RUnlock() + return e.epoch +} diff --git a/consensus/XDPoS/engines/engine_v2_test.go b/consensus/XDPoS/engines/engine_v2_test.go new file mode 100644 index 0000000000..5f38482710 --- /dev/null +++ b/consensus/XDPoS/engines/engine_v2_test.go @@ -0,0 +1,312 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. + +package engines + +import ( + "crypto/ecdsa" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +func TestV2BlockValidation(t *testing.T) { + // Create test header + header := &types.Header{ + Number: big.NewInt(1000), + Time: uint64(time.Now().Unix()), + Difficulty: big.NewInt(1), + Extra: make([]byte, 97), // vanity + seal + } + + // Verify difficulty is set correctly for V2 + if header.Difficulty.Cmp(big.NewInt(1)) != 0 { + t.Error("V2 block should have difficulty 1") + } +} + +func TestV2SignatureRecovery(t *testing.T) { + // Generate a test key + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("Failed to generate key: %v", err) + } + + // Create a message + message := []byte("test message") + hash := crypto.Keccak256Hash(message) + + // Sign the message + sig, err := crypto.Sign(hash.Bytes(), key) + if err != nil { + t.Fatalf("Failed to sign: %v", err) + } + + // Recover public key + pubkey, err := crypto.SigToPub(hash.Bytes(), sig) + if err != nil { + t.Fatalf("Failed to recover pubkey: %v", err) + } + + // Verify address matches + expected := crypto.PubkeyToAddress(key.PublicKey) + recovered := crypto.PubkeyToAddress(*pubkey) + + if expected != recovered { + t.Errorf("Address mismatch: expected %s, got %s", expected.Hex(), recovered.Hex()) + } +} + +func TestV2VoteCreation(t *testing.T) { + key, _ := crypto.GenerateKey() + signer := crypto.PubkeyToAddress(key.PublicKey) + + vote := &types.Vote{ + ProposedBlockInfo: &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + }, + } + + // Sign the vote + hash := vote.Hash() + sig, err := crypto.Sign(hash.Bytes(), key) + if err != nil { + t.Fatalf("Failed to sign vote: %v", err) + } + vote.Signature = sig + + // Verify signature + pubkey, err := crypto.SigToPub(hash.Bytes(), sig) + if err != nil { + t.Fatalf("Failed to recover signer: %v", err) + } + + recovered := crypto.PubkeyToAddress(*pubkey) + if recovered != signer { + t.Errorf("Signer mismatch: expected %s, got %s", signer.Hex(), recovered.Hex()) + } +} + +func TestV2TimeoutCreation(t *testing.T) { + key, _ := crypto.GenerateKey() + signer := crypto.PubkeyToAddress(key.PublicKey) + + timeout := &types.Timeout{ + Round: 5, + HighQC: &types.QuorumCert{ + Round: 4, + }, + } + + // Sign the timeout + hash := timeout.Hash() + sig, err := crypto.Sign(hash.Bytes(), key) + if err != nil { + t.Fatalf("Failed to sign timeout: %v", err) + } + timeout.Signature = sig + + // Verify + pubkey, _ := crypto.SigToPub(hash.Bytes(), sig) + recovered := crypto.PubkeyToAddress(*pubkey) + if recovered != signer { + t.Errorf("Signer mismatch") + } +} + +func TestV2QCCreation(t *testing.T) { + // Create multiple signers + keys := make([]*ecdsa.PrivateKey, 5) + signers := make([]common.Address, 5) + for i := 0; i < 5; i++ { + keys[i], _ = crypto.GenerateKey() + signers[i] = crypto.PubkeyToAddress(keys[i].PublicKey) + } + + // Create votes + blockInfo := &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + } + + votes := make([]*types.Vote, 5) + for i := 0; i < 5; i++ { + vote := &types.Vote{ + ProposedBlockInfo: blockInfo, + } + hash := vote.Hash() + sig, _ := crypto.Sign(hash.Bytes(), keys[i]) + vote.Signature = sig + votes[i] = vote + } + + // Create QC from votes + qc := &types.QuorumCert{ + ProposedBlockInfo: blockInfo, + Signatures: make([]types.Signature, 5), + } + for i, vote := range votes { + qc.Signatures[i] = types.Signature{ + Signature: vote.Signature, + } + } + + // Verify QC has correct number of signatures + if len(qc.Signatures) != 5 { + t.Errorf("Expected 5 signatures, got %d", len(qc.Signatures)) + } +} + +func TestV2RoundCalculation(t *testing.T) { + // Test round calculation from block number + epochSize := uint64(900) + + tests := []struct { + blockNumber uint64 + expected uint64 + }{ + {0, 0}, + {1, 1}, + {899, 899}, + {900, 0}, // New epoch starts + {901, 1}, + {1800, 0}, + } + + for _, tt := range tests { + round := tt.blockNumber % epochSize + if round != tt.expected { + t.Errorf("Block %d: expected round %d, got %d", tt.blockNumber, tt.expected, round) + } + } +} + +func TestV2CertThreshold(t *testing.T) { + tests := []struct { + committeeSize int + expected int + }{ + {150, 101}, // 2/3 + 1 of 150 + {100, 67}, // 2/3 + 1 of 100 + {50, 34}, // 2/3 + 1 of 50 + {3, 3}, // Minimum + } + + for _, tt := range tests { + threshold := (tt.committeeSize * 2 / 3) + 1 + if threshold != tt.expected { + t.Errorf("Committee %d: expected threshold %d, got %d", tt.committeeSize, tt.expected, threshold) + } + } +} + +func TestV2SyncInfo(t *testing.T) { + // Create sync info + syncInfo := &types.SyncInfo{ + HighestQC: &types.QuorumCert{ + ProposedBlockInfo: &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 10, + }, + }, + HighestTC: &types.TimeoutCert{ + Round: 9, + }, + } + + // Verify fields + if syncInfo.HighestQC.Round != 10 { + t.Error("HighestQC round mismatch") + } + if syncInfo.HighestTC.Round != 9 { + t.Error("HighestTC round mismatch") + } +} + +func TestV2EpochSwitch(t *testing.T) { + epochSize := uint64(900) + + tests := []struct { + blockNumber uint64 + isSwitch bool + }{ + {0, true}, // Genesis is epoch switch + {1, false}, + {899, false}, + {900, true}, + {1800, true}, + {2699, false}, + {2700, true}, + } + + for _, tt := range tests { + isSwitch := tt.blockNumber%epochSize == 0 + if isSwitch != tt.isSwitch { + t.Errorf("Block %d: expected isSwitch=%v, got %v", tt.blockNumber, tt.isSwitch, isSwitch) + } + } +} + +// Benchmark tests + +func BenchmarkV2SignatureRecovery(b *testing.B) { + key, _ := crypto.GenerateKey() + message := []byte("test message for benchmarking") + hash := crypto.Keccak256Hash(message) + sig, _ := crypto.Sign(hash.Bytes(), key) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = crypto.SigToPub(hash.Bytes(), sig) + } +} + +func BenchmarkV2VoteHash(b *testing.B) { + vote := &types.Vote{ + ProposedBlockInfo: &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = vote.Hash() + } +} + +func BenchmarkV2QCValidation(b *testing.B) { + // Create QC with signatures + keys := make([]*ecdsa.PrivateKey, 100) + signatures := make([][]byte, 100) + + blockInfo := &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + } + + vote := &types.Vote{ProposedBlockInfo: blockInfo} + hash := vote.Hash() + + for i := 0; i < 100; i++ { + keys[i], _ = crypto.GenerateKey() + signatures[i], _ = crypto.Sign(hash.Bytes(), keys[i]) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, sig := range signatures { + _, _ = crypto.SigToPub(hash.Bytes(), sig) + } + } +} diff --git a/consensus/XDPoS/snapshot_test.go b/consensus/XDPoS/snapshot_test.go new file mode 100644 index 0000000000..228e145b3c --- /dev/null +++ b/consensus/XDPoS/snapshot_test.go @@ -0,0 +1,231 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. + +package XDPoS + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestSnapshotCreation(t *testing.T) { + // Create test masternodes + masternodes := make([]common.Address, 5) + for i := 0; i < 5; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + // Create snapshot + snap := NewSnapshot(100, common.Hash{}, masternodes) + + if snap.Number != 100 { + t.Errorf("Expected number 100, got %d", snap.Number) + } + + if len(snap.Masternodes) != 5 { + t.Errorf("Expected 5 masternodes, got %d", len(snap.Masternodes)) + } +} + +func TestSnapshotInturn(t *testing.T) { + masternodes := make([]common.Address, 5) + for i := 0; i < 5; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + tests := []struct { + blockNumber uint64 + signer common.Address + expected bool + }{ + {100, masternodes[0], true}, + {101, masternodes[1], true}, + {102, masternodes[2], true}, + {100, masternodes[1], false}, + } + + for _, tt := range tests { + result := snap.Inturn(tt.blockNumber, tt.signer) + if result != tt.expected { + t.Errorf("Block %d, signer %s: expected %v, got %v", + tt.blockNumber, tt.signer.Hex(), tt.expected, result) + } + } +} + +func TestSnapshotCopy(t *testing.T) { + masternodes := make([]common.Address, 3) + for i := 0; i < 3; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + copy := snap.Copy() + + // Verify copy is independent + copy.Number = 200 + if snap.Number == copy.Number { + t.Error("Copy should be independent of original") + } +} + +func TestSnapshotApply(t *testing.T) { + masternodes := make([]common.Address, 3) + for i := 0; i < 3; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + // Create a header + header := &types.Header{ + Number: big.NewInt(101), + Coinbase: masternodes[1], + } + + // Apply header + newSnap, err := snap.Apply([]*types.Header{header}) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + + if newSnap.Number != 101 { + t.Errorf("Expected number 101, got %d", newSnap.Number) + } +} + +func TestSnapshotSerialization(t *testing.T) { + masternodes := make([]common.Address, 3) + for i := 0; i < 3; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{1, 2, 3}, masternodes) + + // Test JSON marshaling + data, err := snap.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON failed: %v", err) + } + + // Test unmarshaling + snap2 := &Snapshot{} + if err := snap2.UnmarshalJSON(data); err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if snap2.Number != snap.Number { + t.Errorf("Expected number %d, got %d", snap.Number, snap2.Number) + } +} + +func TestSnapshotRecents(t *testing.T) { + masternodes := make([]common.Address, 5) + for i := 0; i < 5; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + // Add recents + snap.Recents[100] = masternodes[0] + snap.Recents[101] = masternodes[1] + + // Check recent + if snap.Recents[100] != masternodes[0] { + t.Error("Recent signer mismatch") + } +} + +func TestSnapshotIsMasternode(t *testing.T) { + masternodes := make([]common.Address, 3) + for i := 0; i < 3; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + // Test masternode check + if !snap.IsMasternode(masternodes[0]) { + t.Error("Should be masternode") + } + + nonMasternode := common.BigToAddress(big.NewInt(100)) + if snap.IsMasternode(nonMasternode) { + t.Error("Should not be masternode") + } +} + +func TestCalculateM1(t *testing.T) { + masternodes := make([]common.Address, 10) + for i := 0; i < 10; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + // Test M1 calculation for different rounds + tests := []struct { + round uint64 + expected int + }{ + {0, 0}, + {1, 1}, + {9, 9}, + {10, 0}, // Wraps around + } + + for _, tt := range tests { + idx := int(tt.round) % len(masternodes) + if idx != tt.expected { + t.Errorf("Round %d: expected index %d, got %d", tt.round, tt.expected, idx) + } + } +} + +// Benchmark tests + +func BenchmarkSnapshotCopy(b *testing.B) { + masternodes := make([]common.Address, 150) + for i := 0; i < 150; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = snap.Copy() + } +} + +func BenchmarkSnapshotInturn(b *testing.B) { + masternodes := make([]common.Address, 150) + for i := 0; i < 150; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = snap.Inturn(uint64(i), masternodes[i%150]) + } +} + +func BenchmarkSnapshotIsMasternode(b *testing.B) { + masternodes := make([]common.Address, 150) + for i := 0; i < 150; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + + snap := NewSnapshot(100, common.Hash{}, masternodes) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = snap.IsMasternode(masternodes[i%150]) + } +} diff --git a/consensus/XDPoS/xdpos_v2.go b/consensus/XDPoS/xdpos_v2.go new file mode 100644 index 0000000000..8f66508638 --- /dev/null +++ b/consensus/XDPoS/xdpos_v2.go @@ -0,0 +1,514 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package XDPoS implements the XDPoS 2.0 consensus algorithm. +package XDPoS + +import ( + "crypto/ecdsa" + "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/rpc" +) + +var ( + // ErrInvalidRound is returned when the round is invalid + ErrInvalidRound = errors.New("invalid round") + + // ErrInvalidQC is returned when the quorum certificate is invalid + ErrInvalidQC = errors.New("invalid quorum certificate") + + // ErrNotInCommittee is returned when signer is not in committee + ErrNotInCommittee = errors.New("signer not in committee") + + // ErrInvalidSignature is returned for invalid signatures + ErrInvalidSignature = errors.New("invalid signature") +) + +// V2Config contains XDPoS 2.0 specific configuration +type V2Config struct { + // SwitchBlock is the block number when V2 activates + SwitchBlock *big.Int + + // CurrentConfig is the current round configuration + CurrentConfig *RoundConfig + + // MinePeriod is the block mining period + MinePeriod int + + // TimeoutPeriod is the timeout period for rounds + TimeoutPeriod int + + // TimeoutSyncThreshold is the threshold for sync timeout + TimeoutSyncThreshold int + + // CertThreshold is the certificate threshold (2/3 + 1) + CertThreshold int +} + +// RoundConfig contains per-round configuration +type RoundConfig struct { + EpochNumber uint64 + Round uint64 + Masternodes []common.Address +} + +// XDPoSV2 implements XDPoS 2.0 consensus +type XDPoSV2 struct { + config *params.XDPoSConfig + v2Config *V2Config + db consensus.ChainHeaderReader + signer common.Address + signFn SignerFn + lock sync.RWMutex + + // Round state + currentRound uint64 + currentEpoch uint64 + highQC *types.QuorumCert + + // Vote/timeout pools + votePool map[common.Hash][]*types.Vote + timeoutPool map[uint64][]*types.Timeout + + // Services + xdcxService interface{} + lendingService interface{} +} + +// SignerFn is a signer callback function +type SignerFn func(signer common.Address, data []byte) ([]byte, error) + +// NewV2 creates a new XDPoS V2 consensus engine +func NewV2(config *params.XDPoSConfig, db consensus.ChainHeaderReader) *XDPoSV2 { + v2 := &XDPoSV2{ + config: config, + db: db, + votePool: make(map[common.Hash][]*types.Vote), + timeoutPool: make(map[uint64][]*types.Timeout), + } + + if config.V2 != nil { + v2.v2Config = &V2Config{ + SwitchBlock: config.V2.SwitchBlock, + MinePeriod: config.V2.MinePeriod, + TimeoutPeriod: config.V2.TimeoutPeriod, + TimeoutSyncThreshold: config.V2.TimeoutSyncThreshold, + CertThreshold: config.V2.CertThreshold, + } + } + + return v2 +} + +// Author retrieves the XDC address of the block author +func (x *XDPoSV2) Author(header *types.Header) (common.Address, error) { + return header.Coinbase, nil +} + +// VerifyHeader checks whether a header conforms to the XDPoS 2.0 rules +func (x *XDPoSV2) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header, seal bool) error { + return x.verifyHeader(chain, header, nil, seal) +} + +func (x *XDPoSV2) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header, seal bool) error { + if header.Number == nil { + return errUnknownBlock + } + + // Don't verify future blocks + if header.Time > uint64(time.Now().Unix()+15) { + return consensus.ErrFutureBlock + } + + // Verify extra data + if len(header.Extra) < ExtraVanity { + return errMissingVanity + } + if len(header.Extra) < ExtraVanity+ExtraSeal { + return errMissingSignature + } + + // Check that difficulty is set correctly + if header.Difficulty == nil || header.Difficulty.Cmp(big.NewInt(1)) != 0 { + return errInvalidDifficulty + } + + // Verify seal if needed + if seal { + return x.verifySeal(chain, header, parents) + } + + return nil +} + +// verifySeal verifies that the signature on the header is valid +func (x *XDPoSV2) verifySeal(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // Get signer from signature + signer, err := ecrecover(header) + if err != nil { + return err + } + + // Verify signer is in masternode list + // This would check the masternode list at the header's epoch + + return nil +} + +// Prepare initializes the consensus fields of a header +func (x *XDPoSV2) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + // Set difficulty to 1 for V2 + header.Difficulty = big.NewInt(1) + + // Get parent + parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + + // Ensure extra data has correct size + if len(header.Extra) < ExtraVanity { + header.Extra = append(header.Extra, make([]byte, ExtraVanity-len(header.Extra))...) + } + header.Extra = header.Extra[:ExtraVanity] + header.Extra = append(header.Extra, make([]byte, ExtraSeal)...) + + return nil +} + +// Finalize implements consensus.Engine, ensures block rewards are calculated +func (x *XDPoSV2) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) { + // Calculate rewards if this is a V2 block + if x.IsV2Block(header.Number) { + x.accumulateRewards(chain, state, header) + } +} + +// FinalizeAndAssemble implements consensus.Engine +func (x *XDPoSV2) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { + x.Finalize(chain, header, state, body) + + // Assemble and return the block + return types.NewBlock(header, body, receipts, nil), nil +} + +// Seal generates a new block with signature +func (x *XDPoSV2) 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 errUnknownBlock + } + + x.lock.RLock() + signer, signFn := x.signer, x.signFn + x.lock.RUnlock() + + if signFn == nil { + return errMissingSignFn + } + + // Sign the block + sig, err := signFn(signer, SealHash(header).Bytes()) + if err != nil { + return err + } + copy(header.Extra[len(header.Extra)-ExtraSeal:], sig) + + select { + case results <- block.WithSeal(header): + default: + log.Warn("Sealing result is not read by miner", "sealhash", SealHash(header)) + } + + return nil +} + +// APIs returns the RPC APIs for XDPoS V2 +func (x *XDPoSV2) APIs(chain consensus.ChainHeaderReader) []rpc.API { + return []rpc.API{{ + Namespace: "xdpos", + Version: "2.0", + Service: &XDPoSV2API{x}, + Public: false, + }} +} + +// Close terminates the consensus engine +func (x *XDPoSV2) Close() error { + return nil +} + +// IsV2Block returns true if the block number is after V2 switch +func (x *XDPoSV2) IsV2Block(number *big.Int) bool { + if x.v2Config == nil || x.v2Config.SwitchBlock == nil { + return false + } + return number.Cmp(x.v2Config.SwitchBlock) >= 0 +} + +// HandleVote processes an incoming vote +func (x *XDPoSV2) HandleVote(vote *types.Vote) error { + x.lock.Lock() + defer x.lock.Unlock() + + // Verify vote signature + if err := x.verifyVote(vote); err != nil { + return err + } + + // Add to pool + hash := vote.ProposedBlockInfo.Hash + x.votePool[hash] = append(x.votePool[hash], vote) + + // Check if we have enough votes for QC + if len(x.votePool[hash]) >= x.getCertThreshold() { + x.createQC(hash) + } + + return nil +} + +// HandleTimeout processes an incoming timeout +func (x *XDPoSV2) HandleTimeout(timeout *types.Timeout) error { + x.lock.Lock() + defer x.lock.Unlock() + + // Verify timeout signature + if err := x.verifyTimeout(timeout); err != nil { + return err + } + + // Add to pool + round := timeout.Round + x.timeoutPool[round] = append(x.timeoutPool[round], timeout) + + // Check if we have enough timeouts + if len(x.timeoutPool[round]) >= x.getCertThreshold() { + x.handleTimeoutQC(round) + } + + return nil +} + +// HandleSyncInfo processes sync info message +func (x *XDPoSV2) HandleSyncInfo(syncInfo *types.SyncInfo) error { + x.lock.Lock() + defer x.lock.Unlock() + + // Update high QC if newer + if syncInfo.HighestQC != nil { + if x.highQC == nil || syncInfo.HighestQC.Round > x.highQC.Round { + x.highQC = syncInfo.HighestQC + } + } + + return nil +} + +// HandleProposedBlock handles a newly proposed block +func (x *XDPoSV2) HandleProposedBlock(chain consensus.ChainHeaderReader, header *types.Header) error { + // Verify the proposal + if err := x.VerifyHeader(chain, header, true); err != nil { + return err + } + + log.Debug("Received valid proposed block", + "number", header.Number, + "hash", header.Hash(), + ) + + return nil +} + +// verifyVote verifies a vote's signature +func (x *XDPoSV2) verifyVote(vote *types.Vote) error { + // Hash the vote data + hash := vote.Hash() + + // Recover signer + pubkey, err := crypto.SigToPub(hash.Bytes(), vote.Signature) + if err != nil { + return ErrInvalidSignature + } + + signer := crypto.PubkeyToAddress(*pubkey) + + // Check signer is in committee + // This would verify against the masternode list + _ = signer + + return nil +} + +// verifyTimeout verifies a timeout's signature +func (x *XDPoSV2) verifyTimeout(timeout *types.Timeout) error { + // Similar to verifyVote + return nil +} + +// getCertThreshold returns the certificate threshold +func (x *XDPoSV2) getCertThreshold() int { + if x.v2Config != nil && x.v2Config.CertThreshold > 0 { + return x.v2Config.CertThreshold + } + // Default: 2/3 + 1 of committee size + return 101 // For 150 masternodes +} + +// createQC creates a quorum certificate from votes +func (x *XDPoSV2) createQC(hash common.Hash) { + votes := x.votePool[hash] + if len(votes) == 0 { + return + } + + // Create QC from votes + qc := &types.QuorumCert{ + ProposedBlockInfo: votes[0].ProposedBlockInfo, + Signatures: make([]types.Signature, len(votes)), + } + + for i, vote := range votes { + qc.Signatures[i] = types.Signature{ + Signature: vote.Signature, + } + } + + // Update high QC + if x.highQC == nil || qc.Round > x.highQC.Round { + x.highQC = qc + } + + log.Info("Created quorum certificate", + "block", hash.Hex(), + "round", qc.Round, + "signatures", len(qc.Signatures), + ) +} + +// handleTimeoutQC handles timeout quorum +func (x *XDPoSV2) handleTimeoutQC(round uint64) { + log.Info("Timeout QC reached", "round", round) + + // Move to next round + x.currentRound = round + 1 + + // Clear old timeouts + delete(x.timeoutPool, round) +} + +// accumulateRewards calculates and distributes block rewards +func (x *XDPoSV2) accumulateRewards(chain consensus.ChainHeaderReader, state *state.StateDB, header *types.Header) { + // Block reward: 250 XDC + reward := new(big.Int).Mul(big.NewInt(250), big.NewInt(1e18)) + + // Add to coinbase + state.AddBalance(header.Coinbase, reward, 0) +} + +// Authorize sets the signer and sign function +func (x *XDPoSV2) Authorize(signer common.Address, signFn SignerFn) { + x.lock.Lock() + defer x.lock.Unlock() + + x.signer = signer + x.signFn = signFn +} + +// SetXDCxService sets the XDCx trading service +func (x *XDPoSV2) SetXDCxService(service interface{}) { + x.lock.Lock() + defer x.lock.Unlock() + x.xdcxService = service +} + +// SetLendingService sets the lending service +func (x *XDPoSV2) SetLendingService(service interface{}) { + x.lock.Lock() + defer x.lock.Unlock() + x.lendingService = service +} + +// GetXDCXService returns the XDCx service +func (x *XDPoSV2) GetXDCXService() interface{} { + x.lock.RLock() + defer x.lock.RUnlock() + return x.xdcxService +} + +// GetLendingService returns the lending service +func (x *XDPoSV2) GetLendingService() interface{} { + x.lock.RLock() + defer x.lock.RUnlock() + return x.lendingService +} + +// IsEpochSwitch checks if a header is an epoch switch block +func (x *XDPoSV2) IsEpochSwitch(header *types.Header) (bool, uint64, error) { + number := header.Number.Uint64() + epochSize := x.config.Epoch + + if epochSize == 0 { + return false, 0, nil + } + + isEpochSwitch := number%epochSize == 0 + epoch := number / epochSize + + return isEpochSwitch, epoch, nil +} + +// ecrecover extracts the Ethereum address from a signed header +func ecrecover(header *types.Header) (common.Address, error) { + if len(header.Extra) < ExtraVanity+ExtraSeal { + return common.Address{}, errMissingSignature + } + + signature := header.Extra[len(header.Extra)-ExtraSeal:] + + // Recover public key from signature + hash := SealHash(header) + pubkey, err := crypto.SigToPub(hash.Bytes(), signature) + if err != nil { + return common.Address{}, err + } + + return crypto.PubkeyToAddress(*pubkey), nil +} + +// XDPoSV2API provides RPC API for XDPoS V2 +type XDPoSV2API struct { + engine *XDPoSV2 +} + +// GetRound returns the current round +func (api *XDPoSV2API) GetRound() uint64 { + api.engine.lock.RLock() + defer api.engine.lock.RUnlock() + return api.engine.currentRound +} + +// GetEpoch returns the current epoch +func (api *XDPoSV2API) GetEpoch() uint64 { + api.engine.lock.RLock() + defer api.engine.lock.RUnlock() + return api.engine.currentEpoch +} diff --git a/core/blockchain_xdc.go b/core/blockchain_xdc.go new file mode 100644 index 0000000000..207b22d98c --- /dev/null +++ b/core/blockchain_xdc.go @@ -0,0 +1,249 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package core + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "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 ( + // ErrInvalidEpoch is returned when epoch validation fails + ErrInvalidEpoch = errors.New("invalid epoch") + + // ErrNotMasternode is returned when signer is not a masternode + ErrNotMasternode = errors.New("signer is not a masternode") + + // ErrMasternodePenalized is returned when masternode is penalized + ErrMasternodePenalized = errors.New("masternode is penalized") +) + +// CheckpointCh is a channel for checkpoint notifications +var CheckpointCh = make(chan int) + +// BlockChainHooks defines hooks for XDPoS consensus integration +type BlockChainHooks struct { + // HookReward is called to distribute block rewards + HookReward func(chain *BlockChain, statedb *state.StateDB, parentState *state.StateDB, header *types.Header, txs []*types.Transaction, receipts []*types.Receipt) ([]*types.Receipt, error) + + // HookPenalty is called to handle validator penalties + HookPenalty func(chain *BlockChain, number *big.Int, parentHash common.Hash, coinbase common.Address) ([]common.Address, error) + + // HookValidator is called to validate block signer + HookValidator func(header *types.Header, signers []common.Address) (common.Address, error) + + // HookVerifyMasterNodes is called to verify masternode list + HookVerifyMasterNodes func(header *types.Header, signers []common.Address) error + + // HookGetSignersFromContract gets signers from smart contract + HookGetSignersFromContract func(chain *BlockChain, block *types.Block) ([]common.Address, error) + + // HookRandomizeSigners randomizes signer order for a round + HookRandomizeSigners func(masternodes []common.Address, round uint64) []common.Address +} + +// XDCBlockchainContext provides XDC-specific blockchain context +type XDCBlockchainContext struct { + // IPCEndpoint is the IPC endpoint for contract calls + IPCEndpoint string + + // Database for XDCx trading state + XDCxDb ethdb.Database + + // Hooks for XDPoS consensus + Hooks *BlockChainHooks +} + +// GetMasternodes returns the masternode list for the given epoch +func (bc *BlockChain) GetMasternodes(epoch uint64) []common.Address { + return rawdb.ReadMasternodeList(bc.db, epoch) +} + +// SetMasternodes stores the masternode list for the given epoch +func (bc *BlockChain) SetMasternodes(epoch uint64, masternodes []common.Address) { + rawdb.WriteMasternodeList(bc.db, epoch, masternodes) +} + +// GetPenalizedValidators returns the penalized validators for the given epoch +func (bc *BlockChain) GetPenalizedValidators(epoch uint64) []common.Address { + return rawdb.ReadPenalizedList(bc.db, epoch) +} + +// SetPenalizedValidators stores the penalized validators for the given epoch +func (bc *BlockChain) SetPenalizedValidators(epoch uint64, penalized []common.Address) { + rawdb.WritePenalizedList(bc.db, epoch, penalized) +} + +// GetBlockSigner returns the signer of a block +func (bc *BlockChain) GetBlockSigner(number uint64) common.Address { + return rawdb.ReadBlockSigner(bc.db, number) +} + +// SetBlockSigner stores the signer of a block +func (bc *BlockChain) SetBlockSigner(number uint64, signer common.Address) { + rawdb.WriteBlockSigner(bc.db, number, signer) +} + +// IsCheckpoint returns true if the block number is a checkpoint (epoch switch) +func (bc *BlockChain) IsCheckpoint(number uint64) bool { + config := bc.Config() + if config.XDPoS == nil { + return false + } + return number%config.XDPoS.Epoch == 0 +} + +// GetEpochNumber returns the epoch number for a given block number +func (bc *BlockChain) GetEpochNumber(blockNumber uint64) uint64 { + config := bc.Config() + if config.XDPoS == nil || config.XDPoS.Epoch == 0 { + return 0 + } + return blockNumber / config.XDPoS.Epoch +} + +// IsGapBlock returns true if the block is a gap block (epoch - gap) +func (bc *BlockChain) IsGapBlock(number uint64) bool { + config := bc.Config() + if config.XDPoS == nil { + return false + } + gap := config.XDPoS.Gap + epoch := config.XDPoS.Epoch + return number%epoch == epoch-gap +} + +// UpdateM1 updates masternode list for the next epoch +// Called at gap block (Epoch - Gap) +func (bc *BlockChain) UpdateM1() error { + currentBlock := bc.CurrentBlock() + if currentBlock == nil { + return errors.New("current block is nil") + } + + config := bc.Config() + if config.XDPoS == nil { + return nil + } + + number := currentBlock.NumberU64() + if !bc.IsGapBlock(number) { + return nil + } + + epoch := bc.GetEpochNumber(number) + 1 // Next epoch + + log.Info("Updating masternode list for next epoch", "currentBlock", number, "nextEpoch", epoch) + + // In a full implementation, this would: + // 1. Get candidates from validator contract + // 2. Sort by stake + // 3. Select top N as masternodes + // 4. Store in database + + return nil +} + +// GetTradingStateRoot gets the trading state root for a block +func (bc *BlockChain) GetTradingStateRoot(blockHash common.Hash) common.Hash { + return rawdb.ReadTradingStateRoot(bc.db, blockHash) +} + +// SetTradingStateRoot sets the trading state root for a block +func (bc *BlockChain) SetTradingStateRoot(blockHash common.Hash, root common.Hash) { + rawdb.WriteTradingStateRoot(bc.db, blockHash, root) +} + +// GetLendingStateRoot gets the lending state root for a block +func (bc *BlockChain) GetLendingStateRoot(blockHash common.Hash) common.Hash { + return rawdb.ReadLendingStateRoot(bc.db, blockHash) +} + +// SetLendingStateRoot sets the lending state root for a block +func (bc *BlockChain) SetLendingStateRoot(blockHash common.Hash, root common.Hash) { + rawdb.WriteLendingStateRoot(bc.db, blockHash, root) +} + +// IsTIPXDCX returns whether XDCX trading is enabled at the given block +func (c *params.ChainConfig) IsTIPXDCX(num *big.Int) bool { + // XDCX is enabled after a certain block + // For mainnet, this is block 0 (always enabled) + return true +} + +// IsTIPXDCXReceiver returns whether XDCX receiver is enabled at the given block +func (c *params.ChainConfig) IsTIPXDCXReceiver(num *big.Int) bool { + return c.IsTIPXDCX(num) +} + +// IsTIPSigning returns whether the new signing scheme is enabled +func (c *params.ChainConfig) IsTIPSigning(num *big.Int) bool { + // New signing scheme enabled after XDPoS 2.0 + if c.XDPoS == nil { + return false + } + return num.Uint64() >= c.XDPoS.V2.SwitchBlock.Uint64() +} + +// GetBlocksHashCache gets cached block hashes at a given height +// Used for fork tracking and finality +func (bc *BlockChain) GetBlocksHashCache(number uint64) []common.Hash { + // This is a placeholder - actual implementation would use LRU cache + block := bc.GetBlockByNumber(number) + if block == nil { + return nil + } + return []common.Hash{block.Hash()} +} + +// UpdateBlocksHashCache updates the block hash cache +func (bc *BlockChain) UpdateBlocksHashCache(block *types.Block) { + // Placeholder for block hash cache update + // In production, this maintains a cache of block hashes per height + // for tracking forks +} + +// AreTwoBlockSamePath checks if two blocks are on the same chain path +func (bc *BlockChain) AreTwoBlockSamePath(hash1, hash2 common.Hash) bool { + block1 := bc.GetBlockByHash(hash1) + block2 := bc.GetBlockByHash(hash2) + + if block1 == nil || block2 == nil { + return false + } + + // Check if one is ancestor of the other + if block1.NumberU64() > block2.NumberU64() { + // Walk back block1 to block2's height + for block1.NumberU64() > block2.NumberU64() { + block1 = bc.GetBlock(block1.ParentHash(), block1.NumberU64()-1) + if block1 == nil { + return false + } + } + return block1.Hash() == block2.Hash() + } + + // Walk back block2 to block1's height + for block2.NumberU64() > block1.NumberU64() { + block2 = bc.GetBlock(block2.ParentHash(), block2.NumberU64()-1) + if block2 == nil { + return false + } + } + return block1.Hash() == block2.Hash() +} diff --git a/core/events_xdc.go b/core/events_xdc.go new file mode 100644 index 0000000000..b9ecf51807 --- /dev/null +++ b/core/events_xdc.go @@ -0,0 +1,122 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package core + +import ( + "github.com/ethereum/go-ethereum/core/types" +) + +// OrderTxPreEvent is posted when an order transaction enters the transaction pool. +type OrderTxPreEvent struct { + Tx *types.OrderTransaction +} + +// LendingTxPreEvent is posted when a lending transaction enters the transaction pool. +type LendingTxPreEvent struct { + Tx *types.LendingTransaction +} + +// NewOrderTxsEvent is posted when new order transactions are processed. +type NewOrderTxsEvent struct { + Txs types.OrderTransactions +} + +// NewLendingTxsEvent is posted when new lending transactions are processed. +type NewLendingTxsEvent struct { + Txs types.LendingTransactions +} + +// EpochSwitchEvent is posted when an epoch switch occurs. +type EpochSwitchEvent struct { + Number uint64 + Epoch uint64 + Masternodes []types.Address +} + +// MasternodeUpdateEvent is posted when the masternode list is updated. +type MasternodeUpdateEvent struct { + Epoch uint64 + Masternodes []types.Address +} + +// PenaltyEvent is posted when a validator is penalized. +type PenaltyEvent struct { + Address types.Address + Epoch uint64 + Reason string +} + +// RewardEvent is posted when rewards are distributed. +type RewardEvent struct { + Block uint64 + Signer types.Address + SignerReward uint64 // in wei + VoterRewards map[types.Address]uint64 +} + +// VoteEvent is posted when a vote is received. +type VoteEvent struct { + Vote *types.Vote +} + +// TimeoutEvent is posted when a timeout is received. +type TimeoutEvent struct { + Timeout *types.Timeout +} + +// SyncInfoEvent is posted when sync info is received. +type SyncInfoEvent struct { + SyncInfo *types.SyncInfo +} + +// QuorumCertEvent is posted when a quorum certificate is formed. +type QuorumCertEvent struct { + QC *types.QuorumCert +} + +// TimeoutCertEvent is posted when a timeout certificate is formed. +type TimeoutCertEvent struct { + TC *types.TimeoutCert +} + +// XDCxTradeEvent is posted when a trade is executed on XDCx. +type XDCxTradeEvent struct { + TakerOrderHash common.Hash + MakerOrderHash common.Hash + Amount *big.Int + Price *big.Int + BlockNumber uint64 +} + +// XDCxOrderEvent is posted when an order status changes. +type XDCxOrderEvent struct { + OrderHash common.Hash + Status string + BlockNumber uint64 +} + +// LendingTradeEvent is posted when a lending trade is executed. +type LendingTradeEvent struct { + LendingId uint64 + BorrowAmount *big.Int + CollateralAmount *big.Int + Term uint64 + Interest uint64 + BlockNumber uint64 +} + +// Import missing types for compilation +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// Address alias for common.Address in events +type Address = common.Address diff --git a/core/rawdb/accessors_xdc.go b/core/rawdb/accessors_xdc.go new file mode 100644 index 0000000000..2ad454a94e --- /dev/null +++ b/core/rawdb/accessors_xdc.go @@ -0,0 +1,280 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package rawdb + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" +) + +// XDPoS-specific database key prefixes +var ( + // Masternode data + masternodePrefix = []byte("masternode-") + masternodeListPrefix = []byte("masternodelist-") + + // Epoch data + epochPrefix = []byte("epoch-") + epochSnapshotPrefix = []byte("epochsnapshot-") + + // Penalty data + penaltyPrefix = []byte("penalty-") + penalizedListPrefix = []byte("penalizedlist-") + + // Reward data + rewardPrefix = []byte("reward-") + rewardEpochPrefix = []byte("rewardepoch-") + + // Block signer data + blockSignerPrefix = []byte("blocksigner-") + + // Vote data + votePrefix = []byte("vote-") + + // Trading state root prefix + tradingStatePrefix = []byte("tradingstate-") + + // Lending state root prefix + lendingStatePrefix = []byte("lendingstate-") + + // QC (Quorum Certificate) data + qcPrefix = []byte("qc-") + + // Timeout pool data + timeoutPoolPrefix = []byte("timeoutpool-") +) + +// 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 +} + +// MasternodeKey returns the database key for a masternode entry +func MasternodeKey(address common.Address) []byte { + return append(masternodePrefix, address.Bytes()...) +} + +// MasternodeListKey returns the database key for masternode list at epoch +func MasternodeListKey(epoch uint64) []byte { + return append(masternodeListPrefix, encodeBlockNumber(epoch)...) +} + +// EpochKey returns the database key for epoch data +func EpochKey(epoch uint64) []byte { + return append(epochPrefix, encodeBlockNumber(epoch)...) +} + +// EpochSnapshotKey returns the database key for epoch snapshot +func EpochSnapshotKey(epoch uint64) []byte { + return append(epochSnapshotPrefix, encodeBlockNumber(epoch)...) +} + +// PenaltyKey returns the database key for penalty data at epoch +func PenaltyKey(epoch uint64) []byte { + return append(penaltyPrefix, encodeBlockNumber(epoch)...) +} + +// PenalizedListKey returns the database key for penalized validators list +func PenalizedListKey(epoch uint64) []byte { + return append(penalizedListPrefix, encodeBlockNumber(epoch)...) +} + +// RewardKey returns the database key for reward at block number +func RewardKey(number uint64) []byte { + return append(rewardPrefix, encodeBlockNumber(number)...) +} + +// RewardEpochKey returns the database key for epoch reward +func RewardEpochKey(epoch uint64) []byte { + return append(rewardEpochPrefix, encodeBlockNumber(epoch)...) +} + +// BlockSignerKey returns the database key for block signer at block number +func BlockSignerKey(number uint64) []byte { + return append(blockSignerPrefix, encodeBlockNumber(number)...) +} + +// VoteKey returns the database key for vote data +func VoteKey(hash common.Hash) []byte { + return append(votePrefix, hash.Bytes()...) +} + +// TradingStateKey returns the database key for trading state root +func TradingStateKey(hash common.Hash) []byte { + return append(tradingStatePrefix, hash.Bytes()...) +} + +// LendingStateKey returns the database key for lending state root +func LendingStateKey(hash common.Hash) []byte { + return append(lendingStatePrefix, hash.Bytes()...) +} + +// QCKey returns the database key for quorum certificate +func QCKey(hash common.Hash) []byte { + return append(qcPrefix, hash.Bytes()...) +} + +// TimeoutPoolKey returns the database key for timeout pool at round +func TimeoutPoolKey(round uint64) []byte { + return append(timeoutPoolPrefix, encodeBlockNumber(round)...) +} + +// ReadMasternodeList reads the masternode list for an epoch +func ReadMasternodeList(db ethdb.Reader, epoch uint64) []common.Address { + data, err := db.Get(MasternodeListKey(epoch)) + if err != nil || len(data) == 0 { + return nil + } + + var masternodes []common.Address + if err := rlp.DecodeBytes(data, &masternodes); err != nil { + log.Error("Invalid masternode list RLP", "epoch", epoch, "err", err) + return nil + } + return masternodes +} + +// WriteMasternodeList writes the masternode list for an epoch +func WriteMasternodeList(db ethdb.KeyValueWriter, epoch uint64, masternodes []common.Address) { + data, err := rlp.EncodeToBytes(masternodes) + if err != nil { + log.Crit("Failed to RLP encode masternode list", "err", err) + } + if err := db.Put(MasternodeListKey(epoch), data); err != nil { + log.Crit("Failed to store masternode list", "err", err) + } +} + +// ReadPenalizedList reads the penalized validators list for an epoch +func ReadPenalizedList(db ethdb.Reader, epoch uint64) []common.Address { + data, err := db.Get(PenalizedListKey(epoch)) + if err != nil || len(data) == 0 { + return nil + } + + var penalized []common.Address + if err := rlp.DecodeBytes(data, &penalized); err != nil { + log.Error("Invalid penalized list RLP", "epoch", epoch, "err", err) + return nil + } + return penalized +} + +// WritePenalizedList writes the penalized validators list for an epoch +func WritePenalizedList(db ethdb.KeyValueWriter, epoch uint64, penalized []common.Address) { + data, err := rlp.EncodeToBytes(penalized) + if err != nil { + log.Crit("Failed to RLP encode penalized list", "err", err) + } + if err := db.Put(PenalizedListKey(epoch), data); err != nil { + log.Crit("Failed to store penalized list", "err", err) + } +} + +// ReadBlockSigner reads the block signer for a block number +func ReadBlockSigner(db ethdb.Reader, number uint64) common.Address { + data, err := db.Get(BlockSignerKey(number)) + if err != nil || len(data) == 0 { + return common.Address{} + } + return common.BytesToAddress(data) +} + +// WriteBlockSigner writes the block signer for a block number +func WriteBlockSigner(db ethdb.KeyValueWriter, number uint64, signer common.Address) { + if err := db.Put(BlockSignerKey(number), signer.Bytes()); err != nil { + log.Crit("Failed to store block signer", "err", err) + } +} + +// ReadTradingStateRoot reads the trading state root for a block +func ReadTradingStateRoot(db ethdb.Reader, blockHash common.Hash) common.Hash { + data, err := db.Get(TradingStateKey(blockHash)) + if err != nil || len(data) == 0 { + return common.Hash{} + } + return common.BytesToHash(data) +} + +// WriteTradingStateRoot writes the trading state root for a block +func WriteTradingStateRoot(db ethdb.KeyValueWriter, blockHash common.Hash, root common.Hash) { + if err := db.Put(TradingStateKey(blockHash), root.Bytes()); err != nil { + log.Crit("Failed to store trading state root", "err", err) + } +} + +// ReadLendingStateRoot reads the lending state root for a block +func ReadLendingStateRoot(db ethdb.Reader, blockHash common.Hash) common.Hash { + data, err := db.Get(LendingStateKey(blockHash)) + if err != nil || len(data) == 0 { + return common.Hash{} + } + return common.BytesToHash(data) +} + +// WriteLendingStateRoot writes the lending state root for a block +func WriteLendingStateRoot(db ethdb.KeyValueWriter, blockHash common.Hash, root common.Hash) { + if err := db.Put(LendingStateKey(blockHash), root.Bytes()); err != nil { + log.Crit("Failed to store lending state root", "err", err) + } +} + +// EpochData stores epoch-specific information +type EpochData struct { + Epoch uint64 `json:"epoch"` + StartBlock uint64 `json:"startBlock"` + EndBlock uint64 `json:"endBlock"` + Masternodes []common.Address `json:"masternodes"` + Penalties []common.Address `json:"penalties"` +} + +// ReadEpochData reads epoch data from database +func ReadEpochData(db ethdb.Reader, epoch uint64) *EpochData { + data, err := db.Get(EpochKey(epoch)) + if err != nil || len(data) == 0 { + return nil + } + + var epochData EpochData + if err := rlp.DecodeBytes(data, &epochData); err != nil { + log.Error("Invalid epoch data RLP", "epoch", epoch, "err", err) + return nil + } + return &epochData +} + +// WriteEpochData writes epoch data to database +func WriteEpochData(db ethdb.KeyValueWriter, epoch uint64, epochData *EpochData) { + data, err := rlp.EncodeToBytes(epochData) + if err != nil { + log.Crit("Failed to RLP encode epoch data", "err", err) + } + if err := db.Put(EpochKey(epoch), data); err != nil { + log.Crit("Failed to store epoch data", "err", err) + } +} + +// HasEpochData checks if epoch data exists +func HasEpochData(db ethdb.Reader, epoch uint64) bool { + has, _ := db.Has(EpochKey(epoch)) + return has +} + +// DeleteEpochData deletes epoch data +func DeleteEpochData(db ethdb.KeyValueWriter, epoch uint64) { + if err := db.Delete(EpochKey(epoch)); err != nil { + log.Crit("Failed to delete epoch data", "err", err) + } +} diff --git a/core/state_processor_xdc.go b/core/state_processor_xdc.go new file mode 100644 index 0000000000..a496e496bd --- /dev/null +++ b/core/state_processor_xdc.go @@ -0,0 +1,235 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package core + +import ( + "math/big" + + "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/log" + "github.com/ethereum/go-ethereum/params" +) + +// XDCStateProcessor extends StateProcessor with XDPoS-specific processing +type XDCStateProcessor struct { + *StateProcessor + config *params.ChainConfig +} + +// NewXDCStateProcessor creates a new XDC state processor +func NewXDCStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *XDCStateProcessor { + return &XDCStateProcessor{ + StateProcessor: NewStateProcessor(config, bc, engine), + config: config, + } +} + +// ProcessXDC processes XDPoS-specific state transitions +func (p *XDCStateProcessor) ProcessXDC( + block *types.Block, + statedb *state.StateDB, + cfg vm.Config, + feeCapacity state.FeeCapacity, +) ([]*types.Receipt, []*types.Log, uint64, error) { + // Call base processor first + receipts, logs, usedGas, err := p.Process(block, statedb, cfg, feeCapacity) + if err != nil { + return nil, nil, 0, err + } + + // Apply XDPoS-specific state changes + header := block.Header() + + // Process rewards at epoch switch + if p.isEpochSwitch(header.Number.Uint64()) { + if err := p.processEpochRewards(statedb, header); err != nil { + log.Error("Failed to process epoch rewards", "err", err) + } + } + + // Process penalties + if err := p.processPenalties(statedb, header); err != nil { + log.Error("Failed to process penalties", "err", err) + } + + return receipts, logs, usedGas, nil +} + +// isEpochSwitch checks if block number is an epoch switch +func (p *XDCStateProcessor) isEpochSwitch(number uint64) bool { + if p.config.XDPoS == nil || p.config.XDPoS.Epoch == 0 { + return false + } + return number%p.config.XDPoS.Epoch == 0 +} + +// processEpochRewards processes rewards at epoch boundaries +func (p *XDCStateProcessor) processEpochRewards(statedb *state.StateDB, header *types.Header) error { + if p.config.XDPoS == nil { + return nil + } + + // Calculate total rewards for the epoch + // Standard block reward: 250 XDC + blockReward := new(big.Int).Mul(big.NewInt(250), big.NewInt(1e18)) + + // In XDPoS, rewards go to: + // - Block signer (coinbase) + // - Voters who voted for the signer + + // For now, just add to coinbase + statedb.AddBalance(header.Coinbase, blockReward, 0) + + log.Debug("Processed epoch rewards", + "block", header.Number, + "signer", header.Coinbase, + "reward", blockReward, + ) + + return nil +} + +// processPenalties handles validator penalties +func (p *XDCStateProcessor) processPenalties(statedb *state.StateDB, header *types.Header) error { + // Penalties would be applied based on: + // - Missing blocks + // - Invalid votes + // - Double signing + + // This is a placeholder - actual implementation would: + // 1. Check missed block count for validators + // 2. Apply slashing if threshold exceeded + // 3. Update validator state + + return nil +} + +// ApplyXDCTransaction applies a transaction with XDPoS-specific handling +func ApplyXDCTransaction( + config *params.ChainConfig, + bc ChainContext, + author *common.Address, + gp *GasPool, + statedb *state.StateDB, + header *types.Header, + tx *types.Transaction, + usedGas *uint64, + cfg vm.Config, + feeCapacity state.FeeCapacity, +) (*types.Receipt, error) { + // Check if this is a special XDPoS transaction + if isXDPoSSpecialTx(tx) { + return applyXDPoSSpecialTx(config, bc, author, gp, statedb, header, tx, usedGas, cfg) + } + + // Apply as normal transaction + return ApplyTransaction(config, bc, author, gp, statedb, header, tx, usedGas, cfg) +} + +// isXDPoSSpecialTx checks if transaction is XDPoS-specific +func isXDPoSSpecialTx(tx *types.Transaction) bool { + to := tx.To() + if to == nil { + return false + } + + // Check for special contract addresses + specialAddresses := []common.Address{ + common.HexToAddress("0x0000000000000000000000000000000000000088"), // Validator contract + common.HexToAddress("0x0000000000000000000000000000000000000089"), // Block signer contract + common.HexToAddress("0x0000000000000000000000000000000000000090"), // Randomize contract + } + + for _, addr := range specialAddresses { + if *to == addr { + return true + } + } + + return false +} + +// applyXDPoSSpecialTx applies XDPoS-specific transactions +func applyXDPoSSpecialTx( + config *params.ChainConfig, + bc ChainContext, + author *common.Address, + gp *GasPool, + statedb *state.StateDB, + header *types.Header, + tx *types.Transaction, + usedGas *uint64, + cfg vm.Config, +) (*types.Receipt, error) { + // Special handling for XDPoS contract interactions + // This would handle: + // - Validator registration/resignation + // - Vote casting + // - Reward claims + // - etc. + + // For now, apply as normal transaction + return ApplyTransaction(config, bc, author, gp, statedb, header, tx, usedGas, cfg) +} + +// CalculateXDCReward calculates the block reward for XDPoS +func CalculateXDCReward(blockNumber *big.Int, config *params.XDPoSConfig) *big.Int { + if config == nil { + return big.NewInt(0) + } + + // Base reward: 250 XDC per block + baseReward := new(big.Int).Mul(big.NewInt(250), big.NewInt(1e18)) + + // Could add halvings or other adjustments here based on block number + + return baseReward +} + +// DistributeRewards distributes rewards to signer and voters +func DistributeRewards( + statedb *state.StateDB, + header *types.Header, + reward *big.Int, + voterRewardPercent int, +) { + if reward.Sign() <= 0 { + return + } + + // Calculate voter reward portion (e.g., 40%) + voterPortion := new(big.Int).Mul(reward, big.NewInt(int64(voterRewardPercent))) + voterPortion.Div(voterPortion, big.NewInt(100)) + + // Signer gets remaining + signerPortion := new(big.Int).Sub(reward, voterPortion) + + // Add signer portion to coinbase + statedb.AddBalance(header.Coinbase, signerPortion, 0) + + log.Debug("Distributed rewards", + "block", header.Number, + "signer", header.Coinbase, + "signerReward", signerPortion, + "voterPortion", voterPortion, + ) + + // Voter distribution would require: + // 1. Getting voter list from validator contract + // 2. Calculating each voter's share based on stake + // 3. Distributing proportionally +} + +// Import required types +import ( + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/vm" +) diff --git a/core/txpool/xdc_pool.go b/core/txpool/xdc_pool.go new file mode 100644 index 0000000000..b6552b4be0 --- /dev/null +++ b/core/txpool/xdc_pool.go @@ -0,0 +1,230 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package txpool contains XDPoS-specific transaction pool functionality. +package txpool + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" +) + +// XDCOrderPool manages order transactions for XDCx +type XDCOrderPool struct { + mu sync.RWMutex + pending map[common.Address]types.OrderTransactions + queue map[common.Address]types.OrderTransactions + all map[common.Hash]*types.OrderTransaction + maxSize int + + // Event feeds + txFeed event.Feed + scope event.SubscriptionScope +} + +// NewXDCOrderPool creates a new order pool +func NewXDCOrderPool(maxSize int) *XDCOrderPool { + return &XDCOrderPool{ + pending: make(map[common.Address]types.OrderTransactions), + queue: make(map[common.Address]types.OrderTransactions), + all: make(map[common.Hash]*types.OrderTransaction), + maxSize: maxSize, + } +} + +// Add adds an order transaction to the pool +func (p *XDCOrderPool) Add(tx *types.OrderTransaction) error { + p.mu.Lock() + defer p.mu.Unlock() + + hash := tx.GetHash() + if _, exists := p.all[hash]; exists { + return ErrAlreadyKnown + } + + // Check pool size + if len(p.all) >= p.maxSize { + return ErrPoolFull + } + + p.all[hash] = tx + p.pending[tx.UserAddress] = append(p.pending[tx.UserAddress], tx) + + log.Debug("Added order transaction", "hash", hash) + return nil +} + +// AddRemotes adds multiple remote order transactions +func (p *XDCOrderPool) AddRemotes(txs []*types.OrderTransaction) []error { + errs := make([]error, len(txs)) + for i, tx := range txs { + errs[i] = p.Add(tx) + } + return errs +} + +// Get retrieves a transaction by hash +func (p *XDCOrderPool) Get(hash common.Hash) *types.OrderTransaction { + p.mu.RLock() + defer p.mu.RUnlock() + return p.all[hash] +} + +// Pending returns all pending order transactions +func (p *XDCOrderPool) Pending() (map[common.Address]types.OrderTransactions, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + pending := make(map[common.Address]types.OrderTransactions) + for addr, txs := range p.pending { + pending[addr] = append(types.OrderTransactions{}, txs...) + } + return pending, nil +} + +// SubscribeTxPreEvent subscribes to new transaction events +func (p *XDCOrderPool) SubscribeTxPreEvent(ch chan<- OrderTxPreEvent) event.Subscription { + return p.scope.Track(p.txFeed.Subscribe(ch)) +} + +// Remove removes a transaction from the pool +func (p *XDCOrderPool) Remove(hash common.Hash) { + p.mu.Lock() + defer p.mu.Unlock() + + tx, exists := p.all[hash] + if !exists { + return + } + + delete(p.all, hash) + + // Remove from pending + addr := tx.UserAddress + txs := p.pending[addr] + for i, t := range txs { + if t.GetHash() == hash { + p.pending[addr] = append(txs[:i], txs[i+1:]...) + break + } + } +} + +// Clear removes all transactions +func (p *XDCOrderPool) Clear() { + p.mu.Lock() + defer p.mu.Unlock() + + p.pending = make(map[common.Address]types.OrderTransactions) + p.queue = make(map[common.Address]types.OrderTransactions) + p.all = make(map[common.Hash]*types.OrderTransaction) +} + +// Count returns the number of transactions +func (p *XDCOrderPool) Count() int { + p.mu.RLock() + defer p.mu.RUnlock() + return len(p.all) +} + +// XDCLendingPool manages lending transactions +type XDCLendingPool struct { + mu sync.RWMutex + pending map[common.Address]types.LendingTransactions + all map[common.Hash]*types.LendingTransaction + maxSize int + + // Event feeds + txFeed event.Feed + scope event.SubscriptionScope +} + +// NewXDCLendingPool creates a new lending pool +func NewXDCLendingPool(maxSize int) *XDCLendingPool { + return &XDCLendingPool{ + pending: make(map[common.Address]types.LendingTransactions), + all: make(map[common.Hash]*types.LendingTransaction), + maxSize: maxSize, + } +} + +// Add adds a lending transaction to the pool +func (p *XDCLendingPool) Add(tx *types.LendingTransaction) error { + p.mu.Lock() + defer p.mu.Unlock() + + hash := tx.Hash() + if _, exists := p.all[hash]; exists { + return ErrAlreadyKnown + } + + if len(p.all) >= p.maxSize { + return ErrPoolFull + } + + p.all[hash] = tx + p.pending[tx.UserAddress] = append(p.pending[tx.UserAddress], tx) + + log.Debug("Added lending transaction", "hash", hash) + return nil +} + +// AddRemotes adds multiple remote lending transactions +func (p *XDCLendingPool) AddRemotes(txs []*types.LendingTransaction) []error { + errs := make([]error, len(txs)) + for i, tx := range txs { + errs[i] = p.Add(tx) + } + return errs +} + +// Pending returns all pending lending transactions +func (p *XDCLendingPool) Pending() (map[common.Address]types.LendingTransactions, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + pending := make(map[common.Address]types.LendingTransactions) + for addr, txs := range p.pending { + pending[addr] = append(types.LendingTransactions{}, txs...) + } + return pending, nil +} + +// SubscribeTxPreEvent subscribes to new transaction events +func (p *XDCLendingPool) SubscribeTxPreEvent(ch chan<- LendingTxPreEvent) event.Subscription { + return p.scope.Track(p.txFeed.Subscribe(ch)) +} + +// OrderTxPreEvent is emitted when order tx enters pool +type OrderTxPreEvent struct { + Tx *types.OrderTransaction +} + +// LendingTxPreEvent is emitted when lending tx enters pool +type LendingTxPreEvent struct { + Tx *types.LendingTransaction +} + +// Errors +var ( + ErrAlreadyKnown = &PoolError{"transaction already known"} + ErrPoolFull = &PoolError{"transaction pool full"} +) + +type PoolError struct { + message string +} + +func (e *PoolError) Error() string { + return e.message +} diff --git a/core/types/order_transaction.go b/core/types/order_transaction.go new file mode 100644 index 0000000000..9f60796314 --- /dev/null +++ b/core/types/order_transaction.go @@ -0,0 +1,246 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package types + +import ( + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// OrderTransaction represents an XDCx order transaction +type OrderTransaction struct { + Nonce uint64 `json:"nonce"` + Quantity *big.Int `json:"quantity"` + Price *big.Int `json:"price"` + ExchangeAddress common.Address `json:"exchangeAddress"` + UserAddress common.Address `json:"userAddress"` + BaseToken common.Address `json:"baseToken"` + QuoteToken common.Address `json:"quoteToken"` + Status string `json:"status"` + Side string `json:"side"` // "BUY" or "SELL" + Type string `json:"type"` // "LO" (limit) or "MO" (market) + Hash common.Hash `json:"hash"` + OrderID uint64 `json:"orderId"` + PairName string `json:"pairName"` + + // Signature + V *big.Int `json:"v"` + R *big.Int `json:"r"` + S *big.Int `json:"s"` + + // Cache + hash atomic.Value + size atomic.Value +} + +// OrderTransactions is a slice of order transactions +type OrderTransactions []*OrderTransaction + +// Len returns the length of the slice +func (s OrderTransactions) Len() int { return len(s) } + +// Swap swaps two elements +func (s OrderTransactions) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetRlp returns the RLP encoding of an order transaction +func (s OrderTransactions) GetRlp(i int) []byte { + enc, _ := rlp.EncodeToBytes(s[i]) + return enc +} + +// ComputeHash computes the hash of the order transaction +func (tx *OrderTransaction) ComputeHash() common.Hash { + if hash := tx.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + // Hash the key fields + data, _ := rlp.EncodeToBytes([]interface{}{ + tx.Nonce, + tx.Quantity, + tx.Price, + tx.ExchangeAddress, + tx.UserAddress, + tx.BaseToken, + tx.QuoteToken, + tx.Side, + tx.Type, + }) + hash := crypto.Keccak256Hash(data) + tx.hash.Store(hash) + return hash +} + +// GetHash returns the cached hash or computes it +func (tx *OrderTransaction) GetHash() common.Hash { + return tx.ComputeHash() +} + +// Size returns the approximate size of the transaction +func (tx *OrderTransaction) Size() common.StorageSize { + if size := tx.size.Load(); size != nil { + return size.(common.StorageSize) + } + + c := common.StorageSize(0) + c += 8 // Nonce + if tx.Quantity != nil { + c += common.StorageSize(len(tx.Quantity.Bytes())) + } + if tx.Price != nil { + c += common.StorageSize(len(tx.Price.Bytes())) + } + c += 20 * 4 // addresses + c += 32 // hash + c += common.StorageSize(len(tx.Status) + len(tx.Side) + len(tx.Type) + len(tx.PairName)) + + tx.size.Store(c) + return c +} + +// EncodingSize returns the RLP encoding size +func (tx *OrderTransaction) EncodingSize() int { + enc, _ := rlp.EncodeToBytes(tx) + return len(enc) +} + +// IsBuy returns true if this is a buy order +func (tx *OrderTransaction) IsBuy() bool { + return tx.Side == "BUY" +} + +// IsSell returns true if this is a sell order +func (tx *OrderTransaction) IsSell() bool { + return tx.Side == "SELL" +} + +// IsLimit returns true if this is a limit order +func (tx *OrderTransaction) IsLimit() bool { + return tx.Type == "LO" +} + +// IsMarket returns true if this is a market order +func (tx *OrderTransaction) IsMarket() bool { + return tx.Type == "MO" +} + +// OrderTxByNonce sorts order transactions by nonce +type OrderTxByNonce OrderTransactions + +func (s OrderTxByNonce) Len() int { return len(s) } +func (s OrderTxByNonce) Less(i, j int) bool { return s[i].Nonce < s[j].Nonce } +func (s OrderTxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// OrderTxByPrice sorts order transactions by price +type OrderTxByPrice struct { + txs OrderTransactions + side string +} + +func (s OrderTxByPrice) Len() int { return len(s.txs) } +func (s OrderTxByPrice) Less(i, j int) bool { + if s.side == "BUY" { + // Higher price first for buy orders + return s.txs[i].Price.Cmp(s.txs[j].Price) > 0 + } + // Lower price first for sell orders + return s.txs[i].Price.Cmp(s.txs[j].Price) < 0 +} +func (s OrderTxByPrice) Swap(i, j int) { s.txs[i], s.txs[j] = s.txs[j], s.txs[i] } + +// LendingTransaction represents an XDCx lending transaction +type LendingTransaction struct { + Nonce uint64 `json:"nonce"` + Quantity *big.Int `json:"quantity"` + Interest uint64 `json:"interest"` // Interest rate in basis points + Term uint64 `json:"term"` // Term in seconds + RelayerAddress common.Address `json:"relayerAddress"` + UserAddress common.Address `json:"userAddress"` + LendingToken common.Address `json:"lendingToken"` + CollateralToken common.Address `json:"collateralToken"` + AutoTopUp bool `json:"autoTopUp"` + Status string `json:"status"` + Side string `json:"side"` // "INVEST" or "BORROW" + Type string `json:"type"` // "LO" or "MO" + LendingId uint64 `json:"lendingId"` + LendingTradeId uint64 `json:"lendingTradeId"` + ExtraData string `json:"extraData"` + + // Signature + V *big.Int `json:"v"` + R *big.Int `json:"r"` + S *big.Int `json:"s"` + + // Cache + hash atomic.Value +} + +// LendingTransactions is a slice of lending transactions +type LendingTransactions []*LendingTransaction + +// Len returns the length of the slice +func (s LendingTransactions) Len() int { return len(s) } + +// Swap swaps two elements +func (s LendingTransactions) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetRlp returns the RLP encoding of a lending transaction +func (s LendingTransactions) GetRlp(i int) []byte { + enc, _ := rlp.EncodeToBytes(s[i]) + return enc +} + +// Hash returns the hash of the lending transaction +func (tx *LendingTransaction) Hash() common.Hash { + if hash := tx.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + data, _ := rlp.EncodeToBytes([]interface{}{ + tx.Nonce, + tx.Quantity, + tx.Interest, + tx.Term, + tx.RelayerAddress, + tx.UserAddress, + tx.LendingToken, + tx.CollateralToken, + tx.Side, + tx.Type, + }) + hash := crypto.Keccak256Hash(data) + tx.hash.Store(hash) + return hash +} + +// Nonce returns the transaction nonce +func (tx *LendingTransaction) GetNonce() uint64 { + return tx.Nonce +} + +// IsInvest returns true if this is an invest order +func (tx *LendingTransaction) IsInvest() bool { + return tx.Side == "INVEST" +} + +// IsBorrow returns true if this is a borrow order +func (tx *LendingTransaction) IsBorrow() bool { + return tx.Side == "BORROW" +} + +// LendingTxByNonce sorts lending transactions by nonce +type LendingTxByNonce LendingTransactions + +func (s LendingTxByNonce) Len() int { return len(s) } +func (s LendingTxByNonce) Less(i, j int) bool { return s[i].Nonce < s[j].Nonce } +func (s LendingTxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/core/types/vote.go b/core/types/vote.go new file mode 100644 index 0000000000..fffe616687 --- /dev/null +++ b/core/types/vote.go @@ -0,0 +1,273 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package types + +import ( + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// BlockInfo contains information about a proposed block +type BlockInfo struct { + Hash common.Hash `json:"hash"` + Number *big.Int `json:"number"` + Round uint64 `json:"round"` +} + +// Vote represents a vote for a proposed block in XDPoS 2.0 +type Vote struct { + ProposedBlockInfo *BlockInfo `json:"proposedBlockInfo"` + Signature []byte `json:"signature"` + GapNumber uint64 `json:"gapNumber"` + + // Cache + hash atomic.Value +} + +// Hash returns the hash of the vote +func (v *Vote) Hash() common.Hash { + if hash := v.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + // Calculate hash from RLP encoding (excluding signature) + data, _ := rlp.EncodeToBytes([]interface{}{ + v.ProposedBlockInfo, + v.GapNumber, + }) + hash := crypto.Keccak256Hash(data) + v.hash.Store(hash) + return hash +} + +// Copy creates a deep copy of the vote +func (v *Vote) Copy() *Vote { + cpy := &Vote{ + GapNumber: v.GapNumber, + } + + if v.ProposedBlockInfo != nil { + cpy.ProposedBlockInfo = &BlockInfo{ + Hash: v.ProposedBlockInfo.Hash, + Number: new(big.Int).Set(v.ProposedBlockInfo.Number), + Round: v.ProposedBlockInfo.Round, + } + } + + if v.Signature != nil { + cpy.Signature = make([]byte, len(v.Signature)) + copy(cpy.Signature, v.Signature) + } + + return cpy +} + +// Timeout represents a timeout message in XDPoS 2.0 +type Timeout struct { + Round uint64 `json:"round"` + Signature []byte `json:"signature"` + HighQC *QuorumCert `json:"highQC"` + GapNumber uint64 `json:"gapNumber"` + + // Cache + hash atomic.Value +} + +// Hash returns the hash of the timeout +func (t *Timeout) Hash() common.Hash { + if hash := t.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + // Calculate hash from RLP encoding (excluding signature) + data, _ := rlp.EncodeToBytes([]interface{}{ + t.Round, + t.HighQC, + t.GapNumber, + }) + hash := crypto.Keccak256Hash(data) + t.hash.Store(hash) + return hash +} + +// Copy creates a deep copy of the timeout +func (t *Timeout) Copy() *Timeout { + cpy := &Timeout{ + Round: t.Round, + GapNumber: t.GapNumber, + } + + if t.Signature != nil { + cpy.Signature = make([]byte, len(t.Signature)) + copy(cpy.Signature, t.Signature) + } + + if t.HighQC != nil { + cpy.HighQC = t.HighQC.Copy() + } + + return cpy +} + +// SyncInfo carries synchronization information +type SyncInfo struct { + HighestQC *QuorumCert `json:"highestQC"` + HighestTC *TimeoutCert `json:"highestTC"` + + // Cache + hash atomic.Value +} + +// Hash returns the hash of the sync info +func (s *SyncInfo) Hash() common.Hash { + if hash := s.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + data, _ := rlp.EncodeToBytes([]interface{}{ + s.HighestQC, + s.HighestTC, + }) + hash := crypto.Keccak256Hash(data) + s.hash.Store(hash) + return hash +} + +// Copy creates a deep copy of sync info +func (s *SyncInfo) Copy() *SyncInfo { + cpy := &SyncInfo{} + + if s.HighestQC != nil { + cpy.HighestQC = s.HighestQC.Copy() + } + + if s.HighestTC != nil { + cpy.HighestTC = s.HighestTC.Copy() + } + + return cpy +} + +// QuorumCert represents a quorum certificate +type QuorumCert struct { + ProposedBlockInfo *BlockInfo `json:"proposedBlockInfo"` + Signatures []Signature `json:"signatures"` + GapNumber uint64 `json:"gapNumber"` + Round uint64 `json:"round"` +} + +// Signature represents a signature from a validator +type Signature struct { + Signer common.Address `json:"signer,omitempty"` + Signature []byte `json:"signature"` +} + +// Copy creates a deep copy of the quorum cert +func (qc *QuorumCert) Copy() *QuorumCert { + cpy := &QuorumCert{ + GapNumber: qc.GapNumber, + Round: qc.Round, + } + + if qc.ProposedBlockInfo != nil { + cpy.ProposedBlockInfo = &BlockInfo{ + Hash: qc.ProposedBlockInfo.Hash, + Number: new(big.Int).Set(qc.ProposedBlockInfo.Number), + Round: qc.ProposedBlockInfo.Round, + } + } + + if qc.Signatures != nil { + cpy.Signatures = make([]Signature, len(qc.Signatures)) + for i, sig := range qc.Signatures { + cpy.Signatures[i] = Signature{ + Signer: sig.Signer, + } + if sig.Signature != nil { + cpy.Signatures[i].Signature = make([]byte, len(sig.Signature)) + copy(cpy.Signatures[i].Signature, sig.Signature) + } + } + } + + return cpy +} + +// TimeoutCert represents a timeout certificate +type TimeoutCert struct { + Round uint64 `json:"round"` + Signatures []Signature `json:"signatures"` + GapNumber uint64 `json:"gapNumber"` +} + +// Copy creates a deep copy of the timeout cert +func (tc *TimeoutCert) Copy() *TimeoutCert { + cpy := &TimeoutCert{ + Round: tc.Round, + GapNumber: tc.GapNumber, + } + + if tc.Signatures != nil { + cpy.Signatures = make([]Signature, len(tc.Signatures)) + for i, sig := range tc.Signatures { + cpy.Signatures[i] = Signature{ + Signer: sig.Signer, + } + if sig.Signature != nil { + cpy.Signatures[i].Signature = make([]byte, len(sig.Signature)) + copy(cpy.Signatures[i].Signature, sig.Signature) + } + } + } + + return cpy +} + +// ExtraFields_v2 represents v2 extra data in block header +type ExtraFields_v2 struct { + Round uint64 `json:"round"` + QuorumCert *QuorumCert `json:"quorumCert"` +} + +// VotePool contains votes grouped by block hash +type VotePool struct { + votes map[common.Hash][]*Vote +} + +// NewVotePool creates a new vote pool +func NewVotePool() *VotePool { + return &VotePool{ + votes: make(map[common.Hash][]*Vote), + } +} + +// Add adds a vote to the pool +func (p *VotePool) Add(vote *Vote) { + hash := vote.ProposedBlockInfo.Hash + p.votes[hash] = append(p.votes[hash], vote) +} + +// Get returns all votes for a block +func (p *VotePool) Get(hash common.Hash) []*Vote { + return p.votes[hash] +} + +// Count returns the number of votes for a block +func (p *VotePool) Count(hash common.Hash) int { + return len(p.votes[hash]) +} + +// Clear removes all votes for a block +func (p *VotePool) Clear(hash common.Hash) { + delete(p.votes, hash) +} diff --git a/eth/bft/bft.go b/eth/bft/bft.go new file mode 100644 index 0000000000..d0290e18f3 --- /dev/null +++ b/eth/bft/bft.go @@ -0,0 +1,207 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package bft implements Byzantine Fault Tolerant consensus message handling +// for XDPoS 2.0. +package bft + +import ( + "math/big" + "sync" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// BroadcastFns contains the broadcast functions for BFT messages +type BroadcastFns struct { + Vote func(*types.Vote) + Timeout func(*types.Timeout) + SyncInfo func(*types.SyncInfo) +} + +// BlockChain defines the blockchain interface needed by Bfter +type BlockChain interface { + CurrentBlock() *types.Block + GetBlock(hash common.Hash, number uint64) *types.Block + GetBlockByHash(hash common.Hash) *types.Block + GetBlockByNumber(number uint64) *types.Block + Config() *params.ChainConfig +} + +// Bfter handles BFT consensus messages +type Bfter struct { + broadcasts BroadcastFns + blockchain BlockChain + heighter func() uint64 + + // Consensus engine integration + engine consensus.Engine + + // State + running int32 + epochNum uint64 + mu sync.RWMutex + + // Quit channel + quit chan struct{} +} + +// New creates a new Bfter instance +func New(broadcasts BroadcastFns, blockchain BlockChain, heighter func() uint64) *Bfter { + return &Bfter{ + broadcasts: broadcasts, + blockchain: blockchain, + heighter: heighter, + quit: make(chan struct{}), + } +} + +// Start starts the BFT message handler +func (b *Bfter) Start() { + if !atomic.CompareAndSwapInt32(&b.running, 0, 1) { + return + } + log.Info("BFT message handler started") +} + +// Stop stops the BFT message handler +func (b *Bfter) Stop() { + if !atomic.CompareAndSwapInt32(&b.running, 1, 0) { + return + } + close(b.quit) + log.Info("BFT message handler stopped") +} + +// SetConsensusFuns sets the consensus engine +func (b *Bfter) SetConsensusFuns(engine consensus.Engine) { + b.mu.Lock() + defer b.mu.Unlock() + b.engine = engine +} + +// InitEpochNumber initializes the epoch number from the current block +func (b *Bfter) InitEpochNumber() { + b.mu.Lock() + defer b.mu.Unlock() + // Epoch initialization would be done based on consensus engine + // For now, set to 0 and let the engine update it + b.epochNum = 0 +} + +// Vote handles incoming vote messages +func (b *Bfter) Vote(peerID string, vote *types.Vote) { + if atomic.LoadInt32(&b.running) == 0 { + return + } + + b.mu.RLock() + engine := b.engine + b.mu.RUnlock() + + if engine == nil { + log.Debug("BFT vote received but no consensus engine set", "peer", peerID) + return + } + + log.Debug("BFT vote received", + "peer", peerID, + "blockHash", vote.ProposedBlockInfo.Hash.Hex(), + "blockNumber", vote.ProposedBlockInfo.Number, + "round", vote.ProposedBlockInfo.Round, + ) + + // Forward to consensus engine for processing + // The actual handling depends on the XDPoS engine implementation +} + +// Timeout handles incoming timeout messages +func (b *Bfter) Timeout(peerID string, timeout *types.Timeout) { + if atomic.LoadInt32(&b.running) == 0 { + return + } + + b.mu.RLock() + engine := b.engine + b.mu.RUnlock() + + if engine == nil { + log.Debug("BFT timeout received but no consensus engine set", "peer", peerID) + return + } + + log.Debug("BFT timeout received", + "peer", peerID, + "round", timeout.Round, + ) + + // Forward to consensus engine for processing +} + +// SyncInfo handles incoming sync info messages +func (b *Bfter) SyncInfo(peerID string, syncInfo *types.SyncInfo) { + if atomic.LoadInt32(&b.running) == 0 { + return + } + + b.mu.RLock() + engine := b.engine + b.mu.RUnlock() + + if engine == nil { + log.Debug("BFT syncInfo received but no consensus engine set", "peer", peerID) + return + } + + log.Debug("BFT syncInfo received", "peer", peerID) + + // Forward to consensus engine for processing +} + +// BroadcastVote broadcasts a vote to all peers +func (b *Bfter) BroadcastVote(vote *types.Vote) { + if b.broadcasts.Vote != nil { + b.broadcasts.Vote(vote) + } +} + +// BroadcastTimeout broadcasts a timeout to all peers +func (b *Bfter) BroadcastTimeout(timeout *types.Timeout) { + if b.broadcasts.Timeout != nil { + b.broadcasts.Timeout(timeout) + } +} + +// BroadcastSyncInfo broadcasts sync info to all peers +func (b *Bfter) BroadcastSyncInfo(syncInfo *types.SyncInfo) { + if b.broadcasts.SyncInfo != nil { + b.broadcasts.SyncInfo(syncInfo) + } +} + +// GetEpochNumber returns the current epoch number +func (b *Bfter) GetEpochNumber() uint64 { + b.mu.RLock() + defer b.mu.RUnlock() + return b.epochNum +} + +// SetEpochNumber sets the current epoch number +func (b *Bfter) SetEpochNumber(epoch uint64) { + b.mu.Lock() + defer b.mu.Unlock() + b.epochNum = epoch +} + +// Import params for type reference +import "github.com/ethereum/go-ethereum/params" diff --git a/eth/handler_xdc.go b/eth/handler_xdc.go new file mode 100644 index 0000000000..0ccfd0a7ce --- /dev/null +++ b/eth/handler_xdc.go @@ -0,0 +1,368 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package eth + +import ( + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" +) + +// XDCHandler extends the base handler with XDPoS-specific functionality +type XDCHandler struct { + networkID uint64 + txpool TxPool + orderpool OrderPool + lendingpool LendingPool + chain *core.BlockChain + maxPeers int + + // Accept transactions flag + acceptTxs uint32 + + // XDPoS peer management + xdcPeers *xdcPeerSet + + // Event subscriptions + orderTxCh chan core.OrderTxPreEvent + lendingTxCh chan core.LendingTxPreEvent + orderTxSub event.Subscription + lendingTxSub event.Subscription + + // Vote and consensus message channels + voteCh chan *types.Vote + timeoutCh chan *types.Timeout + syncInfoCh chan *types.SyncInfo + + // Quit channel + quitSync chan struct{} +} + +// TxPool interface for transaction pool +type TxPool interface { + Pending(enforceTips bool) map[common.Address][]*types.Transaction + SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription + AddRemotes([]*types.Transaction) []error +} + +// NewXDCHandler creates a new XDC protocol handler +func NewXDCHandler(config *HandlerConfig) (*XDCHandler, error) { + h := &XDCHandler{ + networkID: config.Network, + chain: config.Chain, + txpool: config.TxPool, + maxPeers: config.MaxPeers, + xdcPeers: newXDCPeerSet(), + orderTxCh: make(chan core.OrderTxPreEvent, 4096), + lendingTxCh: make(chan core.LendingTxPreEvent, 4096), + voteCh: make(chan *types.Vote, 4096), + timeoutCh: make(chan *types.Timeout, 4096), + syncInfoCh: make(chan *types.SyncInfo, 4096), + quitSync: make(chan struct{}), + } + return h, nil +} + +// HandlerConfig contains configuration for the handler +type HandlerConfig struct { + Network uint64 + Chain *core.BlockChain + TxPool TxPool + MaxPeers int +} + +// Start starts the XDC handler +func (h *XDCHandler) Start(maxPeers int) { + h.maxPeers = maxPeers + atomic.StoreUint32(&h.acceptTxs, 1) + + // Start broadcast loops + go h.orderTxBroadcastLoop() + go h.lendingTxBroadcastLoop() + go h.consensusMsgLoop() +} + +// Stop stops the XDC handler +func (h *XDCHandler) Stop() { + close(h.quitSync) + h.xdcPeers.Close() +} + +// SetOrderPool sets the order pool +func (h *XDCHandler) SetOrderPool(orderpool OrderPool) { + h.orderpool = orderpool + if orderpool != nil { + h.orderTxSub = orderpool.SubscribeTxPreEvent(h.orderTxCh) + } +} + +// SetLendingPool sets the lending pool +func (h *XDCHandler) SetLendingPool(lendingpool LendingPool) { + h.lendingpool = lendingpool + if lendingpool != nil { + h.lendingTxSub = lendingpool.SubscribeTxPreEvent(h.lendingTxCh) + } +} + +// HandleMsg handles an incoming message from a peer +func (h *XDCHandler) HandleMsg(peer *p2p.Peer, rw p2p.MsgReadWriter, msg p2p.Msg) error { + switch msg.Code { + case OrderTxMsgCode: + return h.handleOrderTxMsg(peer, msg) + case LendingTxMsgCode: + return h.handleLendingTxMsg(peer, msg) + case VoteMsgCode: + return h.handleVoteMsg(peer, msg) + case TimeoutMsgCode: + return h.handleTimeoutMsg(peer, msg) + case SyncInfoMsgCode: + return h.handleSyncInfoMsg(peer, msg) + } + return nil +} + +// handleOrderTxMsg handles order transaction messages +func (h *XDCHandler) handleOrderTxMsg(peer *p2p.Peer, msg p2p.Msg) error { + if atomic.LoadUint32(&h.acceptTxs) == 0 { + return nil + } + + var txs []*types.OrderTransaction + if err := msg.Decode(&txs); err != nil { + return err + } + + if h.orderpool != nil { + h.orderpool.AddRemotes(txs) + } + + return nil +} + +// handleLendingTxMsg handles lending transaction messages +func (h *XDCHandler) handleLendingTxMsg(peer *p2p.Peer, msg p2p.Msg) error { + if atomic.LoadUint32(&h.acceptTxs) == 0 { + return nil + } + + var txs []*types.LendingTransaction + if err := msg.Decode(&txs); err != nil { + return err + } + + if h.lendingpool != nil { + h.lendingpool.AddRemotes(txs) + } + + return nil +} + +// handleVoteMsg handles vote messages +func (h *XDCHandler) handleVoteMsg(peer *p2p.Peer, msg p2p.Msg) error { + var vote types.Vote + if err := msg.Decode(&vote); err != nil { + return err + } + + select { + case h.voteCh <- &vote: + default: + log.Warn("Vote channel full, dropping vote") + } + + return nil +} + +// handleTimeoutMsg handles timeout messages +func (h *XDCHandler) handleTimeoutMsg(peer *p2p.Peer, msg p2p.Msg) error { + var timeout types.Timeout + if err := msg.Decode(&timeout); err != nil { + return err + } + + select { + case h.timeoutCh <- &timeout: + default: + log.Warn("Timeout channel full, dropping timeout") + } + + return nil +} + +// handleSyncInfoMsg handles sync info messages +func (h *XDCHandler) handleSyncInfoMsg(peer *p2p.Peer, msg p2p.Msg) error { + var syncInfo types.SyncInfo + if err := msg.Decode(&syncInfo); err != nil { + return err + } + + select { + case h.syncInfoCh <- &syncInfo: + default: + log.Warn("SyncInfo channel full, dropping syncInfo") + } + + return nil +} + +// orderTxBroadcastLoop broadcasts order transactions +func (h *XDCHandler) orderTxBroadcastLoop() { + if h.orderTxSub == nil { + return + } + for { + select { + case event := <-h.orderTxCh: + h.BroadcastOrderTx(event.Tx) + case <-h.orderTxSub.Err(): + return + case <-h.quitSync: + return + } + } +} + +// lendingTxBroadcastLoop broadcasts lending transactions +func (h *XDCHandler) lendingTxBroadcastLoop() { + if h.lendingTxSub == nil { + return + } + for { + select { + case event := <-h.lendingTxCh: + h.BroadcastLendingTx(event.Tx) + case <-h.lendingTxSub.Err(): + return + case <-h.quitSync: + return + } + } +} + +// consensusMsgLoop handles consensus messages +func (h *XDCHandler) consensusMsgLoop() { + for { + select { + case vote := <-h.voteCh: + // Forward to consensus engine + log.Debug("Processing vote", "hash", vote.Hash()) + case timeout := <-h.timeoutCh: + // Forward to consensus engine + log.Debug("Processing timeout", "round", timeout.Round) + case syncInfo := <-h.syncInfoCh: + // Forward to consensus engine + log.Debug("Processing syncInfo") + _ = syncInfo + case <-h.quitSync: + return + } + } +} + +// BroadcastOrderTx broadcasts an order transaction to peers +func (h *XDCHandler) BroadcastOrderTx(tx *types.OrderTransaction) { + hash := tx.GetHash() + peers := h.xdcPeers.PeersWithoutTx(hash) + for _, peer := range peers { + peer.MarkOrderTransaction(hash) + } + log.Trace("Broadcast order transaction", "hash", hash, "recipients", len(peers)) +} + +// BroadcastLendingTx broadcasts a lending transaction to peers +func (h *XDCHandler) BroadcastLendingTx(tx *types.LendingTransaction) { + hash := tx.Hash() + peers := h.xdcPeers.PeersWithoutTx(hash) + for _, peer := range peers { + peer.MarkLendingTransaction(hash) + } + log.Trace("Broadcast lending transaction", "hash", hash, "recipients", len(peers)) +} + +// BroadcastVote broadcasts a vote to peers +func (h *XDCHandler) BroadcastVote(vote *types.Vote) { + hash := vote.Hash() + peers := h.xdcPeers.PeersWithoutVote(hash) + for _, peer := range peers { + peer.MarkVote(hash) + } + log.Debug("Broadcast vote", + "hash", hash, + "blockHash", vote.ProposedBlockInfo.Hash, + "recipients", len(peers), + ) +} + +// BroadcastTimeout broadcasts a timeout to peers +func (h *XDCHandler) BroadcastTimeout(timeout *types.Timeout) { + hash := timeout.Hash() + peers := h.xdcPeers.PeersWithoutTimeout(hash) + for _, peer := range peers { + peer.MarkTimeout(hash) + } + log.Debug("Broadcast timeout", "round", timeout.Round, "recipients", len(peers)) +} + +// BroadcastSyncInfo broadcasts sync info to peers +func (h *XDCHandler) BroadcastSyncInfo(syncInfo *types.SyncInfo) { + hash := syncInfo.Hash() + peers := h.xdcPeers.PeersWithoutSyncInfo(hash) + for _, peer := range peers { + peer.MarkSyncInfo(hash) + } + log.Debug("Broadcast syncInfo", "recipients", len(peers)) +} + +// BroadcastBlock broadcasts a new block to peers +func (h *XDCHandler) BroadcastBlock(block *types.Block, propagate bool) { + hash := block.Hash() + peers := h.xdcPeers.PeersWithoutBlock(hash) + + if propagate { + td := h.chain.GetTd(block.ParentHash(), block.NumberU64()-1) + if td == nil { + log.Error("Propagating block with unknown parent", "number", block.Number(), "hash", hash) + return + } + td = new(big.Int).Add(td, block.Difficulty()) + + for _, peer := range peers { + peer.MarkBlock(hash) + } + log.Debug("Propagated block", "hash", hash, "recipients", len(peers), + "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) + } +} + +// NodeInfo represents XDC node information +type XDCNodeInfo struct { + Network uint64 `json:"network"` + Difficulty *big.Int `json:"difficulty"` + Genesis common.Hash `json:"genesis"` + Head common.Hash `json:"head"` + Epoch uint64 `json:"epoch"` +} + +// NodeInfo returns XDC-specific node information +func (h *XDCHandler) NodeInfo() *XDCNodeInfo { + currentBlock := h.chain.CurrentBlock() + return &XDCNodeInfo{ + Network: h.networkID, + Difficulty: h.chain.GetTd(currentBlock.Hash(), currentBlock.NumberU64()), + Genesis: h.chain.Genesis().Hash(), + Head: currentBlock.Hash(), + } +} diff --git a/eth/peer_xdc.go b/eth/peer_xdc.go new file mode 100644 index 0000000000..5f78656687 --- /dev/null +++ b/eth/peer_xdc.go @@ -0,0 +1,419 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eth + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/p2p" + mapset "github.com/deckarep/golang-set/v2" +) + +const ( + maxKnownTxsXDC = 32768 // Maximum transactions hashes to keep in the known list + maxKnownOrderTxs = 32768 // Maximum order transactions hashes + maxKnownLendingTxs = 32768 // Maximum lending transactions hashes + maxKnownBlocksXDC = 1024 // Maximum block hashes to keep in the known list + maxKnownVote = 131072 // Maximum vote hashes + maxKnownTimeout = 131072 // Maximum timeout hashes + maxKnownSyncInfo = 131072 // Maximum sync info hashes +) + +// XDCPeerInfo represents XDPoS-specific peer metadata +type XDCPeerInfo struct { + Version int `json:"version"` + Difficulty *big.Int `json:"difficulty"` + Head string `json:"head"` + Epoch uint64 `json:"epoch,omitempty"` + IsMaster bool `json:"isMaster,omitempty"` +} + +// xdcPeer extends the base peer with XDPoS-specific functionality +type xdcPeer struct { + id string + version int + head common.Hash + td *big.Int + lock sync.RWMutex + + // Known hashes for deduplication + knownTxs mapset.Set[common.Hash] + knownBlocks mapset.Set[common.Hash] + knownOrderTxs mapset.Set[common.Hash] + knownLendingTxs mapset.Set[common.Hash] + knownVotes mapset.Set[common.Hash] + knownTimeouts mapset.Set[common.Hash] + knownSyncInfos mapset.Set[common.Hash] + + // Masternode tracking + isMasternode bool + epoch uint64 +} + +// newXDCPeer creates a new XDPoS peer +func newXDCPeer(version int, id string) *xdcPeer { + return &xdcPeer{ + id: id, + version: version, + td: big.NewInt(0), + knownTxs: mapset.NewSet[common.Hash](), + knownBlocks: mapset.NewSet[common.Hash](), + knownOrderTxs: mapset.NewSet[common.Hash](), + knownLendingTxs: mapset.NewSet[common.Hash](), + knownVotes: mapset.NewSet[common.Hash](), + knownTimeouts: mapset.NewSet[common.Hash](), + knownSyncInfos: mapset.NewSet[common.Hash](), + } +} + +// Info returns XDPoS-specific peer info +func (p *xdcPeer) Info() *XDCPeerInfo { + p.lock.RLock() + defer p.lock.RUnlock() + + return &XDCPeerInfo{ + Version: p.version, + Difficulty: new(big.Int).Set(p.td), + Head: p.head.Hex(), + Epoch: p.epoch, + IsMaster: p.isMasternode, + } +} + +// Head retrieves the current head hash and total difficulty +func (p *xdcPeer) Head() (hash common.Hash, td *big.Int) { + p.lock.RLock() + defer p.lock.RUnlock() + + copy(hash[:], p.head[:]) + return hash, new(big.Int).Set(p.td) +} + +// SetHead updates the head hash and total difficulty +func (p *xdcPeer) SetHead(hash common.Hash, td *big.Int) { + p.lock.Lock() + defer p.lock.Unlock() + + copy(p.head[:], hash[:]) + p.td.Set(td) +} + +// MarkBlock marks a block as known +func (p *xdcPeer) MarkBlock(hash common.Hash) { + for p.knownBlocks.Cardinality() >= maxKnownBlocksXDC { + p.knownBlocks.Pop() + } + p.knownBlocks.Add(hash) +} + +// MarkTransaction marks a transaction as known +func (p *xdcPeer) MarkTransaction(hash common.Hash) { + for p.knownTxs.Cardinality() >= maxKnownTxsXDC { + p.knownTxs.Pop() + } + p.knownTxs.Add(hash) +} + +// MarkOrderTransaction marks an order transaction as known +func (p *xdcPeer) MarkOrderTransaction(hash common.Hash) { + for p.knownOrderTxs.Cardinality() >= maxKnownOrderTxs { + p.knownOrderTxs.Pop() + } + p.knownOrderTxs.Add(hash) +} + +// MarkLendingTransaction marks a lending transaction as known +func (p *xdcPeer) MarkLendingTransaction(hash common.Hash) { + for p.knownLendingTxs.Cardinality() >= maxKnownLendingTxs { + p.knownLendingTxs.Pop() + } + p.knownLendingTxs.Add(hash) +} + +// MarkVote marks a vote as known +func (p *xdcPeer) MarkVote(hash common.Hash) { + for p.knownVotes.Cardinality() >= maxKnownVote { + p.knownVotes.Pop() + } + p.knownVotes.Add(hash) +} + +// MarkTimeout marks a timeout as known +func (p *xdcPeer) MarkTimeout(hash common.Hash) { + for p.knownTimeouts.Cardinality() >= maxKnownTimeout { + p.knownTimeouts.Pop() + } + p.knownTimeouts.Add(hash) +} + +// MarkSyncInfo marks a sync info as known +func (p *xdcPeer) MarkSyncInfo(hash common.Hash) { + for p.knownSyncInfos.Cardinality() >= maxKnownSyncInfo { + p.knownSyncInfos.Pop() + } + p.knownSyncInfos.Add(hash) +} + +// HasBlock checks if a block is known +func (p *xdcPeer) HasBlock(hash common.Hash) bool { + return p.knownBlocks.Contains(hash) +} + +// HasTransaction checks if a transaction is known +func (p *xdcPeer) HasTransaction(hash common.Hash) bool { + return p.knownTxs.Contains(hash) +} + +// HasOrderTransaction checks if an order transaction is known +func (p *xdcPeer) HasOrderTransaction(hash common.Hash) bool { + return p.knownOrderTxs.Contains(hash) +} + +// HasLendingTransaction checks if a lending transaction is known +func (p *xdcPeer) HasLendingTransaction(hash common.Hash) bool { + return p.knownLendingTxs.Contains(hash) +} + +// HasVote checks if a vote is known +func (p *xdcPeer) HasVote(hash common.Hash) bool { + return p.knownVotes.Contains(hash) +} + +// HasTimeout checks if a timeout is known +func (p *xdcPeer) HasTimeout(hash common.Hash) bool { + return p.knownTimeouts.Contains(hash) +} + +// HasSyncInfo checks if a sync info is known +func (p *xdcPeer) HasSyncInfo(hash common.Hash) bool { + return p.knownSyncInfos.Contains(hash) +} + +// SetMasternode sets whether this peer is a masternode +func (p *xdcPeer) SetMasternode(isMaster bool) { + p.lock.Lock() + defer p.lock.Unlock() + p.isMasternode = isMaster +} + +// IsMasternode returns whether this peer is a masternode +func (p *xdcPeer) IsMasternode() bool { + p.lock.RLock() + defer p.lock.RUnlock() + return p.isMasternode +} + +// SetEpoch sets the current epoch +func (p *xdcPeer) SetEpoch(epoch uint64) { + p.lock.Lock() + defer p.lock.Unlock() + p.epoch = epoch +} + +// xdcPeerSet manages a set of XDPoS peers +type xdcPeerSet struct { + peers map[string]*xdcPeer + lock sync.RWMutex + closed bool +} + +// newXDCPeerSet creates a new XDPoS peer set +func newXDCPeerSet() *xdcPeerSet { + return &xdcPeerSet{ + peers: make(map[string]*xdcPeer), + } +} + +// Register adds a peer to the set +func (ps *xdcPeerSet) Register(p *xdcPeer) error { + ps.lock.Lock() + defer ps.lock.Unlock() + + if ps.closed { + return errClosed + } + if _, ok := ps.peers[p.id]; ok { + return errAlreadyRegistered + } + ps.peers[p.id] = p + return nil +} + +// Unregister removes a peer from the set +func (ps *xdcPeerSet) Unregister(id string) error { + ps.lock.Lock() + defer ps.lock.Unlock() + + if _, ok := ps.peers[id]; !ok { + return errNotRegistered + } + delete(ps.peers, id) + return nil +} + +// Peer retrieves a peer by ID +func (ps *xdcPeerSet) Peer(id string) *xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + return ps.peers[id] +} + +// Len returns the number of peers +func (ps *xdcPeerSet) Len() int { + ps.lock.RLock() + defer ps.lock.RUnlock() + + return len(ps.peers) +} + +// PeersWithoutBlock returns peers without a known block +func (ps *xdcPeerSet) PeersWithoutBlock(hash common.Hash) []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.HasBlock(hash) { + list = append(list, p) + } + } + return list +} + +// PeersWithoutTx returns peers without a known transaction +func (ps *xdcPeerSet) PeersWithoutTx(hash common.Hash) []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.HasTransaction(hash) { + list = append(list, p) + } + } + return list +} + +// PeersWithoutVote returns peers without a known vote +func (ps *xdcPeerSet) PeersWithoutVote(hash common.Hash) []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.HasVote(hash) { + list = append(list, p) + } + } + return list +} + +// PeersWithoutTimeout returns peers without a known timeout +func (ps *xdcPeerSet) PeersWithoutTimeout(hash common.Hash) []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.HasTimeout(hash) { + list = append(list, p) + } + } + return list +} + +// PeersWithoutSyncInfo returns peers without a known sync info +func (ps *xdcPeerSet) PeersWithoutSyncInfo(hash common.Hash) []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.HasSyncInfo(hash) { + list = append(list, p) + } + } + return list +} + +// MasternodePeers returns all masternode peers +func (ps *xdcPeerSet) MasternodePeers() []*xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*xdcPeer, 0) + for _, p := range ps.peers { + if p.IsMasternode() { + list = append(list, p) + } + } + return list +} + +// BestPeer returns the peer with highest total difficulty +func (ps *xdcPeerSet) BestPeer() *xdcPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + var ( + bestPeer *xdcPeer + bestTd *big.Int + ) + for _, p := range ps.peers { + if _, td := p.Head(); bestPeer == nil || td.Cmp(bestTd) > 0 { + bestPeer, bestTd = p, td + } + } + return bestPeer +} + +// Close shuts down the peer set +func (ps *xdcPeerSet) Close() { + ps.lock.Lock() + defer ps.lock.Unlock() + + ps.closed = true +} + +// SendVote sends a vote message to a peer (placeholder for p2p integration) +func SendVote(rw p2p.MsgReadWriter, vote *types.Vote) error { + return p2p.Send(rw, VoteMsgCode, vote) +} + +// SendTimeout sends a timeout message to a peer +func SendTimeout(rw p2p.MsgReadWriter, timeout *types.Timeout) error { + return p2p.Send(rw, TimeoutMsgCode, timeout) +} + +// SendSyncInfo sends a sync info message to a peer +func SendSyncInfo(rw p2p.MsgReadWriter, syncInfo *types.SyncInfo) error { + return p2p.Send(rw, SyncInfoMsgCode, syncInfo) +} + +// SendOrderTransactions sends order transactions to a peer +func SendOrderTransactions(rw p2p.MsgReadWriter, txs types.OrderTransactions) error { + return p2p.Send(rw, OrderTxMsgCode, txs) +} + +// SendLendingTransactions sends lending transactions to a peer +func SendLendingTransactions(rw p2p.MsgReadWriter, txs types.LendingTransactions) error { + return p2p.Send(rw, LendingTxMsgCode, txs) +} diff --git a/eth/protocol_xdc.go b/eth/protocol_xdc.go new file mode 100644 index 0000000000..d022285e4f --- /dev/null +++ b/eth/protocol_xdc.go @@ -0,0 +1,140 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eth + +import ( + "fmt" + "io" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rlp" +) + +// XDPoS protocol version constants +const ( + xdpos2 = 100 // XDPoS 2.0 protocol version +) + +// XDC protocol message codes (extensions to standard eth protocol) +const ( + // XDPoS consensus messages (starting at 0xe0 to avoid conflicts) + VoteMsgCode = 0xe0 + TimeoutMsgCode = 0xe1 + SyncInfoMsgCode = 0xe2 + + // Order/Lending transaction messages + OrderTxMsgCode = 0x08 + LendingTxMsgCode = 0x09 +) + +// XDC error codes +const ( + ErrMsgTooLargeXDC = iota + 100 + ErrDecodeXDC + ErrInvalidMsgCodeXDC + ErrSuspendedPeerXDC +) + +func errCodeXDCString(e int) string { + switch e { + case ErrMsgTooLargeXDC: + return "XDC: Message too long" + case ErrDecodeXDC: + return "XDC: Invalid message" + case ErrInvalidMsgCodeXDC: + return "XDC: Invalid message code" + case ErrSuspendedPeerXDC: + return "XDC: Suspended peer" + default: + return fmt.Sprintf("XDC: Unknown error %d", e) + } +} + +// OrderPool interface for XDCx order pool +type OrderPool interface { + // AddRemotes should add the given transactions to the pool. + AddRemotes([]*types.OrderTransaction) []error + + // Pending should return pending transactions. + Pending() (map[common.Address]types.OrderTransactions, error) + + // SubscribeTxPreEvent should return an event subscription of + // TxPreEvent and send events to the given channel. + SubscribeTxPreEvent(chan<- core.OrderTxPreEvent) event.Subscription +} + +// LendingPool interface for XDCx lending pool +type LendingPool interface { + // AddRemotes should add the given transactions to the pool. + AddRemotes([]*types.LendingTransaction) []error + + // Pending should return pending transactions. + Pending() (map[common.Address]types.LendingTransactions, error) + + // SubscribeTxPreEvent should return an event subscription of + // TxPreEvent and send events to the given channel. + SubscribeTxPreEvent(chan<- core.LendingTxPreEvent) event.Subscription +} + +// XDCStatusData extends statusData with XDPoS-specific fields +type XDCStatusData struct { + ProtocolVersion uint32 + NetworkId uint64 + TD *big.Int + CurrentBlock common.Hash + GenesisBlock common.Hash + Epoch uint64 // Current epoch number +} + +// hashOrNumberXDC is a combined field for specifying an origin block. +// Duplicated from protocol.go to avoid circular imports in some cases +type hashOrNumberXDC struct { + Hash common.Hash // Block hash from which to retrieve headers (excludes Number) + Number uint64 // Block number from which to retrieve headers (excludes Hash) +} + +// EncodeRLP is a specialized encoder for hashOrNumberXDC +func (hn *hashOrNumberXDC) EncodeRLP(w io.Writer) error { + if hn.Hash == (common.Hash{}) { + return rlp.Encode(w, hn.Number) + } + if hn.Number != 0 { + return fmt.Errorf("both origin hash (%x) and number (%d) provided", hn.Hash, hn.Number) + } + return rlp.Encode(w, hn.Hash) +} + +// DecodeRLP is a specialized decoder for hashOrNumberXDC +func (hn *hashOrNumberXDC) DecodeRLP(s *rlp.Stream) error { + _, size, _ := s.Kind() + origin, err := s.Raw() + if err == nil { + switch { + case size == 32: + err = rlp.DecodeBytes(origin, &hn.Hash) + case size <= 8: + err = rlp.DecodeBytes(origin, &hn.Number) + default: + err = fmt.Errorf("invalid input size %d for origin", size) + } + } + return err +} diff --git a/internal/ethapi/api_xdc_test.go b/internal/ethapi/api_xdc_test.go new file mode 100644 index 0000000000..62da5a714c --- /dev/null +++ b/internal/ethapi/api_xdc_test.go @@ -0,0 +1,207 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. + +package ethapi + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func TestGetMasternodes(t *testing.T) { + // Setup mock backend + // This is a placeholder test - full implementation would use testutil + + t.Run("returns masternode list", func(t *testing.T) { + // Test would verify GetMasternodes returns correct list + }) + + t.Run("handles empty epoch", func(t *testing.T) { + // Test would verify behavior when epoch has no masternodes + }) +} + +func TestGetCandidates(t *testing.T) { + t.Run("returns candidate list", func(t *testing.T) { + // Test would verify GetCandidates returns correct list + }) +} + +func TestGetCandidateInfo(t *testing.T) { + t.Run("returns candidate info", func(t *testing.T) { + // Test would verify candidate info fields + }) + + t.Run("handles non-existent candidate", func(t *testing.T) { + // Test error handling for invalid candidate + }) +} + +func TestGetBlockSignersByNumber(t *testing.T) { + t.Run("returns signers for block", func(t *testing.T) { + // Test would verify signers list + }) +} + +func TestXDCRewardCalculation(t *testing.T) { + tests := []struct { + name string + blockNumber uint64 + expectedReward *big.Int + }{ + { + name: "genesis block has no reward", + blockNumber: 0, + expectedReward: big.NewInt(0), + }, + { + name: "regular block has standard reward", + blockNumber: 100, + expectedReward: new(big.Int).Mul(big.NewInt(250), big.NewInt(1e18)), // 250 XDC + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test implementation would go here + }) + } +} + +func TestXDCEpochCalculation(t *testing.T) { + epochSize := uint64(900) + + tests := []struct { + blockNumber uint64 + expectedEpoch uint64 + }{ + {0, 0}, + {899, 0}, + {900, 1}, + {1800, 2}, + {2700, 3}, + } + + for _, tt := range tests { + epoch := tt.blockNumber / epochSize + if epoch != tt.expectedEpoch { + t.Errorf("Block %d: expected epoch %d, got %d", tt.blockNumber, tt.expectedEpoch, epoch) + } + } +} + +func TestGapBlockCalculation(t *testing.T) { + epochSize := uint64(900) + gapSize := uint64(50) + + tests := []struct { + blockNumber uint64 + isGapBlock bool + }{ + {0, false}, + {849, false}, + {850, true}, // epoch 1 gap block (900 - 50 = 850) + {851, false}, + {1750, true}, // epoch 2 gap block (1800 - 50 = 1750) + } + + for _, tt := range tests { + isGap := tt.blockNumber % epochSize == epochSize - gapSize + if isGap != tt.isGapBlock { + t.Errorf("Block %d: expected isGapBlock=%v, got %v", tt.blockNumber, tt.isGapBlock, isGap) + } + } +} + +func TestHexutilConversions(t *testing.T) { + // Test hexutil conversions used in XDC API + + t.Run("uint64 conversion", func(t *testing.T) { + val := uint64(12345) + hex := hexutil.Uint64(val) + if uint64(hex) != val { + t.Errorf("Expected %d, got %d", val, uint64(hex)) + } + }) + + t.Run("big int conversion", func(t *testing.T) { + val := big.NewInt(1000000000000000000) + hex := (*hexutil.Big)(val) + if hex.ToInt().Cmp(val) != 0 { + t.Errorf("Big int conversion mismatch") + } + }) +} + +func TestMasternodeAddress(t *testing.T) { + // Test masternode address validation + validAddr := common.HexToAddress("0x0000000000000000000000000000000000000001") + + if validAddr == (common.Address{}) { + t.Error("Expected non-zero address") + } +} + +// Mock backend for testing +type mockXDCBackend struct { + masternodes map[uint64][]common.Address + candidates []common.Address +} + +func newMockXDCBackend() *mockXDCBackend { + return &mockXDCBackend{ + masternodes: make(map[uint64][]common.Address), + candidates: make([]common.Address, 0), + } +} + +func (b *mockXDCBackend) GetMasternodes(epoch uint64) []common.Address { + return b.masternodes[epoch] +} + +func (b *mockXDCBackend) GetCandidates() []common.Address { + return b.candidates +} + +func (b *mockXDCBackend) SetMasternodes(epoch uint64, addrs []common.Address) { + b.masternodes[epoch] = addrs +} + +func (b *mockXDCBackend) SetCandidates(addrs []common.Address) { + b.candidates = addrs +} + +// Benchmark tests +func BenchmarkGetMasternodes(b *testing.B) { + backend := newMockXDCBackend() + + // Setup 150 masternodes + masternodes := make([]common.Address, 150) + for i := 0; i < 150; i++ { + masternodes[i] = common.BigToAddress(big.NewInt(int64(i + 1))) + } + backend.SetMasternodes(1, masternodes) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = backend.GetMasternodes(1) + } +} + +func BenchmarkEpochCalculation(b *testing.B) { + epochSize := uint64(900) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = uint64(i) / epochSize + } +} + +// Context helpers for tests +func testContext() context.Context { + return context.Background() +} diff --git a/internal/web3ext/web3ext_xdc.go b/internal/web3ext/web3ext_xdc.go new file mode 100644 index 0000000000..a3b4195344 --- /dev/null +++ b/internal/web3ext/web3ext_xdc.go @@ -0,0 +1,326 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package web3ext contains XDPoS-specific web3 extensions. +package web3ext + +// XDCJs contains the XDC-specific web3 JavaScript extensions +const XDCJs = ` +web3._extend({ + property: 'xdc', + methods: [ + new web3._extend.Method({ + name: 'getMasternodes', + call: 'xdc_getMasternodes', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getMasternodesByNumber', + call: 'xdc_getMasternodesByNumber', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getCandidates', + call: 'xdc_getCandidates', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getCandidateOwner', + call: 'xdc_getCandidateOwner', + params: 2, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null] + }), + new web3._extend.Method({ + name: 'getCandidateCap', + call: 'xdc_getCandidateCap', + params: 2, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null] + }), + new web3._extend.Method({ + name: 'getBlockSignersByNumber', + call: 'xdc_getBlockSignersByNumber', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getBlockSignersByHash', + call: 'xdc_getBlockSignersByHash', + params: 1 + }), + new web3._extend.Method({ + name: 'getEpoch', + call: 'xdc_getEpoch', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getCurrentEpoch', + call: 'xdc_getCurrentEpoch', + params: 0 + }), + new web3._extend.Method({ + name: 'getReward', + call: 'xdc_getReward', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getPenalized', + call: 'xdc_getPenalized', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'networkInformation', + call: 'xdc_networkInformation', + params: 0 + }), + new web3._extend.Method({ + name: 'getStakeBalance', + call: 'xdc_getStakeBalance', + params: 2, + inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getVoterBalance', + call: 'xdc_getVoterBalance', + params: 3, + inputFormatter: [null, null, web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getVoterCap', + call: 'xdc_getVoterCap', + params: 3, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null, null] + }), + new web3._extend.Method({ + name: 'getVoters', + call: 'xdc_getVoters', + params: 2, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null] + }), + new web3._extend.Method({ + name: 'getOwnerCount', + call: 'xdc_getOwnerCount', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getCandidateStatus', + call: 'xdc_getCandidateStatus', + params: 2, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null] + }), + new web3._extend.Method({ + name: 'getMaxCapacity', + call: 'xdc_getMaxCapacity', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getMinCapacity', + call: 'xdc_getMinCapacity', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getKYC', + call: 'xdc_getKYC', + params: 2, + inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getLatestKYC', + call: 'xdc_getLatestKYC', + params: 2, + inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getV2Block', + call: 'xdc_getV2Block', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getMasternodeStatusByNumber', + call: 'xdc_getMasternodeStatusByNumber', + params: 2, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, null] + }), + new web3._extend.Method({ + name: 'getMissedRoundsInEpochByBlockNum', + call: 'xdc_getMissedRoundsInEpochByBlockNum', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getRoundInfo', + call: 'xdc_getRoundInfo', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getEpochInfo', + call: 'xdc_getEpochInfo', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + ], + properties: [ + new web3._extend.Property({ + name: 'epochSize', + getter: 'xdc_epochSize' + }), + new web3._extend.Property({ + name: 'gapSize', + getter: 'xdc_gapSize' + }), + new web3._extend.Property({ + name: 'masternodeCount', + getter: 'xdc_masternodeCount' + }), + ] +}); +` + +// XDCxJs contains the XDCx DEX specific web3 JavaScript extensions +const XDCxJs = ` +web3._extend({ + property: 'XDCx', + methods: [ + new web3._extend.Method({ + name: 'getOrderById', + call: 'XDCx_getOrderById', + params: 3, + inputFormatter: [null, null, null] + }), + new web3._extend.Method({ + name: 'getOrderCount', + call: 'XDCx_getOrderCount', + params: 1 + }), + new web3._extend.Method({ + name: 'getBestBid', + call: 'XDCx_getBestBid', + params: 2 + }), + new web3._extend.Method({ + name: 'getBestAsk', + call: 'XDCx_getBestAsk', + params: 2 + }), + new web3._extend.Method({ + name: 'getPriceAtDepth', + call: 'XDCx_getPriceAtDepth', + params: 3 + }), + new web3._extend.Method({ + name: 'getBidsTree', + call: 'XDCx_getBidsTree', + params: 2 + }), + new web3._extend.Method({ + name: 'getAsksTree', + call: 'XDCx_getAsksTree', + params: 2 + }), + new web3._extend.Method({ + name: 'getOrderNonce', + call: 'XDCx_getOrderNonce', + params: 1 + }), + new web3._extend.Method({ + name: 'getTradingState', + call: 'XDCx_getTradingState', + params: 2 + }), + new web3._extend.Method({ + name: 'getLiquidationPrice', + call: 'XDCx_getLiquidationPrice', + params: 3 + }), + new web3._extend.Method({ + name: 'getLendingOrderCount', + call: 'XDCx_getLendingOrderCount', + params: 1 + }), + new web3._extend.Method({ + name: 'getBorrowingOrderCount', + call: 'XDCx_getBorrowingOrderCount', + params: 1 + }), + new web3._extend.Method({ + name: 'getInvestingOrderCount', + call: 'XDCx_getInvestingOrderCount', + params: 1 + }), + new web3._extend.Method({ + name: 'getLendingTradeCount', + call: 'XDCx_getLendingTradeCount', + params: 1 + }), + ] +}); +` + +// XDCLendingJs contains lending-specific web3 JavaScript extensions +const XDCLendingJs = ` +web3._extend({ + property: 'XDCLending', + methods: [ + new web3._extend.Method({ + name: 'getLendingState', + call: 'XDCLending_getLendingState', + params: 2 + }), + new web3._extend.Method({ + name: 'getLendingOrderById', + call: 'XDCLending_getLendingOrderById', + params: 4 + }), + new web3._extend.Method({ + name: 'getInvestingById', + call: 'XDCLending_getInvestingById', + params: 4 + }), + new web3._extend.Method({ + name: 'getBorrowingById', + call: 'XDCLending_getBorrowingById', + params: 4 + }), + new web3._extend.Method({ + name: 'getLendingTradeById', + call: 'XDCLending_getLendingTradeById', + params: 3 + }), + new web3._extend.Method({ + name: 'getTopBid', + call: 'XDCLending_getTopBid', + params: 3 + }), + new web3._extend.Method({ + name: 'getTopAsk', + call: 'XDCLending_getTopAsk', + params: 3 + }), + new web3._extend.Method({ + name: 'getLiquidatedTrade', + call: 'XDCLending_getLiquidatedTrade', + params: 3 + }), + ] +}); +` + +// AllXDCModules returns all XDC-specific JavaScript modules +func AllXDCModules() string { + return XDCJs + XDCxJs + XDCLendingJs +} diff --git a/les/api_backend_xdc.go b/les/api_backend_xdc.go new file mode 100644 index 0000000000..33ae42843a --- /dev/null +++ b/les/api_backend_xdc.go @@ -0,0 +1,164 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Package les implements the Light Ethereum Subprotocol. +package les + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" +) + +// XDCApiBackend implements XDPoS-specific API methods for light clients +type XDCApiBackend struct { + // Embed the base LesApiBackend + // LesApiBackend +} + +// GetMasternodesXDC returns the masternode list for an epoch (light client version) +func (b *XDCApiBackend) GetMasternodesXDC(ctx context.Context, epoch uint64) ([]common.Address, error) { + // Light clients would fetch this from full nodes + // This is a placeholder implementation + return nil, nil +} + +// GetCandidatesXDC returns the candidate list for an epoch (light client version) +func (b *XDCApiBackend) GetCandidatesXDC(ctx context.Context, epoch uint64) ([]common.Address, error) { + // Light clients would fetch this from full nodes + return nil, nil +} + +// GetEpochXDC returns the epoch number for a block +func (b *XDCApiBackend) GetEpochXDC(ctx context.Context, blockNumber uint64) (uint64, error) { + // Would be calculated from block number and epoch size + return 0, nil +} + +// GetBlockSignerXDC returns the signer of a block +func (b *XDCApiBackend) GetBlockSignerXDC(ctx context.Context, blockHash common.Hash) (common.Address, error) { + // Light clients would need to verify this from header + return common.Address{}, nil +} + +// GetPenalizedValidatorsXDC returns the penalized validators for an epoch +func (b *XDCApiBackend) GetPenalizedValidatorsXDC(ctx context.Context, epoch uint64) ([]common.Address, error) { + return nil, nil +} + +// LightChainReaderXDC interface for light chain XDPoS access +type LightChainReaderXDC interface { + // GetHeaderByNumber retrieves a header by number + GetHeaderByNumber(ctx context.Context, number uint64) (*types.Header, error) + + // GetHeaderByHash retrieves a header by hash + GetHeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) + + // GetTd retrieves total difficulty + GetTd(ctx context.Context, hash common.Hash) (*big.Int, error) + + // CurrentHeader returns the current header + CurrentHeader() *types.Header +} + +// XDCLightSyncConfig contains light client sync configuration for XDPoS +type XDCLightSyncConfig struct { + // Whether to sync masternode data + SyncMasternodes bool + + // Whether to sync validator data + SyncValidators bool + + // Checkpoint block hash for initial sync + CheckpointHash common.Hash + + // Checkpoint block number + CheckpointNumber uint64 +} + +// XDCLightVerifier verifies XDPoS data for light clients +type XDCLightVerifier struct { + chain LightChainReaderXDC +} + +// NewXDCLightVerifier creates a new light verifier +func NewXDCLightVerifier(chain LightChainReaderXDC) *XDCLightVerifier { + return &XDCLightVerifier{chain: chain} +} + +// VerifyHeader verifies a header for light client +func (v *XDCLightVerifier) VerifyHeader(ctx context.Context, header *types.Header) error { + // Verify header based on XDPoS rules + // Light clients need to verify: + // 1. Block is signed by a valid masternode + // 2. Block number is correct + // 3. Parent hash matches + return nil +} + +// VerifySignature verifies a block signature +func (v *XDCLightVerifier) VerifySignature(ctx context.Context, header *types.Header) (common.Address, error) { + // Extract signer from header + // Verify signature is valid + return common.Address{}, nil +} + +// VerifyMasternode verifies a signer is a valid masternode +func (v *XDCLightVerifier) VerifyMasternode(ctx context.Context, signer common.Address, blockNumber uint64) (bool, error) { + // Check if signer was a masternode at the given block + return false, nil +} + +// XDCODRBackend interface for On-Demand Retrieval of XDPoS data +type XDCODRBackend interface { + // RetrieveMasternodes retrieves masternode list from network + RetrieveMasternodes(ctx context.Context, epoch uint64) ([]common.Address, error) + + // RetrieveValidatorState retrieves validator state + RetrieveValidatorState(ctx context.Context, address common.Address, blockHash common.Hash) (*state.StateDB, error) +} + +// XDCLightCheckpoint represents a checkpoint for light client sync +type XDCLightCheckpoint struct { + BlockNumber uint64 `json:"blockNumber"` + BlockHash common.Hash `json:"blockHash"` + Epoch uint64 `json:"epoch"` + Masternodes []common.Address `json:"masternodes"` + StateRoot common.Hash `json:"stateRoot"` + TotalStake *big.Int `json:"totalStake"` +} + +// ValidateCheckpoint validates a checkpoint +func ValidateCheckpoint(checkpoint *XDCLightCheckpoint, header *types.Header) error { + // Verify checkpoint matches header + if checkpoint.BlockHash != header.Hash() { + return ErrCheckpointMismatch + } + if checkpoint.StateRoot != header.Root { + return ErrCheckpointMismatch + } + return nil +} + +// Errors +var ( + ErrCheckpointMismatch = &CheckpointError{"checkpoint hash mismatch"} + ErrInvalidSignature = &CheckpointError{"invalid block signature"} + ErrNotMasternode = &CheckpointError{"signer is not a masternode"} +) + +type CheckpointError struct { + message string +} + +func (e *CheckpointError) Error() string { + return e.message +} diff --git a/miner/worker_xdc.go b/miner/worker_xdc.go new file mode 100644 index 0000000000..74d0c6bc07 --- /dev/null +++ b/miner/worker_xdc.go @@ -0,0 +1,335 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package miner + +import ( + "errors" + "math/big" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +var ( + // ErrNotAuthorized is returned when the signer is not authorized + ErrNotAuthorized = errors.New("signer not authorized") + + // ErrWrongDifficulty is returned when difficulty check fails + ErrWrongDifficulty = errors.New("wrong difficulty") +) + +// XDCWorkerConfig contains XDPoS-specific worker configuration +type XDCWorkerConfig struct { + // Recommit interval for block sealing + Recommit time.Duration + + // GasFloor is the target gas floor for blocks + GasFloor uint64 + + // GasCeil is the target gas ceiling for blocks + GasCeil uint64 + + // Extra data for blocks + ExtraData []byte +} + +// XDCWorker extends the base worker with XDPoS-specific functionality +type XDCWorker struct { + config *params.ChainConfig + chainConfig *params.ChainConfig + engine consensus.Engine + eth Backend + chain *core.BlockChain + + // Feeds + pendingLogsFeed event.Feed + + // Channels + taskCh chan *types.Block + startCh chan struct{} + exitCh chan struct{} + resubmitIntervalCh chan time.Duration + + // State + running int32 // atomic + syncing int32 // atomic + + // Current work + mu sync.RWMutex + coinbase common.Address + extra []byte + + // Pending block + pendingMu sync.RWMutex + pendingBlock *types.Block + pendingState *state.StateDB + + // XDPoS specific + orderpool OrderPool + lendingpool LendingPool +} + +// OrderPool interface for XDCx order pool integration +type OrderPool interface { + Pending() (map[common.Address]types.OrderTransactions, error) +} + +// LendingPool interface for XDCx lending pool integration +type LendingPool interface { + Pending() (map[common.Address]types.LendingTransactions, error) +} + +// Backend wraps all required backend methods for mining +type Backend interface { + BlockChain() *core.BlockChain + TxPool() TxPool +} + +// TxPool interface for transaction pool +type TxPool interface { + Pending(enforceTips bool) map[common.Address][]*types.Transaction +} + +// NewXDCWorker creates a new XDPoS worker +func NewXDCWorker(config *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *XDCWorker { + worker := &XDCWorker{ + config: config, + chainConfig: config, + engine: engine, + eth: eth, + chain: eth.BlockChain(), + taskCh: make(chan *types.Block), + startCh: make(chan struct{}, 1), + exitCh: make(chan struct{}), + resubmitIntervalCh: make(chan time.Duration), + } + + if init { + go worker.mainLoop() + } + + return worker +} + +// mainLoop is the main event loop for the worker +func (w *XDCWorker) mainLoop() { + for { + select { + case <-w.startCh: + w.commitWork() + case <-w.exitCh: + return + } + } +} + +// start begins the mining process +func (w *XDCWorker) start() { + atomic.StoreInt32(&w.running, 1) + w.startCh <- struct{}{} +} + +// stop halts the mining process +func (w *XDCWorker) stop() { + atomic.StoreInt32(&w.running, 0) +} + +// close terminates all internal goroutines +func (w *XDCWorker) close() { + close(w.exitCh) +} + +// isRunning returns whether the worker is running +func (w *XDCWorker) isRunning() bool { + return atomic.LoadInt32(&w.running) == 1 +} + +// setEtherbase sets the etherbase for mining +func (w *XDCWorker) setEtherbase(addr common.Address) { + w.mu.Lock() + defer w.mu.Unlock() + w.coinbase = addr +} + +// setExtra sets extra data for blocks +func (w *XDCWorker) setExtra(extra []byte) { + w.mu.Lock() + defer w.mu.Unlock() + w.extra = extra +} + +// pending returns the pending block and state +func (w *XDCWorker) pending() (*types.Block, *state.StateDB) { + w.pendingMu.RLock() + defer w.pendingMu.RUnlock() + + if w.pendingBlock != nil { + return w.pendingBlock, w.pendingState.Copy() + } + return nil, nil +} + +// pendingBlock returns the pending block +func (w *XDCWorker) pendingBlockAndReceipts() (*types.Block, types.Receipts) { + w.pendingMu.RLock() + defer w.pendingMu.RUnlock() + return w.pendingBlock, nil +} + +// commitWork generates a new work based on the parent block +func (w *XDCWorker) commitWork() { + parent := w.chain.CurrentBlock() + if parent == nil { + return + } + + w.mu.RLock() + coinbase := w.coinbase + extra := w.extra + w.mu.RUnlock() + + if coinbase == (common.Address{}) { + log.Error("Refusing to mine without etherbase") + return + } + + // Create new work + num := parent.Number() + header := &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(num, common.Big1), + GasLimit: core.CalcGasLimit(parent.GasLimit(), w.config.XDPoS.GasLimitBoundDivisor), + Extra: extra, + Time: uint64(time.Now().Unix()), + } + + // Set coinbase for XDPoS + if w.config.XDPoS != nil { + header.Coinbase = coinbase + } + + // Prepare header with consensus engine + if err := w.engine.Prepare(w.chain, header); err != nil { + log.Error("Failed to prepare header for sealing", "err", err) + return + } + + // Create state + statedb, err := w.chain.StateAt(parent.Root()) + if err != nil { + log.Error("Failed to create state", "err", err) + return + } + + // Fill transactions + pending := w.eth.TxPool().Pending(true) + txs := make([]*types.Transaction, 0) + for _, list := range pending { + txs = append(txs, list...) + } + + // Apply transactions + receipts, logs := w.applyTransactions(txs, statedb, header) + + // Finalize block with consensus engine + block, err := w.engine.FinalizeAndAssemble(w.chain, header, statedb, &types.Body{Transactions: txs}, receipts) + if err != nil { + log.Error("Failed to finalize block", "err", err) + return + } + + // Store pending work + w.pendingMu.Lock() + w.pendingBlock = block + w.pendingState = statedb + w.pendingMu.Unlock() + + // Submit to seal + w.taskCh <- block + + log.Info("Commit new sealing work", "number", block.Number(), "txs", len(txs), "logs", len(logs)) +} + +// applyTransactions applies transactions to the state +func (w *XDCWorker) applyTransactions(txs []*types.Transaction, statedb *state.StateDB, header *types.Header) ([]*types.Receipt, []*types.Log) { + var ( + receipts []*types.Receipt + logs []*types.Log + gasPool = new(core.GasPool).AddGas(header.GasLimit) + ) + + for _, tx := range txs { + statedb.Prepare(tx.Hash(), len(receipts)) + + receipt, err := core.ApplyTransaction(w.config, w.chain, nil, gasPool, statedb, header, tx, &header.GasUsed, *w.chain.GetVMConfig()) + if err != nil { + continue + } + + receipts = append(receipts, receipt) + logs = append(logs, receipt.Logs...) + } + + return receipts, logs +} + +// setOrderPool sets the order pool for XDCx integration +func (w *XDCWorker) setOrderPool(orderpool OrderPool) { + w.orderpool = orderpool +} + +// setLendingPool sets the lending pool for XDCx integration +func (w *XDCWorker) setLendingPool(lendingpool LendingPool) { + w.lendingpool = lendingpool +} + +// getOrderTransactions gets pending order transactions +func (w *XDCWorker) getOrderTransactions() (types.OrderTransactions, error) { + if w.orderpool == nil { + return nil, nil + } + + pending, err := w.orderpool.Pending() + if err != nil { + return nil, err + } + + var txs types.OrderTransactions + for _, list := range pending { + txs = append(txs, list...) + } + return txs, nil +} + +// getLendingTransactions gets pending lending transactions +func (w *XDCWorker) getLendingTransactions() (types.LendingTransactions, error) { + if w.lendingpool == nil { + return nil, nil + } + + pending, err := w.lendingpool.Pending() + if err != nil { + return nil, err + } + + var txs types.LendingTransactions + for _, list := range pending { + txs = append(txs, list...) + } + return txs, nil +} diff --git a/node/config_xdc.go b/node/config_xdc.go new file mode 100644 index 0000000000..c4a3af080a --- /dev/null +++ b/node/config_xdc.go @@ -0,0 +1,225 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package node + +import ( + "crypto/ecdsa" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +// XDCConfig contains XDPoS-specific node configuration +type XDCConfig struct { + // EnableMasternode enables masternode mode + EnableMasternode bool + + // Masternode account address + MasternodeAddr common.Address + + // Masternode signing key + MasternodeKey *ecdsa.PrivateKey + + // StakingAddress is the address that holds masternode stake + StakingAddress common.Address + + // RewardAddress is the address to receive rewards + RewardAddress common.Address + + // Enable XDCx DEX + EnableXDCx bool + + // XDCx database path + XDCxDataDir string + + // Enable lending + EnableLending bool + + // Lending database path + LendingDataDir string + + // Announce masternode to network + Announce bool + + // SlashingEnabled enables slashing for misbehavior + SlashingEnabled bool + + // GapBlockNum is the gap block before epoch switch + GapBlockNum uint64 + + // LogLevel for XDPoS consensus + LogLevel int +} + +// DefaultXDCConfig returns default XDC configuration +func DefaultXDCConfig() *XDCConfig { + return &XDCConfig{ + EnableMasternode: false, + EnableXDCx: false, + EnableLending: false, + Announce: true, + SlashingEnabled: true, + GapBlockNum: 50, + LogLevel: 3, + } +} + +// ResolveXDCxDataDir resolves the XDCx data directory +func (c *XDCConfig) ResolveXDCxDataDir(baseDir string) string { + if c.XDCxDataDir != "" { + return c.XDCxDataDir + } + return filepath.Join(baseDir, "XDCx") +} + +// ResolveLendingDataDir resolves the lending data directory +func (c *XDCConfig) ResolveLendingDataDir(baseDir string) string { + if c.LendingDataDir != "" { + return c.LendingDataDir + } + return filepath.Join(baseDir, "lending") +} + +// Validate validates the XDC configuration +func (c *XDCConfig) Validate() error { + if c.EnableMasternode { + if c.MasternodeAddr == (common.Address{}) { + return ErrMasternodeAddressRequired + } + if c.MasternodeKey == nil { + return ErrMasternodeKeyRequired + } + } + return nil +} + +// Error types +type ConfigError struct { + message string +} + +func (e *ConfigError) Error() string { + return e.message +} + +var ( + ErrMasternodeAddressRequired = &ConfigError{"masternode address required when masternode mode enabled"} + ErrMasternodeKeyRequired = &ConfigError{"masternode key required when masternode mode enabled"} +) + +// XDCNodeConfig extends the base node config with XDC options +type XDCNodeConfig struct { + // Embed base Config + Config + + // XDC specific options + XDC *XDCConfig +} + +// SetXDCDefaults sets default values for XDC configuration +func (c *XDCNodeConfig) SetXDCDefaults() { + if c.XDC == nil { + c.XDC = DefaultXDCConfig() + } +} + +// EnsureXDCDirectories creates necessary XDC directories +func (c *XDCNodeConfig) EnsureXDCDirectories() error { + if c.XDC == nil { + return nil + } + + baseDir := c.DataDir + + // Create XDCx directory if enabled + if c.XDC.EnableXDCx { + xdcxDir := c.XDC.ResolveXDCxDataDir(baseDir) + if err := os.MkdirAll(xdcxDir, 0755); err != nil { + return err + } + log.Info("XDCx data directory", "path", xdcxDir) + } + + // Create lending directory if enabled + if c.XDC.EnableLending { + lendingDir := c.XDC.ResolveLendingDataDir(baseDir) + if err := os.MkdirAll(lendingDir, 0755); err != nil { + return err + } + log.Info("Lending data directory", "path", lendingDir) + } + + return nil +} + +// XDCStartupInfo logs XDC-specific startup information +func (c *XDCNodeConfig) XDCStartupInfo() { + if c.XDC == nil { + return + } + + log.Info("XDC Network Configuration", + "masternode", c.XDC.EnableMasternode, + "xdcx", c.XDC.EnableXDCx, + "lending", c.XDC.EnableLending, + ) + + if c.XDC.EnableMasternode { + log.Info("Masternode Configuration", + "address", c.XDC.MasternodeAddr.Hex(), + "announce", c.XDC.Announce, + ) + } +} + +// ApplyXDCFlags applies XDC-specific command line flags +func ApplyXDCFlags(cfg *XDCConfig, ctx interface{}) { + // This would be integrated with the command line flag parsing + // to apply flags like --masternode, --xdcx, etc. +} + +// XDCChainType represents the type of XDC chain +type XDCChainType string + +const ( + XDCMainnet XDCChainType = "mainnet" + XDCApothem XDCChainType = "apothem" // Testnet + XDCDevnet XDCChainType = "devnet" + XDCPrivate XDCChainType = "private" +) + +// GetXDCChainType returns the chain type based on network ID +func GetXDCChainType(networkID uint64) XDCChainType { + switch networkID { + case 50: + return XDCMainnet + case 51: + return XDCApothem + case 551: + return XDCDevnet + default: + return XDCPrivate + } +} + +// XDCNetworkInfo returns network information for the chain type +func XDCNetworkInfo(chainType XDCChainType) (networkID uint64, chainID uint64) { + switch chainType { + case XDCMainnet: + return 50, 50 + case XDCApothem: + return 51, 51 + case XDCDevnet: + return 551, 551 + default: + return 0, 0 + } +} diff --git a/p2p/dial_xdc.go b/p2p/dial_xdc.go new file mode 100644 index 0000000000..b90962dd05 --- /dev/null +++ b/p2p/dial_xdc.go @@ -0,0 +1,262 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package p2p + +import ( + "context" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +// XDCDialScheduler extends the dial scheduler with masternode prioritization +type XDCDialScheduler struct { + mu sync.Mutex + masternodeNodes map[enode.ID]*enode.Node + priorityQueue []*enode.Node + dialer NodeDialer + maxDialing int + dialingCount int +} + +// NewXDCDialScheduler creates a new XDC dial scheduler +func NewXDCDialScheduler(dialer NodeDialer, maxDialing int) *XDCDialScheduler { + return &XDCDialScheduler{ + masternodeNodes: make(map[enode.ID]*enode.Node), + priorityQueue: make([]*enode.Node, 0), + dialer: dialer, + maxDialing: maxDialing, + } +} + +// SetMasternodes sets the current masternode list +func (s *XDCDialScheduler) SetMasternodes(nodes []*enode.Node) { + s.mu.Lock() + defer s.mu.Unlock() + + s.masternodeNodes = make(map[enode.ID]*enode.Node) + s.priorityQueue = make([]*enode.Node, 0, len(nodes)) + + for _, node := range nodes { + s.masternodeNodes[node.ID()] = node + s.priorityQueue = append(s.priorityQueue, node) + } + + log.Debug("Updated dial scheduler masternode list", "count", len(nodes)) +} + +// IsMasternode checks if a node ID is a masternode +func (s *XDCDialScheduler) IsMasternode(id enode.ID) bool { + s.mu.Lock() + defer s.mu.Unlock() + + _, exists := s.masternodeNodes[id] + return exists +} + +// GetPriorityNode returns the next priority node to dial +func (s *XDCDialScheduler) GetPriorityNode() *enode.Node { + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.priorityQueue) == 0 { + return nil + } + + node := s.priorityQueue[0] + s.priorityQueue = s.priorityQueue[1:] + return node +} + +// AddPriorityNode adds a node to the priority queue +func (s *XDCDialScheduler) AddPriorityNode(node *enode.Node) { + s.mu.Lock() + defer s.mu.Unlock() + + // Don't add duplicates + for _, n := range s.priorityQueue { + if n.ID() == node.ID() { + return + } + } + + s.priorityQueue = append(s.priorityQueue, node) +} + +// DialContext dials a node with context +func (s *XDCDialScheduler) DialContext(ctx context.Context, node *enode.Node) (Conn, error) { + s.mu.Lock() + if s.dialingCount >= s.maxDialing { + s.mu.Unlock() + return nil, errTooManyPeers + } + s.dialingCount++ + s.mu.Unlock() + + defer func() { + s.mu.Lock() + s.dialingCount-- + s.mu.Unlock() + }() + + return s.dialer.Dial(node) +} + +var errTooManyPeers = &DiscReason{DiscTooManyPeers} + +// MasternodeDialer dials masternodes with priority +type MasternodeDialer struct { + scheduler *XDCDialScheduler + interval time.Duration + quit chan struct{} +} + +// NewMasternodeDialer creates a new masternode dialer +func NewMasternodeDialer(scheduler *XDCDialScheduler, interval time.Duration) *MasternodeDialer { + return &MasternodeDialer{ + scheduler: scheduler, + interval: interval, + quit: make(chan struct{}), + } +} + +// Start starts the masternode dialing loop +func (d *MasternodeDialer) Start() { + go d.loop() +} + +// Stop stops the masternode dialer +func (d *MasternodeDialer) Stop() { + close(d.quit) +} + +func (d *MasternodeDialer) loop() { + ticker := time.NewTicker(d.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + d.dialNext() + case <-d.quit: + return + } + } +} + +func (d *MasternodeDialer) dialNext() { + node := d.scheduler.GetPriorityNode() + if node == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := d.scheduler.DialContext(ctx, node) + if err != nil { + log.Debug("Failed to dial masternode", "id", node.ID(), "err", err) + // Re-add to queue for retry + d.scheduler.AddPriorityNode(node) + } +} + +// PeerPriority defines peer priority levels +type PeerPriority int + +const ( + PriorityNormal PeerPriority = iota + PriorityMasternode + PriorityValidator +) + +// XDCPeerSelector selects peers based on priority +type XDCPeerSelector struct { + mu sync.RWMutex + peers map[enode.ID]PeerPriority + maxPeers int +} + +// NewXDCPeerSelector creates a new peer selector +func NewXDCPeerSelector(maxPeers int) *XDCPeerSelector { + return &XDCPeerSelector{ + peers: make(map[enode.ID]PeerPriority), + maxPeers: maxPeers, + } +} + +// SetPriority sets the priority for a peer +func (s *XDCPeerSelector) SetPriority(id enode.ID, priority PeerPriority) { + s.mu.Lock() + defer s.mu.Unlock() + s.peers[id] = priority +} + +// GetPriority gets the priority for a peer +func (s *XDCPeerSelector) GetPriority(id enode.ID) PeerPriority { + s.mu.RLock() + defer s.mu.RUnlock() + return s.peers[id] +} + +// ShouldConnect determines if we should connect to a peer +func (s *XDCPeerSelector) ShouldConnect(id enode.ID, priority PeerPriority) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + // Always accept masternodes and validators + if priority >= PriorityMasternode { + return true + } + + // Count by priority + normalCount := 0 + for _, p := range s.peers { + if p == PriorityNormal { + normalCount++ + } + } + + // Reserve slots for high priority peers + reservedSlots := s.maxPeers / 3 + maxNormal := s.maxPeers - reservedSlots + + return normalCount < maxNormal +} + +// RemovePeer removes a peer from the selector +func (s *XDCPeerSelector) RemovePeer(id enode.ID) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.peers, id) +} + +// GetMasternodePeers returns all masternode peers +func (s *XDCPeerSelector) GetMasternodePeers() []enode.ID { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []enode.ID + for id, priority := range s.peers { + if priority >= PriorityMasternode { + result = append(result, id) + } + } + return result +} + +// MasternodeAddressToNodeID converts a masternode address to enode ID +// This is a placeholder - actual implementation would use the masternode registry +func MasternodeAddressToNodeID(addr common.Address) enode.ID { + // In practice, this would look up the enode from the masternode registry + return enode.ID{} +} diff --git a/p2p/server_xdc.go b/p2p/server_xdc.go new file mode 100644 index 0000000000..c5ee2aeeb6 --- /dev/null +++ b/p2p/server_xdc.go @@ -0,0 +1,221 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. +// +// The XDC Network library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package p2p + +import ( + "errors" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +var ( + // ErrAddPairPeer is returned when adding a paired peer (not an error, signals pairing) + ErrAddPairPeer = errors.New("add pair peer") + + // ErrMasternodeNotFound is returned when masternode is not in the list + ErrMasternodeNotFound = errors.New("masternode not found") +) + +// MasternodeConfig contains XDPoS masternode configuration +type MasternodeConfig struct { + // Enable masternode mode + Enabled bool + + // Masternode account address + Address common.Address + + // Priority dial for masternodes + PriorityDial bool + + // Max masternode peers + MaxMasternodePeers int +} + +// MasternodeManager manages masternode peer connections +type MasternodeManager struct { + mu sync.RWMutex + masternodes map[common.Address]*enode.Node + active map[common.Address]bool + config *MasternodeConfig +} + +// NewMasternodeManager creates a new masternode manager +func NewMasternodeManager(config *MasternodeConfig) *MasternodeManager { + if config == nil { + config = &MasternodeConfig{ + MaxMasternodePeers: 50, + } + } + return &MasternodeManager{ + masternodes: make(map[common.Address]*enode.Node), + active: make(map[common.Address]bool), + config: config, + } +} + +// UpdateMasternodes updates the masternode list +func (m *MasternodeManager) UpdateMasternodes(nodes map[common.Address]*enode.Node) { + m.mu.Lock() + defer m.mu.Unlock() + + // Clear old list + m.masternodes = make(map[common.Address]*enode.Node) + + // Add new masternodes + for addr, node := range nodes { + m.masternodes[addr] = node + } + + log.Info("Updated masternode list", "count", len(m.masternodes)) +} + +// GetMasternodes returns the current masternode list +func (m *MasternodeManager) GetMasternodes() map[common.Address]*enode.Node { + m.mu.RLock() + defer m.mu.RUnlock() + + result := make(map[common.Address]*enode.Node) + for addr, node := range m.masternodes { + result[addr] = node + } + return result +} + +// IsMasternode checks if an address is a masternode +func (m *MasternodeManager) IsMasternode(addr common.Address) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + _, exists := m.masternodes[addr] + return exists +} + +// SetActive marks a masternode as active/inactive +func (m *MasternodeManager) SetActive(addr common.Address, active bool) { + m.mu.Lock() + defer m.mu.Unlock() + + m.active[addr] = active +} + +// IsActive checks if a masternode is active +func (m *MasternodeManager) IsActive(addr common.Address) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.active[addr] +} + +// ActiveCount returns the count of active masternodes +func (m *MasternodeManager) ActiveCount() int { + m.mu.RLock() + defer m.mu.RUnlock() + + count := 0 + for _, active := range m.active { + if active { + count++ + } + } + return count +} + +// GetMasternodeNode gets the enode for a masternode address +func (m *MasternodeManager) GetMasternodeNode(addr common.Address) *enode.Node { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.masternodes[addr] +} + +// GetMasternodeAddresses returns all masternode addresses +func (m *MasternodeManager) GetMasternodeAddresses() []common.Address { + m.mu.RLock() + defer m.mu.RUnlock() + + addrs := make([]common.Address, 0, len(m.masternodes)) + for addr := range m.masternodes { + addrs = append(addrs, addr) + } + return addrs +} + +// PrioritizeMasternodes returns nodes that should be prioritized for connection +func (m *MasternodeManager) PrioritizeMasternodes() []*enode.Node { + m.mu.RLock() + defer m.mu.RUnlock() + + nodes := make([]*enode.Node, 0) + for addr, node := range m.masternodes { + if !m.active[addr] && node != nil { + nodes = append(nodes, node) + } + } + return nodes +} + +// XDCServerConfig extends server config with XDPoS options +type XDCServerConfig struct { + // Base server config + Config + + // Masternode configuration + Masternode *MasternodeConfig + + // Whether to accept non-masternode peers + AcceptNonMasternode bool +} + +// XDCDialer implements a dialer that prioritizes masternodes +type XDCDialer struct { + manager *MasternodeManager + dialer NodeDialer +} + +// NewXDCDialer creates a new XDC dialer +func NewXDCDialer(manager *MasternodeManager, dialer NodeDialer) *XDCDialer { + return &XDCDialer{ + manager: manager, + dialer: dialer, + } +} + +// Dial attempts to dial a node, prioritizing masternodes +func (d *XDCDialer) Dial(dest *enode.Node) (Conn, error) { + // TODO: Implement priority dialing for masternodes + return d.dialer.Dial(dest) +} + +// PeerHook is called when a peer connects or disconnects +type PeerHook func(peer *Peer, added bool) + +// XDCPeerHooks contains hooks for XDPoS peer events +type XDCPeerHooks struct { + OnConnect PeerHook + OnDisconnect PeerHook +} + +// MasternodePeerInfo contains masternode-specific peer info +type MasternodePeerInfo struct { + Address common.Address `json:"address"` + IsMaster bool `json:"isMaster"` + Epoch uint64 `json:"epoch"` + IsValidator bool `json:"isValidator"` +} + +// GetMasternodePeerInfo extracts masternode info from a peer +func GetMasternodePeerInfo(peer *Peer) *MasternodePeerInfo { + // This would be extracted from peer handshake data + return &MasternodePeerInfo{ + IsMaster: false, + } +} diff --git a/scripts/xdc_sync_test.sh b/scripts/xdc_sync_test.sh new file mode 100755 index 0000000000..3706790632 --- /dev/null +++ b/scripts/xdc_sync_test.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# XDC Network Sync Test +# Tests synchronization with XDC mainnet/testnet + +set -e + +# Configuration +DATA_DIR="${DATA_DIR:-/tmp/xdc-sync-test}" +NETWORK="${NETWORK:-apothem}" # mainnet, apothem, or devnet +SYNC_MODE="${SYNC_MODE:-full}" # full, fast, or snap +LOG_LEVEL="${LOG_LEVEL:-3}" +SYNC_DURATION="${SYNC_DURATION:-300}" # seconds to sync + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}XDC Network Sync Test${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" + +# Determine network ID and bootnodes +case $NETWORK in + mainnet) + NETWORK_ID=50 + GENESIS="genesis/xdc_mainnet.json" + echo -e "${YELLOW}Testing mainnet sync (NetworkID: 50)${NC}" + ;; + apothem) + NETWORK_ID=51 + GENESIS="genesis/xdc_apothem.json" + echo -e "${YELLOW}Testing apothem testnet sync (NetworkID: 51)${NC}" + ;; + devnet) + NETWORK_ID=551 + GENESIS="genesis/devnet.json" + echo -e "${YELLOW}Testing devnet sync (NetworkID: 551)${NC}" + ;; + *) + echo -e "${RED}Unknown network: $NETWORK${NC}" + exit 1 + ;; +esac + +# Clean data directory +echo "Cleaning data directory: $DATA_DIR" +rm -rf "$DATA_DIR" +mkdir -p "$DATA_DIR" + +# Find XDC binary +XDC_BIN="./build/bin/XDC" +if [ ! -f "$XDC_BIN" ]; then + echo "Building XDC..." + make XDC +fi + +echo -e "${YELLOW}Starting sync test...${NC}" +echo " Network: $NETWORK" +echo " Sync Mode: $SYNC_MODE" +echo " Duration: ${SYNC_DURATION}s" +echo " Data Dir: $DATA_DIR" +echo "" + +# Initialize genesis if exists +if [ -f "$GENESIS" ]; then + echo "Initializing genesis..." + $XDC_BIN init --datadir "$DATA_DIR" "$GENESIS" +fi + +# Start XDC node +echo "Starting XDC node..." +$XDC_BIN \ + --datadir "$DATA_DIR" \ + --networkid $NETWORK_ID \ + --syncmode "$SYNC_MODE" \ + --verbosity $LOG_LEVEL \ + --maxpeers 50 \ + --cache 1024 \ + --http \ + --http.addr "127.0.0.1" \ + --http.port 8545 \ + --http.api "eth,net,web3,xdc" \ + 2>&1 | tee "$DATA_DIR/sync.log" & + +XDC_PID=$! + +# Wait for RPC to be ready +echo "Waiting for RPC..." +sleep 10 + +# Monitor sync progress +echo -e "${YELLOW}Monitoring sync progress...${NC}" +START_TIME=$(date +%s) +LAST_BLOCK=0 + +while true; do + CURRENT_TIME=$(date +%s) + ELAPSED=$((CURRENT_TIME - START_TIME)) + + if [ $ELAPSED -ge $SYNC_DURATION ]; then + echo "" + echo "Sync duration reached" + break + fi + + # Get current block + RESULT=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + http://127.0.0.1:8545 2>/dev/null || echo '{"result":"0x0"}') + + BLOCK_HEX=$(echo $RESULT | grep -o '"result":"[^"]*"' | cut -d'"' -f4) + + if [ -n "$BLOCK_HEX" ]; then + BLOCK=$((16#${BLOCK_HEX#0x})) + BLOCKS_PER_SEC=$(( (BLOCK - LAST_BLOCK) )) + LAST_BLOCK=$BLOCK + + echo -ne "\rBlock: $BLOCK | Elapsed: ${ELAPSED}s | Speed: ~${BLOCKS_PER_SEC} blocks/sec " + fi + + sleep 1 +done + +# Check sync status +echo "" +echo -e "${YELLOW}Checking sync status...${NC}" + +SYNC_RESULT=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \ + http://127.0.0.1:8545 2>/dev/null) + +echo "Sync status: $SYNC_RESULT" + +# Get peer count +PEERS_RESULT=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' \ + http://127.0.0.1:8545 2>/dev/null) + +PEERS_HEX=$(echo $PEERS_RESULT | grep -o '"result":"[^"]*"' | cut -d'"' -f4) +PEERS=$((16#${PEERS_HEX#0x})) +echo "Connected peers: $PEERS" + +# Stop node +echo "" +echo "Stopping XDC node..." +kill $XDC_PID 2>/dev/null || true +wait $XDC_PID 2>/dev/null || true + +# Check final block count +FINAL_BLOCK=$LAST_BLOCK + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Sync Test Results${NC}" +echo -e "${GREEN}========================================${NC}" +echo " Network: $NETWORK" +echo " Final Block: $FINAL_BLOCK" +echo " Duration: ${SYNC_DURATION}s" +echo " Avg Speed: $(( FINAL_BLOCK / SYNC_DURATION )) blocks/sec" +echo " Peers: $PEERS" + +if [ $FINAL_BLOCK -gt 0 ] && [ $PEERS -gt 0 ]; then + echo -e "${GREEN}✓ Sync test PASSED${NC}" + exit 0 +else + echo -e "${RED}✗ Sync test FAILED${NC}" + exit 1 +fi diff --git a/scripts/xdc_test.sh b/scripts/xdc_test.sh new file mode 100755 index 0000000000..0144d63563 --- /dev/null +++ b/scripts/xdc_test.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# XDC Network Test Suite +# This script runs various tests for the XDC Network + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$ROOT_DIR" + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}XDC Network Test Suite${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" + +# Parse arguments +VERBOSE="" +RACE="" +COVERAGE="" +BENCHMARK="" + +while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + VERBOSE="-v" + shift + ;; + -r|--race) + RACE="-race" + shift + ;; + -c|--coverage) + COVERAGE="-coverprofile=coverage.out" + shift + ;; + -b|--benchmark) + BENCHMARK="true" + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Function to run tests +run_tests() { + local package=$1 + local name=$2 + + echo -e "${YELLOW}Running $name tests...${NC}" + if go test $VERBOSE $RACE $COVERAGE "$package"; then + echo -e "${GREEN}✓ $name tests passed${NC}" + else + echo -e "${RED}✗ $name tests failed${NC}" + exit 1 + fi + echo "" +} + +# Core tests +echo -e "${YELLOW}=== Core Tests ===${NC}" +run_tests "./core/..." "Core" + +# Consensus tests +echo -e "${YELLOW}=== Consensus Tests ===${NC}" +run_tests "./consensus/XDPoS/..." "XDPoS Consensus" + +# eth package tests +echo -e "${YELLOW}=== Eth Package Tests ===${NC}" +run_tests "./eth/..." "Eth" + +# p2p tests +echo -e "${YELLOW}=== P2P Tests ===${NC}" +run_tests "./p2p/..." "P2P" + +# API tests +echo -e "${YELLOW}=== API Tests ===${NC}" +run_tests "./internal/ethapi/..." "API" + +# Contract tests +echo -e "${YELLOW}=== Contract Tests ===${NC}" +run_tests "./contracts/..." "Contracts" + +# Run benchmarks if requested +if [ "$BENCHMARK" = "true" ]; then + echo -e "${YELLOW}=== Running Benchmarks ===${NC}" + + echo "XDPoS Consensus Benchmarks:" + go test -bench=. -benchmem ./consensus/XDPoS/... 2>/dev/null || true + + echo "" + echo "Core Benchmarks:" + go test -bench=. -benchmem ./core/... 2>/dev/null || true + + echo "" +fi + +# Coverage report +if [ -n "$COVERAGE" ]; then + echo -e "${YELLOW}=== Coverage Report ===${NC}" + go tool cover -func=coverage.out | tail -n 1 + echo "" + + # Generate HTML report + go tool cover -html=coverage.out -o coverage.html + echo -e "${GREEN}Coverage report generated: coverage.html${NC}" +fi + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}All tests passed!${NC}" +echo -e "${GREEN}========================================${NC}" diff --git a/tests/xdpos_test.go b/tests/xdpos_test.go new file mode 100644 index 0000000000..ea16a8c703 --- /dev/null +++ b/tests/xdpos_test.go @@ -0,0 +1,265 @@ +// Copyright 2023 The XDC Network Authors +// This file is part of the XDC Network library. + +//go:build integration +// +build integration + +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// TestXDPoSBlockProduction tests XDPoS block production +func TestXDPoSBlockProduction(t *testing.T) { + // This is an integration test that would require a full XDPoS setup + t.Skip("Integration test - requires full setup") + + // Would test: + // 1. Block is produced every 2 seconds + // 2. Block is signed by correct masternode + // 3. Block difficulty is 1 +} + +// TestXDPoSEpochSwitch tests epoch switching +func TestXDPoSEpochSwitch(t *testing.T) { + config := ¶ms.XDPoSConfig{ + Epoch: 900, + Gap: 50, + MaxMasternodes: 150, + } + + tests := []struct { + blockNumber uint64 + isEpochSwitch bool + isGapBlock bool + }{ + {0, true, false}, + {849, false, false}, + {850, false, true}, + {899, false, false}, + {900, true, false}, + {1750, false, true}, + {1800, true, false}, + } + + for _, tt := range tests { + isSwitch := tt.blockNumber % config.Epoch == 0 + isGap := tt.blockNumber % config.Epoch == config.Epoch - config.Gap + + if isSwitch != tt.isEpochSwitch { + t.Errorf("Block %d: expected isEpochSwitch=%v, got %v", tt.blockNumber, tt.isEpochSwitch, isSwitch) + } + if isGap != tt.isGapBlock { + t.Errorf("Block %d: expected isGapBlock=%v, got %v", tt.blockNumber, tt.isGapBlock, isGap) + } + } +} + +// TestXDPoSVoting tests the voting mechanism +func TestXDPoSVoting(t *testing.T) { + // Generate test keys + key, _ := crypto.GenerateKey() + signer := crypto.PubkeyToAddress(key.PublicKey) + + // Create a vote + vote := &types.Vote{ + ProposedBlockInfo: &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + }, + } + + // Sign the vote + hash := vote.Hash() + sig, err := crypto.Sign(hash.Bytes(), key) + if err != nil { + t.Fatalf("Failed to sign vote: %v", err) + } + vote.Signature = sig + + // Verify signature + pubkey, err := crypto.SigToPub(hash.Bytes(), sig) + if err != nil { + t.Fatalf("Failed to recover pubkey: %v", err) + } + + recovered := crypto.PubkeyToAddress(*pubkey) + if recovered != signer { + t.Errorf("Signer mismatch: expected %s, got %s", signer.Hex(), recovered.Hex()) + } +} + +// TestXDPoSTimeout tests the timeout mechanism +func TestXDPoSTimeout(t *testing.T) { + key, _ := crypto.GenerateKey() + + timeout := &types.Timeout{ + Round: 5, + GapNumber: 850, + } + + hash := timeout.Hash() + sig, err := crypto.Sign(hash.Bytes(), key) + if err != nil { + t.Fatalf("Failed to sign timeout: %v", err) + } + timeout.Signature = sig + + // Verify hash is consistent + hash2 := timeout.Hash() + if hash != hash2 { + t.Error("Timeout hash not consistent") + } +} + +// TestXDPoSQC tests quorum certificate creation +func TestXDPoSQC(t *testing.T) { + // Generate multiple signers + numSigners := 100 + keys := make([]*ecdsa.PrivateKey, numSigners) + signatures := make([]types.Signature, numSigners) + + blockInfo := &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + } + + vote := &types.Vote{ProposedBlockInfo: blockInfo} + hash := vote.Hash() + + for i := 0; i < numSigners; i++ { + keys[i], _ = crypto.GenerateKey() + sig, _ := crypto.Sign(hash.Bytes(), keys[i]) + signatures[i] = types.Signature{ + Signer: crypto.PubkeyToAddress(keys[i].PublicKey), + Signature: sig, + } + } + + qc := &types.QuorumCert{ + ProposedBlockInfo: blockInfo, + Signatures: signatures, + Round: 1, + } + + // Verify QC has enough signatures (2/3 + 1 of 150 = 101) + // But we only have 100 signers, so this would fail in real scenario + threshold := 101 + if len(qc.Signatures) >= threshold { + t.Log("QC has sufficient signatures") + } else { + t.Logf("QC has %d signatures, need %d", len(qc.Signatures), threshold) + } +} + +// TestXDPoSReward tests reward calculation +func TestXDPoSReward(t *testing.T) { + baseReward := new(big.Int).Mul(big.NewInt(250), big.NewInt(1e18)) + + // Verify reward is 250 XDC + expected := "250000000000000000000" + if baseReward.String() != expected { + t.Errorf("Expected reward %s, got %s", expected, baseReward.String()) + } + + // Test reward distribution + voterPercent := 40 + voterPortion := new(big.Int).Mul(baseReward, big.NewInt(int64(voterPercent))) + voterPortion.Div(voterPortion, big.NewInt(100)) + + signerPortion := new(big.Int).Sub(baseReward, voterPortion) + + // Verify split + total := new(big.Int).Add(signerPortion, voterPortion) + if total.Cmp(baseReward) != 0 { + t.Error("Reward split doesn't add up") + } +} + +// TestXDPoSPenalty tests penalty calculation +func TestXDPoSPenalty(t *testing.T) { + // Test scenarios where penalties apply + + // Missed blocks threshold + missedThreshold := 5 + missedCount := 3 + shouldPenalize := missedCount >= missedThreshold + + if shouldPenalize { + t.Log("Validator should be penalized") + } else { + t.Logf("Validator missed %d/%d blocks, no penalty", missedCount, missedThreshold) + } +} + +// TestXDPoSMasternodeSelection tests masternode selection +func TestXDPoSMasternodeSelection(t *testing.T) { + // Create mock candidates with stakes + type candidate struct { + addr common.Address + stake *big.Int + } + + candidates := []candidate{ + {common.HexToAddress("0x1"), big.NewInt(10000000)}, + {common.HexToAddress("0x2"), big.NewInt(20000000)}, + {common.HexToAddress("0x3"), big.NewInt(15000000)}, + {common.HexToAddress("0x4"), big.NewInt(5000000)}, + } + + // Sort by stake (descending) + // In real implementation, top 150 would be selected + + // Verify sorting + for i := 0; i < len(candidates)-1; i++ { + if candidates[i].stake.Cmp(candidates[i+1].stake) < 0 { + // Not sorted correctly - would need sort + } + } +} + +// Import required types +import ( + "crypto/ecdsa" +) + +// BenchmarkVoteHashing benchmarks vote hashing +func BenchmarkVoteHashing(b *testing.B) { + vote := &types.Vote{ + ProposedBlockInfo: &types.BlockInfo{ + Hash: common.HexToHash("0x123"), + Number: big.NewInt(100), + Round: 1, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = vote.Hash() + } +} + +// BenchmarkSignatureRecovery benchmarks signature recovery +func BenchmarkSignatureRecovery(b *testing.B) { + key, _ := crypto.GenerateKey() + message := []byte("test message") + hash := crypto.Keccak256Hash(message) + sig, _ := crypto.Sign(hash.Bytes(), key) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = crypto.SigToPub(hash.Bytes(), sig) + } +}