Skip to content

Commit

Permalink
Extend cluster type for testing to allow for adding funded hosts to t…
Browse files Browse the repository at this point in the history
…he cluster
  • Loading branch information
ChrisSchinnerl authored and lukechampine committed Dec 7, 2022
1 parent ab0488d commit c5f3edc
Show file tree
Hide file tree
Showing 11 changed files with 550 additions and 18 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,17 @@ jobs:
- name: Test Windows 1.18 # can't run race detector on windows with go 1.18 or lower due to a bug (https://github.com/golang/go/issues/46099)
if: matrix.os == 'windows-latest' && matrix.go-version == '1.18'
uses: n8maninger/action-golang-test@v1
with:
args: "-short"
- name: Test
if: matrix.os != 'windows-latest' || matrix.go-version != '1.18'
uses: n8maninger/action-golang-test@v1
with:
args: "-race;-short"
- name: Test Long Windows 1.18 # can't run race detector on windows with go 1.18 or lower due to a bug (https://github.com/golang/go/issues/46099)
if: matrix.os == 'windows-latest' && matrix.go-version == '1.18'
uses: n8maninger/action-golang-test@v1
- name: Test Long
if: matrix.os != 'windows-latest' || matrix.go-version != '1.18'
uses: n8maninger/action-golang-test@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
54 changes: 49 additions & 5 deletions bus/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type (

// A Syncer can connect to other peers and synchronize the blockchain.
Syncer interface {
Addr() string
SyncerAddress() (string, error)
Peers() []string
Connect(addr string) error
BroadcastTransaction(txn types.Transaction, dependsOn []types.Transaction)
Expand Down Expand Up @@ -86,11 +86,17 @@ type (
SlabsForMigration(n int, failureCutoff time.Time, goodContracts []types.FileContractID) ([]SlabID, error)
SlabForMigration(slabID SlabID) (object.Slab, []MigrationContract, error)
}

// A Miner mines blocks and submits them to the network.
Miner interface {
Mine(addr types.UnlockHash, n int) error
}
)

type bus struct {
s Syncer
cm ChainManager
m Miner
tp TransactionPool
w Wallet
hdb HostDB
Expand All @@ -99,6 +105,14 @@ type bus struct {
os ObjectStore
}

func (b *bus) syncerAddrHandler(jc jape.Context) {
addr, err := b.s.SyncerAddress()
if jc.Check("failed to fetch syncer's address", err) != nil {
return
}
jc.Encode(addr)
}

func (b *bus) syncerPeersHandler(jc jape.Context) {
jc.Encode(b.s.Peers())
}
Expand All @@ -117,6 +131,11 @@ func (b *bus) consensusStateHandler(jc jape.Context) {
})
}

func (b *bus) txpoolFeeHandler(jc jape.Context) {
fee := b.tp.RecommendedFee()
jc.Encode(fee)
}

func (b *bus) txpoolTransactionsHandler(jc jape.Context) {
jc.Encode(b.tp.Transactions())
}
Expand Down Expand Up @@ -556,11 +575,32 @@ func (b *bus) objectsMarkSlabMigrationFailureHandlerPOST(jc jape.Context) {
}
}

// New returns an HTTP handler that serves the bus API.
func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs ContractStore, css ContractSetStore, os ObjectStore) http.Handler {
// TODO: ideally we can get rid of this handler again once we have a way to
// subscribe to consensus through the API and submit blocks through the API.
func (b *bus) minerMineHandlerPOST(jc jape.Context) {
if b.m == nil {
jc.ResponseWriter.WriteHeader(http.StatusNotFound) // miner not enabled
return
}
var uh types.UnlockHash
if jc.DecodeParam("unlockhash", &uh) != nil {
return
}
var n int
if jc.DecodeForm("numBlocks", &n) != nil {
return
}
if jc.Check("failed to mine blocks", b.m.Mine(uh, n)) != nil {
return
}
}

// New returns a new Bus.
func New(s Syncer, cm ChainManager, m Miner, tp TransactionPool, w Wallet, hdb HostDB, cs ContractStore, css ContractSetStore, os ObjectStore) http.Handler {
b := &bus{
s: s,
cm: cm,
m: m,
tp: tp,
w: w,
hdb: hdb,
Expand All @@ -569,13 +609,15 @@ func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs
os: os,
}
return jape.Mux(map[string]jape.Handler{
"GET /syncer/address": b.syncerAddrHandler,
"GET /syncer/peers": b.syncerPeersHandler,
"POST /syncer/connect": b.syncerConnectHandler,

"GET /consensus/state": b.consensusStateHandler,

"GET /txpool/transactions": b.txpoolTransactionsHandler,
"POST /txpool/broadcast": b.txpoolBroadcastHandler,
"GET /txpool/recommendedfee": b.txpoolFeeHandler,
"GET /txpool/transactions": b.txpoolTransactionsHandler,
"POST /txpool/broadcast": b.txpoolBroadcastHandler,

"GET /wallet/balance": b.walletBalanceHandler,
"GET /wallet/address": b.walletAddressHandler,
Expand Down Expand Up @@ -612,5 +654,7 @@ func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs
"GET /migration/slabs": b.objectsMigrationSlabsHandlerGET,
"GET /migration/slab/:id": b.objectsMigrationSlabHandlerGET,
"POST /migration/failed": b.objectsMarkSlabMigrationFailureHandlerPOST,

"POST /mine/:unlockhash": b.minerMineHandlerPOST,
})
}
57 changes: 55 additions & 2 deletions bus/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ type Client struct {
c jape.Client
}

// SyncerAddress returns the address the syncer is listening on.
func (c *Client) SyncerAddress() (addr string, err error) {
err = c.c.GET("/syncer/address", &addr)
return
}

// SyncerPeers returns the current peers of the syncer.
func (c *Client) SyncerPeers() (resp []string, err error) {
err = c.c.GET("/syncer/peers", &resp)
Expand Down Expand Up @@ -67,6 +73,44 @@ func (c *Client) WalletOutputs() (resp []wallet.SiacoinElement, err error) {
return
}

// estimatedSiacoinTxnSize estimates the txn size of a siacoin txn without file
// contract given its number of outputs.
func estimatedSiacoinTxnSize(nOutputs uint64) uint64 {
return 1000 + 60*nOutputs
}

// SendSiacoins is a helper method that sends siacoins to the given outputs.
func (c *Client) SendSiacoins(scos []types.SiacoinOutput) (err error) {
fee, err := c.RecommendedFee()
if err != nil {
return err
}
fee = fee.Mul64(estimatedSiacoinTxnSize(uint64(len(scos))))

var value types.Currency
for _, sco := range scos {
value = value.Add(sco.Value)
}
txn := types.Transaction{
SiacoinOutputs: scos,
MinerFees: []types.Currency{fee},
}
toSign, parents, err := c.WalletFund(&txn, value)
if err != nil {
return err
}
defer func() {
if err != nil {
_ = c.WalletDiscard(txn)
}
}()
err = c.WalletSign(&txn, toSign, types.FullCoveredFields)
if err != nil {
return err
}
return c.BroadcastTransaction(append(parents, txn))
}

// WalletTransactions returns all transactions relevant to the wallet.
func (c *Client) WalletTransactions(since time.Time, max int) (resp []wallet.Transaction, err error) {
err = c.c.GET(fmt.Sprintf("/wallet/transactions?since=%s&max=%d", paramTime(since), max), &resp)
Expand Down Expand Up @@ -268,8 +312,11 @@ func (c *Client) ContractMetadata(types.FileContractID) (ContractMetadata, error
func (c *Client) UpdateContractMetadata(types.FileContractID, ContractMetadata) error {
panic("unimplemented")
}
func (c *Client) RecommendedFee() (types.Currency, error) {
panic("unimplemented")

// RecommendedFee returns the recommended fee for a txn.
func (c *Client) RecommendedFee() (fee types.Currency, err error) {
err = c.c.GET("/txpool/recommendedfee", &fee)
return
}

// ContractsForSlab returns contracts that can be used to download the provided
Expand Down Expand Up @@ -340,6 +387,12 @@ func (c *Client) UploadParams() (up UploadParams, err error) {
panic("unimplemented")
}

// MineBlocks updates the latest failure time of the given slabs
// to the current time.
func (c *Client) MineBlocks(uh types.UnlockHash, n int) error {
return c.c.POST(fmt.Sprintf("/mine/%v?numBlocks=%d", uh, n), nil, nil)
}

// NewClient returns a client that communicates with a renterd store server
// listening on the specified address.
func NewClient(addr, password string) *Client {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ require (
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/text v0.3.6 // indirect
)

replace go.sia.tech/siad => ./../siad
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ gitlab.com/NebulousLabs/writeaheadlog v0.0.0-20200618142844-c59a90f49130/go.mod
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.sia.tech/jape v0.5.0 h1:6BLMZEePInWwQcJO1mcmrPBpF0QuvkrXmHSWGKeR0tY=
go.sia.tech/jape v0.5.0/go.mod h1:bu+ka8FgKq7MNH2JRTTOjfvVNku97V4ffyKr0dKoU90=
go.sia.tech/siad v1.5.9 h1:uhaTYAkJQxXh0NEFRIvgD+9bR/1WmKTWQOPojNPdutA=
go.sia.tech/siad v1.5.9/go.mod h1:ifu7TjXlL9s+47DSmqeMz8LOvthALMysZkJ3Df0daAY=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
Expand Down
146 changes: 146 additions & 0 deletions internal/node/miner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// TODO: remove this file when we can import it from hostd
package node

import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"sync"

"gitlab.com/NebulousLabs/fastrand"
"go.sia.tech/siad/crypto"
"go.sia.tech/siad/modules"
"go.sia.tech/siad/types"
)

const solveAttempts = 1e4

type (
// Consensus defines a minimal interface needed by the miner to interact
// with the consensus set
Consensus interface {
AcceptBlock(types.Block) error
}

// A Miner is a CPU miner that can mine blocks, sending the reward to a
// specified address.
Miner struct {
consensus Consensus

mu sync.Mutex
height types.BlockHeight
target types.Target
currentBlockID types.BlockID
txnsets map[modules.TransactionSetID][]types.TransactionID
transactions []types.Transaction
}
)

var errFailedToSolve = errors.New("failed to solve block")

// ProcessConsensusChange implements modules.ConsensusSetSubscriber.
func (m *Miner) ProcessConsensusChange(cc modules.ConsensusChange) {
m.mu.Lock()
defer m.mu.Unlock()
m.target = cc.ChildTarget
m.currentBlockID = cc.AppliedBlocks[len(cc.AppliedBlocks)-1].ID()
m.height = cc.BlockHeight
}

// ReceiveUpdatedUnconfirmedTransactions implements modules.TransactionPoolSubscriber
func (m *Miner) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) {
m.mu.Lock()
defer m.mu.Unlock()

reverted := make(map[types.TransactionID]bool)
for _, setID := range diff.RevertedTransactions {
for _, txnID := range m.txnsets[setID] {
reverted[txnID] = true
}
}

filtered := m.transactions[:0]
for _, txn := range m.transactions {
if reverted[txn.ID()] {
continue
}
filtered = append(filtered, txn)
}

for _, txnset := range diff.AppliedTransactions {
m.txnsets[txnset.ID] = txnset.IDs
filtered = append(filtered, txnset.Transactions...)
}
m.transactions = filtered
}

// mineBlock attempts to mine a block and add it to the consensus set.
func (m *Miner) mineBlock(addr types.UnlockHash) error {
m.mu.Lock()
block := types.Block{
ParentID: m.currentBlockID,
Timestamp: types.CurrentTimestamp(),
}

randBytes := fastrand.Bytes(types.SpecifierLen)
randTxn := types.Transaction{
ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], randBytes...)},
}
block.Transactions = append([]types.Transaction{randTxn}, m.transactions...)
block.MinerPayouts = append(block.MinerPayouts, types.SiacoinOutput{
Value: block.CalculateSubsidy(m.height + 1),
UnlockHash: addr,
})
target := m.target
m.mu.Unlock()

merkleRoot := block.MerkleRoot()
header := make([]byte, 80)
copy(header, block.ParentID[:])
binary.LittleEndian.PutUint64(header[40:48], uint64(block.Timestamp))
copy(header[48:], merkleRoot[:])

var nonce uint64
var solved bool
for i := 0; i < solveAttempts; i++ {
id := crypto.HashBytes(header)
if bytes.Compare(target[:], id[:]) >= 0 {
block.Nonce = *(*types.BlockNonce)(header[32:40])
solved = true
break
}
binary.LittleEndian.PutUint64(header[32:], nonce)
nonce += types.ASICHardforkFactor
}
if !solved {
return errFailedToSolve
}

if err := m.consensus.AcceptBlock(block); err != nil {
return fmt.Errorf("failed to get block accepted: %w", err)
}
return nil
}

// Mine mines n blocks, sending the reward to addr
func (m *Miner) Mine(addr types.UnlockHash, n int) error {
var err error
for mined := 1; mined <= n; {
// return the error only if the miner failed to solve the block,
// ignore any consensus related errors
if err = m.mineBlock(addr); errors.Is(err, errFailedToSolve) {
return fmt.Errorf("failed to mine block %v: %w", mined, errFailedToSolve)
}
mined++
}
return nil
}

// NewMiner initializes a new CPU miner
func NewMiner(consensus Consensus) *Miner {
return &Miner{
consensus: consensus,
txnsets: make(map[modules.TransactionSetID][]types.TransactionID),
}
}
Loading

0 comments on commit c5f3edc

Please sign in to comment.