├── .gitignore ├── Dockerfile ├── bigquery ├── blocks.schema.json ├── transactions.schema.json └── transfers.schema.json ├── cluster ├── etherquery-controller.yml └── geth-service.yml ├── etherquery ├── blocks.go ├── etherquery.go ├── trace.go ├── transactions.go ├── transfer.go └── upload.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.6-onbuild 2 | 3 | EXPOSE 8545 4 | EXPOSE 30303 5 | -------------------------------------------------------------------------------- /bigquery/blocks.schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "number", 4 | "type": "INTEGER", 5 | "mode": "NULLABLE" 6 | }, 7 | { 8 | "name": "hash", 9 | "type": "STRING", 10 | "mode": "NULLABLE" 11 | }, 12 | { 13 | "name": "parentHash", 14 | "type": "STRING", 15 | "mode": "NULLABLE" 16 | }, 17 | { 18 | "name": "nonce", 19 | "type": "STRING", 20 | "mode": "NULLABLE" 21 | }, 22 | { 23 | "name": "miner", 24 | "type": "STRING", 25 | "mode": "NULLABLE" 26 | }, 27 | { 28 | "name": "difficulty", 29 | "type": "STRING", 30 | "mode": "NULLABLE" 31 | }, 32 | { 33 | "name": "totalDifficulty", 34 | "type": "STRING", 35 | "mode": "NULLABLE" 36 | }, 37 | { 38 | "name": "extraData", 39 | "type": "BYTES", 40 | "mode": "NULLABLE" 41 | }, 42 | { 43 | "name": "size", 44 | "type": "INTEGER", 45 | "mode": "NULLABLE" 46 | }, 47 | { 48 | "name": "gasLimit", 49 | "type": "INTEGER", 50 | "mode": "NULLABLE" 51 | }, 52 | { 53 | "name": "gasUsed", 54 | "type": "INTEGER", 55 | "mode": "NULLABLE" 56 | }, 57 | { 58 | "name": "timestamp", 59 | "type": "TIMESTAMP", 60 | "mode": "NULLABLE" 61 | }, 62 | { 63 | "name": "transactionCount", 64 | "type": "INTEGER", 65 | "mode": "NULLABLE" 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /bigquery/transactions.schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "blockNumber", 4 | "type": "INTEGER", 5 | "mode": "NULLABLE" 6 | }, 7 | { 8 | "name": "blockHash", 9 | "type": "STRING", 10 | "mode": "NULLABLE" 11 | }, 12 | { 13 | "name": "timestamp", 14 | "type": "TIMESTAMP", 15 | "mode": "NULLABLE" 16 | }, 17 | { 18 | "name": "hash", 19 | "type": "STRING", 20 | "mode": "NULLABLE" 21 | }, 22 | { 23 | "name": "from", 24 | "type": "STRING", 25 | "mode": "NULLABLE" 26 | }, 27 | { 28 | "name": "to", 29 | "type": "STRING", 30 | "mode": "NULLABLE" 31 | }, 32 | { 33 | "name": "gas", 34 | "type": "INTEGER", 35 | "mode": "NULLABLE" 36 | }, 37 | { 38 | "name": "gasUsed", 39 | "type": "INTEGER", 40 | "mode": "NULLABLE" 41 | }, 42 | { 43 | "name": "gasPrice", 44 | "type": "INTEGER", 45 | "mode": "NULLABLE" 46 | }, 47 | { 48 | "name": "input", 49 | "type": "BYTES", 50 | "mode": "NULLABLE" 51 | }, 52 | { 53 | "name": "logs", 54 | "type": "RECORD", 55 | "mode": "REPEATED", 56 | "fields": [ 57 | { 58 | "name": "address", 59 | "type": "STRING", 60 | "mode": "NULLABLE" 61 | }, 62 | { 63 | "name": "topics", 64 | "type": "STRING", 65 | "mode": "REPEATED" 66 | }, 67 | { 68 | "name": "data", 69 | "type": "BYTES", 70 | "mode": "NULLABLE" 71 | } 72 | ] 73 | }, 74 | { 75 | "name": "nonce", 76 | "type": "INTEGER", 77 | "mode": "NULLABLE" 78 | }, 79 | { 80 | "name": "value", 81 | "type": "STRING", 82 | "mode": "NULLABLE" 83 | }, 84 | { 85 | "name": "contractAddress", 86 | "type": "STRING", 87 | "mode": "NULLABLE" 88 | }, 89 | { 90 | "name": "error", 91 | "type": "STRING", 92 | "mode": "NULLABLE" 93 | } 94 | ] 95 | -------------------------------------------------------------------------------- /bigquery/transfers.schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "blockNumber", 4 | "type": "INTEGER", 5 | "mode": "NULLABLE" 6 | }, 7 | { 8 | "name": "blockHash", 9 | "type": "STRING", 10 | "mode": "NULLABLE" 11 | }, 12 | { 13 | "name": "timestamp", 14 | "type": "TIMESTAMP", 15 | "mode": "NULLABLE" 16 | }, 17 | { 18 | "name": "transactionHash", 19 | "type": "STRING", 20 | "mode": "NULLABLE" 21 | }, 22 | { 23 | "name": "transferIndex", 24 | "type": "INTEGER", 25 | "mode": "NULLABLE" 26 | }, 27 | { 28 | "name": "depth", 29 | "type": "INTEGER", 30 | "mode": "NULLABLE" 31 | }, 32 | { 33 | "name": "from", 34 | "type": "STRING", 35 | "mode": "NULLABLE" 36 | }, 37 | { 38 | "name": "to", 39 | "type": "STRING", 40 | "mode": "NULLABLE" 41 | }, 42 | { 43 | "name": "fromBalance", 44 | "type": "STRING", 45 | "mode": "NULLABLE" 46 | }, 47 | { 48 | "name": "toBalance", 49 | "type": "STRING", 50 | "mode": "NULLABLE" 51 | }, 52 | { 53 | "name": "value", 54 | "type": "STRING", 55 | "mode": "NULLABLE" 56 | }, 57 | { 58 | "name": "type", 59 | "type": "STRING", 60 | "mode": "NULLABLE" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /cluster/etherquery-controller.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: etherquery-controller 5 | spec: 6 | replicas: 1 7 | selector: 8 | app: etherquery 9 | template: 10 | metadata: 11 | labels: 12 | app: etherquery 13 | spec: 14 | containers: 15 | - name: etherquery 16 | image: gcr.io/etherquery/etherquery:v1.1.5 17 | ports: 18 | - containerPort: 8545 19 | name: json-rpc 20 | command: ["/go/bin/etherquery", "--rpc", "--rpcaddr", "0.0.0.0", "--datadir", "/mnt/blockchain"] 21 | volumeMounts: 22 | - name: ethereum-blockchain 23 | mountPath: "/mnt/blockchain" 24 | volumes: 25 | - name: ethereum-blockchain 26 | gcePersistentDisk: 27 | pdName: ethereum-blockchain 28 | fsType: ext4 29 | -------------------------------------------------------------------------------- /cluster/geth-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: geth 5 | labels: 6 | name: geth 7 | spec: 8 | ports: 9 | - port: 8545 10 | selector: 11 | app: etherquery 12 | -------------------------------------------------------------------------------- /etherquery/blocks.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/core/state" 5 | "github.com/ethereum/go-ethereum/core/types" 6 | "github.com/ethereum/go-ethereum/rpc" 7 | "google.golang.org/api/bigquery/v2" 8 | ) 9 | 10 | func blockToJsonValue(d *blockData) *bigquery.TableDataInsertAllRequestRows { 11 | return &bigquery.TableDataInsertAllRequestRows{ 12 | InsertId: d.block.Hash().Hex(), 13 | Json: map[string]bigquery.JsonValue{ 14 | "number": d.block.Number().Uint64(), 15 | "hash": d.block.Hash(), 16 | "parentHash": d.block.ParentHash(), 17 | "nonce": rpc.NewHexNumber(d.block.Header().Nonce.Uint64()), 18 | "miner": d.block.Coinbase(), 19 | "difficulty": rpc.NewHexNumber(d.block.Difficulty()), 20 | "totalDifficulty": rpc.NewHexNumber(d.totalDifficulty), 21 | "extraData": d.block.Extra(), 22 | "size": d.block.Size().Int64(), 23 | "gasLimit": d.block.GasLimit().Uint64(), 24 | "gasUsed": d.block.GasLimit().Uint64(), 25 | "timestamp": d.block.Time().Uint64(), 26 | "transactionCount": len(d.block.Transactions()), 27 | }, 28 | } 29 | } 30 | 31 | type blockExporter struct { 32 | writer *batchedBigqueryWriter 33 | } 34 | 35 | func (self *blockExporter) setWriter(writer *batchedBigqueryWriter) { self.writer = writer } 36 | func (self *blockExporter) getTableName() string { return "blocks" } 37 | func (self *blockExporter) exportGenesis(block *types.Block, world state.World) {} 38 | 39 | func (self *blockExporter) export(data *blockData) { 40 | self.writer.add(blockToJsonValue(data)) 41 | } 42 | -------------------------------------------------------------------------------- /etherquery/etherquery.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "log" 7 | "math/big" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/core" 12 | "github.com/ethereum/go-ethereum/core/state" 13 | "github.com/ethereum/go-ethereum/core/types" 14 | "github.com/ethereum/go-ethereum/eth" 15 | "github.com/ethereum/go-ethereum/ethdb" 16 | "github.com/ethereum/go-ethereum/event" 17 | "github.com/ethereum/go-ethereum/node" 18 | "github.com/ethereum/go-ethereum/p2p" 19 | "github.com/ethereum/go-ethereum/rpc" 20 | "golang.org/x/net/context" 21 | "golang.org/x/oauth2/google" 22 | "google.golang.org/api/bigquery/v2" 23 | ) 24 | 25 | const DATA_VERSION uint64 = 3 26 | 27 | type EtherQueryConfig struct { 28 | Project string 29 | Dataset string 30 | BatchInterval time.Duration 31 | BatchSize int 32 | } 33 | 34 | type EtherQuery struct { 35 | bqService *bigquery.Service 36 | config *EtherQueryConfig 37 | db ethdb.Database 38 | ethereum *eth.Ethereum 39 | headSub event.Subscription 40 | mux *event.TypeMux 41 | server *p2p.Server 42 | } 43 | 44 | func New(config *EtherQueryConfig, ctx *node.ServiceContext) (node.Service, error) { 45 | var ethereum *eth.Ethereum 46 | if err := ctx.Service(ðereum); err != nil { 47 | return nil, err 48 | } 49 | 50 | db, err := ctx.OpenDatabase("etherquery", 16, 16) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return &EtherQuery { 56 | bqService: nil, 57 | config: config, 58 | db: db, 59 | ethereum: ethereum, 60 | headSub: nil, 61 | mux: ctx.EventMux, 62 | server: nil, 63 | }, nil 64 | } 65 | 66 | func (eq *EtherQuery) Protocols() ([]p2p.Protocol) { 67 | return []p2p.Protocol{} 68 | } 69 | 70 | func (eq *EtherQuery) APIs() ([]rpc.API) { 71 | return []rpc.API{} 72 | } 73 | 74 | // valueTransfer represents a transfer of ether from one account to another 75 | type valueTransfer struct { 76 | depth int 77 | transactionHash common.Hash 78 | src common.Address 79 | srcBalance *big.Int 80 | dest common.Address 81 | destBalance *big.Int 82 | value *big.Int 83 | kind string 84 | } 85 | 86 | type blockData struct { 87 | block *types.Block 88 | trace *traceData 89 | totalDifficulty *big.Int 90 | } 91 | 92 | type exporter interface { 93 | setWriter(*batchedBigqueryWriter) 94 | exportGenesis(*types.Block, state.World) 95 | export(*blockData) 96 | getTableName() string 97 | } 98 | 99 | var EXPORTERS []exporter = []exporter{ 100 | &blockExporter{}, 101 | &transactionExporter{}, 102 | &transferExporter{}, 103 | } 104 | 105 | func (eq *EtherQuery) processBlocks(ch <-chan *types.Block) { 106 | for _, exporter := range EXPORTERS { 107 | writer := newBatchedBigqueryWriter( 108 | eq.bqService, eq.config.Project, eq.config.Dataset, exporter.getTableName(), 109 | eq.config.BatchInterval, eq.config.BatchSize) 110 | exporter.setWriter(writer) 111 | writer.start() 112 | } 113 | 114 | for block := range ch { 115 | if block.Number().Uint64() == 0 { 116 | log.Printf("Processing genesis block...") 117 | statedb, err := state.New(eq.ethereum.BlockChain().GetBlockByHash(block.Hash()).Root(), eq.ethereum.ChainDb()) 118 | if err != nil { 119 | log.Fatalf("Failed to get state DB for genesis block: %v", err) 120 | } 121 | world := statedb.RawDump() 122 | for _, exporter := range EXPORTERS { 123 | exporter.exportGenesis(block, world) 124 | } 125 | } 126 | 127 | if block.Number().Uint64() % 1000 == 0 { 128 | log.Printf("Processing block %v @%v...", block.Number().Uint64(), time.Unix(block.Time().Int64(), 0)); 129 | } 130 | 131 | trace, err := traceBlock(eq.ethereum, block) 132 | if err != nil { 133 | log.Printf("Unable to trace transactions in block %v: %v", block.Number().Uint64(), err) 134 | continue 135 | } 136 | 137 | blockData := &blockData{ 138 | block: block, 139 | trace: trace, 140 | totalDifficulty: eq.ethereum.BlockChain().GetTdByHash(block.Hash()), 141 | } 142 | 143 | for _, exporter := range EXPORTERS { 144 | exporter.export(blockData) 145 | } 146 | eq.putLastBlock(block.Number().Uint64()) 147 | } 148 | } 149 | 150 | func (eq *EtherQuery) getInt(key string) (uint64, error) { 151 | data, err := eq.db.Get([]byte(key)) 152 | if err != nil { 153 | return 0, err 154 | } 155 | 156 | var value uint64 157 | err = binary.Read(bytes.NewReader(data), binary.LittleEndian, &value) 158 | if err != nil { 159 | return 0, err 160 | } 161 | 162 | return value, nil 163 | } 164 | 165 | func (eq *EtherQuery) putInt(key string, value uint64) error { 166 | buf := new(bytes.Buffer) 167 | err := binary.Write(buf, binary.LittleEndian, value) 168 | if err != nil { 169 | return err 170 | } 171 | return eq.db.Put([]byte(key), buf.Bytes()) 172 | } 173 | 174 | func (eq *EtherQuery) getLastBlock() uint64 { 175 | dataVersion, err := eq.getInt("dataVersion") 176 | if err != nil || dataVersion < DATA_VERSION { 177 | log.Printf("Obsolete dataVersion") 178 | eq.putInt("dataVersion", DATA_VERSION) 179 | eq.putInt("lastBlock", 0) 180 | return 0 181 | } 182 | lastBlock, err := eq.getInt("lastBlock") 183 | if err != nil { 184 | return 0 185 | } 186 | return lastBlock 187 | } 188 | 189 | func (eq *EtherQuery) putLastBlock(block uint64) { 190 | eq.putInt("lastBlock", block) 191 | } 192 | 193 | func (eq *EtherQuery) consumeBlocks() { 194 | blocks := make(chan *types.Block, 256) 195 | go eq.processBlocks(blocks) 196 | defer close(blocks) 197 | 198 | chain := eq.ethereum.BlockChain() 199 | lastBlock := eq.getLastBlock() 200 | 201 | // First catch up 202 | for lastBlock < chain.CurrentBlock().Number().Uint64() { 203 | blocks <- chain.GetBlockByNumber(lastBlock) 204 | lastBlock += 1 205 | } 206 | 207 | log.Printf("Caught up; subscribing to new blocks.") 208 | 209 | // Now, subscribe to new blocks as they arrive 210 | ch := eq.mux.Subscribe(core.ChainHeadEvent{}).Chan() 211 | for e := range ch { 212 | if event, ok := e.Data.(core.ChainHeadEvent); ok { 213 | newBlock := event.Block.Number().Uint64() 214 | for ; lastBlock <= newBlock; lastBlock++ { 215 | blocks <- chain.GetBlockByNumber(lastBlock) 216 | } 217 | } else { 218 | log.Printf("Expected ChainHeadEvent, got %T", e.Data) 219 | } 220 | } 221 | } 222 | 223 | func (eq *EtherQuery) Start(server *p2p.Server) error { 224 | log.Print("Starting etherquery service.") 225 | 226 | eq.server = server 227 | 228 | client, err := google.DefaultClient(context.Background(), bigquery.BigqueryInsertdataScope) 229 | if err != nil { 230 | return err 231 | } 232 | bqService, err := bigquery.New(client) 233 | if err != nil { 234 | return err 235 | } 236 | eq.bqService = bqService 237 | 238 | go eq.consumeBlocks() 239 | return nil 240 | } 241 | 242 | func (eq *EtherQuery) Stop() error { 243 | log.Print("Stopping etherquery service.") 244 | eq.headSub.Unsubscribe() 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /etherquery/trace.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/big" 7 | 8 | "github.com/ethereum/go-ethereum/core" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/core/state" 11 | "github.com/ethereum/go-ethereum/core/types" 12 | "github.com/ethereum/go-ethereum/core/vm" 13 | "github.com/ethereum/go-ethereum/eth" 14 | ) 15 | 16 | // transactionTrace contains information about the trace of a single transaction's execution 17 | type transactionTrace struct { 18 | receipt *types.Receipt 19 | logs []*vm.Log 20 | err error 21 | } 22 | 23 | // traceData contains information about the trace of a block's transactions 24 | type traceData struct { 25 | transactions []*transactionTrace 26 | transfers []*valueTransfer 27 | } 28 | 29 | // callStackFrame holds internal state on a frame of the call stack during a transaction execution 30 | type callStackFrame struct { 31 | op vm.OpCode 32 | accountAddress common.Address 33 | transfers []*valueTransfer 34 | } 35 | 36 | // transactionTracer holds state for the trace of a transaction execution 37 | type transactionTracer struct { 38 | statedb *state.StateDB 39 | src common.Address 40 | stack []*callStackFrame 41 | tx *types.Transaction 42 | err error 43 | } 44 | 45 | func newTransfer(statedb *state.StateDB, depth int, txHash common.Hash, src, dest common.Address, 46 | value *big.Int, kind string) *valueTransfer { 47 | srcBalance := new(big.Int) 48 | if src != (common.Address{}) { 49 | srcBalance.Sub(statedb.GetBalance(src), value) 50 | } 51 | 52 | return &valueTransfer{ 53 | depth: depth, 54 | transactionHash: txHash, 55 | src: src, 56 | srcBalance: srcBalance, 57 | dest: dest, 58 | destBalance: new(big.Int).Add(statedb.GetBalance(dest), value), 59 | value: value, 60 | kind: kind, 61 | } 62 | } 63 | 64 | func newTransactionTracer(statedb *state.StateDB, tx *types.Transaction) (*transactionTracer) { 65 | from, _ := tx.FromFrontier() 66 | to := common.Address{} 67 | kind := "CREATION" 68 | if tx.To() != nil { 69 | to = *tx.To() 70 | kind = "TRANSACTION" 71 | } 72 | 73 | var transfers []*valueTransfer 74 | if tx.Value().Cmp(big.NewInt(0)) != 0 { 75 | transfers = []*valueTransfer{newTransfer(statedb, 0, tx.Hash(), from, to, tx.Value(), kind)} 76 | } 77 | 78 | return &transactionTracer{ 79 | statedb: statedb, 80 | src: from, 81 | stack: []*callStackFrame{ 82 | &callStackFrame{ 83 | accountAddress: to, 84 | transfers: transfers, 85 | }, 86 | }, 87 | tx: tx, 88 | } 89 | } 90 | 91 | func (self *transactionTracer) fixupCreationAddresses(transfers []*valueTransfer, 92 | address common.Address) { 93 | for _, transfer := range transfers { 94 | if transfer.src == (common.Address{}) { 95 | transfer.src = address 96 | } else if transfer.dest == (common.Address{}) { 97 | transfer.dest = address 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * addStructLog implements the vm.StructLogCollector interface. 104 | * 105 | * We're interested here in capturing value transfers between accounts. To do so, we need to watch 106 | * for several opcodes: CREATE, CALL, CALLCODE, DELEGATECALL, and SUICIDE. CREATE and CALL can 107 | * result in transfers to other accounts. CALLCODE and DELEGATECALL don't transfer value, but do 108 | * create new failure domains, so we track them too. SUICIDE results in a transfer of any remaining 109 | * balance back to the calling account. 110 | * 111 | * Since a failed call, due to out of gas, invalid opcodes, etc, causes all operations for that call 112 | * to be reverted, we need to track the set of transfers that are pending in each call, which 113 | * consists of the value transfer made in the current call, if any, and any transfers from 114 | * successful operations so far. When a call errors, we discard any pending transsfers it had. If 115 | * it returns successfully - detected by noticing the VM depth has decreased by one - we add that 116 | * frame's transfers to our own. 117 | */ 118 | func (self *transactionTracer) AddStructLog(entry vm.StructLog) { 119 | //log.Printf("Depth: %v, Op: %v", entry.Depth, entry.Op) 120 | // If an error occurred (eg, out of gas), discard the current stack frame 121 | if entry.Err != nil { 122 | self.stack = self.stack[:len(self.stack) - 1] 123 | if len(self.stack) == 0 { 124 | self.err = entry.Err 125 | } 126 | return 127 | } 128 | 129 | // If we just returned from a call 130 | if entry.Depth == len(self.stack) - 1 { 131 | returnFrame := self.stack[len(self.stack) - 1] 132 | self.stack = self.stack[:len(self.stack) - 1] 133 | topFrame := self.stack[len(self.stack) - 1] 134 | 135 | if topFrame.op == vm.CREATE { 136 | // Now we know our new address, fill it in everywhere. 137 | topFrame.accountAddress = common.BigToAddress(entry.Stack[len(entry.Stack) - 1]) 138 | self.fixupCreationAddresses(returnFrame.transfers, topFrame.accountAddress) 139 | } 140 | 141 | // Our call succeded, so add any transfers that happened to the current stack frame 142 | topFrame.transfers = append(topFrame.transfers, returnFrame.transfers...) 143 | } else if entry.Depth != len(self.stack) { 144 | log.Panicf("Unexpected stack transition: was %v, now %v", len(self.stack), entry.Depth) 145 | } 146 | 147 | switch entry.Op { 148 | case vm.CREATE: 149 | // CREATE adds a frame to the stack, but we don't know their address yet - we'll fill it in 150 | // when the call returns. 151 | value := entry.Stack[len(entry.Stack) - 1] 152 | src := self.stack[len(self.stack) - 1].accountAddress 153 | 154 | var transfers []*valueTransfer 155 | if value.Cmp(big.NewInt(0)) != 0 { 156 | transfers = []*valueTransfer{ 157 | newTransfer(self.statedb, len(self.stack), self.tx.Hash(), src, common.Address{}, 158 | value, "CREATION")} 159 | } 160 | 161 | frame := &callStackFrame{ 162 | op: entry.Op, 163 | accountAddress: common.Address{}, 164 | transfers: transfers, 165 | } 166 | self.stack = append(self.stack, frame) 167 | case vm.CALL: 168 | // CALL adds a frame to the stack with the target address and value 169 | value := entry.Stack[len(entry.Stack) - 3] 170 | dest := common.BigToAddress(entry.Stack[len(entry.Stack) - 2]) 171 | 172 | var transfers []*valueTransfer 173 | if value.Cmp(big.NewInt(0)) != 0 { 174 | src := self.stack[len(self.stack) - 1].accountAddress 175 | transfers = append(transfers, 176 | newTransfer(self.statedb, len(self.stack), self.tx.Hash(), src, dest, value, 177 | "TRANSFER")) 178 | } 179 | 180 | frame := &callStackFrame{ 181 | op: entry.Op, 182 | accountAddress: dest, 183 | transfers: transfers, 184 | } 185 | self.stack = append(self.stack, frame) 186 | case vm.CALLCODE: fallthrough 187 | case vm.DELEGATECALL: 188 | // CALLCODE and DELEGATECALL don't transfer value or change the from address, but do create 189 | // a separate failure domain. 190 | frame := &callStackFrame{ 191 | op: entry.Op, 192 | accountAddress: self.stack[len(self.stack) - 1].accountAddress, 193 | } 194 | self.stack = append(self.stack, frame) 195 | case vm.SUICIDE: 196 | // SUICIDE results in a transfer back to the calling address. 197 | frame := self.stack[len(self.stack) - 1] 198 | value := self.statedb.GetBalance(frame.accountAddress) 199 | 200 | dest := self.src 201 | if len(self.stack) > 1 { 202 | dest = self.stack[len(self.stack) - 2].accountAddress 203 | } 204 | 205 | if value.Cmp(big.NewInt(0)) != 0 { 206 | frame.transfers = append(frame.transfers, newTransfer(self.statedb, len(self.stack), 207 | self.tx.Hash(), frame.accountAddress, dest, value, "SELFDESTRUCT")) 208 | } 209 | } 210 | } 211 | 212 | // Checks invariants, and returns the error the transaction encountered, if any 213 | func (self *transactionTracer) finish(receipt *types.Receipt) error { 214 | if len(self.stack) > 1 { 215 | log.Panicf("Transaction not done: %v frames still on the stack", len(self.stack)) 216 | } else if len(self.stack) == 1 { 217 | // Find any unset addresses due to contract creation and set them 218 | self.fixupCreationAddresses(self.stack[0].transfers, receipt.ContractAddress) 219 | } 220 | 221 | return self.err 222 | } 223 | 224 | func recordRewards(statedb *state.StateDB, block *types.Block, data *traceData) { 225 | reward := new(big.Int).Set(core.BlockReward) 226 | uncleReward := new(big.Int).Div(core.BlockReward, big.NewInt(32)) 227 | 228 | for _, uncle := range block.Uncles() { 229 | r := new(big.Int) 230 | r.Add(uncle.Number, big.NewInt(8)) 231 | r.Sub(r, block.Number()) 232 | r.Mul(r, core.BlockReward) 233 | r.Div(r, big.NewInt(8)) 234 | 235 | data.transfers = append(data.transfers, 236 | newTransfer(statedb, 0, common.Hash{}, common.Address{}, uncle.Coinbase, r, "UNCLE")) 237 | 238 | reward.Add(reward, uncleReward) 239 | } 240 | 241 | data.transfers = append(data.transfers, 242 | newTransfer(statedb, 0, common.Hash{}, common.Address{}, block.Coinbase(), reward, "MINED")) 243 | } 244 | 245 | /* Traces a block. Assumes it's already validated. */ 246 | func traceBlock(ethereum *eth.Ethereum, block *types.Block) (*traceData, error) { 247 | data := &traceData{} 248 | if len(block.Transactions()) == 0 { 249 | return data, nil 250 | } 251 | 252 | vmConfig := vm.Config{ 253 | Debug: true, 254 | EnableJit: false, 255 | ForceJit: false, 256 | Logger: vm.LogConfig{ 257 | DisableMemory: false, 258 | DisableStack: false, 259 | DisableStorage: false, 260 | FullStorage: false, 261 | }, 262 | } 263 | 264 | blockchain := ethereum.BlockChain() 265 | 266 | parent := blockchain.GetBlockByHash(block.ParentHash()) 267 | if parent == nil { 268 | return nil, fmt.Errorf("Could not retrieve parent block for hash %v", 269 | block.ParentHash().Hex()) 270 | } 271 | 272 | statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), ethereum.ChainDb()) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | gasPool := new(core.GasPool).AddGas(block.GasLimit()) 278 | usedGas := big.NewInt(0) 279 | 280 | recordRewards(statedb, block, data) 281 | 282 | for i, tx := range block.Transactions() { 283 | trace := &transactionTrace{} 284 | tracer := newTransactionTracer(statedb, tx) 285 | vmConfig.Logger.Collector = tracer 286 | 287 | statedb.StartRecord(tx.Hash(), block.Hash(), i) 288 | 289 | trace.receipt, trace.logs, _, err = core.ApplyTransaction(blockchain.Config(), blockchain, 290 | gasPool, statedb, block.Header(), tx, usedGas, vmConfig) 291 | if err != nil { 292 | log.Fatalf("Failed to trace transaction %v: %v", tx.Hash().Hex(), err) 293 | continue 294 | } 295 | 296 | // Account for transaction fees 297 | from, _ := tx.FromFrontier() 298 | data.transfers = append(data.transfers, newTransfer(statedb, 0, tx.Hash(), from, 299 | block.Coinbase(), new(big.Int).Mul(trace.receipt.GasUsed, tx.GasPrice()), "FEE")) 300 | 301 | trace.err = tracer.finish(trace.receipt) 302 | if len(tracer.stack) == 1 { 303 | data.transfers = append(data.transfers, tracer.stack[0].transfers...) 304 | } 305 | 306 | data.transactions = append(data.transactions, trace) 307 | } 308 | return data, nil 309 | } 310 | -------------------------------------------------------------------------------- /etherquery/transactions.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/ethereum/go-ethereum/rpc" 6 | "github.com/ethereum/go-ethereum/core/state" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/ethereum/go-ethereum/core/vm" 9 | "google.golang.org/api/bigquery/v2" 10 | ) 11 | 12 | func logToJsonValue(log *vm.Log) map[string]bigquery.JsonValue { 13 | return map[string]bigquery.JsonValue { 14 | "address": log.Address, 15 | "topics": log.Topics, 16 | "data": log.Data, 17 | } 18 | } 19 | 20 | func logsToJsonValue(logs []*vm.Log) []map[string]bigquery.JsonValue { 21 | loginfos := make([]map[string]bigquery.JsonValue, len(logs)) 22 | for i := 0; i < len(logs); i++ { 23 | loginfos[i] = logToJsonValue(logs[i]) 24 | } 25 | return loginfos 26 | } 27 | 28 | func transactionToJsonValue(block *types.Block, tx *types.Transaction, trace *transactionTrace) *bigquery.TableDataInsertAllRequestRows { 29 | from, _ := tx.FromFrontier() 30 | 31 | var contractAddress *common.Address = nil 32 | if trace.receipt.ContractAddress != (common.Address{}) { 33 | contractAddress = &trace.receipt.ContractAddress 34 | } 35 | 36 | var errorMsg *string 37 | if trace.err != nil { 38 | msg := trace.err.Error() 39 | errorMsg = &msg 40 | } 41 | 42 | return &bigquery.TableDataInsertAllRequestRows{ 43 | InsertId: tx.Hash().Hex(), 44 | Json: map[string]bigquery.JsonValue{ 45 | "blockNumber": block.Number().Uint64(), 46 | "blockHash": block.Hash(), 47 | "timestamp": block.Time().Uint64(), 48 | "hash": tx.Hash(), 49 | "from": from, 50 | "to": tx.To(), 51 | "gas": tx.Gas().Uint64(), 52 | "gasUsed": trace.receipt.GasUsed, 53 | "gasPrice": tx.GasPrice().Uint64(), 54 | "input": tx.Data(), 55 | "logs": logsToJsonValue(trace.receipt.Logs), 56 | "nonce": tx.Nonce(), 57 | "value": rpc.NewHexNumber(tx.Value()), 58 | "contractAddress": contractAddress, 59 | "error": errorMsg, 60 | }, 61 | } 62 | } 63 | 64 | type transactionExporter struct { 65 | writer *batchedBigqueryWriter 66 | } 67 | 68 | func (self *transactionExporter) setWriter(writer *batchedBigqueryWriter) { self.writer = writer } 69 | func (self *transactionExporter) getTableName() string { return "transactions" } 70 | func (self *transactionExporter) exportGenesis(block *types.Block, world state.World) {} 71 | 72 | func (self *transactionExporter) export(data *blockData) { 73 | for i, tx := range data.block.Transactions() { 74 | self.writer.add(transactionToJsonValue(data.block, tx, data.trace.transactions[i])) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /etherquery/transfer.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "log" 5 | "math/big" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/core/state" 9 | "github.com/ethereum/go-ethereum/core/types" 10 | "github.com/ethereum/go-ethereum/rpc" 11 | "google.golang.org/api/bigquery/v2" 12 | ) 13 | 14 | func transferToJsonValue(block *types.Block, idx int, transfer *valueTransfer) *bigquery.TableDataInsertAllRequestRows { 15 | insertId := big.NewInt(int64(idx)) 16 | insertId.Add(insertId, block.Hash().Big()) 17 | 18 | var transactionHash *common.Hash 19 | if transfer.transactionHash != (common.Hash{}) { 20 | transactionHash = &transfer.transactionHash 21 | } 22 | 23 | return &bigquery.TableDataInsertAllRequestRows{ 24 | InsertId: insertId.Text(16), 25 | Json: map[string]bigquery.JsonValue{ 26 | "blockNumber": block.Number().Uint64(), 27 | "blockHash": block.Hash(), 28 | "timestamp": block.Time().Uint64(), 29 | "transactionHash": transactionHash, 30 | "transferIndex": idx, 31 | "depth": transfer.depth, 32 | "from": transfer.src, 33 | "to": transfer.dest, 34 | "fromBalance": rpc.NewHexNumber(transfer.srcBalance), 35 | "toBalance": rpc.NewHexNumber(transfer.destBalance), 36 | "value": rpc.NewHexNumber(transfer.value), 37 | "type": transfer.kind, 38 | }, 39 | } 40 | } 41 | 42 | type transferExporter struct { 43 | writer *batchedBigqueryWriter 44 | } 45 | 46 | func (self *transferExporter) setWriter(writer *batchedBigqueryWriter) { self.writer = writer } 47 | func (self *transferExporter) getTableName() string { return "transfers" } 48 | 49 | func (self *transferExporter) export(data *blockData) { 50 | for i, transfer := range data.trace.transfers { 51 | self.writer.add(transferToJsonValue(data.block, i, transfer)) 52 | } 53 | } 54 | 55 | func (self *transferExporter) exportGenesis(block *types.Block, world state.World) { 56 | i := 0 57 | for address, account := range world.Accounts { 58 | balance, ok := new(big.Int).SetString(account.Balance, 10) 59 | if !ok { 60 | log.Panicf("Could not decode balance of genesis account") 61 | } 62 | transfer := &valueTransfer{ 63 | depth: 0, 64 | transactionHash: common.Hash{}, 65 | src: common.Address{}, 66 | srcBalance: big.NewInt(0), 67 | dest: common.HexToAddress(address), 68 | destBalance: balance, 69 | value: balance, 70 | kind: "GENESIS", 71 | } 72 | self.writer.add(transferToJsonValue(block, i, transfer)) 73 | i += 1 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /etherquery/upload.go: -------------------------------------------------------------------------------- 1 | package etherquery 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | 8 | "google.golang.org/api/bigquery/v2" 9 | "google.golang.org/api/googleapi" 10 | ) 11 | 12 | func backoff(delay time.Duration) func(bool) time.Duration { 13 | return func(grow bool) (d time.Duration) { 14 | d = delay + time.Millisecond * time.Duration(rand.Intn(1000)) 15 | if grow { 16 | delay *= time.Duration(2) 17 | } 18 | return 19 | } 20 | } 21 | 22 | func retryRequest(initialDelay, retries int, request func() (interface{}, error)) (interface{}, error) { 23 | delayer := backoff(time.Second * time.Duration(initialDelay)) 24 | 25 | trying: for { 26 | result, err := request() 27 | if err == nil { 28 | return result, err 29 | } 30 | 31 | switch err := err.(type) { 32 | case *googleapi.Error: 33 | if err.Code / 500 == 5 || err.Code == 403 { 34 | delay := delayer(true) 35 | log.Printf("Got status %v, backing off for %v before retrying.", err.Code, delay) 36 | time.Sleep(delay) 37 | continue trying 38 | } 39 | } 40 | 41 | if retries > 0 { 42 | delay := delayer(false) 43 | log.Printf("Got error %v, retrying after %v seconds.", err, delay) 44 | time.Sleep(delay) 45 | retries -= 1 46 | } else { 47 | return result, err 48 | } 49 | } 50 | 51 | } 52 | 53 | func uploadData(service *bigquery.Service, project string, dataset string, table string, rows []*bigquery.TableDataInsertAllRequestRows) { 54 | start := time.Now() 55 | 56 | request := &bigquery.TableDataInsertAllRequest{ 57 | Rows: rows, 58 | } 59 | 60 | // Try inserting the data until it succeeds or we give up 61 | r, err := retryRequest(1, 3, func() (interface{}, error) { 62 | call := service.Tabledata.InsertAll(project, dataset, table, request) 63 | return call.Do() 64 | }) 65 | if err != nil { 66 | log.Printf("Got error %v while submitting job; giving up.", err) 67 | return 68 | } 69 | response := r.(*bigquery.TableDataInsertAllResponse) 70 | 71 | if res := response.InsertErrors; len(res) > 0 { 72 | log.Printf("Got %v insert errors:", len(res)) 73 | for i := 0; i < len(res); i++ { 74 | log.Printf(" %v", res[i]) 75 | } 76 | return 77 | } 78 | log.Printf("Successfully streamed %v records to table '%v' after %v", len(rows), table, time.Since(start)) 79 | } 80 | 81 | type batchedBigqueryWriter struct { 82 | service *bigquery.Service 83 | project string 84 | dataset string 85 | table string 86 | batchInterval time.Duration 87 | batchSize int 88 | ch chan<- *bigquery.TableDataInsertAllRequestRows 89 | more bool 90 | } 91 | 92 | func newBatchedBigqueryWriter(service *bigquery.Service, project, dataset, table string, interval time.Duration, size int) *batchedBigqueryWriter { 93 | return &batchedBigqueryWriter{ 94 | service: service, 95 | project: project, 96 | dataset: dataset, 97 | table: table, 98 | batchInterval: interval, 99 | batchSize: size, 100 | } 101 | } 102 | 103 | func (self *batchedBigqueryWriter) start() { 104 | ch := make(chan *bigquery.TableDataInsertAllRequestRows) 105 | self.ch = ch 106 | 107 | go func() { 108 | self.more = true 109 | for self.more { 110 | batch := make([]*bigquery.TableDataInsertAllRequestRows, 1, self.batchSize) 111 | if batch[0], self.more = <-ch; !self.more { 112 | return 113 | } 114 | 115 | timeout := time.After(self.batchInterval) 116 | batching: for { 117 | var row *bigquery.TableDataInsertAllRequestRows; 118 | select { 119 | case row, self.more = <-ch: 120 | batch = append(batch, row) 121 | if len(batch) >= self.batchSize { 122 | break batching 123 | } 124 | case <-timeout: 125 | break batching 126 | } 127 | } 128 | go uploadData(self.service, self.project, self.dataset, self.table, batch) 129 | } 130 | }() 131 | } 132 | 133 | func (self *batchedBigqueryWriter) add(record *bigquery.TableDataInsertAllRequestRows) { 134 | self.ch <- record 135 | } 136 | 137 | func (self *batchedBigqueryWriter) stop() { 138 | self.more = false 139 | } 140 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/arachnid/etherquery/etherquery" 10 | "github.com/ethereum/go-ethereum/cmd/utils" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/console" 13 | "github.com/ethereum/go-ethereum/eth" 14 | "github.com/ethereum/go-ethereum/logger" 15 | "github.com/ethereum/go-ethereum/logger/glog" 16 | "github.com/ethereum/go-ethereum/metrics" 17 | "github.com/ethereum/go-ethereum/node" 18 | "github.com/ethereum/go-ethereum/params" 19 | "github.com/ethereum/go-ethereum/release" 20 | "github.com/ethereum/go-ethereum/rlp" 21 | "gopkg.in/urfave/cli.v1" 22 | ) 23 | 24 | const ( 25 | ClientIdentifier = "Geth-etherquery" 26 | Version = "1.5.0-unstable" 27 | VersionMajor = 1 28 | VersionMinor = 5 29 | VersionPatch = 0 30 | VersionOracle = "0xfa7b9770ca4cb04296cac84f37736d4041251cdf" 31 | ) 32 | 33 | var ( 34 | gitCommit string // set via linker flagg 35 | nodeNameVersion string 36 | app *cli.App 37 | ) 38 | 39 | func init() { 40 | if gitCommit == "" { 41 | nodeNameVersion = Version 42 | } else { 43 | nodeNameVersion = Version + "-" + gitCommit[:8] 44 | } 45 | 46 | app = utils.NewApp(Version, "the go-ethereum command line interface") 47 | app.Action = geth 48 | app.Commands = []cli.Command{ 49 | { 50 | Action: version, 51 | Name: "version", 52 | Usage: "print ethereum version numbers", 53 | Description: ` 54 | The output of this command is supposed to be machine-readable. 55 | `, 56 | }, 57 | } 58 | 59 | app.Flags = []cli.Flag{ 60 | utils.IdentityFlag, 61 | utils.PasswordFileFlag, 62 | utils.GenesisFileFlag, 63 | utils.BootnodesFlag, 64 | utils.DataDirFlag, 65 | utils.KeyStoreDirFlag, 66 | utils.BlockchainVersionFlag, 67 | utils.OlympicFlag, 68 | utils.FastSyncFlag, 69 | utils.CacheFlag, 70 | utils.LightKDFFlag, 71 | utils.JSpathFlag, 72 | utils.ListenPortFlag, 73 | utils.MaxPeersFlag, 74 | utils.MaxPendingPeersFlag, 75 | utils.EtherbaseFlag, 76 | utils.GasPriceFlag, 77 | utils.MinerThreadsFlag, 78 | utils.MiningEnabledFlag, 79 | utils.MiningGPUFlag, 80 | utils.AutoDAGFlag, 81 | utils.TargetGasLimitFlag, 82 | utils.NATFlag, 83 | utils.NatspecEnabledFlag, 84 | utils.NoDiscoverFlag, 85 | utils.NodeKeyFileFlag, 86 | utils.NodeKeyHexFlag, 87 | utils.RPCEnabledFlag, 88 | utils.RPCListenAddrFlag, 89 | utils.RPCPortFlag, 90 | utils.RPCApiFlag, 91 | utils.WSEnabledFlag, 92 | utils.WSListenAddrFlag, 93 | utils.WSPortFlag, 94 | utils.WSApiFlag, 95 | utils.WSAllowedOriginsFlag, 96 | utils.IPCDisabledFlag, 97 | utils.IPCApiFlag, 98 | utils.IPCPathFlag, 99 | utils.ExecFlag, 100 | utils.PreloadJSFlag, 101 | utils.WhisperEnabledFlag, 102 | utils.DevModeFlag, 103 | utils.TestNetFlag, 104 | utils.VMForceJitFlag, 105 | utils.VMJitCacheFlag, 106 | utils.VMEnableJitFlag, 107 | utils.NetworkIdFlag, 108 | utils.RPCCORSDomainFlag, 109 | utils.MetricsEnabledFlag, 110 | utils.SolcPathFlag, 111 | utils.GpoMinGasPriceFlag, 112 | utils.GpoMaxGasPriceFlag, 113 | utils.GpoFullBlockRatioFlag, 114 | utils.GpobaseStepDownFlag, 115 | utils.GpobaseStepUpFlag, 116 | utils.GpobaseCorrectionFactorFlag, 117 | utils.ExtraDataFlag, 118 | cli.StringFlag{ 119 | Name: "gcpproject", 120 | Usage: "GCP project ID", 121 | Value: "etherquery", 122 | }, 123 | cli.StringFlag{ 124 | Name: "dataset", 125 | Usage: "BigQuery dataset ID", 126 | Value: "ethereum", 127 | }, 128 | cli.IntFlag{ 129 | Name: "blocksrev", 130 | Usage: "blocks service revision number", 131 | }, 132 | } 133 | app.Flags = append(app.Flags) 134 | 135 | app.Before = func(ctx *cli.Context) error { 136 | runtime.GOMAXPROCS(runtime.NumCPU()) 137 | // Start system runtime metrics collection 138 | go metrics.CollectProcessMetrics(3 * time.Second) 139 | 140 | utils.SetupNetwork(ctx) 141 | 142 | // Deprecation warning. 143 | if ctx.GlobalIsSet(utils.GenesisFileFlag.Name) { 144 | common.PrintDepricationWarning("--genesis is deprecated. Switch to use 'geth init /path/to/file'") 145 | } 146 | 147 | return nil 148 | } 149 | 150 | app.After = func(ctx *cli.Context) error { 151 | logger.Flush() 152 | console.Stdin.Close() // Resets terminal mode. 153 | return nil 154 | } 155 | } 156 | 157 | func main() { 158 | if err := app.Run(os.Args); err != nil { 159 | fmt.Fprintln(os.Stderr, err) 160 | os.Exit(1) 161 | } 162 | } 163 | 164 | func makeDefaultExtra() []byte { 165 | var clientInfo = struct { 166 | Version uint 167 | Name string 168 | GoVersion string 169 | Os string 170 | }{uint(VersionMajor<<16 | VersionMinor<<8 | VersionPatch), ClientIdentifier, runtime.Version(), runtime.GOOS} 171 | extra, err := rlp.EncodeToBytes(clientInfo) 172 | if err != nil { 173 | glog.V(logger.Warn).Infoln("error setting canonical miner information:", err) 174 | } 175 | 176 | if uint64(len(extra)) > params.MaximumExtraDataSize.Uint64() { 177 | glog.V(logger.Warn).Infoln("error setting canonical miner information: extra exceeds", params.MaximumExtraDataSize) 178 | glog.V(logger.Debug).Infof("extra: %x\n", extra) 179 | return nil 180 | } 181 | return extra 182 | } 183 | 184 | // geth is the main entry point into the system if no special subcommand is ran. 185 | // It creates a default node based on the command line arguments and runs it in 186 | // blocking mode, waiting for it to be shut down. 187 | func geth(ctx *cli.Context) { 188 | relconf := release.Config{ 189 | Oracle: common.HexToAddress(VersionOracle), 190 | Major: uint32(1), 191 | Minor: uint32(5), 192 | Patch: uint32(0), 193 | } 194 | node := utils.MakeSystemNode(ClientIdentifier, nodeNameVersion, relconf, makeDefaultExtra(), ctx) 195 | startNode(ctx, node) 196 | node.Wait() 197 | } 198 | 199 | func startNode(ctx *cli.Context, stack *node.Node) { 200 | eqConfig := ðerquery.EtherQueryConfig{ 201 | Project: ctx.GlobalString("gcpproject"), 202 | Dataset: ctx.GlobalString("dataset"), 203 | BatchInterval: time.Second * 15, 204 | BatchSize: 500, 205 | } 206 | 207 | if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { 208 | return etherquery.New(eqConfig, ctx); 209 | }); err != nil { 210 | utils.Fatalf("Failed to register the etherquery service: %v", err) 211 | } 212 | 213 | // Start up the node itself 214 | utils.StartNode(stack) 215 | 216 | var ethereum *eth.Ethereum 217 | if err := stack.Service(ðereum); err != nil { 218 | utils.Fatalf("ethereum service not running: %v", err) 219 | } 220 | 221 | // Start auxiliary services if enabled 222 | if ctx.GlobalBool(utils.MiningEnabledFlag.Name) { 223 | if err := ethereum.StartMining(ctx.GlobalInt(utils.MinerThreadsFlag.Name), ctx.GlobalString(utils.MiningGPUFlag.Name)); err != nil { 224 | utils.Fatalf("Failed to start mining: %v", err) 225 | } 226 | } 227 | } 228 | 229 | func version(c *cli.Context) { 230 | fmt.Println(ClientIdentifier) 231 | fmt.Println("Version:", Version) 232 | if gitCommit != "" { 233 | fmt.Println("Git Commit:", gitCommit) 234 | } 235 | fmt.Println("Protocol Versions:", eth.ProtocolVersions) 236 | fmt.Println("Network Id:", c.GlobalInt(utils.NetworkIdFlag.Name)) 237 | fmt.Println("Go Version:", runtime.Version()) 238 | fmt.Println("OS:", runtime.GOOS) 239 | fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) 240 | fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) 241 | } --------------------------------------------------------------------------------