package main
import (
"flag"
"fmt"
"math/big"
"math/rand"
"os"
"path"
"sync"
"time"
"github.com/harmony-one/harmony/consensus"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/internal/ctxerror"
"github.com/harmony-one/harmony/internal/shardchain"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
bls2 "github.com/harmony-one/bls/ffi/go/bls"
"github.com/harmony-one/harmony/api/client"
proto_node "github.com/harmony-one/harmony/api/proto/node"
"github.com/harmony-one/harmony/common/denominations"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/harmony-one/harmony/internal/genesis"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/node"
"github.com/harmony-one/harmony/p2p"
p2p_host "github.com/harmony-one/harmony/p2p/host"
"github.com/harmony-one/harmony/p2p/p2pimpl"
)
var (
version string
builtBy string
builtAt string
commit string
stateMutex sync . Mutex
)
const (
checkFrequency = 2 //checkfrequency checks whether the transaction generator is ready to send the next batch of transactions.
)
// Settings is the settings for TX generation. No Cross-Shard Support!
type Settings struct {
NumOfAddress int
MaxNumTxsPerBatch int
}
func printVersion ( me string ) {
fmt . Fprintf ( os . Stderr , "Harmony (C) 2019. %v, version %v-%v (%v %v)\n" , path . Base ( me ) , version , commit , builtBy , builtAt )
os . Exit ( 0 )
}
// The main entrance for the transaction generator program which simulate transactions and send to the network for
// processing.
var (
ip = flag . String ( "ip" , "127.0.0.1" , "IP of the node" )
port = flag . String ( "port" , "9999" , "port of the node." )
numTxns = flag . Int ( "numTxns" , 100 , "number of transactions to send per message" )
logFolder = flag . String ( "log_folder" , "latest" , "the folder collecting the logs of this execution" )
duration = flag . Int ( "duration" , 30 , "duration of the tx generation in second. If it's negative, the experiment runs forever." )
versionFlag = flag . Bool ( "version" , false , "Output version info" )
crossShardRatio = flag . Int ( "cross_shard_ratio" , 30 , "The percentage of cross shard transactions." ) //Keeping this for backward compatibility
shardIDFlag = flag . Int ( "shardID" , 0 , "The shardID the node belongs to." )
// Key file to store the private key
keyFile = flag . String ( "key" , "./.txgenkey" , "the private key file of the txgen" )
// logging verbosity
verbosity = flag . Int ( "verbosity" , 5 , "Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail (default: 5)" )
)
func setUpTXGen ( ) * node . Node {
nodePriKey , _ , err := utils . LoadKeyFromFile ( * keyFile )
if err != nil {
panic ( err )
}
peerPubKey := bls . RandPrivateKey ( ) . GetPublicKey ( )
if peerPubKey == nil {
panic ( fmt . Errorf ( "generate key error" ) )
}
shardID := * shardIDFlag
selfPeer := p2p . Peer { IP : * ip , Port : * port , ConsensusPubKey : peerPubKey }
gsif , err := consensus . NewGenesisStakeInfoFinder ( )
// Nodes containing blockchain data to mirror the shards' data in the network
myhost , err := p2pimpl . NewHost ( & selfPeer , nodePriKey )
if err != nil {
panic ( "unable to new host in txgen" )
}
if err != nil {
fmt . Fprintf ( os . Stderr , "Error :%v \n" , err )
os . Exit ( 1 )
}
consensusObj , err := consensus . New ( myhost , uint32 ( shardID ) , p2p . Peer { } , nil )
chainDBFactory := & shardchain . MemDBFactory { }
txGen := node . New ( myhost , consensusObj , chainDBFactory , false ) //Changed it : no longer archival node.
txGen . Client = client . NewClient ( txGen . GetHost ( ) , uint32 ( shardID ) )
consensusObj . SetStakeInfoFinder ( gsif )
consensusObj . ChainReader = txGen . Blockchain ( )
consensusObj . PublicKeys = nil
genesisShardingConfig := core . ShardingSchedule . InstanceForEpoch ( big . NewInt ( core . GenesisEpoch ) )
startIdx := 0
endIdx := startIdx + genesisShardingConfig . NumNodesPerShard ( )
for _ , acct := range genesis . HarmonyAccounts [ startIdx : endIdx ] {
pub := & bls2 . PublicKey { }
if err := pub . DeserializeHexStr ( acct . BlsPublicKey ) ; err != nil {
fmt . Printf ( "Can not deserialize public key. err: %v" , err )
os . Exit ( 1 )
}
consensusObj . PublicKeys = append ( consensusObj . PublicKeys , pub )
}
txGen . NodeConfig . SetRole ( nodeconfig . ClientNode )
if shardID == 0 {
txGen . NodeConfig . SetIsBeacon ( true )
txGen . NodeConfig . SetShardGroupID ( p2p . GroupIDBeacon )
} else {
txGen . NodeConfig . SetShardGroupID ( p2p . NewGroupIDByShardID ( p2p . ShardID ( shardID ) ) )
}
txGen . NodeConfig . SetIsClient ( true )
return txGen
}
func main ( ) {
flag . Var ( & utils . BootNodes , "bootnodes" , "a list of bootnode multiaddress" )
flag . Parse ( )
if * versionFlag {
printVersion ( os . Args [ 0 ] )
}
// Logging setup
utils . SetLogContext ( * port , * ip )
utils . SetLogVerbosity ( log . Lvl ( * verbosity ) )
if len ( utils . BootNodes ) == 0 {
bootNodeAddrs , err := utils . StringsToAddrs ( utils . DefaultBootNodeAddrStrings )
if err != nil {
panic ( err )
}
utils . BootNodes = bootNodeAddrs
}
// Init with LibP2P enabled, FIXME: (leochen) right now we support only one shard
setting := Settings {
NumOfAddress : 10000 ,
MaxNumTxsPerBatch : * numTxns ,
}
shardID := * shardIDFlag
utils . GetLogInstance ( ) . Debug ( "Cross Shard Ratio Is Set But not used" , "cx ratio" , * crossShardRatio )
// TODO(Richard): refactor this chuck to a single method
// Setup a logger to stdout and log file.
logFileName := fmt . Sprintf ( "./%v/txgen.log" , * logFolder )
h := log . MultiHandler (
log . StreamHandler ( os . Stdout , log . TerminalFormat ( false ) ) ,
log . Must . FileHandler ( logFileName , log . LogfmtFormat ( ) ) , // Log to file
)
log . Root ( ) . SetHandler ( h )
txGen := setUpTXGen ( )
txGen . ServiceManagerSetup ( )
txGen . RunServices ( )
start := time . Now ( )
totalTime := float64 ( * duration )
utils . GetLogInstance ( ) . Debug ( "Total Duration" , "totalTime" , totalTime , "RunForever" , isDurationForever ( totalTime ) )
ticker := time . NewTicker ( checkFrequency * time . Second )
txGen . DoSyncWithoutConsensus ( )
syncLoop :
for {
t := time . Now ( )
if totalTime > 0 && t . Sub ( start ) . Seconds ( ) >= totalTime {
utils . GetLogInstance ( ) . Debug ( "Generator timer ended in syncLoop." , "duration" , ( int ( t . Sub ( start ) ) ) , "startTime" , start , "totalTime" , totalTime )
break syncLoop
}
select {
case <- ticker . C :
if txGen . State . String ( ) == "NodeReadyForConsensus" {
utils . GetLogInstance ( ) . Debug ( "Generator is now in Sync." , "txgen node" , txGen . SelfPeer , "Node State" , txGen . State . String ( ) )
ticker . Stop ( )
break syncLoop
}
}
}
readySignal := make ( chan uint32 )
// This func is used to update the client's blockchain when new blocks are received from the leaders
updateBlocksFunc := func ( blocks [ ] * types . Block ) {
utils . GetLogInstance ( ) . Info ( "[Txgen] Received new block" , "block num" , blocks [ 0 ] . NumberU64 ( ) )
for _ , block := range blocks {
shardID := block . ShardID ( )
if txGen . Consensus . ShardID == shardID {
utils . GetLogInstance ( ) . Info ( "Got block from leader" , "txNum" , len ( block . Transactions ( ) ) , "shardID" , shardID , "preHash" , block . ParentHash ( ) . Hex ( ) , "currentBlock" , txGen . Blockchain ( ) . CurrentBlock ( ) . NumberU64 ( ) , "incoming block" , block . NumberU64 ( ) )
if block . NumberU64 ( ) - txGen . Blockchain ( ) . CurrentBlock ( ) . NumberU64 ( ) == 1 {
if err := txGen . AddNewBlock ( block ) ; err != nil {
utils . GetLogInstance ( ) . Error ( "Error when adding new block" , "error" , err )
}
stateMutex . Lock ( )
if err := txGen . Worker . UpdateCurrent ( block . Coinbase ( ) ) ; err != nil {
ctxerror . Warn ( utils . GetLogger ( ) , err ,
"(*Worker).UpdateCurrent failed" )
}
stateMutex . Unlock ( )
readySignal <- shardID
}
} else {
continue
}
}
}
txGen . Client . UpdateBlocks = updateBlocksFunc
// Start the client server to listen to leader's message
go func ( ) {
// wait for 3 seconds for client to send ping message to leader
// FIXME (leo) the readySignal should be set once we really sent ping message to leader
time . Sleep ( 1 * time . Second ) // wait for nodes to be ready
readySignal <- uint32 ( shardID )
} ( )
pushLoop :
for {
t := time . Now ( )
utils . GetLogInstance ( ) . Debug ( "Current running time" , "running time" , t . Sub ( start ) . Seconds ( ) , "totaltime" , totalTime )
if ! isDurationForever ( totalTime ) && t . Sub ( start ) . Seconds ( ) >= totalTime {
utils . GetLogInstance ( ) . Debug ( "Generator timer ended." , "duration" , ( int ( t . Sub ( start ) ) ) , "startTime" , start , "totalTime" , totalTime )
break pushLoop
}
if shardID != 0 {
if otherHeight , flag := txGen . IsSameHeight ( ) ; flag {
if otherHeight >= 1 {
go func ( ) {
readySignal <- uint32 ( shardID )
utils . GetLogInstance ( ) . Debug ( "Same blockchain height so readySignal generated" )
time . Sleep ( 3 * time . Second ) // wait for nodes to be ready
} ( )
}
}
}
select {
case shardID := <- readySignal :
lock := sync . Mutex { }
txs , err := GenerateSimulatedTransactionsAccount ( uint32 ( shardID ) , txGen , setting )
if err != nil {
utils . GetLogInstance ( ) . Debug ( "Error in Generating Txns" , "Err" , err )
}
lock . Lock ( )
SendTxsToShard ( txGen , txs , uint32 ( shardID ) )
lock . Unlock ( )
case <- time . After ( 10 * time . Second ) :
utils . GetLogInstance ( ) . Warn ( "No new block is received so far" )
}
}
}
// SendTxsToShard sends txs to shard, currently just to beacon shard
func SendTxsToShard ( clientNode * node . Node , txs types . Transactions , shardID uint32 ) {
msg := proto_node . ConstructTransactionListMessageAccount ( txs )
var err error
if shardID == 0 {
err = clientNode . GetHost ( ) . SendMessageToGroups ( [ ] p2p . GroupID { p2p . GroupIDBeaconClient } , p2p_host . ConstructP2pMessage ( byte ( 0 ) , msg ) )
} else {
clientGroup := p2p . NewClientGroupIDByShardID ( p2p . ShardID ( shardID ) )
err = clientNode . GetHost ( ) . SendMessageToGroups ( [ ] p2p . GroupID { clientGroup } , p2p_host . ConstructP2pMessage ( byte ( 0 ) , msg ) )
}
if err != nil {
utils . GetLogInstance ( ) . Debug ( "Error in Sending Txns" , "Err" , err )
}
}
// GenerateSimulatedTransactionsAccount generates simulated transaction for account model.
func GenerateSimulatedTransactionsAccount ( shardID uint32 , node * node . Node , setting Settings ) ( types . Transactions , error ) {
TxnsToGenerate := setting . MaxNumTxsPerBatch // TODO: make use of settings
txs := make ( [ ] * types . Transaction , TxnsToGenerate )
rounds := ( TxnsToGenerate / 100 )
remainder := TxnsToGenerate % 100
for i := 0 ; i < 100 ; i ++ {
baseNonce := node . Worker . GetCurrentState ( ) . GetNonce ( crypto . PubkeyToAddress ( node . TestBankKeys [ i ] . PublicKey ) )
for j := 0 ; j < rounds ; j ++ {
randomUserAddress := crypto . PubkeyToAddress ( node . TestBankKeys [ rand . Intn ( 100 ) ] . PublicKey )
randAmount := rand . Float32 ( )
tx , _ := types . SignTx ( types . NewTransaction ( baseNonce + uint64 ( j ) , randomUserAddress , shardID , big . NewInt ( int64 ( denominations . One * randAmount ) ) , params . TxGas , nil , nil ) , types . HomesteadSigner { } , node . TestBankKeys [ i ] )
txs [ 100 * j + i ] = tx
}
if i < remainder {
randomUserAddress := crypto . PubkeyToAddress ( node . TestBankKeys [ rand . Intn ( 100 ) ] . PublicKey )
randAmount := rand . Float32 ( )
tx , _ := types . SignTx ( types . NewTransaction ( baseNonce + uint64 ( rounds ) , randomUserAddress , shardID , big . NewInt ( int64 ( denominations . One * randAmount ) ) , params . TxGas , nil , nil ) , types . HomesteadSigner { } , node . TestBankKeys [ i ] )
txs [ 100 * rounds + i ] = tx
}
}
return txs , nil
}
func isDurationForever ( duration float64 ) bool {
return duration <= 0
}