forked from cerc-io/ipld-eth-server
Update dependencies
- uses newer version of go-ethereum required for go1.11
This commit is contained in:
+26
-6
@@ -29,7 +29,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
abiFlag = flag.String("abi", "", "Path to the Ethereum contract ABI json to bind")
|
||||
abiFlag = flag.String("abi", "", "Path to the Ethereum contract ABI json to bind, - for STDIN")
|
||||
binFlag = flag.String("bin", "", "Path to the Ethereum contract bytecode (generate deploy method)")
|
||||
typFlag = flag.String("type", "", "Struct name for the binding (default = package name)")
|
||||
|
||||
@@ -75,16 +75,27 @@ func main() {
|
||||
bins []string
|
||||
types []string
|
||||
)
|
||||
if *solFlag != "" {
|
||||
if *solFlag != "" || *abiFlag == "-" {
|
||||
// Generate the list of types to exclude from binding
|
||||
exclude := make(map[string]bool)
|
||||
for _, kind := range strings.Split(*excFlag, ",") {
|
||||
exclude[strings.ToLower(kind)] = true
|
||||
}
|
||||
contracts, err := compiler.CompileSolidity(*solcFlag, *solFlag)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to build Solidity contract: %v\n", err)
|
||||
os.Exit(-1)
|
||||
|
||||
var contracts map[string]*compiler.Contract
|
||||
var err error
|
||||
if *solFlag != "" {
|
||||
contracts, err = compiler.CompileSolidity(*solcFlag, *solFlag)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to build Solidity contract: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
} else {
|
||||
contracts, err = contractsFromStdin()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read input ABIs from STDIN: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
// Gather all non-excluded contract for binding
|
||||
for name, contract := range contracts {
|
||||
@@ -138,3 +149,12 @@ func main() {
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func contractsFromStdin() (map[string]*compiler.Contract, error) {
|
||||
bytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return compiler.ParseCombinedJSON(bytes, "", "", "", "")
|
||||
}
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
+877
@@ -0,0 +1,877 @@
|
||||
Clef
|
||||
----
|
||||
Clef can be used to sign transactions and data and is meant as a replacement for geth's account management.
|
||||
This allows DApps not to depend on geth's account management. When a DApp wants to sign data it can send the data to
|
||||
the signer, the signer will then provide the user with context and asks the user for permission to sign the data. If
|
||||
the users grants the signing request the signer will send the signature back to the DApp.
|
||||
|
||||
This setup allows a DApp to connect to a remote Ethereum node and send transactions that are locally signed. This can
|
||||
help in situations when a DApp is connected to a remote node because a local Ethereum node is not available, not
|
||||
synchronised with the chain or a particular Ethereum node that has no built-in (or limited) account management.
|
||||
|
||||
Clef can run as a daemon on the same machine, or off a usb-stick like [usb armory](https://inversepath.com/usbarmory),
|
||||
or a separate VM in a [QubesOS](https://www.qubes-os.org/) type os setup.
|
||||
|
||||
Check out
|
||||
|
||||
* the [tutorial](tutorial.md) for some concrete examples on how the signer works.
|
||||
* the [setup docs](docs/setup.md) for some information on how to configure it to work on QubesOS or USBArmory.
|
||||
|
||||
|
||||
## Command line flags
|
||||
Clef accepts the following command line options:
|
||||
```
|
||||
COMMANDS:
|
||||
init Initialize the signer, generate secret storage
|
||||
attest Attest that a js-file is to be used
|
||||
addpw Store a credential for a keystore file
|
||||
help Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--loglevel value log level to emit to the screen (default: 4)
|
||||
--keystore value Directory for the keystore (default: "$HOME/.ethereum/keystore")
|
||||
--configdir value Directory for clef configuration (default: "$HOME/.clef")
|
||||
--networkid value Network identifier (integer, 1=Frontier, 2=Morden (disused), 3=Ropsten, 4=Rinkeby) (default: 1)
|
||||
--lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength
|
||||
--nousb Disables monitoring for and managing USB hardware wallets
|
||||
--rpcaddr value HTTP-RPC server listening interface (default: "localhost")
|
||||
--rpcport value HTTP-RPC server listening port (default: 8550)
|
||||
--signersecret value A file containing the password used to encrypt signer credentials, e.g. keystore credentials and ruleset hash
|
||||
--4bytedb value File containing 4byte-identifiers (default: "./4byte.json")
|
||||
--4bytedb-custom value File used for writing new 4byte-identifiers submitted via API (default: "./4byte-custom.json")
|
||||
--auditlog value File used to emit audit logs. Set to "" to disable (default: "audit.log")
|
||||
--rules value Enable rule-engine (default: "rules.json")
|
||||
--stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when the signer is started by an external process.
|
||||
--stdio-ui-test Mechanism to test interface between signer and UI. Requires 'stdio-ui'.
|
||||
--help, -h show help
|
||||
--version, -v print the version
|
||||
|
||||
```
|
||||
|
||||
|
||||
Example:
|
||||
```
|
||||
signer -keystore /my/keystore -chainid 4
|
||||
```
|
||||
|
||||
|
||||
## Security model
|
||||
|
||||
The security model of the signer is as follows:
|
||||
|
||||
* One critical component (the signer binary / daemon) is responsible for handling cryptographic operations: signing, private keys, encryption/decryption of keystore files.
|
||||
* The signer binary has a well-defined 'external' API.
|
||||
* The 'external' API is considered UNTRUSTED.
|
||||
* The signer binary also communicates with whatever process that invoked the binary, via stdin/stdout.
|
||||
* This channel is considered 'trusted'. Over this channel, approvals and passwords are communicated.
|
||||
|
||||
The general flow for signing a transaction using e.g. geth is as follows:
|
||||

|
||||
|
||||
In this case, `geth` would be started with `--externalsigner=http://localhost:8550` and would relay requests to `eth.sendTransaction`.
|
||||
|
||||
## TODOs
|
||||
|
||||
Some snags and todos
|
||||
|
||||
* [ ] The signer should take a startup param "--no-change", for UIs that do not contain the capability
|
||||
to perform changes to things, only approve/deny. Such a UI should be able to start the signer in
|
||||
a more secure mode by telling it that it only wants approve/deny capabilities.
|
||||
|
||||
* [x] It would be nice if the signer could collect new 4byte-id:s/method selectors, and have a
|
||||
secondary database for those (`4byte_custom.json`). Users could then (optionally) submit their collections for
|
||||
inclusion upstream.
|
||||
|
||||
* It should be possible to configure the signer to check if an account is indeed known to it, before
|
||||
passing on to the UI. The reason it currently does not, is that it would make it possible to enumerate
|
||||
accounts if it immediately returned "unknown account".
|
||||
* [x] It should be possible to configure the signer to auto-allow listing (certain) accounts, instead of asking every time.
|
||||
* [x] Done Upon startup, the signer should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode),
|
||||
invoking methods with the following info:
|
||||
* [x] Version info about the signer
|
||||
* [x] Address of API (http/ipc)
|
||||
* [ ] List of known accounts
|
||||
* [ ] Have a default timeout on signing operations, so that if the user has not answered withing e.g. 60 seconds, the request is rejected.
|
||||
* [ ] `account_signRawTransaction`
|
||||
* [ ] `account_bulkSignTransactions([] transactions)` should
|
||||
* only exist if enabled via config/flag
|
||||
* only allow non-data-sending transactions
|
||||
* all txs must use the same `from`-account
|
||||
* let the user confirm, showing
|
||||
* the total amount
|
||||
* the number of unique recipients
|
||||
|
||||
* Geth todos
|
||||
- The signer should pass the `Origin` header as call-info to the UI. As of right now, the way that info about the request is
|
||||
put together is a bit of a hack into the http server. This could probably be greatly improved
|
||||
- Relay: Geth should be started in `geth --external_signer localhost:8550`.
|
||||
- Currently, the Geth APIs use `common.Address` in the arguments to transaction submission (e.g `to` field). This
|
||||
type is 20 `bytes`, and is incapable of carrying checksum information. The signer uses `common.MixedcaseAddress`, which
|
||||
retains the original input.
|
||||
- The Geth api should switch to use the same type, and relay `to`-account verbatim to the external api.
|
||||
|
||||
* [x] Storage
|
||||
* [x] An encrypted key-value storage should be implemented
|
||||
* See [rules.md](rules.md) for more info about this.
|
||||
|
||||
* Another potential thing to introduce is pairing.
|
||||
* To prevent spurious requests which users just accept, implement a way to "pair" the caller with the signer (external API).
|
||||
* Thus geth/mist/cpp would cryptographically handshake and afterwards the caller would be allowed to make signing requests.
|
||||
* This feature would make the addition of rules less dangerous.
|
||||
|
||||
* Wallets / accounts. Add API methods for wallets.
|
||||
|
||||
## Communication
|
||||
|
||||
### External API
|
||||
|
||||
The signer listens to HTTP requests on `rpcaddr`:`rpcport`, with the same JSONRPC standard as Geth. The messages are
|
||||
expected to be JSON [jsonrpc 2.0 standard](http://www.jsonrpc.org/specification).
|
||||
|
||||
Some of these call can require user interaction. Clients must be aware that responses
|
||||
may be delayed significanlty or may never be received if a users decides to ignore the confirmation request.
|
||||
|
||||
The External API is **untrusted** : it does not accept credentials over this api, nor does it expect
|
||||
that requests have any authority.
|
||||
|
||||
### UI API
|
||||
|
||||
The signer has one native console-based UI, for operation without any standalone tools.
|
||||
However, there is also an API to communicate with an external UI. To enable that UI,
|
||||
the signer needs to be executed with the `--stdio-ui` option, which allocates the
|
||||
`stdin`/`stdout` for the UI-api.
|
||||
|
||||
An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`.
|
||||
|
||||
The model is as follows:
|
||||
|
||||
* The user starts the UI app (`pythonsigner.py`).
|
||||
* The UI app starts the `signer` with `--stdio-ui`, and listens to the
|
||||
process output for confirmation-requests.
|
||||
* The `signer` opens the external http api.
|
||||
* When the `signer` receives requests, it sends a `jsonrpc` request via `stdout`.
|
||||
* The UI app prompts the user accordingly, and responds to the `signer`
|
||||
* The `signer` signs (or not), and responds to the original request.
|
||||
|
||||
## External API
|
||||
|
||||
See the [external api changelog](extapi_changelog.md) for information about changes to this API.
|
||||
|
||||
### Encoding
|
||||
- number: positive integers that are hex encoded
|
||||
- data: hex encoded data
|
||||
- string: ASCII string
|
||||
|
||||
All hex encoded values must be prefixed with `0x`.
|
||||
|
||||
## Methods
|
||||
|
||||
### account_new
|
||||
|
||||
#### Create new password protected account
|
||||
|
||||
The signer will generate a new private key, encrypts it according to [web3 keystore spec](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) and stores it in the keystore directory.
|
||||
The client is responsible for creating a backup of the keystore. If the keystore is lost there is no method of retrieving lost accounts.
|
||||
|
||||
#### Arguments
|
||||
|
||||
None
|
||||
|
||||
#### Result
|
||||
- address [string]: account address that is derived from the generated key
|
||||
- url [string]: location of the keyfile
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 0,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_new",
|
||||
"params": []
|
||||
}
|
||||
|
||||
{
|
||||
"id": 0,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"address": "0xbea9183f8f4f03d427f6bcea17388bdff1cab133",
|
||||
"url": "keystore:///my/keystore/UTC--2017-08-24T08-40-15.419655028Z--bea9183f8f4f03d427f6bcea17388bdff1cab133"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### account_list
|
||||
|
||||
#### List available accounts
|
||||
List all accounts that this signer currently manages
|
||||
|
||||
#### Arguments
|
||||
|
||||
None
|
||||
|
||||
#### Result
|
||||
- array with account records:
|
||||
- account.address [string]: account address that is derived from the generated key
|
||||
- account.type [string]: type of the
|
||||
- account.url [string]: location of the account
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_list"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": [
|
||||
{
|
||||
"address": "0xafb2f771f58513609765698f65d3f2f0224a956f",
|
||||
"type": "account",
|
||||
"url": "keystore:///tmp/keystore/UTC--2017-08-24T07-26-47.162109726Z--afb2f771f58513609765698f65d3f2f0224a956f"
|
||||
},
|
||||
{
|
||||
"address": "0xbea9183f8f4f03d427f6bcea17388bdff1cab133",
|
||||
"type": "account",
|
||||
"url": "keystore:///tmp/keystore/UTC--2017-08-24T08-40-15.419655028Z--bea9183f8f4f03d427f6bcea17388bdff1cab133"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### account_signTransaction
|
||||
|
||||
#### Sign transactions
|
||||
Signs a transactions and responds with the signed transaction in RLP encoded form.
|
||||
|
||||
#### Arguments
|
||||
2. transaction object:
|
||||
- `from` [address]: account to send the transaction from
|
||||
- `to` [address]: receiver account. If omitted or `0x`, will cause contract creation.
|
||||
- `gas` [number]: maximum amount of gas to burn
|
||||
- `gasPrice` [number]: gas price
|
||||
- `value` [number:optional]: amount of Wei to send with the transaction
|
||||
- `data` [data:optional]: input data
|
||||
- `nonce` [number]: account nonce
|
||||
3. method signature [string:optional]
|
||||
- The method signature, if present, is to aid decoding the calldata. Should consist of `methodname(paramtype,...)`, e.g. `transfer(uint256,address)`. The signer may use this data to parse the supplied calldata, and show the user. The data, however, is considered totally untrusted, and reliability is not expected.
|
||||
|
||||
|
||||
#### Result
|
||||
- signed transaction in RLP encoded form [data]
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_signTransaction",
|
||||
"params": [
|
||||
{
|
||||
"from": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db",
|
||||
"gas": "0x55555",
|
||||
"gasPrice": "0x1234",
|
||||
"input": "0xabcd",
|
||||
"nonce": "0x0",
|
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"value": "0x1234"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 67,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Request denied"
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Sample call with ABI-data
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_signTransaction",
|
||||
"params": [
|
||||
{
|
||||
"from": "0x694267f14675d7e1b9494fd8d72fefe1755710fa",
|
||||
"gas": "0x333",
|
||||
"gasPrice": "0x1",
|
||||
"nonce": "0x0",
|
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"value": "0x0",
|
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"
|
||||
},
|
||||
"safeSend(address)"
|
||||
],
|
||||
"id": 67
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 67,
|
||||
"result": {
|
||||
"raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663",
|
||||
"tx": {
|
||||
"nonce": "0x0",
|
||||
"gasPrice": "0x1",
|
||||
"gas": "0x333",
|
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"value": "0x0",
|
||||
"input": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012",
|
||||
"v": "0x26",
|
||||
"r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e",
|
||||
"s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663",
|
||||
"hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Bash example:
|
||||
```bash
|
||||
#curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/
|
||||
|
||||
{"jsonrpc":"2.0","id":67,"result":{"raw":"0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","tx":{"nonce":"0x0","gasPrice":"0x1","gas":"0x333","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0","value":"0x0","input":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012","v":"0x26","r":"0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e","s":"0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","hash":"0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"}}}
|
||||
```
|
||||
|
||||
|
||||
### account_sign
|
||||
|
||||
#### Sign data
|
||||
Signs a chunk of data and returns the calculated signature.
|
||||
|
||||
#### Arguments
|
||||
- account [address]: account to sign with
|
||||
- data [data]: data to sign
|
||||
|
||||
#### Result
|
||||
- calculated signature [data]
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_sign",
|
||||
"params": [
|
||||
"0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db",
|
||||
"0xaabbccdd"
|
||||
]
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"jsonrpc": "2.0",
|
||||
"result": "0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c"
|
||||
}
|
||||
```
|
||||
|
||||
### account_ecRecover
|
||||
|
||||
#### Recover address
|
||||
Derive the address from the account that was used to sign data from the data and signature.
|
||||
|
||||
#### Arguments
|
||||
- data [data]: data that was signed
|
||||
- signature [data]: the signature to verify
|
||||
|
||||
#### Result
|
||||
- derived account [address]
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 4,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_ecRecover",
|
||||
"params": [
|
||||
"0xaabbccdd",
|
||||
"0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c"
|
||||
]
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 4,
|
||||
"jsonrpc": "2.0",
|
||||
"result": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### account_import
|
||||
|
||||
#### Import account
|
||||
Import a private key into the keystore. The imported key is expected to be encrypted according to the web3 keystore
|
||||
format.
|
||||
|
||||
#### Arguments
|
||||
- account [object]: key in [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) (retrieved with account_export)
|
||||
|
||||
#### Result
|
||||
- imported key [object]:
|
||||
- key.address [address]: address of the imported key
|
||||
- key.type [string]: type of the account
|
||||
- key.url [string]: key URL
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 6,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_import",
|
||||
"params": [
|
||||
{
|
||||
"address": "c7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||
"crypto": {
|
||||
"cipher": "aes-128-ctr",
|
||||
"cipherparams": {
|
||||
"iv": "401c39a7c7af0388491c3d3ecb39f532"
|
||||
},
|
||||
"ciphertext": "eb045260b18dd35cd0e6d99ead52f8fa1e63a6b0af2d52a8de198e59ad783204",
|
||||
"kdf": "scrypt",
|
||||
"kdfparams": {
|
||||
"dklen": 32,
|
||||
"n": 262144,
|
||||
"p": 1,
|
||||
"r": 8,
|
||||
"salt": "9a657e3618527c9b5580ded60c12092e5038922667b7b76b906496f021bb841a"
|
||||
},
|
||||
"mac": "880dc10bc06e9cec78eb9830aeb1e7a4a26b4c2c19615c94acb632992b952806"
|
||||
},
|
||||
"id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9",
|
||||
"version": 3
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 6,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"address": "0xc7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||
"type": "account",
|
||||
"url": "keystore:///tmp/keystore/UTC--2017-08-24T11-00-42.032024108Z--c7412fc59930fd90099c917a50e5f11d0934b2f5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### account_export
|
||||
|
||||
#### Export account from keystore
|
||||
Export a private key from the keystore. The exported private key is encrypted with the original passphrase. When the
|
||||
key is imported later this passphrase is required.
|
||||
|
||||
#### Arguments
|
||||
- account [address]: export private key that is associated with this account
|
||||
|
||||
#### Result
|
||||
- exported key, see [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) for
|
||||
more information
|
||||
|
||||
#### Sample call
|
||||
```json
|
||||
{
|
||||
"id": 5,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_export",
|
||||
"params": [
|
||||
"0xc7412fc59930fd90099c917a50e5f11d0934b2f5"
|
||||
]
|
||||
}
|
||||
```
|
||||
Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 5,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"address": "c7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||
"crypto": {
|
||||
"cipher": "aes-128-ctr",
|
||||
"cipherparams": {
|
||||
"iv": "401c39a7c7af0388491c3d3ecb39f532"
|
||||
},
|
||||
"ciphertext": "eb045260b18dd35cd0e6d99ead52f8fa1e63a6b0af2d52a8de198e59ad783204",
|
||||
"kdf": "scrypt",
|
||||
"kdfparams": {
|
||||
"dklen": 32,
|
||||
"n": 262144,
|
||||
"p": 1,
|
||||
"r": 8,
|
||||
"salt": "9a657e3618527c9b5580ded60c12092e5038922667b7b76b906496f021bb841a"
|
||||
},
|
||||
"mac": "880dc10bc06e9cec78eb9830aeb1e7a4a26b4c2c19615c94acb632992b952806"
|
||||
},
|
||||
"id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9",
|
||||
"version": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## UI API
|
||||
|
||||
These methods needs to be implemented by a UI listener.
|
||||
|
||||
By starting the signer with the switch `--stdio-ui-test`, the signer will invoke all known methods, and expect the UI to respond with
|
||||
denials. This can be used during development to ensure that the API is (at least somewhat) correctly implemented.
|
||||
See `pythonsigner`, which can be invoked via `python3 pythonsigner.py test` to perform the 'denial-handshake-test'.
|
||||
|
||||
All methods in this API uses object-based parameters, so that there can be no mixups of parameters: each piece of data is accessed by key.
|
||||
|
||||
See the [ui api changelog](intapi_changelog.md) for information about changes to this API.
|
||||
|
||||
OBS! A slight deviation from `json` standard is in place: every request and response should be confined to a single line.
|
||||
Whereas the `json` specification allows for linebreaks, linebreaks __should not__ be used in this communication channel, to make
|
||||
things simpler for both parties.
|
||||
|
||||
### ApproveTx
|
||||
|
||||
Invoked when there's a transaction for approval.
|
||||
|
||||
|
||||
#### Sample call
|
||||
|
||||
Here's a method invocation:
|
||||
```bash
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/
|
||||
```
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "ApproveTx",
|
||||
"params": [
|
||||
{
|
||||
"transaction": {
|
||||
"from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa",
|
||||
"to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"gas": "0x333",
|
||||
"gasPrice": "0x1",
|
||||
"value": "0x0",
|
||||
"nonce": "0x0",
|
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012",
|
||||
"input": null
|
||||
},
|
||||
"call_info": [
|
||||
{
|
||||
"type": "WARNING",
|
||||
"message": "Invalid checksum on to-address"
|
||||
},
|
||||
{
|
||||
"type": "Info",
|
||||
"message": "safeSend(address: 0x0000000000000000000000000000000000000012)"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"remote": "127.0.0.1:48486",
|
||||
"local": "localhost:8550",
|
||||
"scheme": "HTTP/1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The same method invocation, but with invalid data:
|
||||
```bash
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000002000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/
|
||||
```
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "ApproveTx",
|
||||
"params": [
|
||||
{
|
||||
"transaction": {
|
||||
"from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa",
|
||||
"to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"gas": "0x333",
|
||||
"gasPrice": "0x1",
|
||||
"value": "0x0",
|
||||
"nonce": "0x0",
|
||||
"data": "0x4401a6e40000000000000002000000000000000000000000000000000000000000000012",
|
||||
"input": null
|
||||
},
|
||||
"call_info": [
|
||||
{
|
||||
"type": "WARNING",
|
||||
"message": "Invalid checksum on to-address"
|
||||
},
|
||||
{
|
||||
"type": "WARNING",
|
||||
"message": "Transaction data did not match ABI-interface: WARNING: Supplied data is stuffed with extra data. \nWant 0000000000000002000000000000000000000000000000000000000000000012\nHave 0000000000000000000000000000000000000000000000000000000000000012\nfor method safeSend(address)"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"remote": "127.0.0.1:48492",
|
||||
"local": "localhost:8550",
|
||||
"scheme": "HTTP/1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
One which has missing `to`, but with no `data`:
|
||||
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "ApproveTx",
|
||||
"params": [
|
||||
{
|
||||
"transaction": {
|
||||
"from": "",
|
||||
"to": null,
|
||||
"gas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"value": "0x0",
|
||||
"nonce": "0x0",
|
||||
"data": null,
|
||||
"input": null
|
||||
},
|
||||
"call_info": [
|
||||
{
|
||||
"type": "CRITICAL",
|
||||
"message": "Tx will create contract with empty code!"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"remote": "signer binary",
|
||||
"local": "main",
|
||||
"scheme": "in-proc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### ApproveExport
|
||||
|
||||
Invoked when a request to export an account has been made.
|
||||
|
||||
#### Sample call
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"method": "ApproveExport",
|
||||
"params": [
|
||||
{
|
||||
"address": "0x0000000000000000000000000000000000000000",
|
||||
"meta": {
|
||||
"remote": "signer binary",
|
||||
"local": "main",
|
||||
"scheme": "in-proc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### ApproveListing
|
||||
|
||||
Invoked when a request for account listing has been made.
|
||||
|
||||
#### Sample call
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "ApproveListing",
|
||||
"params": [
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"type": "Account",
|
||||
"url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-20T14-44-54.089682944Z--123409812340981234098123409812deadbeef42",
|
||||
"address": "0x123409812340981234098123409812deadbeef42"
|
||||
},
|
||||
{
|
||||
"type": "Account",
|
||||
"url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-23T21-59-03.199240693Z--cafebabedeadbeef34098123409812deadbeef42",
|
||||
"address": "0xcafebabedeadbeef34098123409812deadbeef42"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"remote": "signer binary",
|
||||
"local": "main",
|
||||
"scheme": "in-proc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### ApproveSignData
|
||||
|
||||
#### Sample call
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "ApproveSignData",
|
||||
"params": [
|
||||
{
|
||||
"address": "0x123409812340981234098123409812deadbeef42",
|
||||
"raw_data": "0x01020304",
|
||||
"message": "\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004",
|
||||
"hash": "0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310",
|
||||
"meta": {
|
||||
"remote": "signer binary",
|
||||
"local": "main",
|
||||
"scheme": "in-proc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### ShowInfo
|
||||
|
||||
The UI should show the info to the user. Does not expect response.
|
||||
|
||||
#### Sample call
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 9,
|
||||
"method": "ShowInfo",
|
||||
"params": [
|
||||
{
|
||||
"text": "Tests completed"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### ShowError
|
||||
|
||||
The UI should show the info to the user. Does not expect response.
|
||||
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "ShowError",
|
||||
"params": [
|
||||
{
|
||||
"text": "Testing 'ShowError'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### OnApproved
|
||||
|
||||
`OnApprovedTx` is called when a transaction has been approved and signed. The call contains the return value that will be sent to the external caller. The return value from this method is ignored - the reason for having this callback is to allow the ruleset to keep track of approved transactions.
|
||||
|
||||
When implementing rate-limited rules, this callback should be used.
|
||||
|
||||
TLDR; Use this method to keep track of signed transactions, instead of using the data in `ApproveTx`.
|
||||
|
||||
### OnSignerStartup
|
||||
|
||||
This method provide the UI with information about what API version the signer uses (both internal and external) aswell as build-info and external api,
|
||||
in k/v-form.
|
||||
|
||||
Example call:
|
||||
```json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "OnSignerStartup",
|
||||
"params": [
|
||||
{
|
||||
"info": {
|
||||
"extapi_http": "http://localhost:8550",
|
||||
"extapi_ipc": null,
|
||||
"extapi_version": "2.0.0",
|
||||
"intapi_version": "1.2.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Rules for UI apis
|
||||
|
||||
A UI should conform to the following rules.
|
||||
|
||||
* A UI MUST NOT load any external resources that were not embedded/part of the UI package.
|
||||
* For example, not load icons, stylesheets from the internet
|
||||
* Not load files from the filesystem, unless they reside in the same local directory (e.g. config files)
|
||||
* A Graphical UI MUST show the blocky-identicon for ethereum addresses.
|
||||
* A UI MUST warn display approproate warning if the destination-account is formatted with invalid checksum.
|
||||
* A UI MUST NOT open any ports or services
|
||||
* The signer opens the public port
|
||||
* A UI SHOULD verify the permissions on the signer binary, and refuse to execute or warn if permissions allow non-user write.
|
||||
* A UI SHOULD inform the user about the `SHA256` or `MD5` hash of the binary being executed
|
||||
* A UI SHOULD NOT maintain a secondary storage of data, e.g. list of accounts
|
||||
* The signer provides accounts
|
||||
* A UI SHOULD, to the best extent possible, use static linking / bundling, so that requried libraries are bundled
|
||||
along with the UI.
|
||||
|
||||
|
||||
### UI Implementations
|
||||
|
||||
There are a couple of implementation for a UI. We'll try to keep this list up to date.
|
||||
|
||||
| Name | Repo | UI type| No external resources| Blocky support| Verifies permissions | Hash information | No secondary storage | Statically linked| Can modify parameters|
|
||||
| ---- | ---- | -------| ---- | ---- | ---- |---- | ---- | ---- | ---- |
|
||||
| QtSigner| https://github.com/holiman/qtsigner/| Python3/QT-based| :+1:| :+1:| :+1:| :+1:| :+1:| :x: | :+1: (partially)|
|
||||
| GtkSigner| https://github.com/holiman/gtksigner| Python3/GTK-based| :+1:| :x:| :x:| :+1:| :+1:| :x: | :x: |
|
||||
| Frame | https://github.com/floating/frame/commits/go-signer| Electron-based| :x:| :x:| :x:| :x:| ?| :x: | :x: |
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+23
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
This implements a dispatcher which listens to localhost:8550, and proxies
|
||||
requests via qrexec to the service qubes.EthSign on a target domain
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import socketserver,subprocess
|
||||
|
||||
PORT=8550
|
||||
TARGET_DOMAIN= 'debian-work'
|
||||
|
||||
class Dispatcher(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
post_data = self.rfile.read(int(self.headers['Content-Length']))
|
||||
p = subprocess.Popen(['/usr/bin/qrexec-client-vm',TARGET_DOMAIN,'qubes.Clefsign'],stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
output = p.communicate(post_data)[0]
|
||||
self.wfile.write(output)
|
||||
|
||||
|
||||
with socketserver.TCPServer(("",PORT), Dispatcher) as httpd:
|
||||
print("Serving at port", PORT)
|
||||
httpd.serve_forever()
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
SIGNER_BIN="/home/user/tools/clef/clef"
|
||||
SIGNER_CMD="/home/user/tools/gtksigner/gtkui.py -s $SIGNER_BIN"
|
||||
|
||||
# Start clef if not already started
|
||||
if [ ! -S /home/user/.clef/clef.ipc ]; then
|
||||
$SIGNER_CMD &
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Should be started by now
|
||||
if [ -S /home/user/.clef/clef.ipc ]; then
|
||||
# Post incoming request to HTTP channel
|
||||
curl -H "Content-Type: application/json" -X POST -d @- http://localhost:8550 2>/dev/null
|
||||
fi
|
||||
Generated
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Generated
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
+198
@@ -0,0 +1,198 @@
|
||||
# Setting up Clef
|
||||
|
||||
This document describes how Clef can be used in a more secure manner than executing it from your everyday laptop,
|
||||
in order to ensure that the keys remain safe in the event that your computer should get compromised.
|
||||
|
||||
## Qubes OS
|
||||
|
||||
|
||||
### Background
|
||||
|
||||
The Qubes operating system is based around virtual machines (qubes), where a set of virtual machines are configured, typically for
|
||||
different purposes such as:
|
||||
|
||||
- personal
|
||||
- Your personal email, browsing etc
|
||||
- work
|
||||
- Work email etc
|
||||
- vault
|
||||
- a VM without network access, where gpg-keys and/or keepass credentials are stored.
|
||||
|
||||
A couple of dedicated virtual machines handle externalities:
|
||||
|
||||
- sys-net provides networking to all other (network-enabled) machines
|
||||
- sys-firewall handles firewall rules
|
||||
- sys-usb handles USB devices, and can map usb-devices to certain qubes.
|
||||
|
||||
The goal of this document is to describe how we can set up clef to provide secure transaction
|
||||
signing from a `vault` vm, to another networked qube which runs Dapps.
|
||||
|
||||
### Setup
|
||||
|
||||
There are two ways that this can be achieved: integrated via Qubes or integrated via networking.
|
||||
|
||||
|
||||
#### 1. Qubes Integrated
|
||||
|
||||
Qubes provdes a facility for inter-qubes communication via `qrexec`. A qube can request to make a cross-qube RPC request
|
||||
to another qube. The OS then asks the user if the call is permitted.
|
||||
|
||||

|
||||
|
||||
A policy-file can be created to allow such interaction. On the `target` domain, a service is invoked which can read the
|
||||
`stdin` from the `client` qube.
|
||||
|
||||
This is how [Split GPG](https://www.qubes-os.org/doc/split-gpg/) is implemented. We can set up Clef the same way:
|
||||
|
||||
##### Server
|
||||
|
||||

|
||||
|
||||
On the `target` qubes, we need to define the rpc service.
|
||||
|
||||
[qubes.Clefsign](qubes/qubes.Clefsign):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
SIGNER_BIN="/home/user/tools/clef/clef"
|
||||
SIGNER_CMD="/home/user/tools/gtksigner/gtkui.py -s $SIGNER_BIN"
|
||||
|
||||
# Start clef if not already started
|
||||
if [ ! -S /home/user/.clef/clef.ipc ]; then
|
||||
$SIGNER_CMD &
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Should be started by now
|
||||
if [ -S /home/user/.clef/clef.ipc ]; then
|
||||
# Post incoming request to HTTP channel
|
||||
curl -H "Content-Type: application/json" -X POST -d @- http://localhost:8550 2>/dev/null
|
||||
fi
|
||||
|
||||
```
|
||||
This RPC service is not complete (see notes about HTTP headers below), but works as a proof-of-concept.
|
||||
It will forward the data received on `stdin` (forwarded by the OS) to Clef's HTTP channel.
|
||||
|
||||
It would have been possible to send data directly to the `/home/user/.clef/.clef.ipc`
|
||||
socket via e.g `nc -U /home/user/.clef/clef.ipc`, but the reason for sending the request
|
||||
data over `HTTP` instead of `IPC` is that we want the ability to forward `HTTP` headers.
|
||||
|
||||
To enable the service:
|
||||
|
||||
``` bash
|
||||
sudo cp qubes.Clefsign /etc/qubes-rpc/
|
||||
sudo chmod +x /etc/qubes-rpc/ qubes.Clefsign
|
||||
```
|
||||
|
||||
This setup uses [gtksigner](https://github.com/holiman/gtksigner), which is a very minimal GTK-based UI that works well
|
||||
with minimal requirements.
|
||||
|
||||
##### Client
|
||||
|
||||
|
||||
On the `client` qube, we need to create a listener which will receive the request from the Dapp, and proxy it.
|
||||
|
||||
|
||||
[qubes-client.py](qubes/client/qubes-client.py):
|
||||
|
||||
```python
|
||||
|
||||
"""
|
||||
This implements a dispatcher which listens to localhost:8550, and proxies
|
||||
requests via qrexec to the service qubes.EthSign on a target domain
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import socketserver,subprocess
|
||||
|
||||
PORT=8550
|
||||
TARGET_DOMAIN= 'debian-work'
|
||||
|
||||
class Dispatcher(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
post_data = self.rfile.read(int(self.headers['Content-Length']))
|
||||
p = subprocess.Popen(['/usr/bin/qrexec-client-vm',TARGET_DOMAIN,'qubes.Clefsign'],stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
output = p.communicate(post_data)[0]
|
||||
self.wfile.write(output)
|
||||
|
||||
|
||||
with socketserver.TCPServer(("",PORT), Dispatcher) as httpd:
|
||||
print("Serving at port", PORT)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
```
|
||||
|
||||
#### Testing
|
||||
|
||||
To test the flow, if we have set up `debian-work` as the `target`, we can do
|
||||
|
||||
```bash
|
||||
$ cat newaccnt.json
|
||||
{ "id": 0, "jsonrpc": "2.0","method": "account_new","params": []}
|
||||
|
||||
$ cat newaccnt.json| qrexec-client-vm debian-work qubes.Clefsign
|
||||
```
|
||||
|
||||
This should pop up first a dialog to allow the IPC call:
|
||||
|
||||

|
||||
|
||||
Followed by a GTK-dialog to approve the operation
|
||||
|
||||

|
||||
|
||||
To test the full flow, we use the client wrapper. Start it on the `client` qube:
|
||||
```
|
||||
[user@work qubes]$ python3 qubes-client.py
|
||||
```
|
||||
|
||||
Make the request over http (`client` qube):
|
||||
```
|
||||
[user@work clef]$ cat newaccnt.json | curl -X POST -d @- http://localhost:8550
|
||||
```
|
||||
And it should show the same popups again.
|
||||
|
||||
##### Pros and cons
|
||||
|
||||
The benefits of this setup are:
|
||||
|
||||
- This is the qubes-os intended model for inter-qube communication,
|
||||
- and thus benefits from qubes-os dialogs and policies for user approval
|
||||
|
||||
However, it comes with a couple of drawbacks:
|
||||
|
||||
- The `qubes-gpg-client` must forward the http request via RPC to the `target` qube. When doing so, the proxy
|
||||
will either drop important headers, or replace them.
|
||||
- The `Host` header is most likely `localhost`
|
||||
- The `Origin` header must be forwarded
|
||||
- Information about the remote ip must be added as a `X-Forwarded-For`. However, Clef cannot always trust an `XFF` header,
|
||||
since malicious clients may lie about `XFF` in order to fool the http server into believing it comes from another address.
|
||||
- Even with a policy in place to allow rpc-calls between `caller` and `target`, there will be several popups:
|
||||
- One qubes-specific where the user specifies the `target` vm
|
||||
- One clef-specific to approve the transaction
|
||||
|
||||
|
||||
#### 2. Network integrated
|
||||
|
||||
The second way to set up Clef on a qubes system is to allow networking, and have Clef listen to a port which is accessible
|
||||
form other qubes.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
## USBArmory
|
||||
|
||||
The [USB armory](https://inversepath.com/usbarmory) is an open source hardware design with an 800 Mhz ARM processor. It is a pocket-size
|
||||
computer. When inserted into a laptop, it identifies itself as a USB network interface, basically adding another network
|
||||
to your computer. Over this new network interface, you can SSH into the device.
|
||||
|
||||
Running Clef off a USB armory means that you can use the armory as a very versatile offline computer, which only
|
||||
ever connects to a local network between your computer and the device itself.
|
||||
|
||||
Needless to say, the while this model should be fairly secure against remote attacks, an attacker with physical access
|
||||
to the USB Armory would trivially be able to extract the contents of the device filesystem.
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
### Changelog for external API
|
||||
|
||||
|
||||
|
||||
#### 2.0.0
|
||||
|
||||
* Commit `73abaf04b1372fa4c43201fb1b8019fe6b0a6f8d`, move `from` into `transaction` object in `signTransaction`. This
|
||||
makes the `accounts_signTransaction` identical to the old `eth_signTransaction`.
|
||||
|
||||
|
||||
#### 1.0.0
|
||||
|
||||
Initial release.
|
||||
|
||||
### Versioning
|
||||
|
||||
The API uses [semantic versioning](https://semver.org/).
|
||||
|
||||
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the:
|
||||
|
||||
* MAJOR version when you make incompatible API changes,
|
||||
* MINOR version when you add functionality in a backwards-compatible manner, and
|
||||
* PATCH version when you make backwards-compatible bug fixes.
|
||||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
### Changelog for internal API (ui-api)
|
||||
|
||||
### 2.0.0
|
||||
|
||||
* Modify how `call_info` on a transaction is conveyed. New format:
|
||||
|
||||
```
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "ApproveTx",
|
||||
"params": [
|
||||
{
|
||||
"transaction": {
|
||||
"from": "0x82A2A876D39022B3019932D30Cd9c97ad5616813",
|
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||
"gas": "0x333",
|
||||
"gasPrice": "0x123",
|
||||
"value": "0x10",
|
||||
"nonce": "0x0",
|
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012",
|
||||
"input": null
|
||||
},
|
||||
"call_info": [
|
||||
{
|
||||
"type": "WARNING",
|
||||
"message": "Invalid checksum on to-address"
|
||||
},
|
||||
{
|
||||
"type": "WARNING",
|
||||
"message": "Tx contains data, but provided ABI signature could not be matched: Did not match: test (0 matches)"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"remote": "127.0.0.1:54286",
|
||||
"local": "localhost:8550",
|
||||
"scheme": "HTTP/1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2.0
|
||||
|
||||
* Add `OnStartup` method, to provide the UI with information about what API version
|
||||
the signer uses (both internal and external) aswell as build-info and external api.
|
||||
|
||||
Example call:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "OnSignerStartup",
|
||||
"params": [
|
||||
{
|
||||
"info": {
|
||||
"extapi_http": "http://localhost:8550",
|
||||
"extapi_ipc": null,
|
||||
"extapi_version": "2.0.0",
|
||||
"intapi_version": "1.2.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.1.0
|
||||
|
||||
* Add `OnApproved` method
|
||||
|
||||
#### 1.0.0
|
||||
|
||||
Initial release.
|
||||
|
||||
### Versioning
|
||||
|
||||
The API uses [semantic versioning](https://semver.org/).
|
||||
|
||||
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the:
|
||||
|
||||
* MAJOR version when you make incompatible API changes,
|
||||
* MINOR version when you add functionality in a backwards-compatible manner, and
|
||||
* PATCH version when you make backwards-compatible bug fixes.
|
||||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// signer is a utility that can be used so sign transactions and
|
||||
// arbitrary data.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/signer/core"
|
||||
"github.com/ethereum/go-ethereum/signer/rules"
|
||||
"github.com/ethereum/go-ethereum/signer/storage"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// ExternalAPIVersion -- see extapi_changelog.md
|
||||
const ExternalAPIVersion = "2.0.0"
|
||||
|
||||
// InternalAPIVersion -- see intapi_changelog.md
|
||||
const InternalAPIVersion = "2.0.0"
|
||||
|
||||
const legalWarning = `
|
||||
WARNING!
|
||||
|
||||
Clef is alpha software, and not yet publically released. This software has _not_ been audited, and there
|
||||
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
|
||||
unless you agree to take full responsibility for doing so, and know what you are doing.
|
||||
|
||||
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
|
||||
|
||||
`
|
||||
|
||||
var (
|
||||
logLevelFlag = cli.IntFlag{
|
||||
Name: "loglevel",
|
||||
Value: 4,
|
||||
Usage: "log level to emit to the screen",
|
||||
}
|
||||
keystoreFlag = cli.StringFlag{
|
||||
Name: "keystore",
|
||||
Value: filepath.Join(node.DefaultDataDir(), "keystore"),
|
||||
Usage: "Directory for the keystore",
|
||||
}
|
||||
configdirFlag = cli.StringFlag{
|
||||
Name: "configdir",
|
||||
Value: DefaultConfigDir(),
|
||||
Usage: "Directory for Clef configuration",
|
||||
}
|
||||
rpcPortFlag = cli.IntFlag{
|
||||
Name: "rpcport",
|
||||
Usage: "HTTP-RPC server listening port",
|
||||
Value: node.DefaultHTTPPort + 5,
|
||||
}
|
||||
signerSecretFlag = cli.StringFlag{
|
||||
Name: "signersecret",
|
||||
Usage: "A file containing the password used to encrypt Clef credentials, e.g. keystore credentials and ruleset hash",
|
||||
}
|
||||
dBFlag = cli.StringFlag{
|
||||
Name: "4bytedb",
|
||||
Usage: "File containing 4byte-identifiers",
|
||||
Value: "./4byte.json",
|
||||
}
|
||||
customDBFlag = cli.StringFlag{
|
||||
Name: "4bytedb-custom",
|
||||
Usage: "File used for writing new 4byte-identifiers submitted via API",
|
||||
Value: "./4byte-custom.json",
|
||||
}
|
||||
auditLogFlag = cli.StringFlag{
|
||||
Name: "auditlog",
|
||||
Usage: "File used to emit audit logs. Set to \"\" to disable",
|
||||
Value: "audit.log",
|
||||
}
|
||||
ruleFlag = cli.StringFlag{
|
||||
Name: "rules",
|
||||
Usage: "Enable rule-engine",
|
||||
Value: "rules.json",
|
||||
}
|
||||
stdiouiFlag = cli.BoolFlag{
|
||||
Name: "stdio-ui",
|
||||
Usage: "Use STDIN/STDOUT as a channel for an external UI. " +
|
||||
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
|
||||
"interface, and can be used when Clef is started by an external process.",
|
||||
}
|
||||
testFlag = cli.BoolFlag{
|
||||
Name: "stdio-ui-test",
|
||||
Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
|
||||
}
|
||||
app = cli.NewApp()
|
||||
initCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(initializeSecrets),
|
||||
Name: "init",
|
||||
Usage: "Initialize the signer, generate secret storage",
|
||||
ArgsUsage: "",
|
||||
Flags: []cli.Flag{
|
||||
logLevelFlag,
|
||||
configdirFlag,
|
||||
},
|
||||
Description: `
|
||||
The init command generates a master seed which Clef can use to store credentials and data needed for
|
||||
the rule-engine to work.`,
|
||||
}
|
||||
attestCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(attestFile),
|
||||
Name: "attest",
|
||||
Usage: "Attest that a js-file is to be used",
|
||||
ArgsUsage: "<sha256sum>",
|
||||
Flags: []cli.Flag{
|
||||
logLevelFlag,
|
||||
configdirFlag,
|
||||
signerSecretFlag,
|
||||
},
|
||||
Description: `
|
||||
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
|
||||
incoming requests.
|
||||
|
||||
Whenever you make an edit to the rule file, you need to use attestation to tell
|
||||
Clef that the file is 'safe' to execute.`,
|
||||
}
|
||||
|
||||
addCredentialCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(addCredential),
|
||||
Name: "addpw",
|
||||
Usage: "Store a credential for a keystore file",
|
||||
ArgsUsage: "<address> <password>",
|
||||
Flags: []cli.Flag{
|
||||
logLevelFlag,
|
||||
configdirFlag,
|
||||
signerSecretFlag,
|
||||
},
|
||||
Description: `
|
||||
The addpw command stores a password for a given address (keyfile). If you invoke it with only one parameter, it will
|
||||
remove any stored credential for that address (keyfile)
|
||||
`,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
app.Name = "Clef"
|
||||
app.Usage = "Manage Ethereum account operations"
|
||||
app.Flags = []cli.Flag{
|
||||
logLevelFlag,
|
||||
keystoreFlag,
|
||||
configdirFlag,
|
||||
utils.NetworkIdFlag,
|
||||
utils.LightKDFFlag,
|
||||
utils.NoUSBFlag,
|
||||
utils.RPCListenAddrFlag,
|
||||
utils.RPCVirtualHostsFlag,
|
||||
utils.IPCDisabledFlag,
|
||||
utils.IPCPathFlag,
|
||||
utils.RPCEnabledFlag,
|
||||
rpcPortFlag,
|
||||
signerSecretFlag,
|
||||
dBFlag,
|
||||
customDBFlag,
|
||||
auditLogFlag,
|
||||
ruleFlag,
|
||||
stdiouiFlag,
|
||||
testFlag,
|
||||
}
|
||||
app.Action = signer
|
||||
app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand}
|
||||
|
||||
}
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initializeSecrets(c *cli.Context) error {
|
||||
if err := initialize(c); err != nil {
|
||||
return err
|
||||
}
|
||||
configDir := c.String(configdirFlag.Name)
|
||||
|
||||
masterSeed := make([]byte, 256)
|
||||
n, err := io.ReadFull(rand.Reader, masterSeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(masterSeed) {
|
||||
return fmt.Errorf("failed to read enough random")
|
||||
}
|
||||
err = os.Mkdir(configDir, 0700)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
location := filepath.Join(configDir, "secrets.dat")
|
||||
if _, err := os.Stat(location); err == nil {
|
||||
return fmt.Errorf("file %v already exists, will not overwrite", location)
|
||||
}
|
||||
err = ioutil.WriteFile(location, masterSeed, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("A master seed has been generated into %s\n", location)
|
||||
fmt.Printf(`
|
||||
This is required to be able to store credentials, such as :
|
||||
* Passwords for keystores (used by rule engine)
|
||||
* Storage for javascript rules
|
||||
* Hash of rule-file
|
||||
|
||||
You should treat that file with utmost secrecy, and make a backup of it.
|
||||
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
||||
|
||||
`)
|
||||
return nil
|
||||
}
|
||||
func attestFile(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
}
|
||||
if err := initialize(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stretchedKey, err := readMasterKey(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
configDir := ctx.String(configdirFlag.Name)
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
||||
confKey := crypto.Keccak256([]byte("config"), stretchedKey)
|
||||
|
||||
// Initialize the encrypted storages
|
||||
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey)
|
||||
val := ctx.Args().First()
|
||||
configStorage.Put("ruleset_sha256", val)
|
||||
log.Info("Ruleset attestation updated", "sha256", val)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addCredential(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires at leaste one argument.")
|
||||
}
|
||||
if err := initialize(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stretchedKey, err := readMasterKey(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
configDir := ctx.String(configdirFlag.Name)
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
|
||||
|
||||
// Initialize the encrypted storages
|
||||
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
|
||||
key := ctx.Args().First()
|
||||
value := ""
|
||||
if len(ctx.Args()) > 1 {
|
||||
value = ctx.Args().Get(1)
|
||||
}
|
||||
pwStorage.Put(key, value)
|
||||
log.Info("Credential store updated", "key", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func initialize(c *cli.Context) error {
|
||||
// Set up the logger to print everything
|
||||
logOutput := os.Stdout
|
||||
if c.Bool(stdiouiFlag.Name) {
|
||||
logOutput = os.Stderr
|
||||
// If using the stdioui, we can't do the 'confirm'-flow
|
||||
fmt.Fprintf(logOutput, legalWarning)
|
||||
} else {
|
||||
if !confirm(legalWarning) {
|
||||
return fmt.Errorf("aborted by user")
|
||||
}
|
||||
}
|
||||
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true))))
|
||||
return nil
|
||||
}
|
||||
|
||||
func signer(c *cli.Context) error {
|
||||
if err := initialize(c); err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
ui core.SignerUI
|
||||
)
|
||||
if c.Bool(stdiouiFlag.Name) {
|
||||
log.Info("Using stdin/stdout as UI-channel")
|
||||
ui = core.NewStdIOUI()
|
||||
} else {
|
||||
log.Info("Using CLI as UI-channel")
|
||||
ui = core.NewCommandlineUI()
|
||||
}
|
||||
db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name))
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb"))
|
||||
|
||||
var (
|
||||
api core.ExternalAPI
|
||||
)
|
||||
|
||||
configDir := c.String(configdirFlag.Name)
|
||||
if stretchedKey, err := readMasterKey(c); err != nil {
|
||||
log.Info("No master seed provided, rules disabled")
|
||||
} else {
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
||||
|
||||
// Generate domain specific keys
|
||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
|
||||
jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey)
|
||||
confkey := crypto.Keccak256([]byte("config"), stretchedKey)
|
||||
|
||||
// Initialize the encrypted storages
|
||||
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
|
||||
jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
|
||||
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
|
||||
|
||||
//Do we have a rule-file?
|
||||
ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name))
|
||||
if err != nil {
|
||||
log.Info("Could not load rulefile, rules not enabled", "file", "rulefile")
|
||||
} else {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(ruleJS)
|
||||
shasum := hasher.Sum(nil)
|
||||
storedShasum := configStorage.Get("ruleset_sha256")
|
||||
if storedShasum != hex.EncodeToString(shasum) {
|
||||
log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", storedShasum)
|
||||
} else {
|
||||
// Initialize rules
|
||||
ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage, pwStorage)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
ruleEngine.Init(string(ruleJS))
|
||||
ui = ruleEngine
|
||||
log.Info("Rule engine configured", "file", c.String(ruleFlag.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apiImpl := core.NewSignerAPI(
|
||||
c.Int64(utils.NetworkIdFlag.Name),
|
||||
c.String(keystoreFlag.Name),
|
||||
c.Bool(utils.NoUSBFlag.Name),
|
||||
ui, db,
|
||||
c.Bool(utils.LightKDFFlag.Name))
|
||||
|
||||
api = apiImpl
|
||||
|
||||
// Audit logging
|
||||
if logfile := c.String(auditLogFlag.Name); logfile != "" {
|
||||
api, err = core.NewAuditLogger(logfile, api)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
log.Info("Audit logs configured", "file", logfile)
|
||||
}
|
||||
// register signer API with server
|
||||
var (
|
||||
extapiURL = "n/a"
|
||||
ipcapiURL = "n/a"
|
||||
)
|
||||
rpcAPI := []rpc.API{
|
||||
{
|
||||
Namespace: "account",
|
||||
Public: true,
|
||||
Service: api,
|
||||
Version: "1.0"},
|
||||
}
|
||||
if c.Bool(utils.RPCEnabledFlag.Name) {
|
||||
|
||||
vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name))
|
||||
cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name))
|
||||
|
||||
// start http server
|
||||
httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name))
|
||||
listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not start RPC api: %v", err)
|
||||
}
|
||||
extapiURL = fmt.Sprintf("http://%s", httpEndpoint)
|
||||
log.Info("HTTP endpoint opened", "url", extapiURL)
|
||||
|
||||
defer func() {
|
||||
listener.Close()
|
||||
log.Info("HTTP endpoint closed", "url", httpEndpoint)
|
||||
}()
|
||||
|
||||
}
|
||||
if !c.Bool(utils.IPCDisabledFlag.Name) {
|
||||
if c.IsSet(utils.IPCPathFlag.Name) {
|
||||
ipcapiURL = c.String(utils.IPCPathFlag.Name)
|
||||
} else {
|
||||
ipcapiURL = filepath.Join(configDir, "clef.ipc")
|
||||
}
|
||||
|
||||
listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not start IPC api: %v", err)
|
||||
}
|
||||
log.Info("IPC endpoint opened", "url", ipcapiURL)
|
||||
defer func() {
|
||||
listener.Close()
|
||||
log.Info("IPC endpoint closed", "url", ipcapiURL)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
if c.Bool(testFlag.Name) {
|
||||
log.Info("Performing UI test")
|
||||
go testExternalUI(apiImpl)
|
||||
}
|
||||
ui.OnSignerStartup(core.StartupInfo{
|
||||
Info: map[string]interface{}{
|
||||
"extapi_version": ExternalAPIVersion,
|
||||
"intapi_version": InternalAPIVersion,
|
||||
"extapi_http": extapiURL,
|
||||
"extapi_ipc": ipcapiURL,
|
||||
},
|
||||
})
|
||||
|
||||
abortChan := make(chan os.Signal)
|
||||
signal.Notify(abortChan, os.Interrupt)
|
||||
|
||||
sig := <-abortChan
|
||||
log.Info("Exiting...", "signal", sig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitAndTrim splits input separated by a comma
|
||||
// and trims excessive white space from the substrings.
|
||||
func splitAndTrim(input string) []string {
|
||||
result := strings.Split(input, ",")
|
||||
for i, r := range result {
|
||||
result[i] = strings.TrimSpace(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DefaultConfigDir is the default config directory to use for the vaults and other
|
||||
// persistence requirements.
|
||||
func DefaultConfigDir() string {
|
||||
// Try to place the data folder in the user's home dir
|
||||
home := homeDir()
|
||||
if home != "" {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return filepath.Join(home, "Library", "Signer")
|
||||
} else if runtime.GOOS == "windows" {
|
||||
return filepath.Join(home, "AppData", "Roaming", "Signer")
|
||||
} else {
|
||||
return filepath.Join(home, ".clef")
|
||||
}
|
||||
}
|
||||
// As we cannot guess a stable location, return empty and handle later
|
||||
return ""
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if usr, err := user.Current(); err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func readMasterKey(ctx *cli.Context) ([]byte, error) {
|
||||
var (
|
||||
file string
|
||||
configDir = ctx.String(configdirFlag.Name)
|
||||
)
|
||||
if ctx.IsSet(signerSecretFlag.Name) {
|
||||
file = ctx.String(signerSecretFlag.Name)
|
||||
} else {
|
||||
file = filepath.Join(configDir, "secrets.dat")
|
||||
}
|
||||
if err := checkFile(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
masterKey, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(masterKey) < 256 {
|
||||
return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey))
|
||||
}
|
||||
// Create vault location
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10]))
|
||||
err = os.Mkdir(vaultLocation, 0700)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
//!TODO, use KDF to stretch the master key
|
||||
// stretched_key := stretch_key(master_key)
|
||||
|
||||
return masterKey, nil
|
||||
}
|
||||
|
||||
// checkFile is a convenience function to check if a file
|
||||
// * exists
|
||||
// * is mode 0600
|
||||
func checkFile(filename string) error {
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed stat on %s: %v", filename, err)
|
||||
}
|
||||
// Check the unix permission bits
|
||||
if info.Mode().Perm()&077 != 0 {
|
||||
return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// confirm displays a text and asks for user confirmation
|
||||
func confirm(text string) bool {
|
||||
fmt.Printf(text)
|
||||
fmt.Printf("\nEnter 'ok' to proceed:\n>")
|
||||
|
||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
|
||||
if text := strings.TrimSpace(text); text == "ok" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func testExternalUI(api *core.SignerAPI) {
|
||||
|
||||
ctx := context.WithValue(context.Background(), "remote", "clef binary")
|
||||
ctx = context.WithValue(ctx, "scheme", "in-proc")
|
||||
ctx = context.WithValue(ctx, "local", "main")
|
||||
|
||||
errs := make([]string, 0)
|
||||
|
||||
api.UI.ShowInfo("Testing 'ShowInfo'")
|
||||
api.UI.ShowError("Testing 'ShowError'")
|
||||
|
||||
checkErr := func(method string, err error) {
|
||||
if err != nil && err != core.ErrRequestDenied {
|
||||
errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error()))
|
||||
}
|
||||
}
|
||||
var err error
|
||||
|
||||
_, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil)
|
||||
checkErr("SignTransaction", err)
|
||||
_, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304"))
|
||||
checkErr("Sign", err)
|
||||
_, err = api.List(ctx)
|
||||
checkErr("List", err)
|
||||
_, err = api.New(ctx)
|
||||
checkErr("New", err)
|
||||
_, err = api.Export(ctx, common.Address{})
|
||||
checkErr("Export", err)
|
||||
_, err = api.Import(ctx, json.RawMessage{})
|
||||
checkErr("Import", err)
|
||||
|
||||
api.UI.ShowInfo("Tests completed")
|
||||
|
||||
if len(errs) > 0 {
|
||||
log.Error("Got errors")
|
||||
for _, e := range errs {
|
||||
log.Error(e)
|
||||
}
|
||||
} else {
|
||||
log.Info("No errors")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
//Create Account
|
||||
|
||||
curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550
|
||||
|
||||
// List accounts
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/
|
||||
|
||||
// Make Transaction
|
||||
// safeSend(0x12)
|
||||
// 4401a6e40000000000000000000000000000000000000000000000000000000000000012
|
||||
|
||||
// supplied abi
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"test"],"id":67}' http://localhost:8550/
|
||||
|
||||
// Not supplied
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/
|
||||
|
||||
// Sign data
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/
|
||||
|
||||
|
||||
**/
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import os,sys, subprocess
|
||||
from tinyrpc.transports import ServerTransport
|
||||
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
|
||||
from tinyrpc.dispatch import public,RPCDispatcher
|
||||
from tinyrpc.server import RPCServer
|
||||
|
||||
""" This is a POC example of how to write a custom UI for Clef. The UI starts the
|
||||
clef process with the '--stdio-ui' option, and communicates with clef using standard input / output.
|
||||
|
||||
The standard input/output is a relatively secure way to communicate, as it does not require opening any ports
|
||||
or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker
|
||||
can access process memory."""
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urllib as urlparse
|
||||
|
||||
class StdIOTransport(ServerTransport):
|
||||
""" Uses std input/output for RPC """
|
||||
def receive_message(self):
|
||||
return None, urlparse.unquote(sys.stdin.readline())
|
||||
|
||||
def send_reply(self, context, reply):
|
||||
print(reply)
|
||||
|
||||
class PipeTransport(ServerTransport):
|
||||
""" Uses std a pipe for RPC """
|
||||
|
||||
def __init__(self,input, output):
|
||||
self.input = input
|
||||
self.output = output
|
||||
|
||||
def receive_message(self):
|
||||
data = self.input.readline()
|
||||
print(">> {}".format( data))
|
||||
return None, urlparse.unquote(data)
|
||||
|
||||
def send_reply(self, context, reply):
|
||||
print("<< {}".format( reply))
|
||||
self.output.write(reply)
|
||||
self.output.write("\n")
|
||||
|
||||
class StdIOHandler():
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@public
|
||||
def ApproveTx(self,req):
|
||||
"""
|
||||
Example request:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "ApproveTx",
|
||||
"params": [{
|
||||
"transaction": {
|
||||
"to": "0xae967917c465db8578ca9024c205720b1a3651A9",
|
||||
"gas": "0x333",
|
||||
"gasPrice": "0x123",
|
||||
"value": "0x10",
|
||||
"data": "0xd7a5865800000000000000000000000000000000000000000000000000000000000000ff",
|
||||
"nonce": "0x0"
|
||||
},
|
||||
"from": "0xAe967917c465db8578ca9024c205720b1a3651A9",
|
||||
"call_info": "Warning! Could not validate ABI-data against calldata\nSupplied ABI spec does not contain method signature in data: 0xd7a58658",
|
||||
"meta": {
|
||||
"remote": "127.0.0.1:34572",
|
||||
"local": "localhost:8550",
|
||||
"scheme": "HTTP/1.1"
|
||||
}
|
||||
}],
|
||||
"id": 1
|
||||
}
|
||||
|
||||
:param transaction: transaction info
|
||||
:param call_info: info abou the call, e.g. if ABI info could not be
|
||||
:param meta: metadata about the request, e.g. where the call comes from
|
||||
:return:
|
||||
"""
|
||||
transaction = req.get('transaction')
|
||||
_from = req.get('from')
|
||||
call_info = req.get('call_info')
|
||||
meta = req.get('meta')
|
||||
|
||||
return {
|
||||
"approved" : False,
|
||||
#"transaction" : transaction,
|
||||
# "from" : _from,
|
||||
# "password" : None,
|
||||
}
|
||||
|
||||
@public
|
||||
def ApproveSignData(self, req):
|
||||
""" Example request
|
||||
|
||||
"""
|
||||
return {"approved": False, "password" : None}
|
||||
|
||||
@public
|
||||
def ApproveExport(self, req):
|
||||
""" Example request
|
||||
|
||||
"""
|
||||
return {"approved" : False}
|
||||
|
||||
@public
|
||||
def ApproveImport(self, req):
|
||||
""" Example request
|
||||
|
||||
"""
|
||||
return { "approved" : False, "old_password": "", "new_password": ""}
|
||||
|
||||
@public
|
||||
def ApproveListing(self, req):
|
||||
""" Example request
|
||||
|
||||
"""
|
||||
return {'accounts': []}
|
||||
|
||||
@public
|
||||
def ApproveNewAccount(self, req):
|
||||
"""
|
||||
Example request
|
||||
|
||||
:return:
|
||||
"""
|
||||
return {"approved": False,
|
||||
#"password": ""
|
||||
}
|
||||
|
||||
@public
|
||||
def ShowError(self,message = {}):
|
||||
"""
|
||||
Example request:
|
||||
|
||||
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1}
|
||||
|
||||
:param message: to show
|
||||
:return: nothing
|
||||
"""
|
||||
if 'text' in message.keys():
|
||||
sys.stderr.write("Error: {}\n".format( message['text']))
|
||||
return
|
||||
|
||||
@public
|
||||
def ShowInfo(self,message = {}):
|
||||
"""
|
||||
Example request
|
||||
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0}
|
||||
|
||||
:param message: to display
|
||||
:return:nothing
|
||||
"""
|
||||
|
||||
if 'text' in message.keys():
|
||||
sys.stdout.write("Error: {}\n".format( message['text']))
|
||||
return
|
||||
|
||||
def main(args):
|
||||
|
||||
cmd = ["./clef", "--stdio-ui"]
|
||||
if len(args) > 0 and args[0] == "test":
|
||||
cmd.extend(["--stdio-ui-test"])
|
||||
print("cmd: {}".format(" ".join(cmd)))
|
||||
dispatcher = RPCDispatcher()
|
||||
dispatcher.register_instance(StdIOHandler(), '')
|
||||
# line buffered
|
||||
p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
|
||||
rpc_server = RPCServer(
|
||||
PipeTransport(p.stdout, p.stdin),
|
||||
JSONRPCProtocol(),
|
||||
dispatcher
|
||||
)
|
||||
rpc_server.serve_forever()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
# Rules
|
||||
|
||||
The `signer` binary contains a ruleset engine, implemented with [OttoVM](https://github.com/robertkrimen/otto)
|
||||
|
||||
It enables usecases like the following:
|
||||
|
||||
* I want to auto-approve transactions with contract `CasinoDapp`, with up to `0.05 ether` in value to maximum `1 ether` per 24h period
|
||||
* I want to auto-approve transaction to contract `EthAlarmClock` with `data`=`0xdeadbeef`, if `value=0`, `gas < 44k` and `gasPrice < 40Gwei`
|
||||
|
||||
The two main features that are required for this to work well are;
|
||||
|
||||
1. Rule Implementation: how to create, manage and interpret rules in a flexible but secure manner
|
||||
2. Credential managements and credentials; how to provide auto-unlock without exposing keys unnecessarily.
|
||||
|
||||
The section below deals with both of them
|
||||
|
||||
## Rule Implementation
|
||||
|
||||
A ruleset file is implemented as a `js` file. Under the hood, the ruleset-engine is a `SignerUI`, implementing the same methods as the `json-rpc` methods
|
||||
defined in the UI protocol. Example:
|
||||
|
||||
```javascript
|
||||
|
||||
function asBig(str){
|
||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
|
||||
return new BigNumber(str)
|
||||
}
|
||||
|
||||
// Approve transactions to a certain contract if value is below a certain limit
|
||||
function ApproveTx(req){
|
||||
|
||||
var limit = big.Newint("0xb1a2bc2ec50000")
|
||||
var value = asBig(req.transaction.value);
|
||||
|
||||
if(req.transaction.to.toLowerCase()=="0xae967917c465db8578ca9024c205720b1a3651a9")
|
||||
&& value.lt(limit) ){
|
||||
return "Approve"
|
||||
}
|
||||
// If we return "Reject", it will be rejected.
|
||||
// By not returning anything, it will be passed to the next UI, for manual processing
|
||||
}
|
||||
|
||||
//Approve listings if request made from IPC
|
||||
function ApproveListing(req){
|
||||
if (req.metadata.scheme == "ipc"){ return "Approve"}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Whenever the external API is called (and the ruleset is enabled), the `signer` calls the UI, which is an instance of a ruleset-engine. The ruleset-engine
|
||||
invokes the corresponding method. In doing so, there are three possible outcomes:
|
||||
|
||||
1. JS returns "Approve"
|
||||
* Auto-approve request
|
||||
2. JS returns "Reject"
|
||||
* Auto-reject request
|
||||
3. Error occurs, or something else is returned
|
||||
* Pass on to `next` ui: the regular UI channel.
|
||||
|
||||
A more advanced example can be found below, "Example 1: ruleset for a rate-limited window", using `storage` to `Put` and `Get` `string`s by key.
|
||||
|
||||
* At the time of writing, storage only exists as an ephemeral unencrypted implementation, to be used during testing.
|
||||
|
||||
### Things to note
|
||||
|
||||
The Otto vm has a few [caveats](https://github.com/robertkrimen/otto):
|
||||
|
||||
* "use strict" will parse, but does nothing.
|
||||
* The regular expression engine (re2/regexp) is not fully compatible with the ECMA5 specification.
|
||||
* Otto targets ES5. ES6 features (eg: Typed Arrays) are not supported.
|
||||
|
||||
Additionally, a few more have been added
|
||||
|
||||
* The rule execution cannot load external javascript files.
|
||||
* The only preloaded libary is [`bignumber.js`](https://github.com/MikeMcl/bignumber.js) version `2.0.3`. This one is fairly old, and is not aligned with the documentation at the github repository.
|
||||
* Each invocation is made in a fresh virtual machine. This means that you cannot store data in global variables between invocations. This is a deliberate choice -- if you want to store data, use the disk-backed `storage`, since rules should not rely on ephemeral data.
|
||||
* Javascript API parameters are _always_ an object. This is also a design choice, to ensure that parameters are accessed by _key_ and not by order. This is to prevent mistakes due to missing parameters or parameter changes.
|
||||
* The JS engine has access to `storage` and `console`.
|
||||
|
||||
#### Security considerations
|
||||
|
||||
##### Security of ruleset
|
||||
|
||||
Some security precautions can be made, such as:
|
||||
|
||||
* Never load `ruleset.js` unless the file is `readonly` (`r-??-??-?`). If the user wishes to modify the ruleset, he must make it writeable and then set back to readonly.
|
||||
* This is to prevent attacks where files are dropped on the users disk.
|
||||
* Since we're going to have to have some form of secure storage (not defined in this section), we could also store the `sha3` of the `ruleset.js` file in there.
|
||||
* If the user wishes to modify the ruleset, he'd then have to perform e.g. `signer --attest /path/to/ruleset --credential <creds>`
|
||||
|
||||
##### Security of implementation
|
||||
|
||||
The drawbacks of this very flexible solution is that the `signer` needs to contain a javascript engine. This is pretty simple to implement, since it's already
|
||||
implemented for `geth`. There are no known security vulnerabilities in, nor have we had any security-problems with it so far.
|
||||
|
||||
The javascript engine would be an added attack surface; but if the validation of `rulesets` is made good (with hash-based attestation), the actual javascript cannot be considered
|
||||
an attack surface -- if an attacker can control the ruleset, a much simpler attack would be to implement an "always-approve" rule instead of exploiting the js vm. The only benefit
|
||||
to be gained from attacking the actual `signer` process from the `js` side would be if it could somehow extract cryptographic keys from memory.
|
||||
|
||||
##### Security in usability
|
||||
|
||||
Javascript is flexible, but also easy to get wrong, especially when users assume that `js` can handle large integers natively. Typical errors
|
||||
include trying to multiply `gasCost` with `gas` without using `bigint`:s.
|
||||
|
||||
It's unclear whether any other DSL could be more secure; since there's always the possibility of erroneously implementing a rule.
|
||||
|
||||
|
||||
## Credential management
|
||||
|
||||
The ability to auto-approve transaction means that the signer needs to have necessary credentials to decrypt keyfiles. These passwords are hereafter called `ksp` (keystore pass).
|
||||
|
||||
### Example implementation
|
||||
|
||||
Upon startup of the signer, the signer is given a switch: `--seed <path/to/masterseed>`
|
||||
The `seed` contains a blob of bytes, which is the master seed for the `signer`.
|
||||
|
||||
The `signer` uses the `seed` to:
|
||||
|
||||
* Generate the `path` where the settings are stored.
|
||||
* `./settings/1df094eb-c2b1-4689-90dd-790046d38025/vault.dat`
|
||||
* `./settings/1df094eb-c2b1-4689-90dd-790046d38025/rules.js`
|
||||
* Generate the encryption password for `vault.dat`.
|
||||
|
||||
The `vault.dat` would be an encrypted container storing the following information:
|
||||
|
||||
* `ksp` entries
|
||||
* `sha256` hash of `rules.js`
|
||||
* Information about pair:ed callers (not yet specified)
|
||||
|
||||
### Security considerations
|
||||
|
||||
This would leave it up to the user to ensure that the `path/to/masterseed` is handled in a secure way. It's difficult to get around this, although one could
|
||||
imagine leveraging OS-level keychains where supported. The setup is however in general similar to how ssh-keys are stored in `.ssh/`.
|
||||
|
||||
|
||||
# Implementation status
|
||||
|
||||
This is now implemented (with ephemeral non-encrypted storage for now, so not yet enabled).
|
||||
|
||||
## Example 1: ruleset for a rate-limited window
|
||||
|
||||
|
||||
```javascript
|
||||
|
||||
function big(str){
|
||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
|
||||
return new BigNumber(str)
|
||||
}
|
||||
|
||||
// Time window: 1 week
|
||||
var window = 1000* 3600*24*7;
|
||||
|
||||
// Limit : 1 ether
|
||||
var limit = new BigNumber("1e18");
|
||||
|
||||
function isLimitOk(transaction){
|
||||
var value = big(transaction.value)
|
||||
// Start of our window function
|
||||
var windowstart = new Date().getTime() - window;
|
||||
|
||||
var txs = [];
|
||||
var stored = storage.Get('txs');
|
||||
|
||||
if(stored != ""){
|
||||
txs = JSON.parse(stored)
|
||||
}
|
||||
// First, remove all that have passed out of the time-window
|
||||
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
|
||||
console.log(txs, newtxs.length);
|
||||
|
||||
// Secondly, aggregate the current sum
|
||||
sum = new BigNumber(0)
|
||||
|
||||
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
|
||||
console.log("ApproveTx > Sum so far", sum);
|
||||
console.log("ApproveTx > Requested", value.toNumber());
|
||||
|
||||
// Would we exceed weekly limit ?
|
||||
return sum.plus(value).lt(limit)
|
||||
|
||||
}
|
||||
function ApproveTx(r){
|
||||
if (isLimitOk(r.transaction)){
|
||||
return "Approve"
|
||||
}
|
||||
return "Nope"
|
||||
}
|
||||
|
||||
/**
|
||||
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
|
||||
* 'response_str' contains the return value that will be sent to the external caller.
|
||||
* The return value from this method is ignore - the reason for having this callback is to allow the
|
||||
* ruleset to keep track of approved transactions.
|
||||
*
|
||||
* When implementing rate-limited rules, this callback should be used.
|
||||
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
|
||||
* then accepts the transaction, this method will be called.
|
||||
*
|
||||
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
|
||||
*/
|
||||
function OnApprovedTx(resp){
|
||||
var value = big(resp.tx.value)
|
||||
var txs = []
|
||||
// Load stored transactions
|
||||
var stored = storage.Get('txs');
|
||||
if(stored != ""){
|
||||
txs = JSON.parse(stored)
|
||||
}
|
||||
// Add this to the storage
|
||||
txs.push({tstamp: new Date().getTime(), value: value});
|
||||
storage.Put("txs", JSON.stringify(txs));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Example 2: allow destination
|
||||
|
||||
```javascript
|
||||
|
||||
function ApproveTx(r){
|
||||
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
|
||||
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
|
||||
// Otherwise goes to manual processing
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Example 3: Allow listing
|
||||
|
||||
```javascript
|
||||
|
||||
function ApproveListing(){
|
||||
return "Approve"
|
||||
}
|
||||
|
||||
```
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
+198
@@ -0,0 +1,198 @@
|
||||
## Initializing the signer
|
||||
|
||||
First, initialize the master seed.
|
||||
|
||||
```text
|
||||
#./signer init
|
||||
|
||||
WARNING!
|
||||
|
||||
The signer is alpha software, and not yet publically released. This software has _not_ been audited, and there
|
||||
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
|
||||
unless you agree to take full responsibility for doing so, and know what you are doing.
|
||||
|
||||
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
|
||||
|
||||
|
||||
Enter 'ok' to proceed:
|
||||
>ok
|
||||
A master seed has been generated into /home/martin/.signer/secrets.dat
|
||||
|
||||
This is required to be able to store credentials, such as :
|
||||
* Passwords for keystores (used by rule engine)
|
||||
* Storage for javascript rules
|
||||
* Hash of rule-file
|
||||
|
||||
You should treat that file with utmost secrecy, and make a backup of it.
|
||||
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
||||
```
|
||||
|
||||
(for readability purposes, we'll remove the WARNING printout in the rest of this document)
|
||||
|
||||
## Creating rules
|
||||
|
||||
Now, you can create a rule-file.
|
||||
|
||||
```javascript
|
||||
function ApproveListing(){
|
||||
return "Approve"
|
||||
}
|
||||
```
|
||||
Get the `sha256` hash....
|
||||
```text
|
||||
#sha256sum rules.js
|
||||
6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js
|
||||
```
|
||||
...And then `attest` the file:
|
||||
```text
|
||||
#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72
|
||||
|
||||
INFO [02-21|12:14:38] Ruleset attestation updated sha256=6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72
|
||||
```
|
||||
At this point, we then start the signer with the rule-file:
|
||||
|
||||
```text
|
||||
#./signer --rules rules.json
|
||||
|
||||
INFO [02-21|12:15:18] Using CLI as UI-channel
|
||||
INFO [02-21|12:15:18] Loaded 4byte db signatures=5509 file=./4byte.json
|
||||
INFO [02-21|12:15:18] Could not load rulefile, rules not enabled file=rulefile
|
||||
DEBUG[02-21|12:15:18] FS scan times list=35.335µs set=5.536µs diff=5.073µs
|
||||
DEBUG[02-21|12:15:18] Ledger support enabled
|
||||
DEBUG[02-21|12:15:18] Trezor support enabled
|
||||
INFO [02-21|12:15:18] Audit logs configured file=audit.log
|
||||
INFO [02-21|12:15:18] HTTP endpoint opened url=http://localhost:8550
|
||||
------- Signer info -------
|
||||
* extapi_http : http://localhost:8550
|
||||
* extapi_ipc : <nil>
|
||||
* extapi_version : 2.0.0
|
||||
* intapi_version : 1.2.0
|
||||
|
||||
```
|
||||
|
||||
Any list-requests will now be auto-approved by our rule-file.
|
||||
|
||||
## Under the hood
|
||||
|
||||
While doing the operations above, these files have been created:
|
||||
|
||||
```text
|
||||
#ls -laR ~/.signer/
|
||||
/home/martin/.signer/:
|
||||
total 16
|
||||
drwx------ 3 martin martin 4096 feb 21 12:14 .
|
||||
drwxr-xr-x 71 martin martin 4096 feb 21 12:12 ..
|
||||
drwx------ 2 martin martin 4096 feb 21 12:14 43f73718397aa54d1b22
|
||||
-rwx------ 1 martin martin 256 feb 21 12:12 secrets.dat
|
||||
|
||||
/home/martin/.signer/43f73718397aa54d1b22:
|
||||
total 12
|
||||
drwx------ 2 martin martin 4096 feb 21 12:14 .
|
||||
drwx------ 3 martin martin 4096 feb 21 12:14 ..
|
||||
-rw------- 1 martin martin 159 feb 21 12:14 config.json
|
||||
|
||||
#cat /home/martin/.signer/43f73718397aa54d1b22/config.json
|
||||
{"ruleset_sha256":{"iv":"6v4W4tfJxj3zZFbl","c":"6dt5RTDiTq93yh1qDEjpsat/tsKG7cb+vr3sza26IPL2fvsQ6ZoqFx++CPUa8yy6fD9Bbq41L01ehkKHTG3pOAeqTW6zc/+t0wv3AB6xPmU="}}
|
||||
|
||||
```
|
||||
|
||||
In `~/.signer`, the `secrets.dat` file was created, containing the `master_seed`.
|
||||
The `master_seed` was then used to derive a few other things:
|
||||
|
||||
- `vault_location` : in this case `43f73718397aa54d1b22` .
|
||||
- Thus, if you use a different `master_seed`, another `vault_location` will be used that does not conflict with each other.
|
||||
- Example: `signer --signersecret /path/to/afile ...`
|
||||
- `config.json` which is the encrypted key/value storage for configuration data, containing the key `ruleset_sha256`.
|
||||
|
||||
|
||||
## Adding credentials
|
||||
|
||||
In order to make more useful rules; sign transactions, the signer needs access to the passwords needed to unlock keystores.
|
||||
|
||||
```text
|
||||
#./signer addpw 0x694267f14675d7e1b9494fd8d72fefe1755710fa test
|
||||
|
||||
INFO [02-21|13:43:21] Credential store updated key=0x694267f14675d7e1b9494fd8d72fefe1755710fa
|
||||
```
|
||||
## More advanced rules
|
||||
|
||||
Now let's update the rules to make use of credentials
|
||||
|
||||
```javascript
|
||||
function ApproveListing(){
|
||||
return "Approve"
|
||||
}
|
||||
function ApproveSignData(r){
|
||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
||||
{
|
||||
if(r.message.indexOf("bazonk") >= 0){
|
||||
return "Approve"
|
||||
}
|
||||
return "Reject"
|
||||
}
|
||||
// Otherwise goes to manual processing
|
||||
}
|
||||
|
||||
```
|
||||
In this example,
|
||||
* any requests to sign data with the account `0x694...` will be
|
||||
* auto-approved if the message contains with `bazonk`,
|
||||
* and auto-rejected if it does not.
|
||||
* Any other signing-requests will be passed along for manual approve/reject.
|
||||
|
||||
..attest the new file
|
||||
```text
|
||||
#sha256sum rules.js
|
||||
2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f rules.js
|
||||
|
||||
#./signer attest 2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
|
||||
|
||||
INFO [02-21|14:36:30] Ruleset attestation updated sha256=2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
|
||||
```
|
||||
|
||||
And start the signer:
|
||||
|
||||
```
|
||||
#./signer --rules rules.js
|
||||
|
||||
INFO [02-21|14:41:56] Using CLI as UI-channel
|
||||
INFO [02-21|14:41:56] Loaded 4byte db signatures=5509 file=./4byte.json
|
||||
INFO [02-21|14:41:56] Rule engine configured file=rules.js
|
||||
DEBUG[02-21|14:41:56] FS scan times list=34.607µs set=4.509µs diff=4.87µs
|
||||
DEBUG[02-21|14:41:56] Ledger support enabled
|
||||
DEBUG[02-21|14:41:56] Trezor support enabled
|
||||
INFO [02-21|14:41:56] Audit logs configured file=audit.log
|
||||
INFO [02-21|14:41:56] HTTP endpoint opened url=http://localhost:8550
|
||||
------- Signer info -------
|
||||
* extapi_version : 2.0.0
|
||||
* intapi_version : 1.2.0
|
||||
* extapi_http : http://localhost:8550
|
||||
* extapi_ipc : <nil>
|
||||
INFO [02-21|14:41:56] error occurred during execution error="ReferenceError: 'OnSignerStartup' is not defined"
|
||||
```
|
||||
And then test signing, once with `bazonk` and once without:
|
||||
|
||||
```
|
||||
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bazonk baz gaz')\"],\"id\":67}" http://localhost:8550/
|
||||
{"jsonrpc":"2.0","id":67,"result":"0x93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c"}
|
||||
|
||||
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bonk baz gaz')\"],\"id\":67}" http://localhost:8550/
|
||||
{"jsonrpc":"2.0","id":67,"error":{"code":-32000,"message":"Request denied"}}
|
||||
|
||||
```
|
||||
|
||||
Meanwhile, in the signer output:
|
||||
```text
|
||||
INFO [02-21|14:42:41] Op approved
|
||||
INFO [02-21|14:42:56] Op rejected
|
||||
```
|
||||
|
||||
The signer also stores all traffic over the external API in a log file. The last 4 lines shows the two requests and their responses:
|
||||
|
||||
```text
|
||||
#tail audit.log -n 4
|
||||
t=2018-02-21T14:42:41+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49706\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=202062617a6f6e6b2062617a2067617a0a
|
||||
t=2018-02-21T14:42:42+0100 lvl=info msg=Sign api=signer type=response data=93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c error=nil
|
||||
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49708\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=2020626f6e6b2062617a2067617a0a
|
||||
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=response data= error="Request denied"
|
||||
```
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var newPassphraseFlag = cli.StringFlag{
|
||||
Name: "newpasswordfile",
|
||||
Usage: "the file that contains the new passphrase for the keyfile",
|
||||
}
|
||||
|
||||
var commandChangePassphrase = cli.Command{
|
||||
Name: "changepassphrase",
|
||||
Usage: "change the passphrase on a keyfile",
|
||||
ArgsUsage: "<keyfile>",
|
||||
Description: `
|
||||
Change the passphrase of a keyfile.`,
|
||||
Flags: []cli.Flag{
|
||||
passphraseFlag,
|
||||
newPassphraseFlag,
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
keyfilepath := ctx.Args().First()
|
||||
|
||||
// Read key from file.
|
||||
keyjson, err := ioutil.ReadFile(keyfilepath)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the keyfile at '%s': %v", keyfilepath, err)
|
||||
}
|
||||
|
||||
// Decrypt key with passphrase.
|
||||
passphrase := getPassphrase(ctx)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error decrypting key: %v", err)
|
||||
}
|
||||
|
||||
// Get a new passphrase.
|
||||
fmt.Println("Please provide a new passphrase")
|
||||
var newPhrase string
|
||||
if passFile := ctx.String(newPassphraseFlag.Name); passFile != "" {
|
||||
content, err := ioutil.ReadFile(passFile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read new passphrase file '%s': %v", passFile, err)
|
||||
}
|
||||
newPhrase = strings.TrimRight(string(content), "\r\n")
|
||||
} else {
|
||||
newPhrase = promptPassphrase(true)
|
||||
}
|
||||
|
||||
// Encrypt the key with the new passphrase.
|
||||
newJson, err := keystore.EncryptKey(key, newPhrase, keystore.StandardScryptN, keystore.StandardScryptP)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error encrypting with new passphrase: %v", err)
|
||||
}
|
||||
|
||||
// Then write the new keyfile in place of the old one.
|
||||
if err := ioutil.WriteFile(keyfilepath, newJson, 600); err != nil {
|
||||
utils.Fatalf("Error writing new keyfile to disk: %v", err)
|
||||
}
|
||||
|
||||
// Don't print anything. Just return successfully,
|
||||
// producing a positive exit code.
|
||||
return nil
|
||||
},
|
||||
}
|
||||
+1
-1
@@ -90,7 +90,7 @@ If you want to encrypt an existing private key, it can be specified by setting
|
||||
}
|
||||
|
||||
// Encrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, true)
|
||||
passphrase := promptPassphrase(true)
|
||||
keyjson, err := keystore.EncryptKey(key, passphrase, keystore.StandardScryptN, keystore.StandardScryptP)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error encrypting key: %v", err)
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ make sure to use this feature with great caution!`,
|
||||
}
|
||||
|
||||
// Decrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, false)
|
||||
passphrase := getPassphrase(ctx)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error decrypting key: %v", err)
|
||||
|
||||
+1
-4
@@ -38,6 +38,7 @@ func init() {
|
||||
app.Commands = []cli.Command{
|
||||
commandGenerate,
|
||||
commandInspect,
|
||||
commandChangePassphrase,
|
||||
commandSignMessage,
|
||||
commandVerifyMessage,
|
||||
}
|
||||
@@ -53,10 +54,6 @@ var (
|
||||
Name: "json",
|
||||
Usage: "output JSON instead of human-readable format",
|
||||
}
|
||||
messageFlag = cli.StringFlag{
|
||||
Name: "message",
|
||||
Usage: "the file that contains the message to sign/verify",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ To sign a message contained in a file, use the --msgfile flag.
|
||||
}
|
||||
|
||||
// Decrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, false)
|
||||
passphrase := getPassphrase(ctx)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error decrypting key: %v", err)
|
||||
|
||||
+26
-18
@@ -28,11 +28,32 @@ import (
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// getPassPhrase obtains a passphrase given by the user. It first checks the
|
||||
// --passphrase command line flag and ultimately prompts the user for a
|
||||
// promptPassphrase prompts the user for a passphrase. Set confirmation to true
|
||||
// to require the user to confirm the passphrase.
|
||||
func promptPassphrase(confirmation bool) string {
|
||||
passphrase, err := console.Stdin.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
|
||||
if confirmation {
|
||||
confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase confirmation: %v", err)
|
||||
}
|
||||
if passphrase != confirm {
|
||||
utils.Fatalf("Passphrases do not match")
|
||||
}
|
||||
}
|
||||
|
||||
return passphrase
|
||||
}
|
||||
|
||||
// getPassphrase obtains a passphrase given by the user. It first checks the
|
||||
// --passfile command line flag and ultimately prompts the user for a
|
||||
// passphrase.
|
||||
func getPassPhrase(ctx *cli.Context, confirmation bool) string {
|
||||
// Look for the --passphrase flag.
|
||||
func getPassphrase(ctx *cli.Context) string {
|
||||
// Look for the --passwordfile flag.
|
||||
passphraseFile := ctx.String(passphraseFlag.Name)
|
||||
if passphraseFile != "" {
|
||||
content, err := ioutil.ReadFile(passphraseFile)
|
||||
@@ -44,20 +65,7 @@ func getPassPhrase(ctx *cli.Context, confirmation bool) string {
|
||||
}
|
||||
|
||||
// Otherwise prompt the user for the passphrase.
|
||||
passphrase, err := console.Stdin.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
if confirmation {
|
||||
confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase confirmation: %v", err)
|
||||
}
|
||||
if passphrase != confirm {
|
||||
utils.Fatalf("Passphrases do not match")
|
||||
}
|
||||
}
|
||||
return passphrase
|
||||
return promptPassphrase(false)
|
||||
}
|
||||
|
||||
// signHash is a helper function that calculates a hash for the given message
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ type JSONLogger struct {
|
||||
cfg *vm.LogConfig
|
||||
}
|
||||
|
||||
// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects
|
||||
// into the provided stream.
|
||||
func NewJSONLogger(cfg *vm.LogConfig, writer io.Writer) *JSONLogger {
|
||||
return &JSONLogger{json.NewEncoder(writer), cfg}
|
||||
}
|
||||
|
||||
+16
-15
@@ -21,12 +21,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
goruntime "runtime"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/evm/internal/compiler"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
@@ -76,6 +76,7 @@ func runCmd(ctx *cli.Context) error {
|
||||
logconfig := &vm.LogConfig{
|
||||
DisableMemory: ctx.GlobalBool(DisableMemoryFlag.Name),
|
||||
DisableStack: ctx.GlobalBool(DisableStackFlag.Name),
|
||||
Debug: ctx.GlobalBool(DebugFlag.Name),
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -83,8 +84,9 @@ func runCmd(ctx *cli.Context) error {
|
||||
debugLogger *vm.StructLogger
|
||||
statedb *state.StateDB
|
||||
chainConfig *params.ChainConfig
|
||||
sender = common.StringToAddress("sender")
|
||||
receiver = common.StringToAddress("receiver")
|
||||
sender = common.BytesToAddress([]byte("sender"))
|
||||
receiver = common.BytesToAddress([]byte("receiver"))
|
||||
blockNumber uint64
|
||||
)
|
||||
if ctx.GlobalBool(MachineFlag.Name) {
|
||||
tracer = NewJSONLogger(logconfig, os.Stdout)
|
||||
@@ -96,13 +98,13 @@ func runCmd(ctx *cli.Context) error {
|
||||
}
|
||||
if ctx.GlobalString(GenesisFlag.Name) != "" {
|
||||
gen := readGenesis(ctx.GlobalString(GenesisFlag.Name))
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
db := ethdb.NewMemDatabase()
|
||||
genesis := gen.ToBlock(db)
|
||||
statedb, _ = state.New(genesis.Root(), state.NewDatabase(db))
|
||||
chainConfig = gen.Config
|
||||
blockNumber = gen.Number
|
||||
} else {
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
statedb, _ = state.New(common.Hash{}, state.NewDatabase(db))
|
||||
statedb, _ = state.New(common.Hash{}, state.NewDatabase(ethdb.NewMemDatabase()))
|
||||
}
|
||||
if ctx.GlobalString(SenderFlag.Name) != "" {
|
||||
sender = common.HexToAddress(ctx.GlobalString(SenderFlag.Name))
|
||||
@@ -155,11 +157,12 @@ func runCmd(ctx *cli.Context) error {
|
||||
|
||||
initialGas := ctx.GlobalUint64(GasFlag.Name)
|
||||
runtimeConfig := runtime.Config{
|
||||
Origin: sender,
|
||||
State: statedb,
|
||||
GasLimit: initialGas,
|
||||
GasPrice: utils.GlobalBig(ctx, PriceFlag.Name),
|
||||
Value: utils.GlobalBig(ctx, ValueFlag.Name),
|
||||
Origin: sender,
|
||||
State: statedb,
|
||||
GasLimit: initialGas,
|
||||
GasPrice: utils.GlobalBig(ctx, PriceFlag.Name),
|
||||
Value: utils.GlobalBig(ctx, ValueFlag.Name),
|
||||
BlockNumber: new(big.Int).SetUint64(blockNumber),
|
||||
EVMConfig: vm.Config{
|
||||
Tracer: tracer,
|
||||
Debug: ctx.GlobalBool(DebugFlag.Name) || ctx.GlobalBool(MachineFlag.Name),
|
||||
@@ -234,9 +237,7 @@ Gas used: %d
|
||||
|
||||
`, execTime, mem.HeapObjects, mem.Alloc, mem.TotalAlloc, mem.NumGC, initialGas-leftOverGas)
|
||||
}
|
||||
if tracer != nil {
|
||||
tracer.CaptureEnd(ret, initialGas-leftOverGas, execTime, err)
|
||||
} else {
|
||||
if tracer == nil {
|
||||
fmt.Printf("0x%x\n", ret)
|
||||
if err != nil {
|
||||
fmt.Printf(" error: %v\n", err)
|
||||
|
||||
+2
@@ -38,6 +38,8 @@ var stateTestCommand = cli.Command{
|
||||
ArgsUsage: "<file>",
|
||||
}
|
||||
|
||||
// StatetestResult contains the execution status after running a state test, any
|
||||
// error that might have occurred and a dump of the final state if requested.
|
||||
type StatetestResult struct {
|
||||
Name string `json:"name"`
|
||||
Pass bool `json:"pass"`
|
||||
|
||||
+2
-58
@@ -77,9 +77,6 @@ var (
|
||||
accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with")
|
||||
accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds")
|
||||
|
||||
githubUser = flag.String("github.user", "", "GitHub user to authenticate with for Gist access")
|
||||
githubToken = flag.String("github.token", "", "GitHub personal token to access Gists with")
|
||||
|
||||
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
|
||||
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side")
|
||||
|
||||
@@ -216,7 +213,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
|
||||
// Assemble the raw devp2p protocol stack
|
||||
stack, err := node.New(&node.Config{
|
||||
Name: "geth",
|
||||
Version: params.Version,
|
||||
Version: params.VersionWithMeta,
|
||||
DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"),
|
||||
P2P: p2p.Config{
|
||||
NAT: nat.Any(),
|
||||
@@ -474,7 +471,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
|
||||
amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil))
|
||||
|
||||
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil)
|
||||
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainId)
|
||||
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID)
|
||||
if err != nil {
|
||||
f.lock.Unlock()
|
||||
if err = sendError(conn, err); err != nil {
|
||||
@@ -638,59 +635,6 @@ func sendSuccess(conn *websocket.Conn, msg string) error {
|
||||
return send(conn, map[string]string{"success": msg}, time.Second)
|
||||
}
|
||||
|
||||
// authGitHub tries to authenticate a faucet request using GitHub gists, returning
|
||||
// the username, avatar URL and Ethereum address to fund on success.
|
||||
func authGitHub(url string) (string, string, common.Address, error) {
|
||||
// Retrieve the gist from the GitHub Gist APIs
|
||||
parts := strings.Split(url, "/")
|
||||
req, _ := http.NewRequest("GET", "https://api.github.com/gists/"+parts[len(parts)-1], nil)
|
||||
if *githubUser != "" {
|
||||
req.SetBasicAuth(*githubUser, *githubToken)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
var gist struct {
|
||||
Owner struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"owner"`
|
||||
Files map[string]struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"files"`
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&gist)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
if gist.Owner.Login == "" {
|
||||
return "", "", common.Address{}, errors.New("Anonymous Gists not allowed")
|
||||
}
|
||||
// Iterate over all the files and look for Ethereum addresses
|
||||
var address common.Address
|
||||
for _, file := range gist.Files {
|
||||
content := strings.TrimSpace(file.Content)
|
||||
if len(content) == 2+common.AddressLength*2 {
|
||||
address = common.HexToAddress(content)
|
||||
}
|
||||
}
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
// Validate the user's existence since the API is unhelpful here
|
||||
if res, err = http.Head("https://github.com/" + gist.Owner.Login); err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", "", common.Address{}, errors.New("Invalid user... boom!")
|
||||
}
|
||||
// Everything passed validation, return the gathered infos
|
||||
return gist.Owner.Login + "@github", fmt.Sprintf("https://github.com/%s.png?size=64", gist.Owner.Login), address, nil
|
||||
}
|
||||
|
||||
// authTwitter tries to authenticate a faucet request using Twitter posts, returning
|
||||
// the username, avatar URL and Ethereum address to fund on success.
|
||||
func authTwitter(url string) (string, string, common.Address, error) {
|
||||
|
||||
+2
-2
@@ -340,7 +340,7 @@ func importWallet(ctx *cli.Context) error {
|
||||
if len(keyfile) == 0 {
|
||||
utils.Fatalf("keyfile must be given as argument")
|
||||
}
|
||||
keyJson, err := ioutil.ReadFile(keyfile)
|
||||
keyJSON, err := ioutil.ReadFile(keyfile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not read wallet file: %v", err)
|
||||
}
|
||||
@@ -349,7 +349,7 @@ func importWallet(ctx *cli.Context) error {
|
||||
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
acct, err := ks.ImportPreSaleKey(keyJson, passphrase)
|
||||
acct, err := ks.ImportPreSaleKey(keyJSON, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
+17
-13
@@ -41,7 +41,7 @@ var bugCommand = cli.Command{
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
}
|
||||
|
||||
const issueUrl = "https://github.com/ethereum/go-ethereum/issues/new"
|
||||
const issueURL = "https://github.com/ethereum/go-ethereum/issues/new"
|
||||
|
||||
// reportBug reports a bug by opening a new URL to the go-ethereum GH issue
|
||||
// tracker and setting default values as the issue body.
|
||||
@@ -49,15 +49,17 @@ func reportBug(ctx *cli.Context) error {
|
||||
// execute template and write contents to buff
|
||||
var buff bytes.Buffer
|
||||
|
||||
fmt.Fprintln(&buff, header)
|
||||
fmt.Fprintln(&buff, "Version:", params.Version)
|
||||
fmt.Fprintln(&buff, "#### System information")
|
||||
fmt.Fprintln(&buff)
|
||||
fmt.Fprintln(&buff, "Version:", params.VersionWithMeta)
|
||||
fmt.Fprintln(&buff, "Go Version:", runtime.Version())
|
||||
fmt.Fprintln(&buff, "OS:", runtime.GOOS)
|
||||
printOSDetails(&buff)
|
||||
fmt.Fprintln(&buff, header)
|
||||
|
||||
// open a new GH issue
|
||||
if !browser.Open(issueUrl + "?body=" + url.QueryEscape(buff.String())) {
|
||||
fmt.Printf("Please file a new issue at %s using this template:\n%s", issueUrl, buff.String())
|
||||
if !browser.Open(issueURL + "?body=" + url.QueryEscape(buff.String())) {
|
||||
fmt.Printf("Please file a new issue at %s using this template:\n\n%s", issueURL, buff.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -97,13 +99,15 @@ func printCmdOut(w io.Writer, prefix, path string, args ...string) {
|
||||
fmt.Fprintf(w, "%s%s\n", prefix, bytes.TrimSpace(out))
|
||||
}
|
||||
|
||||
const header = `Please answer these questions before submitting your issue. Thanks!
|
||||
const header = `
|
||||
#### Expected behaviour
|
||||
|
||||
#### What did you do?
|
||||
|
||||
#### What did you expect to see?
|
||||
|
||||
#### What did you see instead?
|
||||
|
||||
#### System details
|
||||
|
||||
#### Actual behaviour
|
||||
|
||||
|
||||
#### Steps to reproduce the behaviour
|
||||
|
||||
|
||||
#### Backtrace
|
||||
`
|
||||
|
||||
+79
-7
@@ -48,7 +48,6 @@ var (
|
||||
ArgsUsage: "<genesisPath>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
@@ -66,7 +65,7 @@ It expects the genesis file as argument.`,
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.GCModeFlag,
|
||||
utils.CacheDatabaseFlag,
|
||||
utils.CacheGCFlag,
|
||||
@@ -87,14 +86,43 @@ processing will proceed even if an individual RLP-file import failure occurs.`,
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.SyncModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
Requires a first argument of the file to write to.
|
||||
Optional second and third arguments control the first and
|
||||
last block to write. In this mode, the file will be appended
|
||||
if already existing.`,
|
||||
if already existing. If the file ends with .gz, the output will
|
||||
be gzipped.`,
|
||||
}
|
||||
importPreimagesCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(importPreimages),
|
||||
Name: "import-preimages",
|
||||
Usage: "Import the preimage database from an RLP stream",
|
||||
ArgsUsage: "<datafile>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.SyncModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The import-preimages command imports hash preimages from an RLP encoded stream.`,
|
||||
}
|
||||
exportPreimagesCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(exportPreimages),
|
||||
Name: "export-preimages",
|
||||
Usage: "Export the preimage database into an RLP stream",
|
||||
ArgsUsage: "<dumpfile>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.SyncModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The export-preimages command export hash preimages to an RLP encoded stream`,
|
||||
}
|
||||
copydbCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(copyDb),
|
||||
@@ -120,7 +148,6 @@ The first argument must be the directory containing the blockchain to download f
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
@@ -134,7 +161,7 @@ Remove blockchain and state databases`,
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.SyncModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
@@ -225,6 +252,13 @@ func importChain(ctx *cli.Context) error {
|
||||
utils.Fatalf("Failed to read database stats: %v", err)
|
||||
}
|
||||
fmt.Println(stats)
|
||||
|
||||
ioStats, err := db.LDB().GetProperty("leveldb.iostats")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read database iostats: %v", err)
|
||||
}
|
||||
fmt.Println(ioStats)
|
||||
|
||||
fmt.Printf("Trie cache misses: %d\n", trie.CacheMisses())
|
||||
fmt.Printf("Trie cache unloads: %d\n\n", trie.CacheUnloads())
|
||||
|
||||
@@ -255,6 +289,12 @@ func importChain(ctx *cli.Context) error {
|
||||
}
|
||||
fmt.Println(stats)
|
||||
|
||||
ioStats, err = db.LDB().GetProperty("leveldb.iostats")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read database iostats: %v", err)
|
||||
}
|
||||
fmt.Println(ioStats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -286,7 +326,39 @@ func exportChain(ctx *cli.Context) error {
|
||||
if err != nil {
|
||||
utils.Fatalf("Export error: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Export done in %v", time.Since(start))
|
||||
fmt.Printf("Export done in %v\n", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
// importPreimages imports preimage data from the specified file.
|
||||
func importPreimages(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
}
|
||||
stack := makeFullNode(ctx)
|
||||
diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)
|
||||
|
||||
start := time.Now()
|
||||
if err := utils.ImportPreimages(diskdb, ctx.Args().First()); err != nil {
|
||||
utils.Fatalf("Export error: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Export done in %v\n", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
// exportPreimages dumps the preimage data to specified json file in streaming way.
|
||||
func exportPreimages(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
}
|
||||
stack := makeFullNode(ctx)
|
||||
diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)
|
||||
|
||||
start := time.Now()
|
||||
if err := utils.ExportPreimages(diskdb, ctx.Args().First()); err != nil {
|
||||
utils.Fatalf("Export error: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Export done in %v\n", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
|
||||
"github.com/naoina/toml"
|
||||
)
|
||||
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0"
|
||||
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0"
|
||||
httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0"
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestConsoleWelcome(t *testing.T) {
|
||||
geth.SetTemplateFunc("goos", func() string { return runtime.GOOS })
|
||||
geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
|
||||
geth.SetTemplateFunc("gover", runtime.Version)
|
||||
geth.SetTemplateFunc("gethver", func() string { return params.Version })
|
||||
geth.SetTemplateFunc("gethver", func() string { return params.VersionWithMeta })
|
||||
geth.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
|
||||
geth.SetTemplateFunc("apis", func() string { return ipcAPIs })
|
||||
|
||||
@@ -133,7 +133,7 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
|
||||
attach.SetTemplateFunc("goos", func() string { return runtime.GOOS })
|
||||
attach.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
|
||||
attach.SetTemplateFunc("gover", runtime.Version)
|
||||
attach.SetTemplateFunc("gethver", func() string { return params.Version })
|
||||
attach.SetTemplateFunc("gethver", func() string { return params.VersionWithMeta })
|
||||
attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
|
||||
attach.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
|
||||
attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") })
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
@@ -131,8 +131,8 @@ func testDAOForkBlockNewChain(t *testing.T, test int, genesis string, expectBloc
|
||||
if genesis != "" {
|
||||
genesisHash = daoGenesisHash
|
||||
}
|
||||
config, err := core.GetChainConfig(db, genesisHash)
|
||||
if err != nil {
|
||||
config := rawdb.ReadChainConfig(db, genesisHash)
|
||||
if config == nil {
|
||||
t.Errorf("test %d: failed to retrieve chain config: %v", test, err)
|
||||
return // we want to return here, the other checks can't make it past this point (nil panic).
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ var customGenesisTests = []struct {
|
||||
"homesteadBlock" : 314,
|
||||
"daoForkBlock" : 141,
|
||||
"daoForkSupport" : true
|
||||
},
|
||||
}
|
||||
}`,
|
||||
query: "eth.getBlock(0).nonce",
|
||||
result: "0x0000000000000042",
|
||||
|
||||
+78
-30
@@ -19,16 +19,19 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
godebug "runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/gosigar"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
@@ -46,8 +49,6 @@ const (
|
||||
var (
|
||||
// Git SHA1 commit hash of the release (set via linker flags)
|
||||
gitCommit = ""
|
||||
// Ethereum address of the Geth release oracle.
|
||||
relOracle = common.HexToAddress("0xfa7b9770ca4cb04296cac84f37736d4041251cdf")
|
||||
// The app that holds all commands and flags.
|
||||
app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
|
||||
// flags that configure the node
|
||||
@@ -65,13 +66,13 @@ var (
|
||||
utils.DashboardAddrFlag,
|
||||
utils.DashboardPortFlag,
|
||||
utils.DashboardRefreshFlag,
|
||||
utils.DashboardAssetsFlag,
|
||||
utils.EthashCacheDirFlag,
|
||||
utils.EthashCachesInMemoryFlag,
|
||||
utils.EthashCachesOnDiskFlag,
|
||||
utils.EthashDatasetDirFlag,
|
||||
utils.EthashDatasetsInMemoryFlag,
|
||||
utils.EthashDatasetsOnDiskFlag,
|
||||
utils.TxPoolLocalsFlag,
|
||||
utils.TxPoolNoLocalsFlag,
|
||||
utils.TxPoolJournalFlag,
|
||||
utils.TxPoolRejournalFlag,
|
||||
@@ -82,8 +83,6 @@ var (
|
||||
utils.TxPoolAccountQueueFlag,
|
||||
utils.TxPoolGlobalQueueFlag,
|
||||
utils.TxPoolLifetimeFlag,
|
||||
utils.FastSyncFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.GCModeFlag,
|
||||
utils.LightServFlag,
|
||||
@@ -96,11 +95,21 @@ var (
|
||||
utils.ListenPortFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
utils.EtherbaseFlag,
|
||||
utils.GasPriceFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.MiningEnabledFlag,
|
||||
utils.TargetGasLimitFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.MinerLegacyThreadsFlag,
|
||||
utils.MinerNotifyFlag,
|
||||
utils.MinerGasTargetFlag,
|
||||
utils.MinerLegacyGasTargetFlag,
|
||||
utils.MinerGasLimitFlag,
|
||||
utils.MinerGasPriceFlag,
|
||||
utils.MinerLegacyGasPriceFlag,
|
||||
utils.MinerEtherbaseFlag,
|
||||
utils.MinerLegacyEtherbaseFlag,
|
||||
utils.MinerExtraDataFlag,
|
||||
utils.MinerLegacyExtraDataFlag,
|
||||
utils.MinerRecommitIntervalFlag,
|
||||
utils.MinerNoVerfiyFlag,
|
||||
utils.NATFlag,
|
||||
utils.NoDiscoverFlag,
|
||||
utils.DiscoveryV5Flag,
|
||||
@@ -121,7 +130,6 @@ var (
|
||||
utils.NoCompactionFlag,
|
||||
utils.GpoBlocksFlag,
|
||||
utils.GpoPercentileFlag,
|
||||
utils.ExtraDataFlag,
|
||||
configFileFlag,
|
||||
}
|
||||
|
||||
@@ -144,18 +152,29 @@ var (
|
||||
utils.WhisperMaxMessageSizeFlag,
|
||||
utils.WhisperMinPOWFlag,
|
||||
}
|
||||
|
||||
metricsFlags = []cli.Flag{
|
||||
utils.MetricsEnableInfluxDBFlag,
|
||||
utils.MetricsInfluxDBEndpointFlag,
|
||||
utils.MetricsInfluxDBDatabaseFlag,
|
||||
utils.MetricsInfluxDBUsernameFlag,
|
||||
utils.MetricsInfluxDBPasswordFlag,
|
||||
utils.MetricsInfluxDBHostTagFlag,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize the CLI app and start Geth
|
||||
app.Action = geth
|
||||
app.HideVersion = true // we have a command to print the version
|
||||
app.Copyright = "Copyright 2013-2017 The go-ethereum Authors"
|
||||
app.Copyright = "Copyright 2013-2018 The go-ethereum Authors"
|
||||
app.Commands = []cli.Command{
|
||||
// See chaincmd.go:
|
||||
initCommand,
|
||||
importCommand,
|
||||
exportCommand,
|
||||
importPreimagesCommand,
|
||||
exportPreimagesCommand,
|
||||
copydbCommand,
|
||||
removedbCommand,
|
||||
dumpCommand,
|
||||
@@ -184,16 +203,40 @@ func init() {
|
||||
app.Flags = append(app.Flags, consoleFlags...)
|
||||
app.Flags = append(app.Flags, debug.Flags...)
|
||||
app.Flags = append(app.Flags, whisperFlags...)
|
||||
app.Flags = append(app.Flags, metricsFlags...)
|
||||
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
if err := debug.Setup(ctx); err != nil {
|
||||
|
||||
logdir := ""
|
||||
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
|
||||
logdir = (&node.Config{DataDir: utils.MakeDataDir(ctx)}).ResolvePath("logs")
|
||||
}
|
||||
if err := debug.Setup(ctx, logdir); err != nil {
|
||||
return err
|
||||
}
|
||||
// Cap the cache allowance and tune the garbage collector
|
||||
var mem gosigar.Mem
|
||||
if err := mem.Get(); err == nil {
|
||||
allowance := int(mem.Total / 1024 / 1024 / 3)
|
||||
if cache := ctx.GlobalInt(utils.CacheFlag.Name); cache > allowance {
|
||||
log.Warn("Sanitizing cache to Go's GC limits", "provided", cache, "updated", allowance)
|
||||
ctx.GlobalSet(utils.CacheFlag.Name, strconv.Itoa(allowance))
|
||||
}
|
||||
}
|
||||
// Ensure Go's GC ignores the database cache for trigger percentage
|
||||
cache := ctx.GlobalInt(utils.CacheFlag.Name)
|
||||
gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))
|
||||
|
||||
log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc))
|
||||
godebug.SetGCPercent(int(gogc))
|
||||
|
||||
// Start metrics export if enabled
|
||||
utils.SetupMetrics(ctx)
|
||||
|
||||
// Start system runtime metrics collection
|
||||
go metrics.CollectProcessMetrics(3 * time.Second)
|
||||
|
||||
utils.SetupNetwork(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -215,6 +258,9 @@ func main() {
|
||||
// It creates a default node based on the command line arguments and runs it in
|
||||
// blocking mode, waiting for it to be shut down.
|
||||
func geth(ctx *cli.Context) error {
|
||||
if args := ctx.Args(); len(args) > 0 {
|
||||
return fmt.Errorf("invalid command: %q", args[0])
|
||||
}
|
||||
node := makeFullNode(ctx)
|
||||
startNode(ctx, node)
|
||||
node.Wait()
|
||||
@@ -225,6 +271,8 @@ func geth(ctx *cli.Context) error {
|
||||
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
|
||||
// miner.
|
||||
func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
debug.Memsize.Add("node", stack)
|
||||
|
||||
// Start up the node itself
|
||||
utils.StartNode(stack)
|
||||
|
||||
@@ -243,7 +291,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
stack.AccountManager().Subscribe(events)
|
||||
|
||||
go func() {
|
||||
// Create an chain state reader for self-derivation
|
||||
// Create a chain state reader for self-derivation
|
||||
rpcClient, err := stack.Attach()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to attach to self: %v", err)
|
||||
@@ -267,11 +315,11 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
status, _ := event.Wallet.Status()
|
||||
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
|
||||
|
||||
derivationPath := accounts.DefaultBaseDerivationPath
|
||||
if event.Wallet.URL().Scheme == "ledger" {
|
||||
event.Wallet.SelfDerive(accounts.DefaultLedgerBaseDerivationPath, stateReader)
|
||||
} else {
|
||||
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
|
||||
derivationPath = accounts.DefaultLedgerBaseDerivationPath
|
||||
}
|
||||
event.Wallet.SelfDerive(derivationPath, stateReader)
|
||||
|
||||
case accounts.WalletDropped:
|
||||
log.Info("Old wallet dropped", "url", event.Wallet.URL())
|
||||
@@ -282,25 +330,25 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
// Start auxiliary services if enabled
|
||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
|
||||
// Mining only makes sense if a full Ethereum node is running
|
||||
if ctx.GlobalBool(utils.LightModeFlag.Name) || ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
|
||||
if ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
|
||||
utils.Fatalf("Light clients do not support mining")
|
||||
}
|
||||
var ethereum *eth.Ethereum
|
||||
if err := stack.Service(ðereum); err != nil {
|
||||
utils.Fatalf("Ethereum service not running: %v", err)
|
||||
}
|
||||
// Use a reduced number of threads if requested
|
||||
if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
|
||||
type threaded interface {
|
||||
SetThreads(threads int)
|
||||
}
|
||||
if th, ok := ethereum.Engine().(threaded); ok {
|
||||
th.SetThreads(threads)
|
||||
}
|
||||
}
|
||||
// Set the gas price to the limits from the CLI and start mining
|
||||
ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
|
||||
if err := ethereum.StartMining(true); err != nil {
|
||||
gasprice := utils.GlobalBig(ctx, utils.MinerLegacyGasPriceFlag.Name)
|
||||
if ctx.IsSet(utils.MinerGasPriceFlag.Name) {
|
||||
gasprice = utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
|
||||
}
|
||||
ethereum.TxPool().SetGasPrice(gasprice)
|
||||
|
||||
threads := ctx.GlobalInt(utils.MinerLegacyThreadsFlag.Name)
|
||||
if ctx.GlobalIsSet(utils.MinerThreadsFlag.Name) {
|
||||
threads = ctx.GlobalInt(utils.MinerThreadsFlag.Name)
|
||||
}
|
||||
if err := ethereum.StartMining(threads); err != nil {
|
||||
utils.Fatalf("Failed to start mining: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ func makedag(ctx *cli.Context) error {
|
||||
|
||||
func version(ctx *cli.Context) error {
|
||||
fmt.Println(strings.Title(clientIdentifier))
|
||||
fmt.Println("Version:", params.Version)
|
||||
fmt.Println("Version:", params.VersionWithMeta)
|
||||
if gitCommit != "" {
|
||||
fmt.Println("Git Commit:", gitCommit)
|
||||
}
|
||||
|
||||
+3
-3
@@ -185,12 +185,12 @@ func resolveMetric(metrics map[string]interface{}, pattern string, path string)
|
||||
parts := strings.SplitN(pattern, "/", 2)
|
||||
if len(parts) > 1 {
|
||||
for _, variation := range strings.Split(parts[0], ",") {
|
||||
if submetrics, ok := metrics[variation].(map[string]interface{}); !ok {
|
||||
submetrics, ok := metrics[variation].(map[string]interface{})
|
||||
if !ok {
|
||||
utils.Fatalf("Failed to retrieve system metrics: %s", path+variation)
|
||||
return nil
|
||||
} else {
|
||||
results = append(results, resolveMetric(submetrics, parts[1], path+variation+"/")...)
|
||||
}
|
||||
results = append(results, resolveMetric(submetrics, parts[1], path+variation+"/")...)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
+29
-9
@@ -33,7 +33,7 @@ import (
|
||||
var AppHelpTemplate = `NAME:
|
||||
{{.App.Name}} - {{.App.Usage}}
|
||||
|
||||
Copyright 2013-2017 The go-ethereum Authors
|
||||
Copyright 2013-2018 The go-ethereum Authors
|
||||
|
||||
USAGE:
|
||||
{{.App.HelpName}} [options]{{if .App.Commands}} command [command options]{{end}} {{if .App.ArgsUsage}}{{.App.ArgsUsage}}{{else}}[arguments...]{{end}}
|
||||
@@ -83,7 +83,8 @@ var AppHelpFlagGroups = []flagGroup{
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
},
|
||||
{Name: "DEVELOPER CHAIN",
|
||||
{
|
||||
Name: "DEVELOPER CHAIN",
|
||||
Flags: []cli.Flag{
|
||||
utils.DeveloperFlag,
|
||||
utils.DeveloperPeriodFlag,
|
||||
@@ -113,6 +114,7 @@ var AppHelpFlagGroups = []flagGroup{
|
||||
{
|
||||
Name: "TRANSACTION POOL",
|
||||
Flags: []cli.Flag{
|
||||
utils.TxPoolLocalsFlag,
|
||||
utils.TxPoolNoLocalsFlag,
|
||||
utils.TxPoolJournalFlag,
|
||||
utils.TxPoolRejournalFlag,
|
||||
@@ -184,10 +186,14 @@ var AppHelpFlagGroups = []flagGroup{
|
||||
Flags: []cli.Flag{
|
||||
utils.MiningEnabledFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.EtherbaseFlag,
|
||||
utils.TargetGasLimitFlag,
|
||||
utils.GasPriceFlag,
|
||||
utils.ExtraDataFlag,
|
||||
utils.MinerNotifyFlag,
|
||||
utils.MinerGasPriceFlag,
|
||||
utils.MinerGasTargetFlag,
|
||||
utils.MinerGasLimitFlag,
|
||||
utils.MinerEtherbaseFlag,
|
||||
utils.MinerExtraDataFlag,
|
||||
utils.MinerRecommitIntervalFlag,
|
||||
utils.MinerNoVerfiyFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -206,11 +212,22 @@ var AppHelpFlagGroups = []flagGroup{
|
||||
{
|
||||
Name: "LOGGING AND DEBUGGING",
|
||||
Flags: append([]cli.Flag{
|
||||
utils.MetricsEnabledFlag,
|
||||
utils.FakePoWFlag,
|
||||
utils.NoCompactionFlag,
|
||||
}, debug.Flags...),
|
||||
},
|
||||
{
|
||||
Name: "METRICS AND STATS",
|
||||
Flags: []cli.Flag{
|
||||
utils.MetricsEnabledFlag,
|
||||
utils.MetricsEnableInfluxDBFlag,
|
||||
utils.MetricsInfluxDBEndpointFlag,
|
||||
utils.MetricsInfluxDBDatabaseFlag,
|
||||
utils.MetricsInfluxDBUsernameFlag,
|
||||
utils.MetricsInfluxDBPasswordFlag,
|
||||
utils.MetricsInfluxDBHostTagFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WHISPER (EXPERIMENTAL)",
|
||||
Flags: whisperFlags,
|
||||
@@ -218,8 +235,11 @@ var AppHelpFlagGroups = []flagGroup{
|
||||
{
|
||||
Name: "DEPRECATED",
|
||||
Flags: []cli.Flag{
|
||||
utils.FastSyncFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.MinerLegacyThreadsFlag,
|
||||
utils.MinerLegacyGasTargetFlag,
|
||||
utils.MinerLegacyGasPriceFlag,
|
||||
utils.MinerLegacyEtherbaseFlag,
|
||||
utils.MinerLegacyExtraDataFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+6
-4
@@ -180,7 +180,10 @@ func main() {
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showNetwork(ctx *cli.Context) error {
|
||||
@@ -275,9 +278,8 @@ func createNode(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
config := &adapters.NodeConfig{
|
||||
Name: ctx.String("name"),
|
||||
}
|
||||
config := adapters.RandomNodeConfig()
|
||||
config.Name = ctx.String("name")
|
||||
if key := ctx.String("key"); key != "" {
|
||||
privKey, err := crypto.HexToECDSA(key)
|
||||
if err != nil {
|
||||
|
||||
+3
-3
@@ -103,8 +103,8 @@ func newCppEthereumGenesisSpec(network string, genesis *core.Genesis) (*cppEther
|
||||
spec.Params.ByzantiumForkBlock = (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64())
|
||||
spec.Params.ConstantinopleForkBlock = (hexutil.Uint64)(math.MaxUint64)
|
||||
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainID.Uint64())
|
||||
spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainID.Uint64())
|
||||
|
||||
spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
|
||||
spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit)
|
||||
@@ -284,7 +284,7 @@ func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []strin
|
||||
spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
|
||||
spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit)
|
||||
spec.Params.GasLimitBoundDivisor = (hexutil.Uint64)(params.GasLimitBoundDivisor)
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainID.Uint64())
|
||||
spec.Params.MaxCodeSize = params.MaxCodeSize
|
||||
spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64()
|
||||
spec.Params.EIP98Transition = math.MaxUint64
|
||||
|
||||
+4
-4
@@ -609,7 +609,7 @@ func deployDashboard(client *sshClient, network string, conf *config, config *da
|
||||
}
|
||||
template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"NetworkID": conf.Genesis.Config.ChainId,
|
||||
"NetworkID": conf.Genesis.Config.ChainID,
|
||||
"NetworkTitle": strings.Title(network),
|
||||
"EthstatsPage": config.ethstats,
|
||||
"ExplorerPage": config.explorer,
|
||||
@@ -678,12 +678,12 @@ func deployDashboard(client *sshClient, network string, conf *config, config *da
|
||||
|
||||
// Build and deploy the dashboard service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// dashboardInfos is returned from an dashboard status check to allow reporting
|
||||
// dashboardInfos is returned from a dashboard status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type dashboardInfos struct {
|
||||
host string
|
||||
|
||||
+3
-3
@@ -100,9 +100,9 @@ func deployEthstats(client *sshClient, network string, port int, secret string,
|
||||
|
||||
// Build and deploy the ethstats service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// ethstatsInfos is returned from an ethstats status check to allow reporting
|
||||
@@ -122,7 +122,7 @@ func (info *ethstatsInfos) Report() map[string]string {
|
||||
"Website address": info.host,
|
||||
"Website listener port": strconv.Itoa(info.port),
|
||||
"Login secret": info.secret,
|
||||
"Banned addresses": fmt.Sprintf("%v", info.banned),
|
||||
"Banned addresses": strings.Join(info.banned, "\n"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ ADD chain.json /chain.json
|
||||
RUN \
|
||||
echo '(cd ../eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \
|
||||
echo '(cd ../etherchain-light && npm start &)' >> explorer.sh && \
|
||||
echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh
|
||||
echo 'exec /parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "explorer.sh"]
|
||||
`
|
||||
@@ -140,9 +140,9 @@ func deployExplorer(client *sshClient, network string, chainspec []byte, config
|
||||
|
||||
// Build and deploy the boot or seal node service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// explorerInfos is returned from a block explorer status check to allow reporting
|
||||
@@ -168,7 +168,7 @@ func (info *explorerInfos) Report() map[string]string {
|
||||
return report
|
||||
}
|
||||
|
||||
// checkExplorer does a health-check against an block explorer server to verify
|
||||
// checkExplorer does a health-check against a block explorer server to verify
|
||||
// whether it's running, and if yes, whether it's responsive.
|
||||
func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
|
||||
// Inspect a possible block explorer container on the host
|
||||
|
||||
+5
-5
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// faucetDockerfile is the Dockerfile required to build an faucet container to
|
||||
// faucetDockerfile is the Dockerfile required to build a faucet container to
|
||||
// grant crypto tokens based on GitHub authentications.
|
||||
var faucetDockerfile = `
|
||||
FROM ethereum/client-go:alltools-latest
|
||||
@@ -133,12 +133,12 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
|
||||
|
||||
// Build and deploy the faucet service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// faucetInfos is returned from an faucet status check to allow reporting various
|
||||
// faucetInfos is returned from a faucet status check to allow reporting various
|
||||
// configuration parameters.
|
||||
type faucetInfos struct {
|
||||
node *nodeInfos
|
||||
@@ -181,7 +181,7 @@ func (info *faucetInfos) Report() map[string]string {
|
||||
return report
|
||||
}
|
||||
|
||||
// checkFaucet does a health-check against an faucet server to verify whether
|
||||
// checkFaucet does a health-check against a faucet server to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
|
||||
// Inspect a possible faucet container on the host
|
||||
|
||||
+2
-2
@@ -81,9 +81,9 @@ func deployNginx(client *sshClient, network string, port int, nocache bool) ([]b
|
||||
|
||||
// Build and deploy the reverse-proxy service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
||||
|
||||
+13
-6
@@ -42,7 +42,7 @@ ADD genesis.json /genesis.json
|
||||
RUN \
|
||||
echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
|
||||
echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
|
||||
echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine --minerthreads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh
|
||||
echo $'exec geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--miner.etherbase {{.Etherbase}} --mine --miner.threads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --miner.gastarget {{.GasTarget}} --miner.gaslimit {{.GasLimit}} --miner.gasprice {{.GasPrice}}' >> geth.sh
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "geth.sh"]
|
||||
`
|
||||
@@ -68,6 +68,7 @@ services:
|
||||
- STATS_NAME={{.Ethstats}}
|
||||
- MINER_NAME={{.Etherbase}}
|
||||
- GAS_TARGET={{.GasTarget}}
|
||||
- GAS_LIMIT={{.GasLimit}}
|
||||
- GAS_PRICE={{.GasPrice}}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
@@ -104,6 +105,7 @@ func deployNode(client *sshClient, network string, bootnodes []string, config *n
|
||||
"Ethstats": config.ethstats,
|
||||
"Etherbase": config.etherbase,
|
||||
"GasTarget": uint64(1000000 * config.gasTarget),
|
||||
"GasLimit": uint64(1000000 * config.gasLimit),
|
||||
"GasPrice": uint64(1000000000 * config.gasPrice),
|
||||
"Unlock": config.keyJSON != "",
|
||||
})
|
||||
@@ -122,6 +124,7 @@ func deployNode(client *sshClient, network string, bootnodes []string, config *n
|
||||
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
"Etherbase": config.etherbase,
|
||||
"GasTarget": config.gasTarget,
|
||||
"GasLimit": config.gasLimit,
|
||||
"GasPrice": config.gasPrice,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
@@ -139,9 +142,9 @@ func deployNode(client *sshClient, network string, bootnodes []string, config *n
|
||||
|
||||
// Build and deploy the boot or seal node service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// nodeInfos is returned from a boot or seal node status check to allow reporting
|
||||
@@ -160,6 +163,7 @@ type nodeInfos struct {
|
||||
keyJSON string
|
||||
keyPass string
|
||||
gasTarget float64
|
||||
gasLimit float64
|
||||
gasPrice float64
|
||||
}
|
||||
|
||||
@@ -175,8 +179,9 @@ func (info *nodeInfos) Report() map[string]string {
|
||||
}
|
||||
if info.gasTarget > 0 {
|
||||
// Miner or signer node
|
||||
report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
|
||||
report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
|
||||
report["Gas floor (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
|
||||
report["Gas ceil (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit)
|
||||
|
||||
if info.etherbase != "" {
|
||||
// Ethash proof-of-work miner
|
||||
@@ -198,7 +203,7 @@ func (info *nodeInfos) Report() map[string]string {
|
||||
return report
|
||||
}
|
||||
|
||||
// checkNode does a health-check against an boot or seal node server to verify
|
||||
// checkNode does a health-check against a boot or seal node server to verify
|
||||
// whether it's running, and if yes, whether it's responsive.
|
||||
func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
|
||||
kind := "bootnode"
|
||||
@@ -217,11 +222,12 @@ func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error)
|
||||
totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
|
||||
lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
|
||||
gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
|
||||
gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64)
|
||||
gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
|
||||
|
||||
// Container available, retrieve its node ID and its genesis json
|
||||
var out []byte
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.id attach", network, kind)); err != nil {
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.id --cache=16 attach", network, kind)); err != nil {
|
||||
return nil, ErrServiceUnreachable
|
||||
}
|
||||
id := bytes.Trim(bytes.TrimSpace(out), "\"")
|
||||
@@ -256,6 +262,7 @@ func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error)
|
||||
keyJSON: keyJSON,
|
||||
keyPass: keyPass,
|
||||
gasTarget: gasTarget,
|
||||
gasLimit: gasLimit,
|
||||
gasPrice: gasPrice,
|
||||
}
|
||||
stats.enode = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.port)
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ ADD genesis.json /genesis.json
|
||||
RUN \
|
||||
echo 'node server.js &' > wallet.sh && \
|
||||
echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \
|
||||
echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh
|
||||
echo $'exec geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*" --rpcvhosts "*"' >> wallet.sh
|
||||
|
||||
RUN \
|
||||
sed -i 's/PuppethNetworkID/{{.NetworkID}}/g' dist/js/etherwallet-master.js && \
|
||||
@@ -120,9 +120,9 @@ func deployWallet(client *sshClient, network string, bootnodes []string, config
|
||||
|
||||
// Build and deploy the boot or seal node service
|
||||
if nocache {
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
||||
}
|
||||
|
||||
// walletInfos is returned from a web wallet status check to allow reporting
|
||||
|
||||
+6
-1
@@ -20,6 +20,7 @@ package main
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
@@ -34,7 +35,7 @@ func main() {
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "network",
|
||||
Usage: "name of the network to administer",
|
||||
Usage: "name of the network to administer (no spaces or hyphens, please)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "loglevel",
|
||||
@@ -47,6 +48,10 @@ func main() {
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
network := c.String("network")
|
||||
if strings.Contains(network, " ") || strings.Contains(network, "-") {
|
||||
log.Crit("No spaces or hyphens allowed in network name")
|
||||
}
|
||||
// Start the wizard and relinquish control
|
||||
makeWizard(c.String("network")).run()
|
||||
return nil
|
||||
|
||||
+30
-22
@@ -45,33 +45,44 @@ type sshClient struct {
|
||||
|
||||
// dial establishes an SSH connection to a remote node using the current user and
|
||||
// the user's configured private RSA key. If that fails, password authentication
|
||||
// is fallen back to. The caller may override the login user via user@server:port.
|
||||
// is fallen back to. server can be a string like user:identity@server:port.
|
||||
func dial(server string, pubkey []byte) (*sshClient, error) {
|
||||
// Figure out a label for the server and a logger
|
||||
label := server
|
||||
if strings.Contains(label, ":") {
|
||||
label = label[:strings.Index(label, ":")]
|
||||
}
|
||||
login := ""
|
||||
// Figure out username, identity, hostname and port
|
||||
hostname := ""
|
||||
hostport := server
|
||||
username := ""
|
||||
identity := "id_rsa" // default
|
||||
|
||||
if strings.Contains(server, "@") {
|
||||
login = label[:strings.Index(label, "@")]
|
||||
label = label[strings.Index(label, "@")+1:]
|
||||
server = server[strings.Index(server, "@")+1:]
|
||||
prefix := server[:strings.Index(server, "@")]
|
||||
if strings.Contains(prefix, ":") {
|
||||
username = prefix[:strings.Index(prefix, ":")]
|
||||
identity = prefix[strings.Index(prefix, ":")+1:]
|
||||
} else {
|
||||
username = prefix
|
||||
}
|
||||
hostport = server[strings.Index(server, "@")+1:]
|
||||
}
|
||||
logger := log.New("server", label)
|
||||
if strings.Contains(hostport, ":") {
|
||||
hostname = hostport[:strings.Index(hostport, ":")]
|
||||
} else {
|
||||
hostname = hostport
|
||||
hostport += ":22"
|
||||
}
|
||||
logger := log.New("server", server)
|
||||
logger.Debug("Attempting to establish SSH connection")
|
||||
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if login == "" {
|
||||
login = user.Username
|
||||
if username == "" {
|
||||
username = user.Username
|
||||
}
|
||||
// Configure the supported authentication methods (private key and password)
|
||||
var auths []ssh.AuthMethod
|
||||
|
||||
path := filepath.Join(user.HomeDir, ".ssh", "id_rsa")
|
||||
path := filepath.Join(user.HomeDir, ".ssh", identity)
|
||||
if buf, err := ioutil.ReadFile(path); err != nil {
|
||||
log.Warn("No SSH key, falling back to passwords", "path", path, "err", err)
|
||||
} else {
|
||||
@@ -94,14 +105,14 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
|
||||
}
|
||||
}
|
||||
auths = append(auths, ssh.PasswordCallback(func() (string, error) {
|
||||
fmt.Printf("What's the login password for %s at %s? (won't be echoed)\n> ", login, server)
|
||||
fmt.Printf("What's the login password for %s at %s? (won't be echoed)\n> ", username, server)
|
||||
blob, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
|
||||
fmt.Println()
|
||||
return string(blob), err
|
||||
}))
|
||||
// Resolve the IP address of the remote server
|
||||
addr, err := net.LookupHost(label)
|
||||
addr, err := net.LookupHost(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,10 +120,7 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
|
||||
return nil, errors.New("no IPs associated with domain")
|
||||
}
|
||||
// Try to dial in to the remote server
|
||||
logger.Trace("Dialing remote SSH server", "user", login)
|
||||
if !strings.Contains(server, ":") {
|
||||
server += ":22"
|
||||
}
|
||||
logger.Trace("Dialing remote SSH server", "user", username)
|
||||
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
// If no public key is known for SSH, ask the user to confirm
|
||||
if pubkey == nil {
|
||||
@@ -139,13 +147,13 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
|
||||
// We have a mismatch, forbid connecting
|
||||
return errors.New("ssh key mismatch, readd the machine to update")
|
||||
}
|
||||
client, err := ssh.Dial("tcp", server, &ssh.ClientConfig{User: login, Auth: auths, HostKeyCallback: keycheck})
|
||||
client, err := ssh.Dial("tcp", hostport, &ssh.ClientConfig{User: username, Auth: auths, HostKeyCallback: keycheck})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Connection established, return our utility wrapper
|
||||
c := &sshClient{
|
||||
server: label,
|
||||
server: hostname,
|
||||
address: addr[0],
|
||||
pubkey: pubkey,
|
||||
client: client,
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ func (w *wizard) deployFaucet() {
|
||||
existed := err == nil
|
||||
|
||||
infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.node.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
infos.node.network = w.conf.Genesis.Config.ChainID.Int64()
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ func (w *wizard) makeGenesis() {
|
||||
// Query the user for some custom extras
|
||||
fmt.Println()
|
||||
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
|
||||
genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
|
||||
genesis.Config.ChainID = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
|
||||
|
||||
// All done, store the genesis and flush to disk
|
||||
log.Info("Configured new genesis block")
|
||||
|
||||
+26
-22
@@ -82,7 +82,6 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s
|
||||
logger.Info("Starting remote server health-check")
|
||||
|
||||
stat := &serverStat{
|
||||
address: client.address,
|
||||
services: make(map[string]map[string]string),
|
||||
}
|
||||
if client == nil {
|
||||
@@ -94,6 +93,8 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s
|
||||
}
|
||||
client = conn
|
||||
}
|
||||
stat.address = client.address
|
||||
|
||||
// Client connected one way or another, run health-checks
|
||||
logger.Debug("Checking for nginx availability")
|
||||
if infos, err := checkNginx(client, w.network); err != nil {
|
||||
@@ -203,7 +204,7 @@ func (stats serverStats) render() {
|
||||
|
||||
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetColWidth(100)
|
||||
table.SetColWidth(40)
|
||||
|
||||
// Find the longest lines for all columns for the hacked separator
|
||||
separator := make([]string, 5)
|
||||
@@ -214,6 +215,9 @@ func (stats serverStats) render() {
|
||||
if len(stat.address) > len(separator[1]) {
|
||||
separator[1] = strings.Repeat("-", len(stat.address))
|
||||
}
|
||||
if len(stat.failure) > len(separator[1]) {
|
||||
separator[1] = strings.Repeat("-", len(stat.failure))
|
||||
}
|
||||
for service, configs := range stat.services {
|
||||
if len(service) > len(separator[2]) {
|
||||
separator[2] = strings.Repeat("-", len(service))
|
||||
@@ -222,8 +226,10 @@ func (stats serverStats) render() {
|
||||
if len(config) > len(separator[3]) {
|
||||
separator[3] = strings.Repeat("-", len(config))
|
||||
}
|
||||
if len(value) > len(separator[4]) {
|
||||
separator[4] = strings.Repeat("-", len(value))
|
||||
for _, val := range strings.Split(value, "\n") {
|
||||
if len(val) > len(separator[4]) {
|
||||
separator[4] = strings.Repeat("-", len(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,7 +254,11 @@ func (stats serverStats) render() {
|
||||
sort.Strings(services)
|
||||
|
||||
if len(services) == 0 {
|
||||
table.Append([]string{server, stats[server].address, "", "", ""})
|
||||
if stats[server].failure != "" {
|
||||
table.Append([]string{server, stats[server].failure, "", "", ""})
|
||||
} else {
|
||||
table.Append([]string{server, stats[server].address, "", "", ""})
|
||||
}
|
||||
}
|
||||
for j, service := range services {
|
||||
// Add an empty line between all services
|
||||
@@ -263,26 +273,20 @@ func (stats serverStats) render() {
|
||||
sort.Strings(configs)
|
||||
|
||||
for k, config := range configs {
|
||||
switch {
|
||||
case j == 0 && k == 0:
|
||||
table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]})
|
||||
case k == 0:
|
||||
table.Append([]string{"", "", service, config, stats[server].services[service][config]})
|
||||
default:
|
||||
table.Append([]string{"", "", "", config, stats[server].services[service][config]})
|
||||
for l, value := range strings.Split(stats[server].services[service][config], "\n") {
|
||||
switch {
|
||||
case j == 0 && k == 0 && l == 0:
|
||||
table.Append([]string{server, stats[server].address, service, config, value})
|
||||
case k == 0 && l == 0:
|
||||
table.Append([]string{"", "", service, config, value})
|
||||
case l == 0:
|
||||
table.Append([]string{"", "", "", config, value})
|
||||
default:
|
||||
table.Append([]string{"", "", "", "", value})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
}
|
||||
|
||||
// protips contains a collection of network infos to report pro-tips
|
||||
// based on.
|
||||
type protips struct {
|
||||
genesis string
|
||||
network int64
|
||||
bootFull []string
|
||||
bootLight []string
|
||||
ethstats string
|
||||
}
|
||||
|
||||
+4
-4
@@ -62,14 +62,14 @@ func (w *wizard) manageServers() {
|
||||
}
|
||||
}
|
||||
|
||||
// makeServer reads a single line from stdin and interprets it as a hostname to
|
||||
// connect to. It tries to establish a new SSH session and also executing some
|
||||
// baseline validations.
|
||||
// makeServer reads a single line from stdin and interprets it as
|
||||
// username:identity@hostname to connect to. It tries to establish a
|
||||
// new SSH session and also executing some baseline validations.
|
||||
//
|
||||
// If connection succeeds, the server is added to the wizards configs!
|
||||
func (w *wizard) makeServer() string {
|
||||
fmt.Println()
|
||||
fmt.Println("Please enter remote server's address:")
|
||||
fmt.Println("What is the remote server's address ([username[:identity]@]hostname[:port])?")
|
||||
|
||||
// Read and dial the server to ensure docker is present
|
||||
input := w.readString()
|
||||
|
||||
+8
-4
@@ -50,13 +50,13 @@ func (w *wizard) deployNode(boot bool) {
|
||||
if boot {
|
||||
infos = &nodeInfos{port: 30303, peersTotal: 512, peersLight: 256}
|
||||
} else {
|
||||
infos = &nodeInfos{port: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18}
|
||||
infos = &nodeInfos{port: 30303, peersTotal: 50, peersLight: 0, gasTarget: 7.5, gasLimit: 10, gasPrice: 1}
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
infos.network = w.conf.Genesis.Config.ChainID.Int64()
|
||||
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
@@ -107,7 +107,7 @@ func (w *wizard) deployNode(boot bool) {
|
||||
// Ethash based miners only need an etherbase to mine against
|
||||
fmt.Println()
|
||||
if infos.etherbase == "" {
|
||||
fmt.Printf("What address should the miner user?\n")
|
||||
fmt.Printf("What address should the miner use?\n")
|
||||
for {
|
||||
if address := w.readAddress(); address != nil {
|
||||
infos.etherbase = address.Hex()
|
||||
@@ -115,7 +115,7 @@ func (w *wizard) deployNode(boot bool) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
|
||||
fmt.Printf("What address should the miner use? (default = %s)\n", infos.etherbase)
|
||||
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
|
||||
}
|
||||
} else if w.conf.Genesis.Config.Clique != nil {
|
||||
@@ -152,6 +152,10 @@ func (w *wizard) deployNode(boot bool) {
|
||||
fmt.Printf("What gas limit should empty blocks target (MGas)? (default = %0.3f)\n", infos.gasTarget)
|
||||
infos.gasTarget = w.readDefaultFloat(infos.gasTarget)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("What gas limit should full blocks target (MGas)? (default = %0.3f)\n", infos.gasLimit)
|
||||
infos.gasLimit = w.readDefaultFloat(infos.gasLimit)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("What gas price should the signer require (GWei)? (default = %0.3f)\n", infos.gasPrice)
|
||||
infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func (w *wizard) deployWallet() {
|
||||
existed := err == nil
|
||||
|
||||
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
infos.network = w.conf.Genesis.Config.ChainID.Int64()
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
"github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var salt = make([]byte, 32)
|
||||
|
||||
func init() {
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
panic("reading from crypto/rand failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func accessNewPass(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
utils.Fatalf("Expected 1 argument - the ref")
|
||||
}
|
||||
|
||||
var (
|
||||
ae *api.AccessEntry
|
||||
accessKey []byte
|
||||
err error
|
||||
ref = args[0]
|
||||
password = getPassPhrase("", 0, makePasswordList(ctx))
|
||||
dryRun = ctx.Bool(SwarmDryRunFlag.Name)
|
||||
)
|
||||
accessKey, ae, err = api.DoPasswordNew(ctx, password, salt)
|
||||
if err != nil {
|
||||
utils.Fatalf("error getting session key: %v", err)
|
||||
}
|
||||
m, err := api.GenerateAccessControlManifest(ctx, ref, accessKey, ae)
|
||||
if dryRun {
|
||||
err = printManifests(m, nil)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error printing the manifests: %v", err)
|
||||
}
|
||||
} else {
|
||||
utils.Fatalf("uploading manifests")
|
||||
err = uploadManifests(ctx, m, nil)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error uploading the manifests: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accessNewPK(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
utils.Fatalf("Expected 1 argument - the ref")
|
||||
}
|
||||
|
||||
var (
|
||||
ae *api.AccessEntry
|
||||
sessionKey []byte
|
||||
err error
|
||||
ref = args[0]
|
||||
privateKey = getPrivKey(ctx)
|
||||
granteePublicKey = ctx.String(SwarmAccessGrantKeyFlag.Name)
|
||||
dryRun = ctx.Bool(SwarmDryRunFlag.Name)
|
||||
)
|
||||
sessionKey, ae, err = api.DoPKNew(ctx, privateKey, granteePublicKey, salt)
|
||||
if err != nil {
|
||||
utils.Fatalf("error getting session key: %v", err)
|
||||
}
|
||||
m, err := api.GenerateAccessControlManifest(ctx, ref, sessionKey, ae)
|
||||
if dryRun {
|
||||
err = printManifests(m, nil)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error printing the manifests: %v", err)
|
||||
}
|
||||
} else {
|
||||
err = uploadManifests(ctx, m, nil)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error uploading the manifests: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accessNewACT(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
utils.Fatalf("Expected 1 argument - the ref")
|
||||
}
|
||||
|
||||
var (
|
||||
ae *api.AccessEntry
|
||||
actManifest *api.Manifest
|
||||
accessKey []byte
|
||||
err error
|
||||
ref = args[0]
|
||||
grantees = []string{}
|
||||
actFilename = ctx.String(SwarmAccessGrantKeysFlag.Name)
|
||||
privateKey = getPrivKey(ctx)
|
||||
dryRun = ctx.Bool(SwarmDryRunFlag.Name)
|
||||
)
|
||||
|
||||
bytes, err := ioutil.ReadFile(actFilename)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error reading the grantee public key list")
|
||||
}
|
||||
grantees = strings.Split(string(bytes), "\n")
|
||||
accessKey, ae, actManifest, err = api.DoACTNew(ctx, privateKey, salt, grantees)
|
||||
if err != nil {
|
||||
utils.Fatalf("error generating ACT manifest: %v", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("error getting session key: %v", err)
|
||||
}
|
||||
m, err := api.GenerateAccessControlManifest(ctx, ref, accessKey, ae)
|
||||
if err != nil {
|
||||
utils.Fatalf("error generating root access manifest: %v", err)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
err = printManifests(m, actManifest)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error printing the manifests: %v", err)
|
||||
}
|
||||
} else {
|
||||
err = uploadManifests(ctx, m, actManifest)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error uploading the manifests: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printManifests(rootAccessManifest, actManifest *api.Manifest) error {
|
||||
js, err := json.Marshal(rootAccessManifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(js))
|
||||
|
||||
if actManifest != nil {
|
||||
js, err := json.Marshal(actManifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(js))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadManifests(ctx *cli.Context, rootAccessManifest, actManifest *api.Manifest) error {
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := client.NewClient(bzzapi)
|
||||
|
||||
var (
|
||||
key string
|
||||
err error
|
||||
)
|
||||
if actManifest != nil {
|
||||
key, err = client.UploadManifest(actManifest, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootAccessManifest.Entries[0].Access.Act = key
|
||||
}
|
||||
key, err = client.UploadManifest(rootAccessManifest, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// makePasswordList reads password lines from the file specified by the global --password flag
|
||||
// and also by the same subcommand --password flag.
|
||||
// This function ia a fork of utils.MakePasswordList to lookup cli context for subcommand.
|
||||
// Function ctx.SetGlobal is not setting the global flag value that can be accessed
|
||||
// by ctx.GlobalString using the current version of cli package.
|
||||
func makePasswordList(ctx *cli.Context) []string {
|
||||
path := ctx.GlobalString(utils.PasswordFileFlag.Name)
|
||||
if path == "" {
|
||||
path = ctx.String(utils.PasswordFileFlag.Name)
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
text, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read password file: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(text), "\n")
|
||||
// Sanitise DOS line endings.
|
||||
for i := range lines {
|
||||
lines[i] = strings.TrimRight(lines[i], "\r")
|
||||
}
|
||||
return lines
|
||||
}
|
||||
+584
@@ -0,0 +1,584 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
gorand "math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/sha3"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
)
|
||||
|
||||
// TestAccessPassword tests for the correct creation of an ACT manifest protected by a password.
|
||||
// The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry
|
||||
// The parties participating - node (publisher), uploads to second node then disappears. Content which was uploaded
|
||||
// is then fetched through 2nd node. since the tested code is not key-aware - we can just
|
||||
// fetch from the 2nd node using HTTP BasicAuth
|
||||
func TestAccessPassword(t *testing.T) {
|
||||
cluster := newTestCluster(t, 1)
|
||||
defer cluster.Shutdown()
|
||||
proxyNode := cluster.Nodes[0]
|
||||
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempDir("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
// write data to file
|
||||
data := "notsorandomdata"
|
||||
dataFilename := filepath.Join(tmp, "data.txt")
|
||||
|
||||
err = ioutil.WriteFile(dataFilename, []byte(data), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hashRegexp := `[a-f\d]{128}`
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
up := runSwarm(t,
|
||||
"--bzzapi",
|
||||
proxyNode.URL, //it doesn't matter through which node we upload content
|
||||
"up",
|
||||
"--encrypt",
|
||||
dataFilename)
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) < 1 {
|
||||
t.Fatal("no matches found")
|
||||
}
|
||||
|
||||
ref := matches[0]
|
||||
|
||||
password := "smth"
|
||||
passwordFilename := filepath.Join(tmp, "password.txt")
|
||||
|
||||
err = ioutil.WriteFile(passwordFilename, []byte(password), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
up = runSwarm(t,
|
||||
"access",
|
||||
"new",
|
||||
"pass",
|
||||
"--dry-run",
|
||||
"--password",
|
||||
passwordFilename,
|
||||
ref,
|
||||
)
|
||||
|
||||
_, matches = up.ExpectRegexp(".+")
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) == 0 {
|
||||
t.Fatalf("stdout not matched")
|
||||
}
|
||||
|
||||
var m api.Manifest
|
||||
|
||||
err = json.Unmarshal([]byte(matches[0]), &m)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal manifest: %v", err)
|
||||
}
|
||||
|
||||
if len(m.Entries) != 1 {
|
||||
t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
|
||||
}
|
||||
|
||||
e := m.Entries[0]
|
||||
|
||||
ct := "application/bzz-manifest+json"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected %q content type, got %q", ct, e.ContentType)
|
||||
}
|
||||
|
||||
if e.Access == nil {
|
||||
t.Fatal("manifest access is nil")
|
||||
}
|
||||
|
||||
a := e.Access
|
||||
|
||||
if a.Type != "pass" {
|
||||
t.Errorf(`got access type %q, expected "pass"`, a.Type)
|
||||
}
|
||||
if len(a.Salt) < 32 {
|
||||
t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
|
||||
}
|
||||
if a.KdfParams == nil {
|
||||
t.Fatal("manifest access kdf params is nil")
|
||||
}
|
||||
|
||||
client := swarm.NewClient(cluster.Nodes[0].URL)
|
||||
|
||||
hash, err := client.UploadManifest(&m, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
|
||||
url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash
|
||||
response, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatal("should be a 401")
|
||||
}
|
||||
authHeader := response.Header.Get("WWW-Authenticate")
|
||||
if authHeader == "" {
|
||||
t.Fatal("should be something here")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.SetBasicAuth("", password)
|
||||
|
||||
response, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status %v, got %v", http.StatusOK, response.StatusCode)
|
||||
}
|
||||
d, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(d) != data {
|
||||
t.Errorf("expected decrypted data %q, got %q", data, string(d))
|
||||
}
|
||||
|
||||
wrongPasswordFilename := filepath.Join(tmp, "password-wrong.txt")
|
||||
|
||||
err = ioutil.WriteFile(wrongPasswordFilename, []byte("just wr0ng"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//download file with 'swarm down' with wrong password
|
||||
up = runSwarm(t,
|
||||
"--bzzapi",
|
||||
proxyNode.URL,
|
||||
"down",
|
||||
"bzz:/"+hash,
|
||||
tmp,
|
||||
"--password",
|
||||
wrongPasswordFilename)
|
||||
|
||||
_, matches = up.ExpectRegexp("unauthorized")
|
||||
if len(matches) != 1 && matches[0] != "unauthorized" {
|
||||
t.Fatal(`"unauthorized" not found in output"`)
|
||||
}
|
||||
up.ExpectExit()
|
||||
}
|
||||
|
||||
// TestAccessPK tests for the correct creation of an ACT manifest between two parties (publisher and grantee).
|
||||
// The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry
|
||||
// The parties participating - node (publisher), uploads to second node (which is also the grantee) then disappears.
|
||||
// Content which was uploaded is then fetched through the grantee's http proxy. Since the tested code is private-key aware,
|
||||
// the test will fail if the proxy's given private key is not granted on the ACT.
|
||||
func TestAccessPK(t *testing.T) {
|
||||
// Setup Swarm and upload a test file to it
|
||||
cluster := newTestCluster(t, 1)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tmp.Close()
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
// write data to file
|
||||
data := "notsorandomdata"
|
||||
_, err = io.WriteString(tmp, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hashRegexp := `[a-f\d]{128}`
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
up := runSwarm(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
"--encrypt",
|
||||
tmp.Name())
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) < 1 {
|
||||
t.Fatal("no matches found")
|
||||
}
|
||||
|
||||
ref := matches[0]
|
||||
|
||||
pk := cluster.Nodes[0].PrivateKey
|
||||
granteePubKey := crypto.CompressPubkey(&pk.PublicKey)
|
||||
|
||||
publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
passFile, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer passFile.Close()
|
||||
defer os.Remove(passFile.Name())
|
||||
_, err = io.WriteString(passFile, testPassphrase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, publisherAccount := getTestAccount(t, publisherDir)
|
||||
up = runSwarm(t,
|
||||
"--bzzaccount",
|
||||
publisherAccount.Address.String(),
|
||||
"--password",
|
||||
passFile.Name(),
|
||||
"--datadir",
|
||||
publisherDir,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"access",
|
||||
"new",
|
||||
"pk",
|
||||
"--dry-run",
|
||||
"--grant-key",
|
||||
hex.EncodeToString(granteePubKey),
|
||||
ref,
|
||||
)
|
||||
|
||||
_, matches = up.ExpectRegexp(".+")
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) == 0 {
|
||||
t.Fatalf("stdout not matched")
|
||||
}
|
||||
|
||||
var m api.Manifest
|
||||
|
||||
err = json.Unmarshal([]byte(matches[0]), &m)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal manifest: %v", err)
|
||||
}
|
||||
|
||||
if len(m.Entries) != 1 {
|
||||
t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
|
||||
}
|
||||
|
||||
e := m.Entries[0]
|
||||
|
||||
ct := "application/bzz-manifest+json"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected %q content type, got %q", ct, e.ContentType)
|
||||
}
|
||||
|
||||
if e.Access == nil {
|
||||
t.Fatal("manifest access is nil")
|
||||
}
|
||||
|
||||
a := e.Access
|
||||
|
||||
if a.Type != "pk" {
|
||||
t.Errorf(`got access type %q, expected "pk"`, a.Type)
|
||||
}
|
||||
if len(a.Salt) < 32 {
|
||||
t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
|
||||
}
|
||||
if a.KdfParams != nil {
|
||||
t.Fatal("manifest access kdf params should be nil")
|
||||
}
|
||||
|
||||
client := swarm.NewClient(cluster.Nodes[0].URL)
|
||||
|
||||
hash, err := client.UploadManifest(&m, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
|
||||
url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash
|
||||
response, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatal("should be a 200")
|
||||
}
|
||||
d, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(d) != data {
|
||||
t.Errorf("expected decrypted data %q, got %q", data, string(d))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessACT tests the e2e creation, uploading and downloading of an ACT type access control
|
||||
// the test fires up a 3 node cluster, then randomly picks 2 nodes which will be acting as grantees to the data
|
||||
// set. the third node should fail decoding the reference as it will not be granted access. the publisher uploads through
|
||||
// one of the nodes then disappears.
|
||||
func TestAccessACT(t *testing.T) {
|
||||
// Setup Swarm and upload a test file to it
|
||||
cluster := newTestCluster(t, 3)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
var uploadThroughNode = cluster.Nodes[0]
|
||||
client := swarm.NewClient(uploadThroughNode.URL)
|
||||
|
||||
r1 := gorand.New(gorand.NewSource(time.Now().UnixNano()))
|
||||
nodeToSkip := r1.Intn(3) // a number between 0 and 2 (node indices in `cluster`)
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tmp.Close()
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
// write data to file
|
||||
data := "notsorandomdata"
|
||||
_, err = io.WriteString(tmp, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hashRegexp := `[a-f\d]{128}`
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
up := runSwarm(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
"--encrypt",
|
||||
tmp.Name())
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) < 1 {
|
||||
t.Fatal("no matches found")
|
||||
}
|
||||
|
||||
ref := matches[0]
|
||||
grantees := []string{}
|
||||
for i, v := range cluster.Nodes {
|
||||
if i == nodeToSkip {
|
||||
continue
|
||||
}
|
||||
pk := v.PrivateKey
|
||||
granteePubKey := crypto.CompressPubkey(&pk.PublicKey)
|
||||
grantees = append(grantees, hex.EncodeToString(granteePubKey))
|
||||
}
|
||||
|
||||
granteesPubkeyListFile, err := ioutil.TempFile("", "grantees-pubkey-list.csv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = granteesPubkeyListFile.WriteString(strings.Join(grantees, "\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer granteesPubkeyListFile.Close()
|
||||
defer os.Remove(granteesPubkeyListFile.Name())
|
||||
|
||||
publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
passFile, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer passFile.Close()
|
||||
defer os.Remove(passFile.Name())
|
||||
_, err = io.WriteString(passFile, testPassphrase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, publisherAccount := getTestAccount(t, publisherDir)
|
||||
up = runSwarm(t,
|
||||
"--bzzaccount",
|
||||
publisherAccount.Address.String(),
|
||||
"--password",
|
||||
passFile.Name(),
|
||||
"--datadir",
|
||||
publisherDir,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"access",
|
||||
"new",
|
||||
"act",
|
||||
"--grant-keys",
|
||||
granteesPubkeyListFile.Name(),
|
||||
ref,
|
||||
)
|
||||
|
||||
_, matches = up.ExpectRegexp(`[a-f\d]{64}`)
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) == 0 {
|
||||
t.Fatalf("stdout not matched")
|
||||
}
|
||||
hash := matches[0]
|
||||
m, _, err := client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal manifest: %v", err)
|
||||
}
|
||||
|
||||
if len(m.Entries) != 1 {
|
||||
t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
|
||||
}
|
||||
|
||||
e := m.Entries[0]
|
||||
|
||||
ct := "application/bzz-manifest+json"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected %q content type, got %q", ct, e.ContentType)
|
||||
}
|
||||
|
||||
if e.Access == nil {
|
||||
t.Fatal("manifest access is nil")
|
||||
}
|
||||
|
||||
a := e.Access
|
||||
|
||||
if a.Type != "act" {
|
||||
t.Fatalf(`got access type %q, expected "act"`, a.Type)
|
||||
}
|
||||
if len(a.Salt) < 32 {
|
||||
t.Fatalf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
|
||||
}
|
||||
if a.KdfParams != nil {
|
||||
t.Fatal("manifest access kdf params should be nil")
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
|
||||
// all nodes except the skipped node should be able to decrypt the content
|
||||
for i, node := range cluster.Nodes {
|
||||
log.Debug("trying to fetch from node", "node index", i)
|
||||
|
||||
url := node.URL + "/" + "bzz:/" + hash
|
||||
response, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log.Debug("got response from node", "response code", response.StatusCode)
|
||||
|
||||
if i == nodeToSkip {
|
||||
log.Debug("reached node to skip", "status code", response.StatusCode)
|
||||
|
||||
if response.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("should be a 401")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatal("should be a 200")
|
||||
}
|
||||
d, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(d) != data {
|
||||
t.Errorf("expected decrypted data %q, got %q", data, string(d))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeypairSanity is a sanity test for the crypto scheme for ACT. it asserts the correct shared secret according to
|
||||
// the specs at https://github.com/ethersphere/swarm-docs/blob/eb857afda906c6e7bb90d37f3f334ccce5eef230/act.md
|
||||
func TestKeypairSanity(t *testing.T) {
|
||||
salt := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
t.Fatalf("reading from crypto/rand failed: %v", err.Error())
|
||||
}
|
||||
sharedSecret := "a85586744a1ddd56a7ed9f33fa24f40dd745b3a941be296a0d60e329dbdb896d"
|
||||
|
||||
for i, v := range []struct {
|
||||
publisherPriv string
|
||||
granteePub string
|
||||
}{
|
||||
{
|
||||
publisherPriv: "ec5541555f3bc6376788425e9d1a62f55a82901683fd7062c5eddcc373a73459",
|
||||
granteePub: "0226f213613e843a413ad35b40f193910d26eb35f00154afcde9ded57479a6224a",
|
||||
},
|
||||
{
|
||||
publisherPriv: "70c7a73011aa56584a0009ab874794ee7e5652fd0c6911cd02f8b6267dd82d2d",
|
||||
granteePub: "02e6f8d5e28faaa899744972bb847b6eb805a160494690c9ee7197ae9f619181db",
|
||||
},
|
||||
} {
|
||||
b, _ := hex.DecodeString(v.granteePub)
|
||||
granteePub, _ := crypto.DecompressPubkey(b)
|
||||
publisherPrivate, _ := crypto.HexToECDSA(v.publisherPriv)
|
||||
|
||||
ssKey, err := api.NewSessionKeyPK(publisherPrivate, granteePub, salt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hasher := sha3.NewKeccak256()
|
||||
hasher.Write(salt)
|
||||
shared, err := hex.DecodeString(sharedSecret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasher.Write(shared)
|
||||
sum := hasher.Sum(nil)
|
||||
|
||||
if !bytes.Equal(ssKey, sum) {
|
||||
t.Fatalf("%d: got a session key mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
var SwarmBootnodes = []string{
|
||||
// Foundation Swarm Gateway Cluster
|
||||
"enode://e5c6f9215c919a5450a7b8c14c22535607b69f2c8e1e7f6f430cb25d7a2c27cd1df4c4f18ad7c1d7e5162e271ffcd3f20b1a1467fb6e790e7d727f3b2193de97@52.232.7.187:30399",
|
||||
"enode://9b2fe07e69ccc7db5fef15793dab7d7d2e697ed92132d6e9548218e68a34613a8671ad03a6658d862b468ed693cae8a0f8f8d37274e4a657ffb59ca84676e45b@52.232.7.187:30400",
|
||||
"enode://76c1059162c93ef9df0f01097c824d17c492634df211ef4c806935b349082233b63b90c23970254b3b7138d630400f7cf9b71e80355a446a8b733296cb04169a@52.232.7.187:30401",
|
||||
"enode://ce46bbe2a8263145d65252d52da06e000ad350ed09c876a71ea9544efa42f63c1e1b6cc56307373aaad8f9dd069c90d0ed2dd1530106200e16f4ca681dd8ae2d@52.232.7.187:30402",
|
||||
"enode://f431e0d6008a6c35c6e670373d828390c8323e53da8158e7bfc43cf07e632cc9e472188be8df01decadea2d4a068f1428caba769b632554a8fb0607bc296988f@52.232.7.187:30403",
|
||||
"enode://174720abfff83d7392f121108ae50ea54e04889afe020df883655c0f6cb95414db945a0228d8982fe000d86fc9f4b7669161adc89cd7cd56f78f01489ab2b99b@52.232.7.187:30404",
|
||||
"enode://2ae89be4be61a689b6f9ecee4360a59e185e010ab750f14b63b4ae43d4180e872e18e3437d4386ce44875dc7cc6eb761acba06412fe3178f3dac1dab3b65703e@52.232.7.187:30405",
|
||||
"enode://24abebe1c0e6d75d6052ce3219a87be8573fd6397b4cb51f0773b83abba9b3d872bfb273cdc07389715b87adfac02f5235f5241442c5089802cbd8d42e310fce@52.232.7.187:30406",
|
||||
"enode://d08dfa46bfbbdbcaafbb6e34abee4786610f6c91e0b76d7881f0334ac10dda41d8c1f2b6eedffb4493293c335c0ad46776443b2208d1fbbb9e1a90b25ee4eef2@52.232.7.187:30407",
|
||||
"enode://8d95eb0f837d27581a43668ed3b8783d69dc4e84aa3edd7a0897e026155c8f59c8702fdc0375ee7bac15757c9c78e1315d9b73e4ce59c936db52ea4ae2f501c7@52.232.7.187:30408",
|
||||
"enode://a5967cc804aebd422baaaba9f06f27c9e695ccab335b61088130f8cbe64e3cdf78793868c7051dfc06eecfe844fad54bc7f6dfaed9db3c7ecef279cb829c25fb@52.232.7.187:30409",
|
||||
"enode://5f00134d81a8f2ebcc46f8766f627f492893eda48138f811b7de2168308171968f01710bca6da05764e74f14bae41652f554e6321f1aed85fa3461e89d075dbf@52.232.7.187:30410",
|
||||
"enode://b2142b79b01a5aa66a5e23cc35e78219a8e97bc2412a6698cee24ae02e87078b725d71730711bd62e25ff1aa8658c6633778af8ac14c63814a337c3dd0ebda9f@52.232.7.187:30411",
|
||||
"enode://1ffa7651094867d6486ce3ef46d27a052c2cb968b618346c6df7040322c7efc3337547ba85d4cbba32e8b31c42c867202554735c06d4c664b9afada2ed0c4b3c@52.232.7.187:30412",
|
||||
"enode://129e0c3d5f5df12273754f6f703d2424409fa4baa599e0b758c55600169313887855e75b082028d2302ec034b303898cd697cc7ae8256ba924ce927510da2c8d@52.232.7.187:30413",
|
||||
"enode://419e2dc0d2f5b022cf16b0e28842658284909fa027a0fbbb5e2b755e7f846ea02a8f0b66a7534981edf6a7bcf8a14855344c6668e2cd4476ccd35a11537c9144@52.232.7.187:30414",
|
||||
"enode://23d55ad900583231b91f2f62e3f72eb498b342afd58b682be3af052eed62b5651094471065981de33d8786f075f05e3cca499503b0ac8ae84b2a06e99f5b0723@52.232.7.187:30415",
|
||||
"enode://bc56e4158c00e9f616d7ea533def20a89bef959df4e62a768ff238ff4e1e9223f57ecff969941c20921bad98749baae311c0fbebce53bf7bbb9d3dc903640990@52.232.7.187:30416",
|
||||
"enode://433ce15199c409875e7e72fffd69fdafe746f17b20f0d5555281722a65fde6c80328fab600d37d8624509adc072c445ce0dad4a1c01cff6acf3132c11d429d4d@52.232.7.187:30417",
|
||||
"enode://632ee95b8f0eac51ef89ceb29313fef3a60050181d66a6b125583b1a225a7694b252edc016efb58aa3b251da756cb73280842a022c658ed405223b2f58626343@52.232.7.187:30418",
|
||||
"enode://4a0f9bcff7a4b9ee453fb298d0fb222592efe121512e30cd72fef631beb8c6a15153a1456eb073ee18551c0e003c569651a101892dc4124e90b933733a498bb5@52.232.7.187:30419",
|
||||
"enode://f0d80fbc72d16df30e19aac3051eb56a7aff0c8367686702e01ea132d8b0b3ee00cadd6a859d2cca98ec68d3d574f8a8a87dba2347ec1e2818dc84bc3fa34fae@52.232.7.187:30420",
|
||||
"enode://a199146906e4f9f2b94b195a8308d9a59a3564b92efaab898a4243fe4c2ad918b7a8e4853d9d901d94fad878270a2669d644591299c3d43de1b298c00b92b4a7@52.232.7.187:30421",
|
||||
"enode://052036ea8736b37adbfb684d90ce43e11b3591b51f31489d7c726b03618dea4f73b1e659deb928e6bf40564edcdcf08351643f42db3d4ca1c2b5db95dad59e94@52.232.7.187:30422",
|
||||
"enode://460e2b8c6da8f12fac96c836e7d108f4b7ec55a1c64631bb8992339e117e1c28328fee83af863196e20af1487a655d13e5ceba90e980e92502d5bac5834c1f71@52.232.7.187:30423",
|
||||
"enode://6d2cdd13741b2e72e9031e1b93c6d9a4e68de2844aa4e939f6a8a8498a7c1d7e2ee4c64217e92a6df08c9a32c6764d173552810ef1bd2ecb356532d389dd2136@52.232.7.187:30424",
|
||||
"enode://62105fc25ce2cd5b299647f47eaa9211502dc76f0e9f461df915782df7242ac3223e3db04356ae6ed2977ccac20f0b16864406e9ca514a40a004cb6a5d0402aa@52.232.7.187:30425",
|
||||
"enode://e0e388fc520fd493c33f0ce16685e6f98fb6aec28f2edc14ee6b179594ee519a896425b0025bb6f0e182dd3e468443f19c70885fbc66560d000093a668a86aa8@52.232.7.187:30426",
|
||||
"enode://63f3353a72521ea10022127a4fe6b4acbef197c3fe668fd9f4805542d8a6fcf79f6335fbab62d180a35e19b739483e740858b113fdd7c13a26ad7b4e318a5aef@52.232.7.187:30427",
|
||||
"enode://33a42b927085678d4aefd4e70b861cfca6ef5f6c143696c4f755973fd29e64c9e658cad57a66a687a7a156da1e3688b1fbdd17bececff2ee009fff038fa5666b@52.232.7.187:30428",
|
||||
"enode://259ab5ab5c1daee3eab7e3819ab3177b82d25c29e6c2444fdd3f956e356afae79a72840ccf2d0665fe82c81ebc3b3734da1178ac9fd5d62c67e674b69f86b6be@52.232.7.187:30429",
|
||||
"enode://558bccad7445ce3fd8db116ed6ab4aed1324fdbdac2348417340c1764dc46d46bffe0728e5b7d5c36f12e794c289f18f57f08f085d2c65c9910a5c7a65b6a66a@52.232.7.187:30430",
|
||||
"enode://abe60937a0657ffded718e3f84a32987286983be257bdd6004775c4b525747c2b598f4fac49c8de324de5ce75b22673fa541a7ce2d555fb7f8ca325744ae3577@52.232.7.187:30431",
|
||||
"enode://bce6f0aaa5b230742680084df71d4f026b3eff7f564265599216a1b06b765303fdc9325de30ffd5dfdaf302ce4b14322891d2faea50ce2ca298d7409f5858339@52.232.7.187:30432",
|
||||
"enode://21b957c4e03277d42be6660730ec1b93f540764f26c6abdb54d006611139c7081248486206dfbf64fcaffd62589e9c6b8ea77a5297e4b21a605f1bcf49483ed0@52.232.7.187:30433",
|
||||
"enode://ff104e30e64f24c3d7328acee8b13354e5551bc8d60bb25ecbd9632d955c7e34bb2d969482d173355baad91c8282f8b592624eb3929151090da3b4448d4d58fb@52.232.7.187:30434",
|
||||
"enode://c76e2b5f81a521bceaec1518926a21380a345df9cf463461562c6845795512497fb67679e155fc96a74350f8b78de8f4c135dd52b106dbbb9795452021d09ea5@52.232.7.187:30435",
|
||||
"enode://3288fd860105164f3e9b69934c4eb18f7146cfab31b5a671f994e21a36e9287766e5f9f075aefbc404538c77f7c2eb2a4495020a7633a1c3970d94e9fa770aeb@52.232.7.187:30436",
|
||||
"enode://6cea859c7396d46b20cfcaa80f9a11cd112f8684f2f782f7b4c0e1e0af9212113429522075101923b9b957603e6c32095a6a07b5e5e35183c521952ee108dfaf@52.232.7.187:30437",
|
||||
"enode://f628ec56e4ca8317cc24cc4ac9b27b95edcce7b96e1c7f3b53e30de4a8580fe44f2f0694a513bdb0a431acaf2824074d6ace4690247bbc34c14f426af8c056ea@52.232.7.187:30438",
|
||||
"enode://055ec8b26fc105c4f97970a1cce9773a5e34c03f511b839db742198a1c571e292c54aa799e9afb991cc8a560529b8cdf3e0c344bc6c282aff2f68eec59361ddf@52.232.7.187:30439",
|
||||
"enode://48cb0d430c328974226aa33a931d8446cd5a8d40f3ead8f4ce7ad60faa1278192eb6d58bed91258d63e81f255fc107eec2425ce2ae8b22350dd556076e160610@52.232.7.187:30440",
|
||||
"enode://3fadb7af7f770d5ffc6b073b8d42834bebb18ce1fe8a4fe270d2b799e7051327093960dc61d9a18870db288f7746a0e6ea2a013cd6ab0e5f97ca08199473aace@52.232.7.187:30441",
|
||||
"enode://a5d7168024c9992769cf380ffa559a64b4f39a29d468f579559863814eb0ae0ed689ac0871a3a2b4c78b03297485ec322d578281131ef5d5c09a4beb6200a97a@52.232.7.187:30442",
|
||||
"enode://9c57744c5b2c2d71abcbe80512652f9234d4ab041b768a2a886ab390fe6f184860f40e113290698652d7e20a8ac74d27ac8671db23eb475b6c5e6253e4693bf8@52.232.7.187:30443",
|
||||
"enode://daca9ff0c3176045a0e0ed228dee00ec86bc0939b135dc6b1caa23745d20fd0332e1ee74ad04020e89df56c7146d831a91b89d15ca3df05ba7618769fefab376@52.232.7.187:30444",
|
||||
"enode://a3f6af59428cb4b9acb198db15ef5554fa43c2b0c18e468a269722d64a27218963a2975eaf82750b6262e42192b5e3669ea51337b4cda62b33987981bc5e0c1a@52.232.7.187:30445",
|
||||
"enode://fe571422fa4651c3354c85dac61911a6a6520dd3c0332967a49d4133ca30e16a8a4946fa73ca2cb5de77917ea701a905e1c3015b2f4defcd53132b61cc84127a@52.232.7.187:30446",
|
||||
|
||||
// Mainframe
|
||||
"enode://ee9a5a571ea6c8a59f9a8bb2c569c865e922b41c91d09b942e8c1d4dd2e1725bd2c26149da14de1f6321a2c6fdf1e07c503c3e093fb61696daebf74d6acd916b@54.186.219.160:30399",
|
||||
"enode://a03f0562ecb8a992ad5242345535e73483cdc18ab934d36bf24b567d43447c2cea68f89f1d51d504dd13acc30f24ebce5a150bea2ccb1b722122ce4271dc199d@52.67.248.147:30399",
|
||||
"enode://e2cbf9eafd85903d3b1c56743035284320695e0072bc8d7396e0542aa5e1c321b236f67eab66b79c2f15d4447fa4bbe74dd67d0467da23e7eb829f60ec8a812b@13.58.169.1:30399",
|
||||
"enode://8b8c6bda6047f1cad9fab2db4d3d02b7aa26279902c32879f7bcd4a7d189fee77fdc36ee151ce6b84279b4792e72578fd529d2274d014132465758fbfee51cee@13.209.13.15:30399",
|
||||
"enode://63f6a8818927e429585287cf2ca0cb9b11fa990b7b9b331c2962cdc6f21807a2473b26e8256225c26caff70d7218e59586d704d49061452c6852e382c885d03c@35.154.106.174:30399",
|
||||
"enode://ed4bd3b794ed73f18e6dcc70c6624dfec63b5654f6ab54e8f40b16eff8afbd342d4230e099ddea40e84423f81b2d2ea79799dc345257b1fec6f6c422c9d008f7@52.213.20.99:30399",
|
||||
}
|
||||
+77
-51
@@ -24,6 +24,7 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
@@ -58,19 +59,27 @@ var (
|
||||
|
||||
//constants for environment variables
|
||||
const (
|
||||
SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR"
|
||||
SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT"
|
||||
SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR"
|
||||
SWARM_ENV_PORT = "SWARM_PORT"
|
||||
SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID"
|
||||
SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE"
|
||||
SWARM_ENV_SWAP_API = "SWARM_SWAP_API"
|
||||
SWARM_ENV_SYNC_ENABLE = "SWARM_SYNC_ENABLE"
|
||||
SWARM_ENV_ENS_API = "SWARM_ENS_API"
|
||||
SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR"
|
||||
SWARM_ENV_CORS = "SWARM_CORS"
|
||||
SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES"
|
||||
GETH_ENV_DATADIR = "GETH_DATADIR"
|
||||
SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR"
|
||||
SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT"
|
||||
SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR"
|
||||
SWARM_ENV_PORT = "SWARM_PORT"
|
||||
SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID"
|
||||
SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE"
|
||||
SWARM_ENV_SWAP_API = "SWARM_SWAP_API"
|
||||
SWARM_ENV_SYNC_DISABLE = "SWARM_SYNC_DISABLE"
|
||||
SWARM_ENV_SYNC_UPDATE_DELAY = "SWARM_ENV_SYNC_UPDATE_DELAY"
|
||||
SWARM_ENV_LIGHT_NODE_ENABLE = "SWARM_LIGHT_NODE_ENABLE"
|
||||
SWARM_ENV_DELIVERY_SKIP_CHECK = "SWARM_DELIVERY_SKIP_CHECK"
|
||||
SWARM_ENV_ENS_API = "SWARM_ENS_API"
|
||||
SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR"
|
||||
SWARM_ENV_CORS = "SWARM_CORS"
|
||||
SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES"
|
||||
SWARM_ENV_PSS_ENABLE = "SWARM_PSS_ENABLE"
|
||||
SWARM_ENV_STORE_PATH = "SWARM_STORE_PATH"
|
||||
SWARM_ENV_STORE_CAPACITY = "SWARM_STORE_CAPACITY"
|
||||
SWARM_ENV_STORE_CACHE_CAPACITY = "SWARM_STORE_CACHE_CAPACITY"
|
||||
SWARM_ACCESS_PASSWORD = "SWARM_ACCESS_PASSWORD"
|
||||
GETH_ENV_DATADIR = "GETH_DATADIR"
|
||||
)
|
||||
|
||||
// These settings ensure that TOML keys use the same names as Go struct fields.
|
||||
@@ -92,10 +101,8 @@ var tomlSettings = toml.Config{
|
||||
|
||||
//before booting the swarm node, build the configuration
|
||||
func buildConfig(ctx *cli.Context) (config *bzzapi.Config, err error) {
|
||||
//check for deprecated flags
|
||||
checkDeprecated(ctx)
|
||||
//start by creating a default config
|
||||
config = bzzapi.NewDefaultConfig()
|
||||
config = bzzapi.NewConfig()
|
||||
//first load settings from config file (if provided)
|
||||
config, err = configFileOverride(config, ctx)
|
||||
if err != nil {
|
||||
@@ -126,7 +133,7 @@ func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context) {
|
||||
log.Debug(printConfig(config))
|
||||
}
|
||||
|
||||
//override the current config with whatever is in the config file, if a config file has been provided
|
||||
//configFileOverride overrides the current config with the config file, if a config file has been provided
|
||||
func configFileOverride(config *bzzapi.Config, ctx *cli.Context) (*bzzapi.Config, error) {
|
||||
var err error
|
||||
|
||||
@@ -136,7 +143,8 @@ func configFileOverride(config *bzzapi.Config, ctx *cli.Context) (*bzzapi.Config
|
||||
if filepath = ctx.GlobalString(SwarmTomlConfigPathFlag.Name); filepath == "" {
|
||||
utils.Fatalf("Config file flag provided with invalid file path")
|
||||
}
|
||||
f, err := os.Open(filepath)
|
||||
var f *os.File
|
||||
f, err = os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,7 +176,7 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
|
||||
|
||||
if networkid := ctx.GlobalString(SwarmNetworkIdFlag.Name); networkid != "" {
|
||||
if id, _ := strconv.Atoi(networkid); id != 0 {
|
||||
currentConfig.NetworkId = uint64(id)
|
||||
currentConfig.NetworkID = uint64(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,12 +199,24 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
|
||||
currentConfig.SwapEnabled = true
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(SwarmSyncEnabledFlag.Name) {
|
||||
currentConfig.SyncEnabled = true
|
||||
if ctx.GlobalIsSet(SwarmSyncDisabledFlag.Name) {
|
||||
currentConfig.SyncEnabled = false
|
||||
}
|
||||
|
||||
currentConfig.SwapApi = ctx.GlobalString(SwarmSwapAPIFlag.Name)
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapApi == "" {
|
||||
if d := ctx.GlobalDuration(SwarmSyncUpdateDelay.Name); d > 0 {
|
||||
currentConfig.SyncUpdateDelay = d
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(SwarmLightNodeEnabled.Name) {
|
||||
currentConfig.LightNodeEnabled = true
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(SwarmDeliverySkipCheckFlag.Name) {
|
||||
currentConfig.DeliverySkipCheck = true
|
||||
}
|
||||
|
||||
currentConfig.SwapAPI = ctx.GlobalString(SwarmSwapAPIFlag.Name)
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapAPI == "" {
|
||||
utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API)
|
||||
}
|
||||
|
||||
@@ -209,16 +229,20 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
|
||||
currentConfig.EnsAPIs = ensAPIs
|
||||
}
|
||||
|
||||
if ensaddr := ctx.GlobalString(DeprecatedEnsAddrFlag.Name); ensaddr != "" {
|
||||
currentConfig.EnsRoot = common.HexToAddress(ensaddr)
|
||||
}
|
||||
|
||||
if cors := ctx.GlobalString(CorsStringFlag.Name); cors != "" {
|
||||
currentConfig.Cors = cors
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(utils.BootnodesFlag.Name) {
|
||||
currentConfig.BootNodes = ctx.GlobalString(utils.BootnodesFlag.Name)
|
||||
if storePath := ctx.GlobalString(SwarmStorePath.Name); storePath != "" {
|
||||
currentConfig.LocalStoreParams.ChunkDbPath = storePath
|
||||
}
|
||||
|
||||
if storeCapacity := ctx.GlobalUint64(SwarmStoreCapacity.Name); storeCapacity != 0 {
|
||||
currentConfig.LocalStoreParams.DbCapacity = storeCapacity
|
||||
}
|
||||
|
||||
if storeCacheCapacity := ctx.GlobalUint(SwarmStoreCacheCapacity.Name); storeCacheCapacity != 0 {
|
||||
currentConfig.LocalStoreParams.CacheCapacity = storeCacheCapacity
|
||||
}
|
||||
|
||||
return currentConfig
|
||||
@@ -239,7 +263,7 @@ func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
|
||||
|
||||
if networkid := os.Getenv(SWARM_ENV_NETWORK_ID); networkid != "" {
|
||||
if id, _ := strconv.Atoi(networkid); id != 0 {
|
||||
currentConfig.NetworkId = uint64(id)
|
||||
currentConfig.NetworkID = uint64(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,17 +286,35 @@ func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
if syncenable := os.Getenv(SWARM_ENV_SYNC_ENABLE); syncenable != "" {
|
||||
if sync, err := strconv.ParseBool(syncenable); err != nil {
|
||||
currentConfig.SyncEnabled = sync
|
||||
if syncdisable := os.Getenv(SWARM_ENV_SYNC_DISABLE); syncdisable != "" {
|
||||
if sync, err := strconv.ParseBool(syncdisable); err != nil {
|
||||
currentConfig.SyncEnabled = !sync
|
||||
}
|
||||
}
|
||||
|
||||
if v := os.Getenv(SWARM_ENV_DELIVERY_SKIP_CHECK); v != "" {
|
||||
if skipCheck, err := strconv.ParseBool(v); err != nil {
|
||||
currentConfig.DeliverySkipCheck = skipCheck
|
||||
}
|
||||
}
|
||||
|
||||
if v := os.Getenv(SWARM_ENV_SYNC_UPDATE_DELAY); v != "" {
|
||||
if d, err := time.ParseDuration(v); err != nil {
|
||||
currentConfig.SyncUpdateDelay = d
|
||||
}
|
||||
}
|
||||
|
||||
if lne := os.Getenv(SWARM_ENV_LIGHT_NODE_ENABLE); lne != "" {
|
||||
if lightnode, err := strconv.ParseBool(lne); err != nil {
|
||||
currentConfig.LightNodeEnabled = lightnode
|
||||
}
|
||||
}
|
||||
|
||||
if swapapi := os.Getenv(SWARM_ENV_SWAP_API); swapapi != "" {
|
||||
currentConfig.SwapApi = swapapi
|
||||
currentConfig.SwapAPI = swapapi
|
||||
}
|
||||
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapApi == "" {
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapAPI == "" {
|
||||
utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API)
|
||||
}
|
||||
|
||||
@@ -288,10 +330,6 @@ func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
|
||||
currentConfig.Cors = cors
|
||||
}
|
||||
|
||||
if bootnodes := os.Getenv(SWARM_ENV_BOOTNODES); bootnodes != "" {
|
||||
currentConfig.BootNodes = bootnodes
|
||||
}
|
||||
|
||||
return currentConfig
|
||||
}
|
||||
|
||||
@@ -312,18 +350,6 @@ func dumpConfig(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//deprecated flags checked here
|
||||
func checkDeprecated(ctx *cli.Context) {
|
||||
// exit if the deprecated --ethapi flag is set
|
||||
if ctx.GlobalString(DeprecatedEthAPIFlag.Name) != "" {
|
||||
utils.Fatalf("--ethapi is no longer a valid command line flag, please use --ens-api and/or --swap-api.")
|
||||
}
|
||||
// warn if --ens-api flag is set
|
||||
if ctx.GlobalString(DeprecatedEnsAddrFlag.Name) != "" {
|
||||
log.Warn("--ens-addr is no longer a valid command line flag, please use --ens-api to specify contract address.")
|
||||
}
|
||||
}
|
||||
|
||||
//validate configuration parameters
|
||||
func validateConfig(cfg *bzzapi.Config) (err error) {
|
||||
for _, ensAPI := range cfg.EnsAPIs {
|
||||
|
||||
+82
-61
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
@@ -34,7 +35,7 @@ import (
|
||||
|
||||
func TestDumpConfig(t *testing.T) {
|
||||
swarm := runSwarm(t, "dumpconfig")
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
defaultConf := api.NewConfig()
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -43,7 +44,7 @@ func TestDumpConfig(t *testing.T) {
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestFailsSwapEnabledNoSwapApi(t *testing.T) {
|
||||
func TestConfigFailsSwapEnabledNoSwapApi(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545",
|
||||
@@ -55,7 +56,7 @@ func TestFailsSwapEnabledNoSwapApi(t *testing.T) {
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestFailsNoBzzAccount(t *testing.T) {
|
||||
func TestConfigFailsNoBzzAccount(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545",
|
||||
@@ -66,7 +67,7 @@ func TestFailsNoBzzAccount(t *testing.T) {
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestCmdLineOverrides(t *testing.T) {
|
||||
func TestConfigCmdLineOverrides(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -85,9 +86,10 @@ func TestCmdLineOverrides(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort,
|
||||
fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name),
|
||||
fmt.Sprintf("--%s", SwarmSyncDisabledFlag.Name),
|
||||
fmt.Sprintf("--%s", CorsStringFlag.Name), "*",
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
fmt.Sprintf("--%s", SwarmDeliverySkipCheckFlag.Name),
|
||||
fmt.Sprintf("--%s", EnsAPIFlag.Name), "",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
@@ -120,12 +122,16 @@ func TestCmdLineOverrides(t *testing.T) {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 42 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 42, info.NetworkId)
|
||||
if info.NetworkID != 42 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 42, info.NetworkID)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
if info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be disabled, but is true")
|
||||
}
|
||||
|
||||
if !info.DeliverySkipCheck {
|
||||
t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not")
|
||||
}
|
||||
|
||||
if info.Cors != "*" {
|
||||
@@ -135,7 +141,7 @@ func TestCmdLineOverrides(t *testing.T) {
|
||||
node.Shutdown()
|
||||
}
|
||||
|
||||
func TestFileOverrides(t *testing.T) {
|
||||
func TestConfigFileOverrides(t *testing.T) {
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
@@ -145,16 +151,16 @@ func TestFileOverrides(t *testing.T) {
|
||||
|
||||
//create a config file
|
||||
//first, create a default conf
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
defaultConf := api.NewConfig()
|
||||
//change some values in order to test if they have been loaded
|
||||
defaultConf.SyncEnabled = true
|
||||
defaultConf.NetworkId = 54
|
||||
defaultConf.SyncEnabled = false
|
||||
defaultConf.DeliverySkipCheck = true
|
||||
defaultConf.NetworkID = 54
|
||||
defaultConf.Port = httpPort
|
||||
defaultConf.StoreParams.DbCapacity = 9000000
|
||||
defaultConf.ChunkerParams.Branches = 64
|
||||
defaultConf.HiveParams.CallInterval = 6000000000
|
||||
defaultConf.DbCapacity = 9000000
|
||||
defaultConf.HiveParams.KeepAliveInterval = 6000000000
|
||||
defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second
|
||||
defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//create a TOML string
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
@@ -215,38 +221,38 @@ func TestFileOverrides(t *testing.T) {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 54 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
if info.NetworkID != 54 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkID)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
if info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be disabled, but is true")
|
||||
}
|
||||
|
||||
if info.StoreParams.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
if !info.DeliverySkipCheck {
|
||||
t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not")
|
||||
}
|
||||
|
||||
if info.ChunkerParams.Branches != 64 {
|
||||
t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches)
|
||||
if info.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkID)
|
||||
}
|
||||
|
||||
if info.HiveParams.CallInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval))
|
||||
if info.HiveParams.KeepAliveInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams KeepAliveInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.KeepAliveInterval))
|
||||
}
|
||||
|
||||
if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second {
|
||||
t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval)
|
||||
}
|
||||
|
||||
if info.SyncParams.KeyBufferSize != 512 {
|
||||
t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
}
|
||||
// if info.SyncParams.KeyBufferSize != 512 {
|
||||
// t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
// }
|
||||
|
||||
node.Shutdown()
|
||||
}
|
||||
|
||||
func TestEnvVars(t *testing.T) {
|
||||
func TestConfigEnvVars(t *testing.T) {
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
@@ -257,7 +263,8 @@ func TestEnvVars(t *testing.T) {
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmPortFlag.EnvVar, httpPort))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmNetworkIdFlag.EnvVar, "999"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", CorsStringFlag.EnvVar, "*"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmSyncEnabledFlag.EnvVar, "true"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmSyncDisabledFlag.EnvVar, "true"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmDeliverySkipCheckFlag.EnvVar, "true"))
|
||||
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
@@ -326,23 +333,27 @@ func TestEnvVars(t *testing.T) {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 999 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 999, info.NetworkId)
|
||||
if info.NetworkID != 999 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 999, info.NetworkID)
|
||||
}
|
||||
|
||||
if info.Cors != "*" {
|
||||
t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
if info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be disabled, but is true")
|
||||
}
|
||||
|
||||
if !info.DeliverySkipCheck {
|
||||
t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not")
|
||||
}
|
||||
|
||||
node.Shutdown()
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func TestCmdLineOverridesFile(t *testing.T) {
|
||||
func TestConfigCmdLineOverridesFile(t *testing.T) {
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
@@ -352,26 +363,27 @@ func TestCmdLineOverridesFile(t *testing.T) {
|
||||
|
||||
//create a config file
|
||||
//first, create a default conf
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
defaultConf := api.NewConfig()
|
||||
//change some values in order to test if they have been loaded
|
||||
defaultConf.SyncEnabled = false
|
||||
defaultConf.NetworkId = 54
|
||||
defaultConf.SyncEnabled = true
|
||||
defaultConf.NetworkID = 54
|
||||
defaultConf.Port = "8588"
|
||||
defaultConf.StoreParams.DbCapacity = 9000000
|
||||
defaultConf.ChunkerParams.Branches = 64
|
||||
defaultConf.HiveParams.CallInterval = 6000000000
|
||||
defaultConf.DbCapacity = 9000000
|
||||
defaultConf.HiveParams.KeepAliveInterval = 6000000000
|
||||
defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second
|
||||
defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//create a TOML file
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
//write file
|
||||
f, err := ioutil.TempFile("", "testconfig.toml")
|
||||
fname := "testconfig.toml"
|
||||
f, err := ioutil.TempFile("", fname)
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
defer os.Remove(fname)
|
||||
//write file
|
||||
_, err = f.WriteString(string(out))
|
||||
if err != nil {
|
||||
@@ -392,7 +404,7 @@ func TestCmdLineOverridesFile(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "77",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort,
|
||||
fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name),
|
||||
fmt.Sprintf("--%s", SwarmSyncDisabledFlag.Name),
|
||||
fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(),
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
"--ens-api", "",
|
||||
@@ -427,33 +439,29 @@ func TestCmdLineOverridesFile(t *testing.T) {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != expectNetworkId {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", expectNetworkId, info.NetworkId)
|
||||
if info.NetworkID != expectNetworkId {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", expectNetworkId, info.NetworkID)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
if info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be disabled, but is true")
|
||||
}
|
||||
|
||||
if info.StoreParams.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
if info.LocalStoreParams.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected Capacity to be %d, got %d", 9000000, info.LocalStoreParams.DbCapacity)
|
||||
}
|
||||
|
||||
if info.ChunkerParams.Branches != 64 {
|
||||
t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches)
|
||||
}
|
||||
|
||||
if info.HiveParams.CallInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval))
|
||||
if info.HiveParams.KeepAliveInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams KeepAliveInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.KeepAliveInterval))
|
||||
}
|
||||
|
||||
if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second {
|
||||
t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval)
|
||||
}
|
||||
|
||||
if info.SyncParams.KeyBufferSize != 512 {
|
||||
t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
}
|
||||
// if info.SyncParams.KeyBufferSize != 512 {
|
||||
// t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
// }
|
||||
|
||||
node.Shutdown()
|
||||
}
|
||||
@@ -552,3 +560,16 @@ func TestValidateConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assignTCPPort() (string, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
l.Close()
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
+16
-12
@@ -23,6 +23,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
@@ -30,11 +31,11 @@ import (
|
||||
|
||||
func dbExport(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database) and <file> (path to write the tar archive to, - for stdout)")
|
||||
if len(args) != 3 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database), <file> (path to write the tar archive to, - for stdout) and the base key")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
store, err := openLDBStore(args[0], common.Hex2Bytes(args[2]))
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
@@ -62,11 +63,11 @@ func dbExport(ctx *cli.Context) {
|
||||
|
||||
func dbImport(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database) and <file> (path to read the tar archive from, - for stdin)")
|
||||
if len(args) != 3 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database), <file> (path to read the tar archive from, - for stdin) and the base key")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
store, err := openLDBStore(args[0], common.Hex2Bytes(args[2]))
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
@@ -94,11 +95,11 @@ func dbImport(ctx *cli.Context) {
|
||||
|
||||
func dbClean(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
utils.Fatalf("invalid arguments, please specify <chunkdb> (path to a local chunk database)")
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("invalid arguments, please specify <chunkdb> (path to a local chunk database) and the base key")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
store, err := openLDBStore(args[0], common.Hex2Bytes(args[1]))
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
@@ -107,10 +108,13 @@ func dbClean(ctx *cli.Context) {
|
||||
store.Cleanup()
|
||||
}
|
||||
|
||||
func openDbStore(path string) (*storage.DbStore, error) {
|
||||
func openLDBStore(path string, basekey []byte) (*storage.LDBStore, error) {
|
||||
if _, err := os.Stat(filepath.Join(path, "CURRENT")); err != nil {
|
||||
return nil, fmt.Errorf("invalid chunkdb path: %s", err)
|
||||
}
|
||||
hash := storage.MakeHashFunc("SHA3")
|
||||
return storage.NewDbStore(path, hash, 10000000, 0)
|
||||
|
||||
storeparams := storage.NewDefaultStoreParams()
|
||||
ldbparams := storage.NewLDBStoreParams(storeparams, path)
|
||||
ldbparams.BaseKey = basekey
|
||||
return storage.NewLDBStore(ldbparams)
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func download(ctx *cli.Context) {
|
||||
log.Debug("downloading content using swarm down")
|
||||
args := ctx.Args()
|
||||
dest := "."
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
utils.Fatalf("Usage: swarm down [options] <bzz locator> [<destination path>]")
|
||||
case 1:
|
||||
log.Trace(fmt.Sprintf("swarm down: no destination path - assuming working dir"))
|
||||
default:
|
||||
log.Trace(fmt.Sprintf("destination path arg: %s", args[1]))
|
||||
if absDest, err := filepath.Abs(args[1]); err == nil {
|
||||
dest = absDest
|
||||
} else {
|
||||
utils.Fatalf("could not get download path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
isRecursive = ctx.Bool(SwarmRecursiveFlag.Name)
|
||||
client = swarm.NewClient(bzzapi)
|
||||
)
|
||||
|
||||
if fi, err := os.Stat(dest); err == nil {
|
||||
if isRecursive && !fi.Mode().IsDir() {
|
||||
utils.Fatalf("destination path is not a directory!")
|
||||
}
|
||||
} else {
|
||||
if !os.IsNotExist(err) {
|
||||
utils.Fatalf("could not stat path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
uri, err := api.Parse(args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("could not parse uri argument: %v", err)
|
||||
}
|
||||
|
||||
dl := func(credentials string) error {
|
||||
// assume behaviour according to --recursive switch
|
||||
if isRecursive {
|
||||
if err := client.DownloadDirectory(uri.Addr, uri.Path, dest, credentials); err != nil {
|
||||
if err == swarm.ErrUnauthorized {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("directory %s: %v", uri.Path, err)
|
||||
}
|
||||
} else {
|
||||
// we are downloading a file
|
||||
log.Debug("downloading file/path from a manifest", "uri.Addr", uri.Addr, "uri.Path", uri.Path)
|
||||
|
||||
err := client.DownloadFile(uri.Addr, uri.Path, dest, credentials)
|
||||
if err != nil {
|
||||
if err == swarm.ErrUnauthorized {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("file %s from address: %s: %v", uri.Path, uri.Addr, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if passwords := makePasswordList(ctx); passwords != nil {
|
||||
password := getPassPhrase(fmt.Sprintf("Downloading %s is restricted", uri), 0, passwords)
|
||||
err = dl(password)
|
||||
} else {
|
||||
err = dl("")
|
||||
}
|
||||
if err != nil {
|
||||
utils.Fatalf("download: %v", err)
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/swarm"
|
||||
)
|
||||
|
||||
// TestCLISwarmExportImport perform the following test:
|
||||
// 1. runs swarm node
|
||||
// 2. uploads a random file
|
||||
// 3. runs an export of the local datastore
|
||||
// 4. runs a second swarm node
|
||||
// 5. imports the exported datastore
|
||||
// 6. fetches the uploaded random file from the second node
|
||||
func TestCLISwarmExportImport(t *testing.T) {
|
||||
cluster := newTestCluster(t, 1)
|
||||
|
||||
// generate random 10mb file
|
||||
f, cleanup := generateRandomFile(t, 10000000)
|
||||
defer cleanup()
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
up := runSwarm(t, "--bzzapi", cluster.Nodes[0].URL, "up", f.Name())
|
||||
_, matches := up.ExpectRegexp(`[a-f\d]{64}`)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
|
||||
var info swarm.Info
|
||||
if err := cluster.Nodes[0].Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cluster.Stop()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
// generate an export.tar
|
||||
exportCmd := runSwarm(t, "db", "export", info.Path+"/chunks", info.Path+"/export.tar", strings.TrimPrefix(info.BzzKey, "0x"))
|
||||
exportCmd.ExpectExit()
|
||||
|
||||
// start second cluster
|
||||
cluster2 := newTestCluster(t, 1)
|
||||
|
||||
var info2 swarm.Info
|
||||
if err := cluster2.Nodes[0].Client.Call(&info2, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// stop second cluster, so that we close LevelDB
|
||||
cluster2.Stop()
|
||||
defer cluster2.Cleanup()
|
||||
|
||||
// import the export.tar
|
||||
importCmd := runSwarm(t, "db", "import", info2.Path+"/chunks", info.Path+"/export.tar", strings.TrimPrefix(info2.BzzKey, "0x"))
|
||||
importCmd.ExpectExit()
|
||||
|
||||
// spin second cluster back up
|
||||
cluster2.StartExistingNodes(t, 1, strings.TrimPrefix(info2.BzzAccount, "0x"))
|
||||
|
||||
// try to fetch imported file
|
||||
res, err := http.Get(cluster2.Nodes[0].URL + "/bzz:/" + hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("expected HTTP status %d, got %s", 200, res.Status)
|
||||
}
|
||||
|
||||
// compare downloaded file with the generated random file
|
||||
mustEqualFiles(t, f, res.Body)
|
||||
}
|
||||
|
||||
func mustEqualFiles(t *testing.T, up io.Reader, down io.Reader) {
|
||||
h := md5.New()
|
||||
upLen, err := io.Copy(h, up)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
upHash := h.Sum(nil)
|
||||
h.Reset()
|
||||
downLen, err := io.Copy(h, down)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
downHash := h.Sum(nil)
|
||||
|
||||
if !bytes.Equal(upHash, downHash) || upLen != downLen {
|
||||
t.Fatalf("downloaded imported file md5=%x (length %v) is not the same as the generated one mp5=%x (length %v)", downHash, downLen, upHash, upLen)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomFile(t *testing.T, size int) (f *os.File, teardown func()) {
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// callback for tmp file cleanup
|
||||
teardown = func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}
|
||||
|
||||
// write 10mb random data to file
|
||||
buf := make([]byte, 10000000)
|
||||
_, err = rand.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ioutil.WriteFile(tmp.Name(), buf, 0755)
|
||||
|
||||
return tmp, teardown
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/swarm/fuse"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func mount(cliContext *cli.Context) {
|
||||
args := cliContext.Args()
|
||||
if len(args) < 2 {
|
||||
utils.Fatalf("Usage: swarm fs mount --ipcpath <path to bzzd.ipc> <manifestHash> <file name>")
|
||||
}
|
||||
|
||||
client, err := dialRPC(cliContext)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error dailing to RPC endpoint: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mf := &fuse.MountInfo{}
|
||||
mountPoint, err := filepath.Abs(filepath.Clean(args[1]))
|
||||
if err != nil {
|
||||
utils.Fatalf("error expanding path for mount point: %v", err)
|
||||
}
|
||||
err = client.CallContext(ctx, mf, "swarmfs_mount", args[0], mountPoint)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error calling the RPC endpoint while mounting: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func unmount(cliContext *cli.Context) {
|
||||
args := cliContext.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
utils.Fatalf("Usage: swarm fs unmount --ipcpath <path to bzzd.ipc> <mount path>")
|
||||
}
|
||||
client, err := dialRPC(cliContext)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error dailing to RPC endpoint: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mf := fuse.MountInfo{}
|
||||
err = client.CallContext(ctx, &mf, "swarmfs_unmount", args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("encountered an error calling the RPC endpoint while unmounting: %v", err)
|
||||
}
|
||||
fmt.Printf("%s\n", mf.LatestManifest) //print the latest manifest hash for user reference
|
||||
}
|
||||
|
||||
func listMounts(cliContext *cli.Context) {
|
||||
client, err := dialRPC(cliContext)
|
||||
if err != nil {
|
||||
utils.Fatalf("had an error dailing to RPC endpoint: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mf := []fuse.MountInfo{}
|
||||
err = client.CallContext(ctx, &mf, "swarmfs_listmounts")
|
||||
if err != nil {
|
||||
utils.Fatalf("encountered an error calling the RPC endpoint while listing mounts: %v", err)
|
||||
}
|
||||
if len(mf) == 0 {
|
||||
fmt.Print("Could not found any swarmfs mounts. Please make sure you've specified the correct RPC endpoint\n")
|
||||
} else {
|
||||
fmt.Printf("Found %d swarmfs mount(s):\n", len(mf))
|
||||
for i, mountInfo := range mf {
|
||||
fmt.Printf("%d:\n", i)
|
||||
fmt.Printf("\tMount point: %s\n", mountInfo.MountPoint)
|
||||
fmt.Printf("\tLatest Manifest: %s\n", mountInfo.LatestManifest)
|
||||
fmt.Printf("\tStart Manifest: %s\n", mountInfo.StartManifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dialRPC(ctx *cli.Context) (*rpc.Client, error) {
|
||||
var endpoint string
|
||||
|
||||
if ctx.IsSet(utils.IPCPathFlag.Name) {
|
||||
endpoint = ctx.String(utils.IPCPathFlag.Name)
|
||||
} else {
|
||||
utils.Fatalf("swarm ipc endpoint not specified")
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
|
||||
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
|
||||
// Backwards compatibility with geth < 1.5 which required
|
||||
// these prefixes.
|
||||
endpoint = endpoint[4:]
|
||||
}
|
||||
return rpc.Dial(endpoint)
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build linux freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.PrintOrigins(true)
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
||||
}
|
||||
|
||||
type testFile struct {
|
||||
filePath string
|
||||
content string
|
||||
}
|
||||
|
||||
// TestCLISwarmFs is a high-level test of swarmfs
|
||||
//
|
||||
// This test fails on travis for macOS as this executable exits with code 1
|
||||
// and without any log messages in the log:
|
||||
// /Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse.
|
||||
// This is the reason for this file not being built on darwin architecture.
|
||||
func TestCLISwarmFs(t *testing.T) {
|
||||
cluster := newTestCluster(t, 3)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
// create a tmp dir
|
||||
mountPoint, err := ioutil.TempDir("", "swarm-test")
|
||||
log.Debug("swarmfs cli test", "1st mount", mountPoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(mountPoint)
|
||||
|
||||
handlingNode := cluster.Nodes[0]
|
||||
mhash := doUploadEmptyDir(t, handlingNode)
|
||||
log.Debug("swarmfs cli test: mounting first run", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath))
|
||||
|
||||
mount := runSwarm(t, []string{
|
||||
"fs",
|
||||
"mount",
|
||||
"--ipcpath", filepath.Join(handlingNode.Dir, handlingNode.IpcPath),
|
||||
mhash,
|
||||
mountPoint,
|
||||
}...)
|
||||
mount.ExpectExit()
|
||||
|
||||
filesToAssert := []*testFile{}
|
||||
|
||||
dirPath, err := createDirInDir(mountPoint, "testSubDir")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dirPath2, err := createDirInDir(dirPath, "AnotherTestSubDir")
|
||||
|
||||
dummyContent := "somerandomtestcontentthatshouldbeasserted"
|
||||
dirs := []string{
|
||||
mountPoint,
|
||||
dirPath,
|
||||
dirPath2,
|
||||
}
|
||||
files := []string{"f1.tmp", "f2.tmp"}
|
||||
for _, d := range dirs {
|
||||
for _, entry := range files {
|
||||
tFile, err := createTestFileInPath(d, entry, dummyContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filesToAssert = append(filesToAssert, tFile)
|
||||
}
|
||||
}
|
||||
if len(filesToAssert) != len(dirs)*len(files) {
|
||||
t.Fatalf("should have %d files to assert now, got %d", len(dirs)*len(files), len(filesToAssert))
|
||||
}
|
||||
hashRegexp := `[a-f\d]{64}`
|
||||
log.Debug("swarmfs cli test: unmounting first run...", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath))
|
||||
|
||||
unmount := runSwarm(t, []string{
|
||||
"fs",
|
||||
"unmount",
|
||||
"--ipcpath", filepath.Join(handlingNode.Dir, handlingNode.IpcPath),
|
||||
mountPoint,
|
||||
}...)
|
||||
_, matches := unmount.ExpectRegexp(hashRegexp)
|
||||
unmount.ExpectExit()
|
||||
|
||||
hash := matches[0]
|
||||
if hash == mhash {
|
||||
t.Fatal("this should not be equal")
|
||||
}
|
||||
log.Debug("swarmfs cli test: asserting no files in mount point")
|
||||
|
||||
//check that there's nothing in the mount folder
|
||||
filesInDir, err := ioutil.ReadDir(mountPoint)
|
||||
if err != nil {
|
||||
t.Fatalf("had an error reading the directory: %v", err)
|
||||
}
|
||||
|
||||
if len(filesInDir) != 0 {
|
||||
t.Fatal("there shouldn't be anything here")
|
||||
}
|
||||
|
||||
secondMountPoint, err := ioutil.TempDir("", "swarm-test")
|
||||
log.Debug("swarmfs cli test", "2nd mount point at", secondMountPoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(secondMountPoint)
|
||||
|
||||
log.Debug("swarmfs cli test: remounting at second mount point", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath))
|
||||
|
||||
//remount, check files
|
||||
newMount := runSwarm(t, []string{
|
||||
"fs",
|
||||
"mount",
|
||||
"--ipcpath", filepath.Join(handlingNode.Dir, handlingNode.IpcPath),
|
||||
hash, // the latest hash
|
||||
secondMountPoint,
|
||||
}...)
|
||||
|
||||
newMount.ExpectExit()
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
filesInDir, err = ioutil.ReadDir(secondMountPoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(filesInDir) == 0 {
|
||||
t.Fatal("there should be something here")
|
||||
}
|
||||
|
||||
log.Debug("swarmfs cli test: traversing file tree to see it matches previous mount")
|
||||
|
||||
for _, file := range filesToAssert {
|
||||
file.filePath = strings.Replace(file.filePath, mountPoint, secondMountPoint, -1)
|
||||
fileBytes, err := ioutil.ReadFile(file.filePath)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fileBytes, bytes.NewBufferString(file.content).Bytes()) {
|
||||
t.Fatal("this should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("swarmfs cli test: unmounting second run", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath))
|
||||
|
||||
unmountSec := runSwarm(t, []string{
|
||||
"fs",
|
||||
"unmount",
|
||||
"--ipcpath", filepath.Join(handlingNode.Dir, handlingNode.IpcPath),
|
||||
secondMountPoint,
|
||||
}...)
|
||||
|
||||
_, matches = unmountSec.ExpectRegexp(hashRegexp)
|
||||
unmountSec.ExpectExit()
|
||||
|
||||
if matches[0] != hash {
|
||||
t.Fatal("these should be equal - no changes made")
|
||||
}
|
||||
}
|
||||
|
||||
func doUploadEmptyDir(t *testing.T, node *testNode) string {
|
||||
// create a tmp dir
|
||||
tmpDir, err := ioutil.TempDir("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
hashRegexp := `[a-f\d]{64}`
|
||||
|
||||
flags := []string{
|
||||
"--bzzapi", node.URL,
|
||||
"--recursive",
|
||||
"up",
|
||||
tmpDir}
|
||||
|
||||
log.Info("swarmfs cli test: uploading dir with 'swarm up'")
|
||||
up := runSwarm(t, flags...)
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
log.Info("swarmfs cli test: dir uploaded", "hash", hash)
|
||||
return hash
|
||||
}
|
||||
|
||||
func createDirInDir(createInDir string, dirToCreate string) (string, error) {
|
||||
fullpath := filepath.Join(createInDir, dirToCreate)
|
||||
err := os.MkdirAll(fullpath, 0777)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fullpath, nil
|
||||
}
|
||||
|
||||
func createTestFileInPath(dir, filename, content string) (*testFile, error) {
|
||||
tFile := &testFile{}
|
||||
filePath := filepath.Join(dir, filename)
|
||||
if file, err := os.Create(filePath); err == nil {
|
||||
tFile.content = content
|
||||
tFile.filePath = filePath
|
||||
|
||||
_, err = io.WriteString(file, content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return tFile, nil
|
||||
}
|
||||
+4
-3
@@ -18,6 +18,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -38,11 +39,11 @@ func hash(ctx *cli.Context) {
|
||||
defer f.Close()
|
||||
|
||||
stat, _ := f.Stat()
|
||||
chunker := storage.NewTreeChunker(storage.NewChunkerParams())
|
||||
key, err := chunker.Split(f, stat.Size(), nil, nil, nil)
|
||||
fileStore := storage.NewFileStore(storage.NewMapChunkStore(), storage.NewFileStoreParams())
|
||||
addr, _, err := fileStore.Store(context.TODO(), f, stat.Size(), false)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%v\n", key)
|
||||
fmt.Printf("%v\n", addr)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ func list(ctx *cli.Context) {
|
||||
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := swarm.NewClient(bzzapi)
|
||||
list, err := client.List(manifest, prefix)
|
||||
list, err := client.List(manifest, prefix, "")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate file and directory list: %s", err)
|
||||
}
|
||||
|
||||
+396
-173
@@ -34,31 +34,39 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/swarm"
|
||||
bzzapi "github.com/ethereum/go-ethereum/swarm/api"
|
||||
swarmmetrics "github.com/ethereum/go-ethereum/swarm/metrics"
|
||||
"github.com/ethereum/go-ethereum/swarm/tracing"
|
||||
sv "github.com/ethereum/go-ethereum/swarm/version"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const clientIdentifier = "swarm"
|
||||
const helpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
var (
|
||||
gitCommit string // Git SHA1 commit hash of the release (set via linker flags)
|
||||
testbetBootNodes = []string{
|
||||
"enode://ec8ae764f7cb0417bdfb009b9d0f18ab3818a3a4e8e7c67dd5f18971a93510a2e6f43cd0b69a27e439a9629457ea804104f37c85e41eed057d3faabbf7744cdf@13.74.157.139:30429",
|
||||
"enode://c2e1fceb3bf3be19dff71eec6cccf19f2dbf7567ee017d130240c670be8594bc9163353ca55dd8df7a4f161dd94b36d0615c17418b5a3cdcbb4e9d99dfa4de37@13.74.157.139:30430",
|
||||
"enode://fe29b82319b734ce1ec68b84657d57145fee237387e63273989d354486731e59f78858e452ef800a020559da22dcca759536e6aa5517c53930d29ce0b1029286@13.74.157.139:30431",
|
||||
"enode://1d7187e7bde45cf0bee489ce9852dd6d1a0d9aa67a33a6b8e6db8a4fbc6fcfa6f0f1a5419343671521b863b187d1c73bad3603bae66421d157ffef357669ddb8@13.74.157.139:30432",
|
||||
"enode://0e4cba800f7b1ee73673afa6a4acead4018f0149d2e3216be3f133318fd165b324cd71b81fbe1e80deac8dbf56e57a49db7be67f8b9bc81bd2b7ee496434fb5d@13.74.157.139:30433",
|
||||
}
|
||||
gitCommit string // Git SHA1 commit hash of the release (set via linker flags)
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -87,10 +95,6 @@ var (
|
||||
Usage: "Network identifier (integer, default 3=swarm testnet)",
|
||||
EnvVar: SWARM_ENV_NETWORK_ID,
|
||||
}
|
||||
SwarmConfigPathFlag = cli.StringFlag{
|
||||
Name: "bzzconfig",
|
||||
Usage: "DEPRECATED: please use --config path/to/TOML-file",
|
||||
}
|
||||
SwarmSwapEnabledFlag = cli.BoolFlag{
|
||||
Name: "swap",
|
||||
Usage: "Swarm SWAP enabled (default false)",
|
||||
@@ -101,10 +105,25 @@ var (
|
||||
Usage: "URL of the Ethereum API provider to use to settle SWAP payments",
|
||||
EnvVar: SWARM_ENV_SWAP_API,
|
||||
}
|
||||
SwarmSyncEnabledFlag = cli.BoolTFlag{
|
||||
Name: "sync",
|
||||
Usage: "Swarm Syncing enabled (default true)",
|
||||
EnvVar: SWARM_ENV_SYNC_ENABLE,
|
||||
SwarmSyncDisabledFlag = cli.BoolTFlag{
|
||||
Name: "nosync",
|
||||
Usage: "Disable swarm syncing",
|
||||
EnvVar: SWARM_ENV_SYNC_DISABLE,
|
||||
}
|
||||
SwarmSyncUpdateDelay = cli.DurationFlag{
|
||||
Name: "sync-update-delay",
|
||||
Usage: "Duration for sync subscriptions update after no new peers are added (default 15s)",
|
||||
EnvVar: SWARM_ENV_SYNC_UPDATE_DELAY,
|
||||
}
|
||||
SwarmLightNodeEnabled = cli.BoolFlag{
|
||||
Name: "lightnode",
|
||||
Usage: "Enable Swarm LightNode (default false)",
|
||||
EnvVar: SWARM_ENV_LIGHT_NODE_ENABLE,
|
||||
}
|
||||
SwarmDeliverySkipCheckFlag = cli.BoolFlag{
|
||||
Name: "delivery-skip-check",
|
||||
Usage: "Skip chunk delivery check (default false)",
|
||||
EnvVar: SWARM_ENV_DELIVERY_SKIP_CHECK,
|
||||
}
|
||||
EnsAPIFlag = cli.StringSliceFlag{
|
||||
Name: "ens-api",
|
||||
@@ -116,40 +135,78 @@ var (
|
||||
Usage: "Swarm HTTP endpoint",
|
||||
Value: "http://127.0.0.1:8500",
|
||||
}
|
||||
SwarmRecursiveUploadFlag = cli.BoolFlag{
|
||||
SwarmRecursiveFlag = cli.BoolFlag{
|
||||
Name: "recursive",
|
||||
Usage: "Upload directories recursively",
|
||||
}
|
||||
SwarmWantManifestFlag = cli.BoolTFlag{
|
||||
Name: "manifest",
|
||||
Usage: "Automatic manifest upload",
|
||||
Usage: "Automatic manifest upload (default true)",
|
||||
}
|
||||
SwarmUploadDefaultPath = cli.StringFlag{
|
||||
Name: "defaultpath",
|
||||
Usage: "path to file served for empty url path (none)",
|
||||
}
|
||||
SwarmAccessGrantKeyFlag = cli.StringFlag{
|
||||
Name: "grant-key",
|
||||
Usage: "grants a given public key access to an ACT",
|
||||
}
|
||||
SwarmAccessGrantKeysFlag = cli.StringFlag{
|
||||
Name: "grant-keys",
|
||||
Usage: "grants a given list of public keys in the following file (separated by line breaks) access to an ACT",
|
||||
}
|
||||
SwarmUpFromStdinFlag = cli.BoolFlag{
|
||||
Name: "stdin",
|
||||
Usage: "reads data to be uploaded from stdin",
|
||||
}
|
||||
SwarmUploadMimeType = cli.StringFlag{
|
||||
Name: "mime",
|
||||
Usage: "force mime type",
|
||||
Usage: "Manually specify MIME type",
|
||||
}
|
||||
SwarmEncryptedFlag = cli.BoolFlag{
|
||||
Name: "encrypt",
|
||||
Usage: "use encrypted upload",
|
||||
}
|
||||
SwarmAccessPasswordFlag = cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "Password",
|
||||
EnvVar: SWARM_ACCESS_PASSWORD,
|
||||
}
|
||||
SwarmDryRunFlag = cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "dry-run",
|
||||
}
|
||||
CorsStringFlag = cli.StringFlag{
|
||||
Name: "corsdomain",
|
||||
Usage: "Domain on which to send Access-Control-Allow-Origin header (multiple domains can be supplied separated by a ',')",
|
||||
EnvVar: SWARM_ENV_CORS,
|
||||
}
|
||||
|
||||
// the following flags are deprecated and should be removed in the future
|
||||
DeprecatedEthAPIFlag = cli.StringFlag{
|
||||
Name: "ethapi",
|
||||
Usage: "DEPRECATED: please use --ens-api and --swap-api",
|
||||
SwarmStorePath = cli.StringFlag{
|
||||
Name: "store.path",
|
||||
Usage: "Path to leveldb chunk DB (default <$GETH_ENV_DIR>/swarm/bzz-<$BZZ_KEY>/chunks)",
|
||||
EnvVar: SWARM_ENV_STORE_PATH,
|
||||
}
|
||||
DeprecatedEnsAddrFlag = cli.StringFlag{
|
||||
Name: "ens-addr",
|
||||
Usage: "DEPRECATED: ENS contract address, please use --ens-api with contract address according to its format",
|
||||
SwarmStoreCapacity = cli.Uint64Flag{
|
||||
Name: "store.size",
|
||||
Usage: "Number of chunks (5M is roughly 20-25GB) (default 5000000)",
|
||||
EnvVar: SWARM_ENV_STORE_CAPACITY,
|
||||
}
|
||||
SwarmStoreCacheCapacity = cli.UintFlag{
|
||||
Name: "store.cache.size",
|
||||
Usage: "Number of recent chunks cached in memory (default 5000)",
|
||||
EnvVar: SWARM_ENV_STORE_CACHE_CAPACITY,
|
||||
}
|
||||
SwarmResourceMultihashFlag = cli.BoolFlag{
|
||||
Name: "multihash",
|
||||
Usage: "Determines how to interpret data for a resource update. If not present, data will be interpreted as raw, literal data that will be included in the resource",
|
||||
}
|
||||
SwarmResourceNameFlag = cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "User-defined name for the new resource",
|
||||
}
|
||||
SwarmResourceDataOnCreateFlag = cli.StringFlag{
|
||||
Name: "data",
|
||||
Usage: "Initializes the resource with the given hex-encoded data. Data must be prefixed by 0x",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -159,12 +216,21 @@ var (
|
||||
SWARM_ERR_SWAP_SET_NO_API = "SWAP is enabled but --swap-api is not set"
|
||||
)
|
||||
|
||||
// this help command gets added to any subcommand that does not define it explicitly
|
||||
var defaultSubcommandHelp = cli.Command{
|
||||
Action: func(ctx *cli.Context) { cli.ShowCommandHelpAndExit(ctx, "", 1) },
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "help",
|
||||
Usage: "shows this help",
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var defaultNodeConfig = node.DefaultConfig
|
||||
|
||||
// This init function sets defaults so cmd/swarm can run alongside geth.
|
||||
func init() {
|
||||
defaultNodeConfig.Name = clientIdentifier
|
||||
defaultNodeConfig.Version = params.VersionWithCommit(gitCommit)
|
||||
defaultNodeConfig.Version = sv.VersionWithCommit(gitCommit)
|
||||
defaultNodeConfig.P2P.ListenAddr = ":30399"
|
||||
defaultNodeConfig.IPCPath = "bzzd.ipc"
|
||||
// Set flag defaults for --help display.
|
||||
@@ -180,91 +246,217 @@ func init() {
|
||||
app.Copyright = "Copyright 2013-2016 The go-ethereum Authors"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Action: version,
|
||||
Name: "version",
|
||||
Usage: "Print version numbers",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
The output of this command is supposed to be machine-readable.
|
||||
`,
|
||||
Action: version,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "version",
|
||||
Usage: "Print version numbers",
|
||||
Description: "The output of this command is supposed to be machine-readable",
|
||||
},
|
||||
{
|
||||
Action: upload,
|
||||
Name: "up",
|
||||
Usage: "upload a file or directory to swarm using the HTTP API",
|
||||
ArgsUsage: " <file>",
|
||||
Description: `
|
||||
"upload a file or directory to swarm using the HTTP API and prints the root hash",
|
||||
`,
|
||||
Action: upload,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "up",
|
||||
Usage: "uploads a file or directory to swarm using the HTTP API",
|
||||
ArgsUsage: "<file>",
|
||||
Flags: []cli.Flag{SwarmEncryptedFlag},
|
||||
Description: "uploads a file or directory to swarm using the HTTP API and prints the root hash",
|
||||
},
|
||||
{
|
||||
Action: list,
|
||||
Name: "ls",
|
||||
Usage: "list files and directories contained in a manifest",
|
||||
ArgsUsage: " <manifest> [<prefix>]",
|
||||
Description: `
|
||||
Lists files and directories contained in a manifest.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: hash,
|
||||
Name: "hash",
|
||||
Usage: "print the swarm hash of a file or directory",
|
||||
ArgsUsage: " <file>",
|
||||
Description: `
|
||||
Prints the swarm hash of file or directory.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "manifest",
|
||||
Usage: "update a MANIFEST",
|
||||
ArgsUsage: "manifest COMMAND",
|
||||
Description: `
|
||||
Updates a MANIFEST by adding/removing/updating the hash of a path.
|
||||
`,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "access",
|
||||
Usage: "encrypts a reference and embeds it into a root manifest",
|
||||
ArgsUsage: "<ref>",
|
||||
Description: "encrypts a reference and embeds it into a root manifest",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: add,
|
||||
Name: "add",
|
||||
Usage: "add a new path to the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <hash> [<content-type>]",
|
||||
Description: `
|
||||
Adds a new path to the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: update,
|
||||
Name: "update",
|
||||
Usage: "update the hash for an already existing path in the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <newhash> [<newcontent-type>]",
|
||||
Description: `
|
||||
Update the hash for an already existing path in the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: remove,
|
||||
Name: "remove",
|
||||
Usage: "removes a path from the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path>",
|
||||
Description: `
|
||||
Removes a path from the manifest
|
||||
`,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "new",
|
||||
Usage: "encrypts a reference and embeds it into a root manifest",
|
||||
ArgsUsage: "<ref>",
|
||||
Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: accessNewPass,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Flags: []cli.Flag{
|
||||
utils.PasswordFileFlag,
|
||||
SwarmDryRunFlag,
|
||||
},
|
||||
Name: "pass",
|
||||
Usage: "encrypts a reference with a password and embeds it into a root manifest",
|
||||
ArgsUsage: "<ref>",
|
||||
Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest",
|
||||
},
|
||||
{
|
||||
Action: accessNewPK,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Flags: []cli.Flag{
|
||||
utils.PasswordFileFlag,
|
||||
SwarmDryRunFlag,
|
||||
SwarmAccessGrantKeyFlag,
|
||||
},
|
||||
Name: "pk",
|
||||
Usage: "encrypts a reference with the node's private key and a given grantee's public key and embeds it into a root manifest",
|
||||
ArgsUsage: "<ref>",
|
||||
Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest",
|
||||
},
|
||||
{
|
||||
Action: accessNewACT,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Flags: []cli.Flag{
|
||||
SwarmAccessGrantKeysFlag,
|
||||
SwarmDryRunFlag,
|
||||
},
|
||||
Name: "act",
|
||||
Usage: "encrypts a reference with the node's private key and a given grantee's public key and embeds it into a root manifest",
|
||||
ArgsUsage: "<ref>",
|
||||
Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Usage: "manage the local chunk database",
|
||||
ArgsUsage: "db COMMAND",
|
||||
Description: `
|
||||
Manage the local chunk database.
|
||||
`,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "resource",
|
||||
Usage: "(Advanced) Create and update Mutable Resources",
|
||||
ArgsUsage: "<create|update|info>",
|
||||
Description: "Works with Mutable Resource Updates",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: dbExport,
|
||||
Name: "export",
|
||||
Usage: "export a local chunk database as a tar archive (use - to send to stdout)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Action: resourceCreate,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "create",
|
||||
Usage: "creates a new Mutable Resource",
|
||||
ArgsUsage: "<frequency>",
|
||||
Description: "creates a new Mutable Resource",
|
||||
Flags: []cli.Flag{SwarmResourceNameFlag, SwarmResourceDataOnCreateFlag, SwarmResourceMultihashFlag},
|
||||
},
|
||||
{
|
||||
Action: resourceUpdate,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "update",
|
||||
Usage: "updates the content of an existing Mutable Resource",
|
||||
ArgsUsage: "<Manifest Address or ENS domain> <0x Hex data>",
|
||||
Description: "updates the content of an existing Mutable Resource",
|
||||
Flags: []cli.Flag{SwarmResourceMultihashFlag},
|
||||
},
|
||||
{
|
||||
Action: resourceInfo,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "info",
|
||||
Usage: "obtains information about an existing Mutable Resource",
|
||||
ArgsUsage: "<Manifest Address or ENS domain>",
|
||||
Description: "obtains information about an existing Mutable Resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: list,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "ls",
|
||||
Usage: "list files and directories contained in a manifest",
|
||||
ArgsUsage: "<manifest> [<prefix>]",
|
||||
Description: "Lists files and directories contained in a manifest",
|
||||
},
|
||||
{
|
||||
Action: hash,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "hash",
|
||||
Usage: "print the swarm hash of a file or directory",
|
||||
ArgsUsage: "<file>",
|
||||
Description: "Prints the swarm hash of file or directory",
|
||||
},
|
||||
{
|
||||
Action: download,
|
||||
Name: "down",
|
||||
Flags: []cli.Flag{SwarmRecursiveFlag, SwarmAccessPasswordFlag},
|
||||
Usage: "downloads a swarm manifest or a file inside a manifest",
|
||||
ArgsUsage: " <uri> [<dir>]",
|
||||
Description: `Downloads a swarm bzz uri to the given dir. When no dir is provided, working directory is assumed. --recursive flag is expected when downloading a manifest with multiple entries.`,
|
||||
},
|
||||
{
|
||||
Name: "manifest",
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Usage: "perform operations on swarm manifests",
|
||||
ArgsUsage: "COMMAND",
|
||||
Description: "Updates a MANIFEST by adding/removing/updating the hash of a path.\nCOMMAND could be: add, update, remove",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: manifestAdd,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "add",
|
||||
Usage: "add a new path to the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <hash>",
|
||||
Description: "Adds a new path to the manifest",
|
||||
},
|
||||
{
|
||||
Action: manifestUpdate,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "update",
|
||||
Usage: "update the hash for an already existing path in the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <newhash>",
|
||||
Description: "Update the hash for an already existing path in the manifest",
|
||||
},
|
||||
{
|
||||
Action: manifestRemove,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "remove",
|
||||
Usage: "removes a path from the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path>",
|
||||
Description: "Removes a path from the manifest",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fs",
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Usage: "perform FUSE operations",
|
||||
ArgsUsage: "fs COMMAND",
|
||||
Description: "Performs FUSE operations by mounting/unmounting/listing mount points. This assumes you already have a Swarm node running locally. For all operation you must reference the correct path to bzzd.ipc in order to communicate with the node",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: mount,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "mount",
|
||||
Flags: []cli.Flag{utils.IPCPathFlag},
|
||||
Usage: "mount a swarm hash to a mount point",
|
||||
ArgsUsage: "swarm fs mount --ipcpath <path to bzzd.ipc> <manifest hash> <mount point>",
|
||||
Description: "Mounts a Swarm manifest hash to a given mount point. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file",
|
||||
},
|
||||
{
|
||||
Action: unmount,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "unmount",
|
||||
Flags: []cli.Flag{utils.IPCPathFlag},
|
||||
Usage: "unmount a swarmfs mount",
|
||||
ArgsUsage: "swarm fs unmount --ipcpath <path to bzzd.ipc> <mount point>",
|
||||
Description: "Unmounts a swarmfs mount residing at <mount point>. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file",
|
||||
},
|
||||
{
|
||||
Action: listMounts,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "list",
|
||||
Flags: []cli.Flag{utils.IPCPathFlag},
|
||||
Usage: "list swarmfs mounts",
|
||||
ArgsUsage: "swarm fs list --ipcpath <path to bzzd.ipc>",
|
||||
Description: "Lists all mounted swarmfs volumes. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Usage: "manage the local chunk database",
|
||||
ArgsUsage: "db COMMAND",
|
||||
Description: "Manage the local chunk database",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: dbExport,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "export",
|
||||
Usage: "export a local chunk database as a tar archive (use - to send to stdout)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Description: `
|
||||
Export a local chunk database as a tar archive (use - to send to stdout).
|
||||
|
||||
@@ -277,46 +469,39 @@ pv(1) tool to get a progress bar:
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: dbImport,
|
||||
Name: "import",
|
||||
Usage: "import chunks from a tar archive into a local chunk database (use - to read from stdin)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Description: `
|
||||
Import chunks from a tar archive into a local chunk database (use - to read from stdin).
|
||||
Action: dbImport,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "import",
|
||||
Usage: "import chunks from a tar archive into a local chunk database (use - to read from stdin)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Description: `Import chunks from a tar archive into a local chunk database (use - to read from stdin).
|
||||
|
||||
swarm db import ~/.ethereum/swarm/bzz-KEY/chunks chunks.tar
|
||||
|
||||
The import may be quite large, consider piping the input through the Unix
|
||||
pv(1) tool to get a progress bar:
|
||||
|
||||
pv chunks.tar | swarm db import ~/.ethereum/swarm/bzz-KEY/chunks -
|
||||
`,
|
||||
pv chunks.tar | swarm db import ~/.ethereum/swarm/bzz-KEY/chunks -`,
|
||||
},
|
||||
{
|
||||
Action: dbClean,
|
||||
Name: "clean",
|
||||
Usage: "remove corrupt entries from a local chunk database",
|
||||
ArgsUsage: "<chunkdb>",
|
||||
Description: `
|
||||
Remove corrupt entries from a local chunk database.
|
||||
`,
|
||||
Action: dbClean,
|
||||
CustomHelpTemplate: helpTemplate,
|
||||
Name: "clean",
|
||||
Usage: "remove corrupt entries from a local chunk database",
|
||||
ArgsUsage: "<chunkdb>",
|
||||
Description: "Remove corrupt entries from a local chunk database",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: func(ctx *cli.Context) {
|
||||
utils.Fatalf("ERROR: 'swarm cleandb' has been removed, please use 'swarm db clean'.")
|
||||
},
|
||||
Name: "cleandb",
|
||||
Usage: "DEPRECATED: use 'swarm db clean'",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
DEPRECATED: use 'swarm db clean'.
|
||||
`,
|
||||
},
|
||||
|
||||
// See config.go
|
||||
DumpConfigCommand,
|
||||
}
|
||||
|
||||
// append a hidden help subcommand to all commands that have subcommands
|
||||
// if a help command was already defined above, that one will take precedence.
|
||||
addDefaultHelpSubcommands(app.Commands)
|
||||
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
@@ -339,10 +524,12 @@ DEPRECATED: use 'swarm db clean'.
|
||||
CorsStringFlag,
|
||||
EnsAPIFlag,
|
||||
SwarmTomlConfigPathFlag,
|
||||
SwarmConfigPathFlag,
|
||||
SwarmSwapEnabledFlag,
|
||||
SwarmSwapAPIFlag,
|
||||
SwarmSyncEnabledFlag,
|
||||
SwarmSyncDisabledFlag,
|
||||
SwarmSyncUpdateDelay,
|
||||
SwarmLightNodeEnabled,
|
||||
SwarmDeliverySkipCheckFlag,
|
||||
SwarmListenAddrFlag,
|
||||
SwarmPortFlag,
|
||||
SwarmAccountFlag,
|
||||
@@ -350,23 +537,34 @@ DEPRECATED: use 'swarm db clean'.
|
||||
ChequebookAddrFlag,
|
||||
// upload flags
|
||||
SwarmApiFlag,
|
||||
SwarmRecursiveUploadFlag,
|
||||
SwarmRecursiveFlag,
|
||||
SwarmWantManifestFlag,
|
||||
SwarmUploadDefaultPath,
|
||||
SwarmUpFromStdinFlag,
|
||||
SwarmUploadMimeType,
|
||||
//deprecated flags
|
||||
DeprecatedEthAPIFlag,
|
||||
DeprecatedEnsAddrFlag,
|
||||
// storage flags
|
||||
SwarmStorePath,
|
||||
SwarmStoreCapacity,
|
||||
SwarmStoreCacheCapacity,
|
||||
}
|
||||
rpcFlags := []cli.Flag{
|
||||
utils.WSEnabledFlag,
|
||||
utils.WSListenAddrFlag,
|
||||
utils.WSPortFlag,
|
||||
utils.WSApiFlag,
|
||||
utils.WSAllowedOriginsFlag,
|
||||
}
|
||||
app.Flags = append(app.Flags, rpcFlags...)
|
||||
app.Flags = append(app.Flags, debug.Flags...)
|
||||
app.Flags = append(app.Flags, swarmmetrics.Flags...)
|
||||
app.Flags = append(app.Flags, tracing.Flags...)
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
if err := debug.Setup(ctx); err != nil {
|
||||
if err := debug.Setup(ctx, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
swarmmetrics.Setup(ctx)
|
||||
tracing.Setup(ctx)
|
||||
return nil
|
||||
}
|
||||
app.After = func(ctx *cli.Context) error {
|
||||
@@ -384,44 +582,50 @@ func main() {
|
||||
|
||||
func version(ctx *cli.Context) error {
|
||||
fmt.Println(strings.Title(clientIdentifier))
|
||||
fmt.Println("Version:", params.Version)
|
||||
fmt.Println("Version:", sv.VersionWithMeta)
|
||||
if gitCommit != "" {
|
||||
fmt.Println("Git Commit:", gitCommit)
|
||||
}
|
||||
fmt.Println("Network Id:", ctx.GlobalInt(utils.NetworkIdFlag.Name))
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("OS:", runtime.GOOS)
|
||||
fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
|
||||
fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
|
||||
return nil
|
||||
}
|
||||
|
||||
func bzzd(ctx *cli.Context) error {
|
||||
//build a valid bzzapi.Config from all available sources:
|
||||
//default config, file config, command line and env vars
|
||||
|
||||
bzzconfig, err := buildConfig(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf("unable to configure swarm: %v", err)
|
||||
}
|
||||
|
||||
cfg := defaultNodeConfig
|
||||
|
||||
//pss operates on ws
|
||||
cfg.WSModules = append(cfg.WSModules, "pss")
|
||||
|
||||
//geth only supports --datadir via command line
|
||||
//in order to be consistent within swarm, if we pass --datadir via environment variable
|
||||
//or via config file, we get the same directory for geth and swarm
|
||||
if _, err := os.Stat(bzzconfig.Path); err == nil {
|
||||
cfg.DataDir = bzzconfig.Path
|
||||
}
|
||||
|
||||
//optionally set the bootnodes before configuring the node
|
||||
setSwarmBootstrapNodes(ctx, &cfg)
|
||||
//setup the ethereum node
|
||||
utils.SetNodeConfig(ctx, &cfg)
|
||||
stack, err := node.New(&cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("can't create node: %v", err)
|
||||
}
|
||||
|
||||
//a few steps need to be done after the config phase is completed,
|
||||
//due to overriding behavior
|
||||
initSwarmNode(bzzconfig, stack, ctx)
|
||||
//register BZZ as node.Service in the ethereum node
|
||||
registerBzzService(bzzconfig, ctx, stack)
|
||||
registerBzzService(bzzconfig, stack)
|
||||
//start the node
|
||||
utils.StartNode(stack)
|
||||
|
||||
@@ -434,35 +638,15 @@ func bzzd(ctx *cli.Context) error {
|
||||
stack.Stop()
|
||||
}()
|
||||
|
||||
// Add bootnodes as initial peers.
|
||||
if bzzconfig.BootNodes != "" {
|
||||
bootnodes := strings.Split(bzzconfig.BootNodes, ",")
|
||||
injectBootnodes(stack.Server(), bootnodes)
|
||||
} else {
|
||||
if bzzconfig.NetworkId == 3 {
|
||||
injectBootnodes(stack.Server(), testbetBootNodes)
|
||||
}
|
||||
}
|
||||
|
||||
stack.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerBzzService(bzzconfig *bzzapi.Config, ctx *cli.Context, stack *node.Node) {
|
||||
|
||||
func registerBzzService(bzzconfig *bzzapi.Config, stack *node.Node) {
|
||||
//define the swarm service boot function
|
||||
boot := func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
var swapClient *ethclient.Client
|
||||
var err error
|
||||
if bzzconfig.SwapApi != "" {
|
||||
log.Info("connecting to SWAP API", "url", bzzconfig.SwapApi)
|
||||
swapClient, err = ethclient.Dial(bzzconfig.SwapApi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to SWAP API %s: %s", bzzconfig.SwapApi, err)
|
||||
}
|
||||
}
|
||||
|
||||
return swarm.NewSwarm(ctx, swapClient, bzzconfig)
|
||||
boot := func(_ *node.ServiceContext) (node.Service, error) {
|
||||
// In production, mockStore must be always nil.
|
||||
return swarm.NewSwarm(bzzconfig, nil)
|
||||
}
|
||||
//register within the ethereum node
|
||||
if err := stack.Register(boot); err != nil {
|
||||
@@ -487,6 +671,26 @@ func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.Pr
|
||||
return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx))
|
||||
}
|
||||
|
||||
// getPrivKey returns the private key of the specified bzzaccount
|
||||
// Used only by client commands, such as `resource`
|
||||
func getPrivKey(ctx *cli.Context) *ecdsa.PrivateKey {
|
||||
// booting up the swarm node just as we do in bzzd action
|
||||
bzzconfig, err := buildConfig(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf("unable to configure swarm: %v", err)
|
||||
}
|
||||
cfg := defaultNodeConfig
|
||||
if _, err := os.Stat(bzzconfig.Path); err == nil {
|
||||
cfg.DataDir = bzzconfig.Path
|
||||
}
|
||||
utils.SetNodeConfig(ctx, &cfg)
|
||||
stack, err := node.New(&cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("can't create node: %v", err)
|
||||
}
|
||||
return getAccount(bzzconfig.BzzAccount, ctx, stack)
|
||||
}
|
||||
|
||||
func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey {
|
||||
var a accounts.Account
|
||||
var err error
|
||||
@@ -541,13 +745,32 @@ func getPassPhrase(prompt string, i int, passwords []string) string {
|
||||
return password
|
||||
}
|
||||
|
||||
func injectBootnodes(srv *p2p.Server, nodes []string) {
|
||||
for _, url := range nodes {
|
||||
n, err := discover.ParseNode(url)
|
||||
if err != nil {
|
||||
log.Error("Invalid swarm bootnode", "err", err)
|
||||
continue
|
||||
// addDefaultHelpSubcommand scans through defined CLI commands and adds
|
||||
// a basic help subcommand to each
|
||||
// if a help command is already defined, it will take precedence over the default.
|
||||
func addDefaultHelpSubcommands(commands []cli.Command) {
|
||||
for i := range commands {
|
||||
cmd := &commands[i]
|
||||
if cmd.Subcommands != nil {
|
||||
cmd.Subcommands = append(cmd.Subcommands, defaultSubcommandHelp)
|
||||
addDefaultHelpSubcommands(cmd.Subcommands)
|
||||
}
|
||||
srv.AddPeer(n)
|
||||
}
|
||||
}
|
||||
|
||||
func setSwarmBootstrapNodes(ctx *cli.Context, cfg *node.Config) {
|
||||
if ctx.GlobalIsSet(utils.BootnodesFlag.Name) || ctx.GlobalIsSet(utils.BootnodesV4Flag.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.P2P.BootstrapNodes = []*discover.Node{}
|
||||
|
||||
for _, url := range SwarmBootnodes {
|
||||
node, err := discover.ParseNode(url)
|
||||
if err != nil {
|
||||
log.Error("Bootstrap URL invalid", "enode", url, "err", err)
|
||||
}
|
||||
cfg.P2P.BootstrapNodes = append(cfg.P2P.BootstrapNodes, node)
|
||||
}
|
||||
log.Debug("added default swarm bootnodes", "length", len(cfg.P2P.BootstrapNodes))
|
||||
}
|
||||
|
||||
+116
-128
@@ -18,10 +18,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
@@ -30,127 +28,118 @@ import (
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const bzzManifestJSON = "application/bzz-manifest+json"
|
||||
|
||||
func add(ctx *cli.Context) {
|
||||
// manifestAdd adds a new entry to the manifest at the given path.
|
||||
// New entry hash, the last argument, must be the hash of a manifest
|
||||
// with only one entry, which meta-data will be added to the original manifest.
|
||||
// On success, this function will print new (updated) manifest's hash.
|
||||
func manifestAdd(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
utils.Fatalf("Need at least three arguments <MHASH> <path> <HASH> [<content-type>]")
|
||||
if len(args) != 3 {
|
||||
utils.Fatalf("Need exactly three arguments <MHASH> <path> <HASH>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := swarm.NewClient(bzzapi)
|
||||
|
||||
m, _, err := client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error downloading manifest to add: %v", err)
|
||||
}
|
||||
l := len(m.Entries)
|
||||
if l == 0 {
|
||||
utils.Fatalf("No entries in manifest %s", hash)
|
||||
} else if l > 1 {
|
||||
utils.Fatalf("Too many entries in manifest %s", hash)
|
||||
}
|
||||
|
||||
newManifest := addEntryToManifest(ctx, mhash, path, hash, ctype)
|
||||
newManifest := addEntryToManifest(client, mhash, path, m.Entries[0])
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func update(ctx *cli.Context) {
|
||||
|
||||
// manifestUpdate replaces an existing entry of the manifest at the given path.
|
||||
// New entry hash, the last argument, must be the hash of a manifest
|
||||
// with only one entry, which meta-data will be added to the original manifest.
|
||||
// On success, this function will print hash of the updated manifest.
|
||||
func manifestUpdate(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
utils.Fatalf("Need at least three arguments <MHASH> <path> <HASH>")
|
||||
if len(args) != 3 {
|
||||
utils.Fatalf("Need exactly three arguments <MHASH> <path> <HASH>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := swarm.NewClient(bzzapi)
|
||||
|
||||
m, _, err := client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error downloading manifest to update: %v", err)
|
||||
}
|
||||
l := len(m.Entries)
|
||||
if l == 0 {
|
||||
utils.Fatalf("No entries in manifest %s", hash)
|
||||
} else if l > 1 {
|
||||
utils.Fatalf("Too many entries in manifest %s", hash)
|
||||
}
|
||||
|
||||
newManifest := updateEntryInManifest(ctx, mhash, path, hash, ctype)
|
||||
newManifest, _, defaultEntryUpdated := updateEntryInManifest(client, mhash, path, m.Entries[0], true)
|
||||
if defaultEntryUpdated {
|
||||
// Print informational message to stderr
|
||||
// allowing the user to get the new manifest hash from stdout
|
||||
// without the need to parse the complete output.
|
||||
fmt.Fprintln(os.Stderr, "Manifest default entry is updated, too")
|
||||
}
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func remove(ctx *cli.Context) {
|
||||
// manifestRemove removes an existing entry of the manifest at the given path.
|
||||
// On success, this function will print hash of the manifest which does not
|
||||
// contain the path.
|
||||
func manifestRemove(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 2 {
|
||||
utils.Fatalf("Need at least two arguments <MHASH> <path>")
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("Need exactly two arguments <MHASH> <path>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
|
||||
newManifest := removeEntryFromManifest(ctx, mhash, path)
|
||||
fmt.Println(newManifest)
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := swarm.NewClient(bzzapi)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
newManifest := removeEntryFromManifest(client, mhash, path)
|
||||
fmt.Println(newManifest)
|
||||
}
|
||||
|
||||
func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) string {
|
||||
func addEntryToManifest(client *swarm.Client, mhash, path string, entry api.ManifestEntry) string {
|
||||
var longestPathEntry = api.ManifestEntry{}
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
mroot, isEncrypted, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" to add is valid and present in swarm
|
||||
_, err = client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Hash to add is not present: %v", err)
|
||||
}
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
for _, e := range mroot.Entries {
|
||||
if path == e.Path {
|
||||
utils.Fatalf("Path %s already present, not adding anything", path)
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if e.ContentType == api.ManifestType {
|
||||
prfxlen := strings.HasPrefix(path, e.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
longestPathEntry = e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,60 +148,59 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := addEntryToManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
newHash := addEntryToManifest(client, longestPathEntry.Hash, newPath, entry)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
for _, e := range mroot.Entries {
|
||||
if longestPathEntry.Path == e.Path {
|
||||
e.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
newMRoot.Entries = append(newMRoot.Entries, e)
|
||||
}
|
||||
mroot = newMRoot
|
||||
} else {
|
||||
// Add the entry in the leaf Manifest
|
||||
newEntry := api.ManifestEntry{
|
||||
Hash: hash,
|
||||
Path: path,
|
||||
ContentType: ctype,
|
||||
}
|
||||
mroot.Entries = append(mroot.Entries, newEntry)
|
||||
entry.Path = path
|
||||
mroot.Entries = append(mroot.Entries, entry)
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
newManifestHash, err := client.UploadManifest(mroot, isEncrypted)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
return newManifestHash
|
||||
|
||||
}
|
||||
|
||||
func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) string {
|
||||
|
||||
// updateEntryInManifest updates an existing entry o path with a new one in the manifest with provided mhash
|
||||
// finding the path recursively through all nested manifests. Argument isRoot is used for default
|
||||
// entry update detection. If the updated entry has the same hash as the default entry, then the
|
||||
// default entry in root manifest will be updated too.
|
||||
// Returned values are the new manifest hash, hash of the entry that was replaced by the new entry and
|
||||
// a a bool that is true if default entry is updated.
|
||||
func updateEntryInManifest(client *swarm.Client, mhash, path string, entry api.ManifestEntry, isRoot bool) (newManifestHash, oldHash string, defaultEntryUpdated bool) {
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
newEntry = api.ManifestEntry{}
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
mroot, isEncrypted, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" with which to update is valid and present in swarm
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
newEntry = entry
|
||||
for _, e := range mroot.Entries {
|
||||
if path == e.Path {
|
||||
newEntry = e
|
||||
// keep the reference of the hash of the entry that should be replaced
|
||||
// for default entry detection
|
||||
oldHash = e.Hash
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if e.ContentType == api.ManifestType {
|
||||
prfxlen := strings.HasPrefix(path, e.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
longestPathEntry = e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,55 +213,55 @@ func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) st
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := updateEntryInManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
var newHash string
|
||||
newHash, oldHash, _ = updateEntryInManifest(client, longestPathEntry.Hash, newPath, entry, false)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
for _, e := range mroot.Entries {
|
||||
if longestPathEntry.Path == e.Path {
|
||||
e.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
newMRoot.Entries = append(newMRoot.Entries, e)
|
||||
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
if newEntry.Path != "" {
|
||||
// update the manifest if the new entry is found and
|
||||
// check if default entry should be updated
|
||||
if newEntry.Path != "" || isRoot {
|
||||
// Replace the hash for leaf Manifest
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if newEntry.Path == entry.Path {
|
||||
myEntry := api.ManifestEntry{
|
||||
Hash: hash,
|
||||
Path: entry.Path,
|
||||
ContentType: ctype,
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, myEntry)
|
||||
} else {
|
||||
for _, e := range mroot.Entries {
|
||||
if newEntry.Path == e.Path {
|
||||
entry.Path = e.Path
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
} else if isRoot && e.Path == "" && e.Hash == oldHash {
|
||||
entry.Path = e.Path
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
defaultEntryUpdated = true
|
||||
} else {
|
||||
newMRoot.Entries = append(newMRoot.Entries, e)
|
||||
}
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
newManifestHash, err = client.UploadManifest(mroot, isEncrypted)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
return newManifestHash
|
||||
return newManifestHash, oldHash, defaultEntryUpdated
|
||||
}
|
||||
|
||||
func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
|
||||
|
||||
func removeEntryFromManifest(client *swarm.Client, mhash, path string) string {
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
entryToRemove = api.ManifestEntry{}
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
mroot, isEncrypted, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
@@ -283,7 +271,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
|
||||
if path == entry.Path {
|
||||
entryToRemove = entry
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
if entry.ContentType == api.ManifestType {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
@@ -299,7 +287,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest remove the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := removeEntryFromManifest(ctx, longestPathEntry.Hash, newPath)
|
||||
newHash := removeEntryFromManifest(client, longestPathEntry.Hash, newPath)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
@@ -323,7 +311,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
newManifestHash, err := client.UploadManifest(mroot, isEncrypted)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
)
|
||||
|
||||
// TestManifestChange tests manifest add, update and remove
|
||||
// cli commands without encryption.
|
||||
func TestManifestChange(t *testing.T) {
|
||||
testManifestChange(t, false)
|
||||
}
|
||||
|
||||
// TestManifestChange tests manifest add, update and remove
|
||||
// cli commands with encryption enabled.
|
||||
func TestManifestChangeEncrypted(t *testing.T) {
|
||||
testManifestChange(t, true)
|
||||
}
|
||||
|
||||
// testManifestChange performs cli commands:
|
||||
// - manifest add
|
||||
// - manifest update
|
||||
// - manifest remove
|
||||
// on a manifest, testing the functionality of this
|
||||
// comands on paths that are in root manifest or a nested one.
|
||||
// Argument encrypt controls whether to use encryption or not.
|
||||
func testManifestChange(t *testing.T, encrypt bool) {
|
||||
t.Parallel()
|
||||
cluster := newTestCluster(t, 1)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
tmp, err := ioutil.TempDir("", "swarm-manifest-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
origDir := filepath.Join(tmp, "orig")
|
||||
if err := os.Mkdir(origDir, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
indexDataFilename := filepath.Join(origDir, "index.html")
|
||||
err = ioutil.WriteFile(indexDataFilename, []byte("<h1>Test</h1>"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Files paths robots.txt and robots.html share the same prefix "robots."
|
||||
// which will result a manifest with a nested manifest under path "robots.".
|
||||
// This will allow testing manifest changes on both root and nested manifest.
|
||||
err = ioutil.WriteFile(filepath.Join(origDir, "robots.txt"), []byte("Disallow: /"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(origDir, "robots.html"), []byte("<strong>No Robots Allowed</strong>"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(origDir, "mutants.txt"), []byte("Frank\nMarcus"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"--recursive",
|
||||
"--defaultpath",
|
||||
indexDataFilename,
|
||||
"up",
|
||||
origDir,
|
||||
}
|
||||
if encrypt {
|
||||
args = append(args, "--encrypt")
|
||||
}
|
||||
|
||||
origManifestHash := runSwarmExpectHash(t, args...)
|
||||
|
||||
checkHashLength(t, origManifestHash, encrypt)
|
||||
|
||||
client := swarm.NewClient(cluster.Nodes[0].URL)
|
||||
|
||||
// upload a new file and use its manifest to add it the original manifest.
|
||||
t.Run("add", func(t *testing.T) {
|
||||
humansData := []byte("Ann\nBob")
|
||||
humansDataFilename := filepath.Join(tmp, "humans.txt")
|
||||
err = ioutil.WriteFile(humansDataFilename, humansData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
humansManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
humansDataFilename,
|
||||
)
|
||||
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"add",
|
||||
origManifestHash,
|
||||
"humans.txt",
|
||||
humansManifestHash,
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "humans.txt" {
|
||||
found = true
|
||||
if e.Size != int64(len(humansData)) {
|
||||
t.Errorf("expected humans.txt size %v, got %v", len(humansData), e.Size)
|
||||
}
|
||||
if e.ModTime.IsZero() {
|
||||
t.Errorf("got zero mod time for humans.txt")
|
||||
}
|
||||
ct := "text/plain; charset=utf-8"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected content type %q, got %q", ct, e.ContentType)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no humans.txt in new manifest")
|
||||
}
|
||||
|
||||
checkFile(t, client, newManifestHash, "humans.txt", humansData)
|
||||
})
|
||||
|
||||
// upload a new file and use its manifest to add it the original manifest,
|
||||
// but ensure that the file will be in the nested manifest of the original one.
|
||||
t.Run("add nested", func(t *testing.T) {
|
||||
robotsData := []byte(`{"disallow": "/"}`)
|
||||
robotsDataFilename := filepath.Join(tmp, "robots.json")
|
||||
err = ioutil.WriteFile(robotsDataFilename, robotsData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
robotsManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
robotsDataFilename,
|
||||
)
|
||||
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"add",
|
||||
origManifestHash,
|
||||
"robots.json",
|
||||
robotsManifestHash,
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
loop:
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "robots." {
|
||||
nestedManifest := downloadManifest(t, client, e.Hash, encrypt)
|
||||
for _, e := range nestedManifest.Entries {
|
||||
if e.Path == "json" {
|
||||
found = true
|
||||
if e.Size != int64(len(robotsData)) {
|
||||
t.Errorf("expected robots.json size %v, got %v", len(robotsData), e.Size)
|
||||
}
|
||||
if e.ModTime.IsZero() {
|
||||
t.Errorf("got zero mod time for robots.json")
|
||||
}
|
||||
ct := "application/json"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected content type %q, got %q", ct, e.ContentType)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no robots.json in new manifest")
|
||||
}
|
||||
|
||||
checkFile(t, client, newManifestHash, "robots.json", robotsData)
|
||||
})
|
||||
|
||||
// upload a new file and use its manifest to change the file it the original manifest.
|
||||
t.Run("update", func(t *testing.T) {
|
||||
indexData := []byte("<h1>Ethereum Swarm</h1>")
|
||||
indexDataFilename := filepath.Join(tmp, "index.html")
|
||||
err = ioutil.WriteFile(indexDataFilename, indexData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
indexManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
indexDataFilename,
|
||||
)
|
||||
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"update",
|
||||
origManifestHash,
|
||||
"index.html",
|
||||
indexManifestHash,
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "index.html" {
|
||||
found = true
|
||||
if e.Size != int64(len(indexData)) {
|
||||
t.Errorf("expected index.html size %v, got %v", len(indexData), e.Size)
|
||||
}
|
||||
if e.ModTime.IsZero() {
|
||||
t.Errorf("got zero mod time for index.html")
|
||||
}
|
||||
ct := "text/html; charset=utf-8"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected content type %q, got %q", ct, e.ContentType)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no index.html in new manifest")
|
||||
}
|
||||
|
||||
checkFile(t, client, newManifestHash, "index.html", indexData)
|
||||
|
||||
// check default entry change
|
||||
checkFile(t, client, newManifestHash, "", indexData)
|
||||
})
|
||||
|
||||
// upload a new file and use its manifest to change the file it the original manifest,
|
||||
// but ensure that the file is in the nested manifest of the original one.
|
||||
t.Run("update nested", func(t *testing.T) {
|
||||
robotsData := []byte(`<string>Only humans allowed!!!</strong>`)
|
||||
robotsDataFilename := filepath.Join(tmp, "robots.html")
|
||||
err = ioutil.WriteFile(robotsDataFilename, robotsData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
humansManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
robotsDataFilename,
|
||||
)
|
||||
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"update",
|
||||
origManifestHash,
|
||||
"robots.html",
|
||||
humansManifestHash,
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
loop:
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "robots." {
|
||||
nestedManifest := downloadManifest(t, client, e.Hash, encrypt)
|
||||
for _, e := range nestedManifest.Entries {
|
||||
if e.Path == "html" {
|
||||
found = true
|
||||
if e.Size != int64(len(robotsData)) {
|
||||
t.Errorf("expected robots.html size %v, got %v", len(robotsData), e.Size)
|
||||
}
|
||||
if e.ModTime.IsZero() {
|
||||
t.Errorf("got zero mod time for robots.html")
|
||||
}
|
||||
ct := "text/html; charset=utf-8"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected content type %q, got %q", ct, e.ContentType)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no robots.html in new manifest")
|
||||
}
|
||||
|
||||
checkFile(t, client, newManifestHash, "robots.html", robotsData)
|
||||
})
|
||||
|
||||
// remove a file from the manifest.
|
||||
t.Run("remove", func(t *testing.T) {
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"remove",
|
||||
origManifestHash,
|
||||
"mutants.txt",
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "mutants.txt" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.Fatal("mutants.txt is not removed")
|
||||
}
|
||||
})
|
||||
|
||||
// remove a file from the manifest, but ensure that the file is in
|
||||
// the nested manifest of the original one.
|
||||
t.Run("remove nested", func(t *testing.T) {
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"remove",
|
||||
origManifestHash,
|
||||
"robots.html",
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
loop:
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "robots." {
|
||||
nestedManifest := downloadManifest(t, client, e.Hash, encrypt)
|
||||
for _, e := range nestedManifest.Entries {
|
||||
if e.Path == "html" {
|
||||
found = true
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.Fatal("robots.html in not removed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestNestedDefaultEntryUpdate tests if the default entry is updated
|
||||
// if the file in nested manifest used for it is also updated.
|
||||
func TestNestedDefaultEntryUpdate(t *testing.T) {
|
||||
testNestedDefaultEntryUpdate(t, false)
|
||||
}
|
||||
|
||||
// TestNestedDefaultEntryUpdateEncrypted tests if the default entry
|
||||
// of encrypted upload is updated if the file in nested manifest
|
||||
// used for it is also updated.
|
||||
func TestNestedDefaultEntryUpdateEncrypted(t *testing.T) {
|
||||
testNestedDefaultEntryUpdate(t, true)
|
||||
}
|
||||
|
||||
func testNestedDefaultEntryUpdate(t *testing.T, encrypt bool) {
|
||||
t.Parallel()
|
||||
cluster := newTestCluster(t, 1)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
tmp, err := ioutil.TempDir("", "swarm-manifest-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
origDir := filepath.Join(tmp, "orig")
|
||||
if err := os.Mkdir(origDir, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
indexData := []byte("<h1>Test</h1>")
|
||||
indexDataFilename := filepath.Join(origDir, "index.html")
|
||||
err = ioutil.WriteFile(indexDataFilename, indexData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add another file with common prefix as the default entry to test updates of
|
||||
// default entry with nested manifests.
|
||||
err = ioutil.WriteFile(filepath.Join(origDir, "index.txt"), []byte("Test"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"--recursive",
|
||||
"--defaultpath",
|
||||
indexDataFilename,
|
||||
"up",
|
||||
origDir,
|
||||
}
|
||||
if encrypt {
|
||||
args = append(args, "--encrypt")
|
||||
}
|
||||
|
||||
origManifestHash := runSwarmExpectHash(t, args...)
|
||||
|
||||
checkHashLength(t, origManifestHash, encrypt)
|
||||
|
||||
client := swarm.NewClient(cluster.Nodes[0].URL)
|
||||
|
||||
newIndexData := []byte("<h1>Ethereum Swarm</h1>")
|
||||
newIndexDataFilename := filepath.Join(tmp, "index.html")
|
||||
err = ioutil.WriteFile(newIndexDataFilename, newIndexData, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newIndexManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"up",
|
||||
newIndexDataFilename,
|
||||
)
|
||||
|
||||
newManifestHash := runSwarmExpectHash(t,
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"manifest",
|
||||
"update",
|
||||
origManifestHash,
|
||||
"index.html",
|
||||
newIndexManifestHash,
|
||||
)
|
||||
|
||||
checkHashLength(t, newManifestHash, encrypt)
|
||||
|
||||
newManifest := downloadManifest(t, client, newManifestHash, encrypt)
|
||||
|
||||
var found bool
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "index." {
|
||||
found = true
|
||||
newManifest = downloadManifest(t, client, e.Hash, encrypt)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no index. path in new manifest")
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, e := range newManifest.Entries {
|
||||
if e.Path == "html" {
|
||||
found = true
|
||||
if e.Size != int64(len(newIndexData)) {
|
||||
t.Errorf("expected index.html size %v, got %v", len(newIndexData), e.Size)
|
||||
}
|
||||
if e.ModTime.IsZero() {
|
||||
t.Errorf("got zero mod time for index.html")
|
||||
}
|
||||
ct := "text/html; charset=utf-8"
|
||||
if e.ContentType != ct {
|
||||
t.Errorf("expected content type %q, got %q", ct, e.ContentType)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no html in new manifest")
|
||||
}
|
||||
|
||||
checkFile(t, client, newManifestHash, "index.html", newIndexData)
|
||||
|
||||
// check default entry change
|
||||
checkFile(t, client, newManifestHash, "", newIndexData)
|
||||
}
|
||||
|
||||
func runSwarmExpectHash(t *testing.T, args ...string) (hash string) {
|
||||
t.Helper()
|
||||
hashRegexp := `[a-f\d]{64,128}`
|
||||
up := runSwarm(t, args...)
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
|
||||
if len(matches) < 1 {
|
||||
t.Fatal("no matches found")
|
||||
}
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
func checkHashLength(t *testing.T, hash string, encrypted bool) {
|
||||
t.Helper()
|
||||
l := len(hash)
|
||||
if encrypted && l != 128 {
|
||||
t.Errorf("expected hash length 128, got %v", l)
|
||||
}
|
||||
if !encrypted && l != 64 {
|
||||
t.Errorf("expected hash length 64, got %v", l)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadManifest(t *testing.T, client *swarm.Client, hash string, encrypted bool) (manifest *api.Manifest) {
|
||||
t.Helper()
|
||||
m, isEncrypted, err := client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if encrypted != isEncrypted {
|
||||
t.Error("new manifest encryption flag is not correct")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func checkFile(t *testing.T, client *swarm.Client, hash, path string, expected []byte) {
|
||||
t.Helper()
|
||||
f, err := client.Download(hash, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, expected) {
|
||||
t.Errorf("expected file content %q, got %q", expected, got)
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Command resource allows the user to create and update signed mutable resource updates
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"github.com/ethereum/go-ethereum/swarm/storage/mru"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func NewGenericSigner(ctx *cli.Context) mru.Signer {
|
||||
return mru.NewGenericSigner(getPrivKey(ctx))
|
||||
}
|
||||
|
||||
// swarm resource create <frequency> [--name <name>] [--data <0x Hexdata> [--multihash=false]]
|
||||
// swarm resource update <Manifest Address or ENS domain> <0x Hexdata> [--multihash=false]
|
||||
// swarm resource info <Manifest Address or ENS domain>
|
||||
|
||||
func resourceCreate(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name)
|
||||
initialData = ctx.String(SwarmResourceDataOnCreateFlag.Name)
|
||||
name = ctx.String(SwarmResourceNameFlag.Name)
|
||||
)
|
||||
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Incorrect number of arguments")
|
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1)
|
||||
return
|
||||
}
|
||||
signer := NewGenericSigner(ctx)
|
||||
frequency, err := strconv.ParseUint(args[0], 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("Frequency formatting error: %s\n", err.Error())
|
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1)
|
||||
return
|
||||
}
|
||||
|
||||
metadata := mru.ResourceMetadata{
|
||||
Name: name,
|
||||
Frequency: frequency,
|
||||
Owner: signer.Address(),
|
||||
}
|
||||
|
||||
var newResourceRequest *mru.Request
|
||||
if initialData != "" {
|
||||
initialDataBytes, err := hexutil.Decode(initialData)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing data: %s\n", err.Error())
|
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1)
|
||||
return
|
||||
}
|
||||
newResourceRequest, err = mru.NewCreateUpdateRequest(&metadata)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error creating new resource request: %s", err)
|
||||
}
|
||||
newResourceRequest.SetData(initialDataBytes, multihash)
|
||||
if err = newResourceRequest.Sign(signer); err != nil {
|
||||
utils.Fatalf("Error signing resource update: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
newResourceRequest, err = mru.NewCreateRequest(&metadata)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error creating new resource request: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
manifestAddress, err := client.CreateResource(newResourceRequest)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error creating resource: %s", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(manifestAddress) // output manifest address to the user in a single line (useful for other commands to pick up)
|
||||
|
||||
}
|
||||
|
||||
func resourceUpdate(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name)
|
||||
)
|
||||
|
||||
if len(args) < 2 {
|
||||
fmt.Println("Incorrect number of arguments")
|
||||
cli.ShowCommandHelpAndExit(ctx, "update", 1)
|
||||
return
|
||||
}
|
||||
signer := NewGenericSigner(ctx)
|
||||
manifestAddressOrDomain := args[0]
|
||||
data, err := hexutil.Decode(args[1])
|
||||
if err != nil {
|
||||
utils.Fatalf("Error parsing data: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve resource status and metadata out of the manifest
|
||||
updateRequest, err := client.GetResourceMetadata(manifestAddressOrDomain)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error retrieving resource status: %s", err.Error())
|
||||
}
|
||||
|
||||
// set the new data
|
||||
updateRequest.SetData(data, multihash)
|
||||
|
||||
// sign update
|
||||
if err = updateRequest.Sign(signer); err != nil {
|
||||
utils.Fatalf("Error signing resource update: %s", err.Error())
|
||||
}
|
||||
|
||||
// post update
|
||||
err = client.UpdateResource(updateRequest)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error updating resource: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func resourceInfo(ctx *cli.Context) {
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
)
|
||||
args := ctx.Args()
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Incorrect number of arguments.")
|
||||
cli.ShowCommandHelpAndExit(ctx, "info", 1)
|
||||
return
|
||||
}
|
||||
manifestAddressOrDomain := args[0]
|
||||
metadata, err := client.GetResourceMetadata(manifestAddressOrDomain)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error retrieving resource metadata: %s", err.Error())
|
||||
return
|
||||
}
|
||||
encodedMetadata, err := metadata.MarshalJSON()
|
||||
if err != nil {
|
||||
utils.Fatalf("Error encoding metadata to JSON for display:%s", err)
|
||||
}
|
||||
fmt.Println(string(encodedMetadata))
|
||||
}
|
||||
+237
-38
@@ -17,12 +17,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -81,6 +86,7 @@ type testCluster struct {
|
||||
//
|
||||
// When starting more than one node, they are connected together using the
|
||||
// admin SetPeer RPC method.
|
||||
|
||||
func newTestCluster(t *testing.T, size int) *testCluster {
|
||||
cluster := &testCluster{}
|
||||
defer func() {
|
||||
@@ -96,18 +102,7 @@ func newTestCluster(t *testing.T, size int) *testCluster {
|
||||
cluster.TmpDir = tmpdir
|
||||
|
||||
// start the nodes
|
||||
cluster.Nodes = make([]*testNode, 0, size)
|
||||
for i := 0; i < size; i++ {
|
||||
dir := filepath.Join(cluster.TmpDir, fmt.Sprintf("swarm%02d", i))
|
||||
if err := os.Mkdir(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
node := newTestNode(t, dir)
|
||||
node.Name = fmt.Sprintf("swarm%02d", i)
|
||||
|
||||
cluster.Nodes = append(cluster.Nodes, node)
|
||||
}
|
||||
cluster.StartNewNodes(t, size)
|
||||
|
||||
if size == 1 {
|
||||
return cluster
|
||||
@@ -145,14 +140,52 @@ func (c *testCluster) Shutdown() {
|
||||
os.RemoveAll(c.TmpDir)
|
||||
}
|
||||
|
||||
func (c *testCluster) Stop() {
|
||||
for _, node := range c.Nodes {
|
||||
node.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCluster) StartNewNodes(t *testing.T, size int) {
|
||||
c.Nodes = make([]*testNode, 0, size)
|
||||
for i := 0; i < size; i++ {
|
||||
dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", i))
|
||||
if err := os.Mkdir(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
node := newTestNode(t, dir)
|
||||
node.Name = fmt.Sprintf("swarm%02d", i)
|
||||
|
||||
c.Nodes = append(c.Nodes, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCluster) StartExistingNodes(t *testing.T, size int, bzzaccount string) {
|
||||
c.Nodes = make([]*testNode, 0, size)
|
||||
for i := 0; i < size; i++ {
|
||||
dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", i))
|
||||
node := existingTestNode(t, dir, bzzaccount)
|
||||
node.Name = fmt.Sprintf("swarm%02d", i)
|
||||
|
||||
c.Nodes = append(c.Nodes, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCluster) Cleanup() {
|
||||
os.RemoveAll(c.TmpDir)
|
||||
}
|
||||
|
||||
type testNode struct {
|
||||
Name string
|
||||
Addr string
|
||||
URL string
|
||||
Enode string
|
||||
Dir string
|
||||
Client *rpc.Client
|
||||
Cmd *cmdtest.TestCmd
|
||||
Name string
|
||||
Addr string
|
||||
URL string
|
||||
Enode string
|
||||
Dir string
|
||||
IpcPath string
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
Client *rpc.Client
|
||||
Cmd *cmdtest.TestCmd
|
||||
}
|
||||
|
||||
const testPassphrase = "swarm-test-passphrase"
|
||||
@@ -181,20 +214,22 @@ func getTestAccount(t *testing.T, dir string) (conf *node.Config, account accoun
|
||||
return conf, account
|
||||
}
|
||||
|
||||
func newTestNode(t *testing.T, dir string) *testNode {
|
||||
|
||||
conf, account := getTestAccount(t, dir)
|
||||
func existingTestNode(t *testing.T, dir string, bzzaccount string) *testNode {
|
||||
conf, _ := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
|
||||
// use a unique IPCPath when running tests on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
conf.IPCPath = fmt.Sprintf("bzzd-%s.ipc", bzzaccount)
|
||||
}
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p2pPort, err := assignTCPPort()
|
||||
ports, err := getAvailableTCPPorts(2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p2pPort := ports[0]
|
||||
httpPort := ports[1]
|
||||
|
||||
// start the node
|
||||
node.Cmd = runSwarm(t,
|
||||
@@ -203,7 +238,7 @@ func newTestNode(t *testing.T, dir string) *testNode {
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
"--ens-api", "",
|
||||
"--bzzaccount", account.Address.String(),
|
||||
"--bzzaccount", bzzaccount,
|
||||
"--bzznetworkid", "321",
|
||||
"--bzzport", httpPort,
|
||||
"--verbosity", "6",
|
||||
@@ -215,6 +250,17 @@ func newTestNode(t *testing.T, dir string) *testNode {
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// ensure that all ports have active listeners
|
||||
// so that the next node will not get the same
|
||||
// when calling getAvailableTCPPorts
|
||||
err = waitTCPPorts(ctx, ports...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
@@ -243,21 +289,174 @@ func newTestNode(t *testing.T, dir string) *testNode {
|
||||
return node
|
||||
}
|
||||
|
||||
func newTestNode(t *testing.T, dir string) *testNode {
|
||||
|
||||
conf, account := getTestAccount(t, dir)
|
||||
ks := keystore.NewKeyStore(path.Join(dir, "keystore"), 1<<18, 1)
|
||||
|
||||
pk := decryptStoreAccount(ks, account.Address.Hex(), []string{testPassphrase})
|
||||
|
||||
node := &testNode{Dir: dir, PrivateKey: pk}
|
||||
|
||||
// assign ports
|
||||
ports, err := getAvailableTCPPorts(2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p2pPort := ports[0]
|
||||
httpPort := ports[1]
|
||||
|
||||
// start the node
|
||||
node.Cmd = runSwarm(t,
|
||||
"--port", p2pPort,
|
||||
"--nodiscover",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
"--ens-api", "",
|
||||
"--bzzaccount", account.Address.String(),
|
||||
"--bzznetworkid", "321",
|
||||
"--bzzport", httpPort,
|
||||
"--verbosity", "6",
|
||||
)
|
||||
node.Cmd.InputLine(testPassphrase)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// ensure that all ports have active listeners
|
||||
// so that the next node will not get the same
|
||||
// when calling getAvailableTCPPorts
|
||||
err = waitTCPPorts(ctx, ports...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.Addr = net.JoinHostPort("127.0.0.1", info.Port)
|
||||
node.URL = "http://" + node.Addr
|
||||
|
||||
var nodeInfo p2p.NodeInfo
|
||||
if err := node.Client.Call(&nodeInfo, "admin_nodeInfo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.Enode = fmt.Sprintf("enode://%s@127.0.0.1:%s", nodeInfo.ID, p2pPort)
|
||||
node.IpcPath = conf.IPCPath
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (n *testNode) Shutdown() {
|
||||
if n.Cmd != nil {
|
||||
n.Cmd.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
func assignTCPPort() (string, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
// getAvailableTCPPorts returns a set of ports that
|
||||
// nothing is listening on at the time.
|
||||
//
|
||||
// Function assignTCPPort cannot be called in sequence
|
||||
// and guardantee that the same port will be returned in
|
||||
// different calls as the listener is closed within the function,
|
||||
// not after all listeners are started and selected unique
|
||||
// available ports.
|
||||
func getAvailableTCPPorts(count int) (ports []string, err error) {
|
||||
for i := 0; i < count; i++ {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// defer close in the loop to be sure the same port will not
|
||||
// be selected in the next iteration
|
||||
defer l.Close()
|
||||
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports = append(ports, port)
|
||||
}
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// waitTCPPorts blocks until tcp connections can be
|
||||
// established on all provided ports. It runs all
|
||||
// ports dialers in parallel, and returns the first
|
||||
// encountered error.
|
||||
// See waitTCPPort also.
|
||||
func waitTCPPorts(ctx context.Context, ports ...string) error {
|
||||
var err error
|
||||
// mu locks err variable that is assigned in
|
||||
// other goroutines
|
||||
var mu sync.Mutex
|
||||
|
||||
// cancel is canceling all goroutines
|
||||
// when the firs error is returned
|
||||
// to prevent unnecessary waiting
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, port := range ports {
|
||||
wg.Add(1)
|
||||
go func(port string) {
|
||||
defer wg.Done()
|
||||
|
||||
e := waitTCPPort(ctx, port)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if e != nil && err == nil {
|
||||
err = e
|
||||
cancel()
|
||||
}
|
||||
}(port)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// waitTCPPort blocks until tcp connection can be established
|
||||
// ona provided port. It has a 3 minute timeout as maximum,
|
||||
// to prevent long waiting, but it can be shortened with
|
||||
// a provided context instance. Dialer has a 10 second timeout
|
||||
// in every iteration, and connection refused error will be
|
||||
// retried in 100 milliseconds periods.
|
||||
func waitTCPPort(ctx context.Context, port string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
c, err := (&net.Dialer{Timeout: 10 * time.Second}).DialContext(ctx, "tcp", "127.0.0.1:"+port)
|
||||
if err != nil {
|
||||
if operr, ok := err.(*net.OpError); ok {
|
||||
if syserr, ok := operr.Err.(*os.SyscallError); ok && syserr.Err == syscall.ECONNREFUSED {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
l.Close()
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoints []string
|
||||
includeLocalhost bool
|
||||
cluster string
|
||||
scheme string
|
||||
filesize int
|
||||
from int
|
||||
to int
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.PrintOrigins(true)
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "smoke-test"
|
||||
app.Usage = ""
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "cluster-endpoint",
|
||||
Value: "testing",
|
||||
Usage: "cluster to point to (open, or testing)",
|
||||
Destination: &cluster,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "cluster-from",
|
||||
Value: 8501,
|
||||
Usage: "swarm node (from)",
|
||||
Destination: &from,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "cluster-to",
|
||||
Value: 8512,
|
||||
Usage: "swarm node (to)",
|
||||
Destination: &to,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cluster-scheme",
|
||||
Value: "http",
|
||||
Usage: "http or https",
|
||||
Destination: &scheme,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "include-localhost",
|
||||
Usage: "whether to include localhost:8500 as an endpoint",
|
||||
Destination: &includeLocalhost,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "filesize",
|
||||
Value: 1,
|
||||
Usage: "file size for generated random file in MB",
|
||||
Destination: &filesize,
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "upload_and_sync",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "upload and sync",
|
||||
Action: cliUploadAndSync,
|
||||
},
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/pborman/uuid"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func generateEndpoints(scheme string, cluster string, from int, to int) {
|
||||
if cluster == "prod" {
|
||||
cluster = ""
|
||||
} else {
|
||||
cluster = cluster + "."
|
||||
}
|
||||
|
||||
for port := from; port <= to; port++ {
|
||||
endpoints = append(endpoints, fmt.Sprintf("%s://%v.%sswarm-gateways.net", scheme, port, cluster))
|
||||
}
|
||||
|
||||
if includeLocalhost {
|
||||
endpoints = append(endpoints, "http://localhost:8500")
|
||||
}
|
||||
}
|
||||
|
||||
func cliUploadAndSync(c *cli.Context) error {
|
||||
defer func(now time.Time) { log.Info("total time", "time", time.Since(now), "size", filesize) }(time.Now())
|
||||
|
||||
generateEndpoints(scheme, cluster, from, to)
|
||||
|
||||
log.Info("uploading to " + endpoints[0] + " and syncing")
|
||||
|
||||
f, cleanup := generateRandomFile(filesize * 1000000)
|
||||
defer cleanup()
|
||||
|
||||
hash, err := upload(f, endpoints[0])
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
fhash, err := digest(f)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("uploaded successfully", "hash", hash, "digest", fmt.Sprintf("%x", fhash))
|
||||
|
||||
if filesize < 10 {
|
||||
time.Sleep(35 * time.Second)
|
||||
} else {
|
||||
time.Sleep(15 * time.Second)
|
||||
time.Sleep(2 * time.Duration(filesize) * time.Second)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint := endpoint
|
||||
ruid := uuid.New()[:8]
|
||||
wg.Add(1)
|
||||
go func(endpoint string, ruid string) {
|
||||
for {
|
||||
err := fetch(hash, endpoint, fhash, ruid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}(endpoint, ruid)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Info("all endpoints synced random file successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetch is getting the requested `hash` from the `endpoint` and compares it with the `original` file
|
||||
func fetch(hash string, endpoint string, original []byte, ruid string) error {
|
||||
log.Trace("sleeping", "ruid", ruid)
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
log.Trace("http get request", "ruid", ruid, "api", endpoint, "hash", hash)
|
||||
res, err := http.Get(endpoint + "/bzz:/" + hash + "/")
|
||||
if err != nil {
|
||||
log.Warn(err.Error(), "ruid", ruid)
|
||||
return err
|
||||
}
|
||||
log.Trace("http get response", "ruid", ruid, "api", endpoint, "hash", hash, "code", res.StatusCode, "len", res.ContentLength)
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
err := fmt.Errorf("expected status code %d, got %v", 200, res.StatusCode)
|
||||
log.Warn(err.Error(), "ruid", ruid)
|
||||
return err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
rdigest, err := digest(res.Body)
|
||||
if err != nil {
|
||||
log.Warn(err.Error(), "ruid", ruid)
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(rdigest, original) {
|
||||
err := fmt.Errorf("downloaded imported file md5=%x is not the same as the generated one=%x", rdigest, original)
|
||||
log.Warn(err.Error(), "ruid", ruid)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace("downloaded file matches random file", "ruid", ruid, "len", res.ContentLength)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// upload is uploading a file `f` to `endpoint` via the `swarm up` cmd
|
||||
func upload(f *os.File, endpoint string) (string, error) {
|
||||
var out bytes.Buffer
|
||||
cmd := exec.Command("swarm", "--bzzapi", endpoint, "up", f.Name())
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := strings.TrimRight(out.String(), "\r\n")
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func digest(r io.Reader) ([]byte, error) {
|
||||
h := md5.New()
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// generateRandomFile is creating a temporary file with the requested byte size
|
||||
func generateRandomFile(size int) (f *os.File, teardown func()) {
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// callback for tmp file cleanup
|
||||
teardown = func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
_, err = rand.Read(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ioutil.WriteFile(tmp.Name(), buf, 0755)
|
||||
|
||||
return tmp, teardown
|
||||
}
|
||||
+16
-4
@@ -40,12 +40,13 @@ func upload(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
recursive = ctx.GlobalBool(SwarmRecursiveUploadFlag.Name)
|
||||
recursive = ctx.GlobalBool(SwarmRecursiveFlag.Name)
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
defaultPath = ctx.GlobalString(SwarmUploadDefaultPath.Name)
|
||||
fromStdin = ctx.GlobalBool(SwarmUpFromStdinFlag.Name)
|
||||
mimeType = ctx.GlobalString(SwarmUploadMimeType.Name)
|
||||
client = swarm.NewClient(bzzapi)
|
||||
toEncrypt = ctx.Bool(SwarmEncryptedFlag.Name)
|
||||
file string
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ func upload(ctx *cli.Context) {
|
||||
utils.Fatalf("Error opening file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
hash, err := client.UploadRaw(f, f.Size)
|
||||
hash, err := client.UploadRaw(f, f.Size, toEncrypt)
|
||||
if err != nil {
|
||||
utils.Fatalf("Upload failed: %s", err)
|
||||
}
|
||||
@@ -97,7 +98,18 @@ func upload(ctx *cli.Context) {
|
||||
if !recursive {
|
||||
return "", errors.New("Argument is a directory and recursive upload is disabled")
|
||||
}
|
||||
return client.UploadDirectory(file, defaultPath, "")
|
||||
if defaultPath != "" {
|
||||
// construct absolute default path
|
||||
absDefaultPath, _ := filepath.Abs(defaultPath)
|
||||
absFile, _ := filepath.Abs(file)
|
||||
// make sure absolute directory ends with only one "/"
|
||||
// to trim it from absolute default path and get relative default path
|
||||
absFile = strings.TrimRight(absFile, "/") + "/"
|
||||
if absDefaultPath != "" && absFile != "" && strings.HasPrefix(absDefaultPath, absFile) {
|
||||
defaultPath = strings.TrimPrefix(absDefaultPath, absFile)
|
||||
}
|
||||
}
|
||||
return client.UploadDirectory(file, defaultPath, "", toEncrypt)
|
||||
}
|
||||
} else {
|
||||
doUpload = func() (string, error) {
|
||||
@@ -110,7 +122,7 @@ func upload(ctx *cli.Context) {
|
||||
mimeType = detectMimeType(file)
|
||||
}
|
||||
f.ContentType = mimeType
|
||||
return client.Upload(f, "")
|
||||
return client.Upload(f, "", toEncrypt)
|
||||
}
|
||||
}
|
||||
hash, err := doUpload()
|
||||
|
||||
+314
-34
@@ -17,60 +17,340 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
var loglevel = flag.Int("loglevel", 3, "verbosity of logs")
|
||||
|
||||
func init() {
|
||||
log.PrintOrigins(true)
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
||||
}
|
||||
|
||||
// TestCLISwarmUp tests that running 'swarm up' makes the resulting file
|
||||
// available from all nodes via the HTTP API
|
||||
func TestCLISwarmUp(t *testing.T) {
|
||||
// start 3 node cluster
|
||||
t.Log("starting 3 node cluster")
|
||||
testCLISwarmUp(false, t)
|
||||
}
|
||||
func TestCLISwarmUpRecursive(t *testing.T) {
|
||||
testCLISwarmUpRecursive(false, t)
|
||||
}
|
||||
|
||||
// TestCLISwarmUpEncrypted tests that running 'swarm encrypted-up' makes the resulting file
|
||||
// available from all nodes via the HTTP API
|
||||
func TestCLISwarmUpEncrypted(t *testing.T) {
|
||||
testCLISwarmUp(true, t)
|
||||
}
|
||||
func TestCLISwarmUpEncryptedRecursive(t *testing.T) {
|
||||
testCLISwarmUpRecursive(true, t)
|
||||
}
|
||||
|
||||
func testCLISwarmUp(toEncrypt bool, t *testing.T) {
|
||||
log.Info("starting 3 node cluster")
|
||||
cluster := newTestCluster(t, 3)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
assertNil(t, err)
|
||||
defer tmp.Close()
|
||||
defer os.Remove(tmp.Name())
|
||||
_, err = io.WriteString(tmp, "data")
|
||||
assertNil(t, err)
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
t.Log("uploading file with 'swarm up'")
|
||||
up := runSwarm(t, "--bzzapi", cluster.Nodes[0].URL, "up", tmp.Name())
|
||||
_, matches := up.ExpectRegexp(`[a-f\d]{64}`)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
t.Logf("file uploaded with hash %s", hash)
|
||||
|
||||
// get the file from the HTTP API of each node
|
||||
for _, node := range cluster.Nodes {
|
||||
t.Logf("getting file from %s", node.Name)
|
||||
res, err := http.Get(node.URL + "/bzz:/" + hash)
|
||||
assertNil(t, err)
|
||||
assertHTTPResponse(t, res, http.StatusOK, "data")
|
||||
}
|
||||
}
|
||||
|
||||
func assertNil(t *testing.T, err error) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tmp.Close()
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
// write data to file
|
||||
data := "notsorandomdata"
|
||||
_, err = io.WriteString(tmp, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hashRegexp := `[a-f\d]{64}`
|
||||
flags := []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"up",
|
||||
tmp.Name()}
|
||||
if toEncrypt {
|
||||
hashRegexp = `[a-f\d]{128}`
|
||||
flags = []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"up",
|
||||
"--encrypt",
|
||||
tmp.Name()}
|
||||
}
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
log.Info(fmt.Sprintf("uploading file with 'swarm up'"))
|
||||
up := runSwarm(t, flags...)
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
log.Info("file uploaded", "hash", hash)
|
||||
|
||||
// get the file from the HTTP API of each node
|
||||
for _, node := range cluster.Nodes {
|
||||
log.Info("getting file from node", "node", node.Name)
|
||||
|
||||
res, err := http.Get(node.URL + "/bzz:/" + hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
reply, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("expected HTTP status 200, got %s", res.Status)
|
||||
}
|
||||
if string(reply) != data {
|
||||
t.Fatalf("expected HTTP body %q, got %q", data, reply)
|
||||
}
|
||||
log.Debug("verifying uploaded file using `swarm down`")
|
||||
//try to get the content with `swarm down`
|
||||
tmpDownload, err := ioutil.TempDir("", "swarm-test")
|
||||
tmpDownload = path.Join(tmpDownload, "tmpfile.tmp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDownload)
|
||||
|
||||
bzzLocator := "bzz:/" + hash
|
||||
flags = []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"down",
|
||||
bzzLocator,
|
||||
tmpDownload,
|
||||
}
|
||||
|
||||
down := runSwarm(t, flags...)
|
||||
down.ExpectExit()
|
||||
|
||||
fi, err := os.Stat(tmpDownload)
|
||||
if err != nil {
|
||||
t.Fatalf("could not stat path: %v", err)
|
||||
}
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsRegular():
|
||||
downloadedBytes, err := ioutil.ReadFile(tmpDownload)
|
||||
if err != nil {
|
||||
t.Fatalf("had an error reading the downloaded file: %v", err)
|
||||
}
|
||||
if !bytes.Equal(downloadedBytes, bytes.NewBufferString(data).Bytes()) {
|
||||
t.Fatalf("retrieved data and posted data not equal!")
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("expected to download regular file, got %s", fi.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Duration(2 * time.Second)
|
||||
httpClient := http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// try to squeeze a timeout by getting an non-existent hash from each node
|
||||
for _, node := range cluster.Nodes {
|
||||
_, err := httpClient.Get(node.URL + "/bzz:/1023e8bae0f70be7d7b5f74343088ba408a218254391490c85ae16278e230340")
|
||||
// we're speeding up the timeout here since netstore has a 60 seconds timeout on a request
|
||||
if err != nil && !strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// this is disabled since it takes 60s due to netstore timeout
|
||||
// if res.StatusCode != 404 {
|
||||
// t.Fatalf("expected HTTP status 404, got %s", res.Status)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func assertHTTPResponse(t *testing.T, res *http.Response, expectedStatus int, expectedBody string) {
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != expectedStatus {
|
||||
t.Fatalf("expected HTTP status %d, got %s", expectedStatus, res.Status)
|
||||
func testCLISwarmUpRecursive(toEncrypt bool, t *testing.T) {
|
||||
fmt.Println("starting 3 node cluster")
|
||||
cluster := newTestCluster(t, 3)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
tmpUploadDir, err := ioutil.TempDir("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assertNil(t, err)
|
||||
if string(data) != expectedBody {
|
||||
t.Fatalf("expected HTTP body %q, got %q", expectedBody, data)
|
||||
defer os.RemoveAll(tmpUploadDir)
|
||||
// create tmp files
|
||||
data := "notsorandomdata"
|
||||
for _, path := range []string{"tmp1", "tmp2"} {
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpUploadDir, path), bytes.NewBufferString(data).Bytes(), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
hashRegexp := `[a-f\d]{64}`
|
||||
flags := []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"--recursive",
|
||||
"up",
|
||||
tmpUploadDir}
|
||||
if toEncrypt {
|
||||
hashRegexp = `[a-f\d]{128}`
|
||||
flags = []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"--recursive",
|
||||
"up",
|
||||
"--encrypt",
|
||||
tmpUploadDir}
|
||||
}
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
log.Info(fmt.Sprintf("uploading file with 'swarm up'"))
|
||||
up := runSwarm(t, flags...)
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
log.Info("dir uploaded", "hash", hash)
|
||||
|
||||
// get the file from the HTTP API of each node
|
||||
for _, node := range cluster.Nodes {
|
||||
log.Info("getting file from node", "node", node.Name)
|
||||
//try to get the content with `swarm down`
|
||||
tmpDownload, err := ioutil.TempDir("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDownload)
|
||||
bzzLocator := "bzz:/" + hash
|
||||
flagss := []string{}
|
||||
flagss = []string{
|
||||
"--bzzapi", cluster.Nodes[0].URL,
|
||||
"down",
|
||||
"--recursive",
|
||||
bzzLocator,
|
||||
tmpDownload,
|
||||
}
|
||||
|
||||
fmt.Println("downloading from swarm with recursive")
|
||||
down := runSwarm(t, flagss...)
|
||||
down.ExpectExit()
|
||||
|
||||
files, err := ioutil.ReadDir(tmpDownload)
|
||||
for _, v := range files {
|
||||
fi, err := os.Stat(path.Join(tmpDownload, v.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("got an error: %v", err)
|
||||
}
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsRegular():
|
||||
if file, err := swarm.Open(path.Join(tmpDownload, v.Name())); err != nil {
|
||||
t.Fatalf("encountered an error opening the file returned from the CLI: %v", err)
|
||||
} else {
|
||||
ff := make([]byte, len(data))
|
||||
io.ReadFull(file, ff)
|
||||
buf := bytes.NewBufferString(data)
|
||||
|
||||
if !bytes.Equal(ff, buf.Bytes()) {
|
||||
t.Fatalf("retrieved data and posted data not equal!")
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Fatalf("this shouldnt happen")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("could not list files at: %v", files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCLISwarmUpDefaultPath tests swarm recursive upload with relative and absolute
|
||||
// default paths and with encryption.
|
||||
func TestCLISwarmUpDefaultPath(t *testing.T) {
|
||||
testCLISwarmUpDefaultPath(false, false, t)
|
||||
testCLISwarmUpDefaultPath(false, true, t)
|
||||
testCLISwarmUpDefaultPath(true, false, t)
|
||||
testCLISwarmUpDefaultPath(true, true, t)
|
||||
}
|
||||
|
||||
func testCLISwarmUpDefaultPath(toEncrypt bool, absDefaultPath bool, t *testing.T) {
|
||||
cluster := newTestCluster(t, 1)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
tmp, err := ioutil.TempDir("", "swarm-defaultpath-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmp, "index.html"), []byte("<h1>Test</h1>"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(tmp, "robots.txt"), []byte("Disallow: /"), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defaultPath := "index.html"
|
||||
if absDefaultPath {
|
||||
defaultPath = filepath.Join(tmp, defaultPath)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--bzzapi",
|
||||
cluster.Nodes[0].URL,
|
||||
"--recursive",
|
||||
"--defaultpath",
|
||||
defaultPath,
|
||||
"up",
|
||||
tmp,
|
||||
}
|
||||
if toEncrypt {
|
||||
args = append(args, "--encrypt")
|
||||
}
|
||||
|
||||
up := runSwarm(t, args...)
|
||||
hashRegexp := `[a-f\d]{64,128}`
|
||||
_, matches := up.ExpectRegexp(hashRegexp)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
|
||||
client := swarm.NewClient(cluster.Nodes[0].URL)
|
||||
|
||||
m, isEncrypted, err := client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if toEncrypt != isEncrypted {
|
||||
t.Error("downloaded manifest is not encrypted")
|
||||
}
|
||||
|
||||
var found bool
|
||||
var entriesCount int
|
||||
for _, e := range m.Entries {
|
||||
entriesCount++
|
||||
if e.Path == "" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("manifest default entry was not found")
|
||||
}
|
||||
|
||||
if entriesCount != 3 {
|
||||
t.Errorf("manifest contains %v entries, expected %v", entriesCount, 3)
|
||||
}
|
||||
}
|
||||
|
||||
+90
-3
@@ -27,8 +27,12 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
@@ -105,6 +109,8 @@ func ImportChain(chain *core.BlockChain, fn string) error {
|
||||
}
|
||||
|
||||
log.Info("Importing blockchain", "file", fn)
|
||||
|
||||
// Open the file handle and potentially unwrap the gzip stream
|
||||
fh, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -180,8 +186,12 @@ func missingBlocks(chain *core.BlockChain, blocks []*types.Block) []*types.Block
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportChain exports a blockchain into the specified file, truncating any data
|
||||
// already present in the file.
|
||||
func ExportChain(blockchain *core.BlockChain, fn string) error {
|
||||
log.Info("Exporting blockchain", "file", fn)
|
||||
|
||||
// Open the file handle and potentially wrap with a gzip stream
|
||||
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -193,7 +203,7 @@ func ExportChain(blockchain *core.BlockChain, fn string) error {
|
||||
writer = gzip.NewWriter(writer)
|
||||
defer writer.(*gzip.Writer).Close()
|
||||
}
|
||||
|
||||
// Iterate over the blocks and export them
|
||||
if err := blockchain.Export(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -202,9 +212,12 @@ func ExportChain(blockchain *core.BlockChain, fn string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportAppendChain exports a blockchain into the specified file, appending to
|
||||
// the file if data already exists in it.
|
||||
func ExportAppendChain(blockchain *core.BlockChain, fn string, first uint64, last uint64) error {
|
||||
log.Info("Exporting blockchain", "file", fn)
|
||||
// TODO verify mode perms
|
||||
|
||||
// Open the file handle and potentially wrap with a gzip stream
|
||||
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,10 +229,84 @@ func ExportAppendChain(blockchain *core.BlockChain, fn string, first uint64, las
|
||||
writer = gzip.NewWriter(writer)
|
||||
defer writer.(*gzip.Writer).Close()
|
||||
}
|
||||
|
||||
// Iterate over the blocks and export them
|
||||
if err := blockchain.ExportN(writer, first, last); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Exported blockchain to", "file", fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportPreimages imports a batch of exported hash preimages into the database.
|
||||
func ImportPreimages(db *ethdb.LDBDatabase, fn string) error {
|
||||
log.Info("Importing preimages", "file", fn)
|
||||
|
||||
// Open the file handle and potentially unwrap the gzip stream
|
||||
fh, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
var reader io.Reader = fh
|
||||
if strings.HasSuffix(fn, ".gz") {
|
||||
if reader, err = gzip.NewReader(reader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stream := rlp.NewStream(reader, 0)
|
||||
|
||||
// Import the preimages in batches to prevent disk trashing
|
||||
preimages := make(map[common.Hash][]byte)
|
||||
|
||||
for {
|
||||
// Read the next entry and ensure it's not junk
|
||||
var blob []byte
|
||||
|
||||
if err := stream.Decode(&blob); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Accumulate the preimages and flush when enough ws gathered
|
||||
preimages[crypto.Keccak256Hash(blob)] = common.CopyBytes(blob)
|
||||
if len(preimages) > 1024 {
|
||||
rawdb.WritePreimages(db, 0, preimages)
|
||||
preimages = make(map[common.Hash][]byte)
|
||||
}
|
||||
}
|
||||
// Flush the last batch preimage data
|
||||
if len(preimages) > 0 {
|
||||
rawdb.WritePreimages(db, 0, preimages)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportPreimages exports all known hash preimages into the specified file,
|
||||
// truncating any data already present in the file.
|
||||
func ExportPreimages(db *ethdb.LDBDatabase, fn string) error {
|
||||
log.Info("Exporting preimages", "file", fn)
|
||||
|
||||
// Open the file handle and potentially wrap with a gzip stream
|
||||
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
var writer io.Writer = fh
|
||||
if strings.HasSuffix(fn, ".gz") {
|
||||
writer = gzip.NewWriter(writer)
|
||||
defer writer.(*gzip.Writer).Close()
|
||||
}
|
||||
// Iterate over the preimages and export them
|
||||
it := db.NewIteratorWithPrefix([]byte("secure-key-"))
|
||||
for it.Next() {
|
||||
if err := rlp.Encode(writer, it.Value()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info("Exported preimages", "file", fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
+189
-68
@@ -24,9 +24,9 @@ import (
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/metrics/influxdb"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
@@ -55,7 +56,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
@@ -64,7 +65,7 @@ var (
|
||||
{{if .cmd.Description}}{{.cmd.Description}}
|
||||
{{end}}{{if .cmd.Subcommands}}
|
||||
SUBCOMMANDS:
|
||||
{{range .cmd.Subcommands}}{{.cmd.Name}}{{with .cmd.ShortName}}, {{.cmd}}{{end}}{{ "\t" }}{{.cmd.Usage}}
|
||||
{{range .cmd.Subcommands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{end}}{{if .categorizedFlags}}
|
||||
{{range $idx, $categorized := .categorizedFlags}}{{$categorized.Name}} OPTIONS:
|
||||
{{range $categorized.Flags}}{{"\t"}}{{.}}
|
||||
@@ -96,7 +97,7 @@ func NewApp(gitCommit, usage string) *cli.App {
|
||||
app.Author = ""
|
||||
//app.Authors = nil
|
||||
app.Email = ""
|
||||
app.Version = params.Version
|
||||
app.Version = params.VersionWithMeta
|
||||
if len(gitCommit) >= 8 {
|
||||
app.Version += "-" + gitCommit[:8]
|
||||
}
|
||||
@@ -156,14 +157,6 @@ var (
|
||||
Usage: "Document Root for HTTPClient file scheme",
|
||||
Value: DirectoryString{homeDir()},
|
||||
}
|
||||
FastSyncFlag = cli.BoolFlag{
|
||||
Name: "fast",
|
||||
Usage: "Enable fast syncing through state downloads",
|
||||
}
|
||||
LightModeFlag = cli.BoolFlag{
|
||||
Name: "light",
|
||||
Usage: "Enable light client mode",
|
||||
}
|
||||
defaultSyncMode = eth.DefaultConfig.SyncMode
|
||||
SyncModeFlag = TextMarshalerFlag{
|
||||
Name: "syncmode",
|
||||
@@ -191,7 +184,7 @@ var (
|
||||
}
|
||||
// Dashboard settings
|
||||
DashboardEnabledFlag = cli.BoolFlag{
|
||||
Name: "dashboard",
|
||||
Name: metrics.DashboardEnabledFlag,
|
||||
Usage: "Enable the dashboard",
|
||||
}
|
||||
DashboardAddrFlag = cli.StringFlag{
|
||||
@@ -209,11 +202,6 @@ var (
|
||||
Usage: "Dashboard metrics collection refresh rate",
|
||||
Value: dashboard.DefaultConfig.Refresh,
|
||||
}
|
||||
DashboardAssetsFlag = cli.StringFlag{
|
||||
Name: "dashboard.assets",
|
||||
Usage: "Developer flag to serve the dashboard from the local file system",
|
||||
Value: dashboard.DefaultConfig.Assets,
|
||||
}
|
||||
// Ethash settings
|
||||
EthashCacheDirFlag = DirectoryFlag{
|
||||
Name: "ethash.cachedir",
|
||||
@@ -245,6 +233,10 @@ var (
|
||||
Value: eth.DefaultConfig.Ethash.DatasetsOnDisk,
|
||||
}
|
||||
// Transaction pool settings
|
||||
TxPoolLocalsFlag = cli.StringFlag{
|
||||
Name: "txpool.locals",
|
||||
Usage: "Comma separated accounts to treat as locals (no flush, priority inclusion)",
|
||||
}
|
||||
TxPoolNoLocalsFlag = cli.BoolFlag{
|
||||
Name: "txpool.nolocals",
|
||||
Usage: "Disables price exemptions for locally submitted transactions",
|
||||
@@ -321,29 +313,71 @@ var (
|
||||
Usage: "Enable mining",
|
||||
}
|
||||
MinerThreadsFlag = cli.IntFlag{
|
||||
Name: "minerthreads",
|
||||
Name: "miner.threads",
|
||||
Usage: "Number of CPU threads to use for mining",
|
||||
Value: runtime.NumCPU(),
|
||||
Value: 0,
|
||||
}
|
||||
TargetGasLimitFlag = cli.Uint64Flag{
|
||||
MinerLegacyThreadsFlag = cli.IntFlag{
|
||||
Name: "minerthreads",
|
||||
Usage: "Number of CPU threads to use for mining (deprecated, use --miner.threads)",
|
||||
Value: 0,
|
||||
}
|
||||
MinerNotifyFlag = cli.StringFlag{
|
||||
Name: "miner.notify",
|
||||
Usage: "Comma separated HTTP URL list to notify of new work packages",
|
||||
}
|
||||
MinerGasTargetFlag = cli.Uint64Flag{
|
||||
Name: "miner.gastarget",
|
||||
Usage: "Target gas floor for mined blocks",
|
||||
Value: eth.DefaultConfig.MinerGasFloor,
|
||||
}
|
||||
MinerLegacyGasTargetFlag = cli.Uint64Flag{
|
||||
Name: "targetgaslimit",
|
||||
Usage: "Target gas limit sets the artificial target gas floor for the blocks to mine",
|
||||
Value: params.GenesisGasLimit,
|
||||
Usage: "Target gas floor for mined blocks (deprecated, use --miner.gastarget)",
|
||||
Value: eth.DefaultConfig.MinerGasFloor,
|
||||
}
|
||||
EtherbaseFlag = cli.StringFlag{
|
||||
Name: "etherbase",
|
||||
Usage: "Public address for block mining rewards (default = first account created)",
|
||||
MinerGasLimitFlag = cli.Uint64Flag{
|
||||
Name: "miner.gaslimit",
|
||||
Usage: "Target gas ceiling for mined blocks",
|
||||
Value: eth.DefaultConfig.MinerGasCeil,
|
||||
}
|
||||
MinerGasPriceFlag = BigFlag{
|
||||
Name: "miner.gasprice",
|
||||
Usage: "Minimal gas price for mining a transactions",
|
||||
Value: eth.DefaultConfig.MinerGasPrice,
|
||||
}
|
||||
MinerLegacyGasPriceFlag = BigFlag{
|
||||
Name: "gasprice",
|
||||
Usage: "Minimal gas price for mining a transactions (deprecated, use --miner.gasprice)",
|
||||
Value: eth.DefaultConfig.MinerGasPrice,
|
||||
}
|
||||
MinerEtherbaseFlag = cli.StringFlag{
|
||||
Name: "miner.etherbase",
|
||||
Usage: "Public address for block mining rewards (default = first account)",
|
||||
Value: "0",
|
||||
}
|
||||
GasPriceFlag = BigFlag{
|
||||
Name: "gasprice",
|
||||
Usage: "Minimal gas price to accept for mining a transactions",
|
||||
Value: eth.DefaultConfig.GasPrice,
|
||||
MinerLegacyEtherbaseFlag = cli.StringFlag{
|
||||
Name: "etherbase",
|
||||
Usage: "Public address for block mining rewards (default = first account, deprecated, use --miner.etherbase)",
|
||||
Value: "0",
|
||||
}
|
||||
ExtraDataFlag = cli.StringFlag{
|
||||
Name: "extradata",
|
||||
MinerExtraDataFlag = cli.StringFlag{
|
||||
Name: "miner.extradata",
|
||||
Usage: "Block extra data set by the miner (default = client version)",
|
||||
}
|
||||
MinerLegacyExtraDataFlag = cli.StringFlag{
|
||||
Name: "extradata",
|
||||
Usage: "Block extra data set by the miner (default = client version, deprecated, use --miner.extradata)",
|
||||
}
|
||||
MinerRecommitIntervalFlag = cli.DurationFlag{
|
||||
Name: "miner.recommit",
|
||||
Usage: "Time interval to recreate the block being mined",
|
||||
Value: eth.DefaultConfig.MinerRecommit,
|
||||
}
|
||||
MinerNoVerfiyFlag = cli.BoolFlag{
|
||||
Name: "miner.noverify",
|
||||
Usage: "Disable remote sealing verification",
|
||||
}
|
||||
// Account settings
|
||||
UnlockedAccountFlag = cli.StringFlag{
|
||||
Name: "unlock",
|
||||
@@ -365,10 +399,6 @@ var (
|
||||
Name: "ethstats",
|
||||
Usage: "Reporting URL of a ethstats service (nodename:secret@host:port)",
|
||||
}
|
||||
MetricsEnabledFlag = cli.BoolFlag{
|
||||
Name: metrics.MetricsEnabledFlag,
|
||||
Usage: "Enable metrics collection and reporting",
|
||||
}
|
||||
FakePoWFlag = cli.BoolFlag{
|
||||
Name: "fakepow",
|
||||
Usage: "Disables proof-of-work verification",
|
||||
@@ -537,6 +567,45 @@ var (
|
||||
Usage: "Minimum POW accepted",
|
||||
Value: whisper.DefaultMinimumPoW,
|
||||
}
|
||||
|
||||
// Metrics flags
|
||||
MetricsEnabledFlag = cli.BoolFlag{
|
||||
Name: metrics.MetricsEnabledFlag,
|
||||
Usage: "Enable metrics collection and reporting",
|
||||
}
|
||||
MetricsEnableInfluxDBFlag = cli.BoolFlag{
|
||||
Name: "metrics.influxdb",
|
||||
Usage: "Enable metrics export/push to an external InfluxDB database",
|
||||
}
|
||||
MetricsInfluxDBEndpointFlag = cli.StringFlag{
|
||||
Name: "metrics.influxdb.endpoint",
|
||||
Usage: "InfluxDB API endpoint to report metrics to",
|
||||
Value: "http://localhost:8086",
|
||||
}
|
||||
MetricsInfluxDBDatabaseFlag = cli.StringFlag{
|
||||
Name: "metrics.influxdb.database",
|
||||
Usage: "InfluxDB database name to push reported metrics to",
|
||||
Value: "geth",
|
||||
}
|
||||
MetricsInfluxDBUsernameFlag = cli.StringFlag{
|
||||
Name: "metrics.influxdb.username",
|
||||
Usage: "Username to authorize access to the database",
|
||||
Value: "test",
|
||||
}
|
||||
MetricsInfluxDBPasswordFlag = cli.StringFlag{
|
||||
Name: "metrics.influxdb.password",
|
||||
Usage: "Password to authorize access to the database",
|
||||
Value: "test",
|
||||
}
|
||||
// The `host` tag is part of every measurement sent to InfluxDB. Queries on tags are faster in InfluxDB.
|
||||
// It is used so that we can group all nodes and average a measurement across all of them, but also so
|
||||
// that we can select a specific node and inspect its measurements.
|
||||
// https://docs.influxdata.com/influxdb/v1.4/concepts/key_concepts/#tag-key
|
||||
MetricsInfluxDBHostTagFlag = cli.StringFlag{
|
||||
Name: "metrics.influxdb.host.tag",
|
||||
Usage: "InfluxDB `host` tag attached to all measurements",
|
||||
Value: "localhost",
|
||||
}
|
||||
)
|
||||
|
||||
// MakeDataDir retrieves the currently requested data directory, terminating
|
||||
@@ -612,8 +681,7 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) {
|
||||
for _, url := range urls {
|
||||
node, err := discover.ParseNode(url)
|
||||
if err != nil {
|
||||
log.Error("Bootstrap URL invalid", "enode", url, "err", err)
|
||||
continue
|
||||
log.Crit("Bootstrap URL invalid", "enode", url, "err", err)
|
||||
}
|
||||
cfg.BootstrapNodes = append(cfg.BootstrapNodes, node)
|
||||
}
|
||||
@@ -779,10 +847,19 @@ func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error
|
||||
// setEtherbase retrieves the etherbase either from the directly specified
|
||||
// command line flags or from the keystore if CLI indexed.
|
||||
func setEtherbase(ctx *cli.Context, ks *keystore.KeyStore, cfg *eth.Config) {
|
||||
if ctx.GlobalIsSet(EtherbaseFlag.Name) {
|
||||
account, err := MakeAddress(ks, ctx.GlobalString(EtherbaseFlag.Name))
|
||||
// Extract the current etherbase, new flag overriding legacy one
|
||||
var etherbase string
|
||||
if ctx.GlobalIsSet(MinerLegacyEtherbaseFlag.Name) {
|
||||
etherbase = ctx.GlobalString(MinerLegacyEtherbaseFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerEtherbaseFlag.Name) {
|
||||
etherbase = ctx.GlobalString(MinerEtherbaseFlag.Name)
|
||||
}
|
||||
// Convert the etherbase into an address and configure it
|
||||
if etherbase != "" {
|
||||
account, err := MakeAddress(ks, etherbase)
|
||||
if err != nil {
|
||||
Fatalf("Option %q: %v", EtherbaseFlag.Name, err)
|
||||
Fatalf("Invalid miner etherbase: %v", err)
|
||||
}
|
||||
cfg.Etherbase = account.Address
|
||||
}
|
||||
@@ -813,12 +890,15 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
|
||||
setBootstrapNodes(ctx, cfg)
|
||||
setBootstrapNodesV5(ctx, cfg)
|
||||
|
||||
lightClient := ctx.GlobalBool(LightModeFlag.Name) || ctx.GlobalString(SyncModeFlag.Name) == "light"
|
||||
lightClient := ctx.GlobalString(SyncModeFlag.Name) == "light"
|
||||
lightServer := ctx.GlobalInt(LightServFlag.Name) != 0
|
||||
lightPeers := ctx.GlobalInt(LightPeersFlag.Name)
|
||||
|
||||
if ctx.GlobalIsSet(MaxPeersFlag.Name) {
|
||||
cfg.MaxPeers = ctx.GlobalInt(MaxPeersFlag.Name)
|
||||
if lightServer && !ctx.GlobalIsSet(LightPeersFlag.Name) {
|
||||
cfg.MaxPeers += lightPeers
|
||||
}
|
||||
} else {
|
||||
if lightServer {
|
||||
cfg.MaxPeers += lightPeers
|
||||
@@ -910,6 +990,16 @@ func setGPO(ctx *cli.Context, cfg *gasprice.Config) {
|
||||
}
|
||||
|
||||
func setTxPool(ctx *cli.Context, cfg *core.TxPoolConfig) {
|
||||
if ctx.GlobalIsSet(TxPoolLocalsFlag.Name) {
|
||||
locals := strings.Split(ctx.GlobalString(TxPoolLocalsFlag.Name), ",")
|
||||
for _, account := range locals {
|
||||
if trimmed := strings.TrimSpace(account); !common.IsHexAddress(trimmed) {
|
||||
Fatalf("Invalid account in --txpool.locals: %s", trimmed)
|
||||
} else {
|
||||
cfg.Locals = append(cfg.Locals, common.HexToAddress(account))
|
||||
}
|
||||
}
|
||||
}
|
||||
if ctx.GlobalIsSet(TxPoolNoLocalsFlag.Name) {
|
||||
cfg.NoLocals = ctx.GlobalBool(TxPoolNoLocalsFlag.Name)
|
||||
}
|
||||
@@ -963,7 +1053,7 @@ func setEthash(ctx *cli.Context, cfg *eth.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// checkExclusive verifies that only a single isntance of the provided flags was
|
||||
// checkExclusive verifies that only a single instance of the provided flags was
|
||||
// set by the user. Each flag might optionally be followed by a string type to
|
||||
// specialize it further.
|
||||
func checkExclusive(ctx *cli.Context, args ...interface{}) {
|
||||
@@ -1015,8 +1105,6 @@ func SetShhConfig(ctx *cli.Context, stack *node.Node, cfg *whisper.Config) {
|
||||
func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||
// Avoid conflicting network flags
|
||||
checkExclusive(ctx, DeveloperFlag, TestnetFlag, RinkebyFlag)
|
||||
checkExclusive(ctx, FastSyncFlag, LightModeFlag, SyncModeFlag)
|
||||
checkExclusive(ctx, LightServFlag, LightModeFlag)
|
||||
checkExclusive(ctx, LightServFlag, SyncModeFlag, "light")
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
@@ -1025,13 +1113,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||
setTxPool(ctx, &cfg.TxPool)
|
||||
setEthash(ctx, cfg)
|
||||
|
||||
switch {
|
||||
case ctx.GlobalIsSet(SyncModeFlag.Name):
|
||||
if ctx.GlobalIsSet(SyncModeFlag.Name) {
|
||||
cfg.SyncMode = *GlobalTextMarshaler(ctx, SyncModeFlag.Name).(*downloader.SyncMode)
|
||||
case ctx.GlobalBool(FastSyncFlag.Name):
|
||||
cfg.SyncMode = downloader.FastSync
|
||||
case ctx.GlobalBool(LightModeFlag.Name):
|
||||
cfg.SyncMode = downloader.LightSync
|
||||
}
|
||||
if ctx.GlobalIsSet(LightServFlag.Name) {
|
||||
cfg.LightServ = ctx.GlobalInt(LightServFlag.Name)
|
||||
@@ -1056,17 +1139,38 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||
if ctx.GlobalIsSet(CacheFlag.Name) || ctx.GlobalIsSet(CacheGCFlag.Name) {
|
||||
cfg.TrieCache = ctx.GlobalInt(CacheFlag.Name) * ctx.GlobalInt(CacheGCFlag.Name) / 100
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerThreadsFlag.Name) {
|
||||
cfg.MinerThreads = ctx.GlobalInt(MinerThreadsFlag.Name)
|
||||
if ctx.GlobalIsSet(MinerNotifyFlag.Name) {
|
||||
cfg.MinerNotify = strings.Split(ctx.GlobalString(MinerNotifyFlag.Name), ",")
|
||||
}
|
||||
if ctx.GlobalIsSet(DocRootFlag.Name) {
|
||||
cfg.DocRoot = ctx.GlobalString(DocRootFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(ExtraDataFlag.Name) {
|
||||
cfg.ExtraData = []byte(ctx.GlobalString(ExtraDataFlag.Name))
|
||||
if ctx.GlobalIsSet(MinerLegacyExtraDataFlag.Name) {
|
||||
cfg.MinerExtraData = []byte(ctx.GlobalString(MinerLegacyExtraDataFlag.Name))
|
||||
}
|
||||
if ctx.GlobalIsSet(GasPriceFlag.Name) {
|
||||
cfg.GasPrice = GlobalBig(ctx, GasPriceFlag.Name)
|
||||
if ctx.GlobalIsSet(MinerExtraDataFlag.Name) {
|
||||
cfg.MinerExtraData = []byte(ctx.GlobalString(MinerExtraDataFlag.Name))
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerLegacyGasTargetFlag.Name) {
|
||||
cfg.MinerGasFloor = ctx.GlobalUint64(MinerLegacyGasTargetFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerGasTargetFlag.Name) {
|
||||
cfg.MinerGasFloor = ctx.GlobalUint64(MinerGasTargetFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerGasLimitFlag.Name) {
|
||||
cfg.MinerGasCeil = ctx.GlobalUint64(MinerGasLimitFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerLegacyGasPriceFlag.Name) {
|
||||
cfg.MinerGasPrice = GlobalBig(ctx, MinerLegacyGasPriceFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerGasPriceFlag.Name) {
|
||||
cfg.MinerGasPrice = GlobalBig(ctx, MinerGasPriceFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerRecommitIntervalFlag.Name) {
|
||||
cfg.MinerRecommit = ctx.Duration(MinerRecommitIntervalFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(MinerNoVerfiyFlag.Name) {
|
||||
cfg.MinerNoverify = ctx.Bool(MinerNoVerfiyFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(VMEnableDebugFlag.Name) {
|
||||
// TODO(fjl): force-enable this in --dev mode
|
||||
@@ -1086,6 +1190,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||
}
|
||||
cfg.Genesis = core.DefaultRinkebyGenesisBlock()
|
||||
case ctx.GlobalBool(DeveloperFlag.Name):
|
||||
if !ctx.GlobalIsSet(NetworkIdFlag.Name) {
|
||||
cfg.NetworkId = 1337
|
||||
}
|
||||
// Create new developer account or reuse existing one
|
||||
var (
|
||||
developer accounts.Account
|
||||
@@ -1105,8 +1212,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||
log.Info("Using developer account", "address", developer.Address)
|
||||
|
||||
cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.GlobalInt(DeveloperPeriodFlag.Name)), developer.Address)
|
||||
if !ctx.GlobalIsSet(GasPriceFlag.Name) {
|
||||
cfg.GasPrice = big.NewInt(1)
|
||||
if !ctx.GlobalIsSet(MinerGasPriceFlag.Name) && !ctx.GlobalIsSet(MinerLegacyGasPriceFlag.Name) {
|
||||
cfg.MinerGasPrice = big.NewInt(1)
|
||||
}
|
||||
}
|
||||
// TODO(fjl): move trie cache generations into config
|
||||
@@ -1120,7 +1227,6 @@ func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
|
||||
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
|
||||
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
|
||||
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
|
||||
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
|
||||
}
|
||||
|
||||
// RegisterEthService adds an Ethereum client to the stack.
|
||||
@@ -1148,7 +1254,7 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||
// RegisterDashboardService adds a dashboard to the stack.
|
||||
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
|
||||
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
return dashboard.New(cfg, commit)
|
||||
return dashboard.New(cfg, commit, ctx.ResolvePath("logs")), nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1162,7 +1268,7 @@ func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
|
||||
}
|
||||
|
||||
// RegisterEthStatsService configures the Ethereum Stats daemon and adds it to
|
||||
// th egiven node.
|
||||
// the given node.
|
||||
func RegisterEthStatsService(stack *node.Node, url string) {
|
||||
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
// Retrieve both eth and les services
|
||||
@@ -1178,10 +1284,25 @@ func RegisterEthStatsService(stack *node.Node, url string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetupNetwork configures the system for either the main net or some test network.
|
||||
func SetupNetwork(ctx *cli.Context) {
|
||||
// TODO(fjl): move target gas limit into config
|
||||
params.TargetGasLimit = ctx.GlobalUint64(TargetGasLimitFlag.Name)
|
||||
func SetupMetrics(ctx *cli.Context) {
|
||||
if metrics.Enabled {
|
||||
log.Info("Enabling metrics collection")
|
||||
var (
|
||||
enableExport = ctx.GlobalBool(MetricsEnableInfluxDBFlag.Name)
|
||||
endpoint = ctx.GlobalString(MetricsInfluxDBEndpointFlag.Name)
|
||||
database = ctx.GlobalString(MetricsInfluxDBDatabaseFlag.Name)
|
||||
username = ctx.GlobalString(MetricsInfluxDBUsernameFlag.Name)
|
||||
password = ctx.GlobalString(MetricsInfluxDBPasswordFlag.Name)
|
||||
hosttag = ctx.GlobalString(MetricsInfluxDBHostTagFlag.Name)
|
||||
)
|
||||
|
||||
if enableExport {
|
||||
log.Info("Enabling metrics export to InfluxDB")
|
||||
go influxdb.InfluxDBWithTags(metrics.DefaultRegistry, 10*time.Second, endpoint, database, username, password, "geth.", map[string]string{
|
||||
"host": hosttag,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MakeChainDatabase open an LevelDB using the flags passed to the client and will hard crash if it fails.
|
||||
@@ -1191,7 +1312,7 @@ func MakeChainDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database {
|
||||
handles = makeDatabaseHandles()
|
||||
)
|
||||
name := "chaindata"
|
||||
if ctx.GlobalBool(LightModeFlag.Name) {
|
||||
if ctx.GlobalString(SyncModeFlag.Name) == "light" {
|
||||
name = "lightchaindata"
|
||||
}
|
||||
chainDb, err := stack.OpenDatabase(name, cache, handles)
|
||||
@@ -1236,7 +1357,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chai
|
||||
DatasetDir: stack.ResolvePath(eth.DefaultConfig.Ethash.DatasetDir),
|
||||
DatasetsInMem: eth.DefaultConfig.Ethash.DatasetsInMem,
|
||||
DatasetsOnDisk: eth.DefaultConfig.Ethash.DatasetsOnDisk,
|
||||
})
|
||||
}, nil, false)
|
||||
}
|
||||
}
|
||||
if gcmode := ctx.GlobalString(GCModeFlag.Name); gcmode != "full" && gcmode != "archive" {
|
||||
|
||||
+60
-42
@@ -140,8 +140,8 @@ func processArgs() {
|
||||
}
|
||||
|
||||
if *asymmetricMode && len(*argPub) > 0 {
|
||||
pub = crypto.ToECDSAPub(common.FromHex(*argPub))
|
||||
if !isKeyValid(pub) {
|
||||
var err error
|
||||
if pub, err = crypto.UnmarshalPubkey(common.FromHex(*argPub)); err != nil {
|
||||
utils.Fatalf("invalid public key")
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,8 @@ func initialize() {
|
||||
if len(*argIP) == 0 {
|
||||
argIP = scanLineA("Please enter your IP and port (e.g. 127.0.0.1:30348): ")
|
||||
}
|
||||
} else if *fileReader {
|
||||
*bootstrapMode = true
|
||||
} else {
|
||||
if len(*argEnode) == 0 {
|
||||
argEnode = scanLineA("Please enter the peer's enode: ")
|
||||
@@ -205,11 +207,22 @@ func initialize() {
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
|
||||
if *mailServerMode {
|
||||
if len(msPassword) == 0 {
|
||||
msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read Mail Server password: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &whisper.Config{
|
||||
MaxMessageSize: uint32(*argMaxSize),
|
||||
MinimumAcceptedPOW: *argPoW,
|
||||
}
|
||||
|
||||
shh = whisper.New(cfg)
|
||||
|
||||
if *argPoW != whisper.DefaultMinimumPoW {
|
||||
err := shh.SetMinimumPoW(*argPoW)
|
||||
if err != nil {
|
||||
@@ -257,18 +270,10 @@ func initialize() {
|
||||
}
|
||||
|
||||
if *mailServerMode {
|
||||
if len(msPassword) == 0 {
|
||||
msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read Mail Server password: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
shh = whisper.New(cfg)
|
||||
shh.RegisterServer(&mailServer)
|
||||
mailServer.Init(shh, *argDBPath, msPassword, *argServerPoW)
|
||||
} else {
|
||||
shh = whisper.New(cfg)
|
||||
if err := mailServer.Init(shh, *argDBPath, msPassword, *argServerPoW); err != nil {
|
||||
utils.Fatalf("Failed to init MailServer: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
server = &p2p.Server{
|
||||
@@ -306,16 +311,16 @@ func startServer() error {
|
||||
configureNode()
|
||||
}
|
||||
|
||||
if !*forwarderMode {
|
||||
if *fileExMode {
|
||||
fmt.Printf("Please type the file name to be send. To quit type: '%s'\n", quitCommand)
|
||||
} else if *fileReader {
|
||||
fmt.Printf("Please type the file name to be decrypted. To quit type: '%s'\n", quitCommand)
|
||||
} else if !*forwarderMode {
|
||||
fmt.Printf("Please type the message. To quit type: '%s'\n", quitCommand)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isKeyValid(k *ecdsa.PublicKey) bool {
|
||||
return k.X != nil && k.Y != nil
|
||||
}
|
||||
|
||||
func configureNode() {
|
||||
var err error
|
||||
var p2pAccept bool
|
||||
@@ -331,9 +336,8 @@ func configureNode() {
|
||||
if b == nil {
|
||||
utils.Fatalf("Error: can not convert hexadecimal string")
|
||||
}
|
||||
pub = crypto.ToECDSAPub(b)
|
||||
if !isKeyValid(pub) {
|
||||
utils.Fatalf("Error: invalid public key")
|
||||
if pub, err = crypto.UnmarshalPubkey(b); err != nil {
|
||||
utils.Fatalf("Error: invalid peer public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,6 +570,7 @@ func sendMsg(payload []byte) common.Hash {
|
||||
if err != nil {
|
||||
utils.Fatalf("failed to create new message: %s", err)
|
||||
}
|
||||
|
||||
envelope, err := msg.Wrap(¶ms)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to seal message: %v \n", err)
|
||||
@@ -601,15 +606,17 @@ func messageLoop() {
|
||||
m2 := af.Retrieve()
|
||||
messages := append(m1, m2...)
|
||||
for _, msg := range messages {
|
||||
reportedOnce := false
|
||||
if !*fileExMode && len(msg.Payload) <= 2048 {
|
||||
printMessageInfo(msg)
|
||||
reportedOnce = true
|
||||
}
|
||||
|
||||
// All messages are saved upon specifying argSaveDir.
|
||||
// fileExMode only specifies how messages are displayed on the console after they are saved.
|
||||
// if fileExMode == true, only the hashes are displayed, since messages might be too big.
|
||||
if len(*argSaveDir) > 0 {
|
||||
writeMessageToFile(*argSaveDir, msg)
|
||||
}
|
||||
|
||||
if !*fileExMode && len(msg.Payload) <= 2048 {
|
||||
printMessageInfo(msg)
|
||||
writeMessageToFile(*argSaveDir, msg, !reportedOnce)
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
@@ -634,7 +641,11 @@ func printMessageInfo(msg *whisper.ReceivedMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeMessageToFile(dir string, msg *whisper.ReceivedMessage) {
|
||||
func writeMessageToFile(dir string, msg *whisper.ReceivedMessage, show bool) {
|
||||
if len(dir) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := fmt.Sprintf("%d", msg.Sent)
|
||||
name := fmt.Sprintf("%x", msg.EnvelopeHash)
|
||||
|
||||
@@ -643,22 +654,24 @@ func writeMessageToFile(dir string, msg *whisper.ReceivedMessage) {
|
||||
address = crypto.PubkeyToAddress(*msg.Src)
|
||||
}
|
||||
|
||||
env := shh.GetEnvelope(msg.EnvelopeHash)
|
||||
if env == nil {
|
||||
fmt.Printf("\nUnexpected error: envelope not found: %x\n", msg.EnvelopeHash)
|
||||
return
|
||||
}
|
||||
|
||||
// this is a sample code; uncomment if you don't want to save your own messages.
|
||||
//if whisper.IsPubKeyEqual(msg.Src, &asymKey.PublicKey) {
|
||||
// fmt.Printf("\n%s <%x>: message from myself received, not saved: '%s'\n", timestamp, address, name)
|
||||
// return
|
||||
//}
|
||||
|
||||
if len(dir) > 0 {
|
||||
fullpath := filepath.Join(dir, name)
|
||||
err := ioutil.WriteFile(fullpath, msg.Raw, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("\n%s {%x}: message received but not saved: %s\n", timestamp, address, err)
|
||||
} else {
|
||||
fmt.Printf("\n%s {%x}: message received and saved as '%s' (%d bytes)\n", timestamp, address, name, len(msg.Raw))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("\n%s {%x}: message received (%d bytes), but not saved: %s\n", timestamp, address, len(msg.Raw), name)
|
||||
fullpath := filepath.Join(dir, name)
|
||||
err := ioutil.WriteFile(fullpath, env.Data, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("\n%s {%x}: message received but not saved: %s\n", timestamp, address, err)
|
||||
} else if show {
|
||||
fmt.Printf("\n%s {%x}: message received and saved as '%s' (%d bytes)\n", timestamp, address, name, len(env.Data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,18 +695,23 @@ func requestExpiredMessagesLoop() {
|
||||
for {
|
||||
timeLow = scanUint("Please enter the lower limit of the time range (unix timestamp): ")
|
||||
timeUpp = scanUint("Please enter the upper limit of the time range (unix timestamp): ")
|
||||
t = scanLine("Please enter the topic (hexadecimal): ")
|
||||
if len(t) >= whisper.TopicLength*2 {
|
||||
t = scanLine("Enter the topic (hex). Press enter to request all messages, regardless of the topic: ")
|
||||
if len(t) == whisper.TopicLength*2 {
|
||||
x, err := hex.DecodeString(t)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to parse the topic: %s", err)
|
||||
fmt.Printf("Failed to parse the topic: %s \n", err)
|
||||
continue
|
||||
}
|
||||
xt = whisper.BytesToTopic(x)
|
||||
bloom = whisper.TopicToBloom(xt)
|
||||
obfuscateBloom(bloom)
|
||||
} else {
|
||||
} else if len(t) == 0 {
|
||||
bloom = whisper.MakeFullNodeBloom()
|
||||
} else {
|
||||
fmt.Println("Error: topic is invalid, request aborted")
|
||||
continue
|
||||
}
|
||||
|
||||
if timeUpp == 0 {
|
||||
timeUpp = 0xFFFFFFFF
|
||||
}
|
||||
@@ -736,7 +754,7 @@ func extractIDFromEnode(s string) []byte {
|
||||
return n.ID[:]
|
||||
}
|
||||
|
||||
// obfuscateBloom adds 16 random bits to the the bloom
|
||||
// obfuscateBloom adds 16 random bits to the bloom
|
||||
// filter, in order to obfuscate the containing topics.
|
||||
// it does so deterministically within every session.
|
||||
// despite additional bits, it will match on average
|
||||
|
||||
Reference in New Issue
Block a user