diff --git a/cmd/goal/ledger.go b/cmd/goal/ledger.go index e36b6b8490..f387cc50d8 100644 --- a/cmd/goal/ledger.go +++ b/cmd/goal/ledger.go @@ -17,13 +17,30 @@ package main import ( + "bytes" "fmt" + "strconv" "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/protocol/transcode" +) + +var ( + blockFilename string + rawBlock bool + base32Encoding bool + strictJSON bool ) func init() { ledgerCmd.AddCommand(supplyCmd) + ledgerCmd.AddCommand(blockCmd) + + blockCmd.Flags().StringVarP(&blockFilename, "out", "o", stdoutFilenameValue, "The filename to dump the block to (if not set, use stdout)") + blockCmd.Flags().BoolVarP(&rawBlock, "raw", "r", false, "Format block as msgpack") + blockCmd.Flags().BoolVar(&base32Encoding, "b32", false, "Encode binary blobs using base32 instead of base64") + blockCmd.Flags().BoolVar(&strictJSON, "strict", false, "Strict JSON decode: turn all keys into strings") } var ledgerCmd = &cobra.Command{ @@ -52,3 +69,45 @@ var supplyCmd = &cobra.Command{ fmt.Printf("Round: %v\nTotal Money: %v microAlgos\nOnline Money: %v microAlgos\n", response.Round, response.TotalMoney, response.OnlineMoney) }, } + +var blockCmd = &cobra.Command{ + Use: "block [round number]", + Short: "Dump a block to a file or stdout", + Long: "Dump a block to a file or stdout", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + round, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + reportErrorf(errParsingRoundNumber, err) + } + + dataDir := ensureSingleDataDir() + client := ensureAlgodClient(dataDir) + response, err := client.RawBlock(round) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Unless the user asked for the raw block, + // print the block encoded as JSON + if !rawBlock { + in := bytes.NewBuffer(response) + out := bytes.NewBuffer(nil) + err = transcode.Transcode(true, base32Encoding, strictJSON, in, out) + if err != nil { + reportErrorf(errEncodingBlockAsJSON, err) + } + response = out.Bytes() + } else { + if base32Encoding || strictJSON { + reportErrorf(errBadBlockArgs) + } + } + + // If blockFilename flag was not set, the default value '-' will write to stdout + err = writeFile(blockFilename, response, 0600) + if err != nil { + reportErrorf(fileWriteError, blockFilename, err) + } + }, +} diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index ddeb675dcc..529cf0b1b3 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -146,4 +146,9 @@ const ( errWalletNotFound = "Wallet '%s' not found" errDefaultWalletNotFound = "Wallet with ID '%s' not found. Was the default wallet deleted?" errGettingToken = "Couldn't get token for wallet '%s' (ID: %s): %s" + + // Ledger + errParsingRoundNumber = "Error parsing round number: %s" + errBadBlockArgs = "Cannot combine --b32=true or --strict=true with --raw" + errEncodingBlockAsJSON = "Error encoding block as json: %s" ) diff --git a/cmd/msgpacktool/main.go b/cmd/msgpacktool/main.go index c836293ba2..a2983cbe11 100644 --- a/cmd/msgpacktool/main.go +++ b/cmd/msgpacktool/main.go @@ -45,6 +45,8 @@ import ( "flag" "fmt" "os" + + "github.com/algorand/go-algorand/protocol/transcode" ) var mpToJSON = flag.Bool("d", false, "Decode msgpack to JSON") @@ -64,7 +66,7 @@ func main() { os.Exit(1) } - err := transcode(*mpToJSON, os.Stdin, os.Stdout) + err := transcode.Transcode(*mpToJSON, *base32Encoding, *strictJSON, os.Stdin, os.Stdout) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index 5894b797bf..5e1e7cd35d 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -39,6 +39,7 @@ const ( authHeader = "X-Algo-API-Token" healthCheckEndpoint = "/health" apiVersionPathPrefix = "/v1" + maxRawResponseBytes = 50e6 ) // unversionedPaths ais a set of paths that should not be prefixed by the API version @@ -87,7 +88,7 @@ func stripTransaction(tx string) string { } // submitForm is a helper used for submitting (ex.) GETs and POSTs to the server -func (client RestClient) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool) error { +func (client RestClient) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool, decodeJSON bool) error { var err error queryURL := client.serverURL queryURL.Path = path @@ -134,11 +135,12 @@ func (client RestClient) submitForm(response interface{}, path string, request i httpClient := &http.Client{} resp, err := httpClient.Do(req) - if err != nil { return err } + // Ensure response isn't too large + resp.Body = http.MaxBytesReader(nil, resp.Body, maxRawResponseBytes) defer resp.Body.Close() err = extractError(resp) @@ -146,20 +148,42 @@ func (client RestClient) submitForm(response interface{}, path string, request i return err } - dec := json.NewDecoder(resp.Body) - return dec.Decode(&response) + if decodeJSON { + dec := json.NewDecoder(resp.Body) + return dec.Decode(&response) + } + + // Response must implement RawResponse + raw, ok := response.(v1.RawResponse) + if !ok { + return fmt.Errorf("can only decode raw response into type implementing v1.RawResponse") + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + raw.SetBytes(bodyBytes) + return nil } // get performs a GET request to the specific path against the server func (client RestClient) get(response interface{}, path string, request interface{}) error { - return client.submitForm(response, path, request, "GET", false /* encodeJSON */) + return client.submitForm(response, path, request, "GET", false /* encodeJSON */, true /* decodeJSON */) +} + +// getRaw behaves identically to get but doesn't json decode the response, and +// the response must implement the v1.RawResponse interface +func (client RestClient) getRaw(response v1.RawResponse, path string, request interface{}) error { + return client.submitForm(response, path, request, "GET", false /* encodeJSON */, false /* decodeJSON */) } // post sends a POST request to the given path with the given request object. // No query parameters will be sent if request is nil. // response must be a pointer to an object as post writes the response there. func (client RestClient) post(response interface{}, path string, request interface{}) error { - return client.submitForm(response, path, request, "POST", true /* encodeJSON */) + return client.submitForm(response, path, request, "POST", true /* encodeJSON */, true /* decodeJSON */) } // Status retrieves the StatusResponse from the running node @@ -219,6 +243,10 @@ type assetsParams struct { Max uint64 `url:"max"` } +type rawblockParams struct { + Raw uint64 `url:"raw"` +} + // TransactionsByAddr returns all transactions for a PK [addr] in the [first, // last] rounds range. func (client RestClient) TransactionsByAddr(addr string, first, last, max uint64) (response v1.TransactionList, err error) { @@ -303,6 +331,12 @@ func (client RestClient) Block(round uint64) (response v1.Block, err error) { return } +// RawBlock gets the encoded, raw msgpack block for the given round +func (client RestClient) RawBlock(round uint64) (response v1.RawBlock, err error) { + err = client.getRaw(&response, fmt.Sprintf("/block/%d", round), rawblockParams{1}) + return +} + // GetGoRoutines gets a dump of the goroutines from pprof // Not supported func (client RestClient) GetGoRoutines(ctx context.Context) (goRoutines string, err error) { diff --git a/daemon/algod/api/server/v1/handlers/responses.go b/daemon/algod/api/server/v1/handlers/responses.go index fab747f38c..b41b30bd99 100644 --- a/daemon/algod/api/server/v1/handlers/responses.go +++ b/daemon/algod/api/server/v1/handlers/responses.go @@ -175,6 +175,18 @@ func (r TransactionParamsResponse) getBody() interface{} { return r.Body } +// RawBlockResponse contains encoded, raw block information +// +// swagger:response RawBlockResponse +type RawBlockResponse struct { + // in: body + Body *v1.RawBlock +} + +func (r RawBlockResponse) getBody() interface{} { + return r.Body +} + // BlockResponse contains block information // // swagger:response BlockResponse diff --git a/daemon/algod/api/spec/v1/model.go b/daemon/algod/api/spec/v1/model.go index 157c48e469..bad26a1d21 100644 --- a/daemon/algod/api/spec/v1/model.go +++ b/daemon/algod/api/spec/v1/model.go @@ -608,6 +608,21 @@ type TransactionParams struct { MinTxnFee uint64 `json:"minFee"` } +// RawResponse is fulfilled by responses that should not be decoded as msgpack +type RawResponse interface { + SetBytes([]byte) +} + +// RawBlock represents an encoded msgpack block +// swagger:model RawBlock +// swagger:strfmt byte +type RawBlock []byte + +// SetBytes fulfills the RawResponse interface on RawBlock +func (rb *RawBlock) SetBytes(b []byte) { + *rb = b +} + // Block contains a block information // swagger:model Block type Block struct { diff --git a/libgoal/libgoal.go b/libgoal/libgoal.go index d51726caac..6760e7d12b 100644 --- a/libgoal/libgoal.go +++ b/libgoal/libgoal.go @@ -672,6 +672,15 @@ func (c *Client) Block(round uint64) (resp v1.Block, err error) { return } +// RawBlock takes a round and returns its block +func (c *Client) RawBlock(round uint64) (resp v1.RawBlock, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + resp, err = algod.RawBlock(round) + } + return +} + // HealthCheck returns an error if something is wrong func (c *Client) HealthCheck() error { algod, err := c.ensureAlgodClient() diff --git a/cmd/msgpacktool/core.go b/protocol/transcode/core.go similarity index 90% rename from cmd/msgpacktool/core.go rename to protocol/transcode/core.go index 3ebc52fcb4..90c9866fba 100644 --- a/cmd/msgpacktool/core.go +++ b/protocol/transcode/core.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package main +package transcode import ( "encoding/base32" @@ -32,7 +32,8 @@ type decoder interface { Decode(v interface{}) error } -func transcode(mpToJSON bool, in io.ReadCloser, out io.WriteCloser) error { +// Transcode turns msgpack to JSON or JSON to msgpack +func Transcode(mpToJSON bool, base32Encoding, strictJSON bool, in io.Reader, out io.Writer) error { canonicalMsgpackHandle := new(codec.MsgpackHandle) canonicalMsgpackHandle.ErrorIfNoField = true canonicalMsgpackHandle.ErrorIfNoArrayExpand = true @@ -62,9 +63,6 @@ func transcode(mpToJSON bool, in io.ReadCloser, out io.WriteCloser) error { enc = codec.NewEncoder(out, canonicalMsgpackHandle) } - defer in.Close() - defer out.Close() - for { var a interface{} err := dec.Decode(&a) @@ -77,7 +75,7 @@ func transcode(mpToJSON bool, in io.ReadCloser, out io.WriteCloser) error { } if mpToJSON { - a = toJSON(a) + a = toJSON(a, base32Encoding, strictJSON) } else { a = fromJSON(a) } @@ -93,7 +91,7 @@ func transcode(mpToJSON bool, in io.ReadCloser, out io.WriteCloser) error { } } -func toJSON(a interface{}) interface{} { +func toJSON(a interface{}, base32Encoding, strictJSON bool) interface{} { switch v := a.(type) { case map[interface{}]interface{}: r := make(map[interface{}]interface{}) @@ -106,16 +104,18 @@ func toJSON(a interface{}) interface{} { eb, ok2 := e.([]byte) if ok1 && ok2 { - if *base32Encoding { + if base32Encoding { r[fmt.Sprintf("%s:b32", ks)] = base32.StdEncoding.EncodeToString(eb) } else { r[fmt.Sprintf("%s:b64", ks)] = base64.StdEncoding.EncodeToString(eb) } } else { - if *strictJSON { + if strictJSON { k = fmt.Sprintf("%v", k) } - r[toJSON(k)] = toJSON(e) + kenc := toJSON(k, base32Encoding, strictJSON) + eenc := toJSON(e, base32Encoding, strictJSON) + r[kenc] = eenc } } return r @@ -123,7 +123,8 @@ func toJSON(a interface{}) interface{} { case []interface{}: r := make([]interface{}, 0) for _, e := range v { - r = append(r, toJSON(e)) + eenc := toJSON(e, base32Encoding, strictJSON) + r = append(r, eenc) } return r diff --git a/cmd/msgpacktool/core_test.go b/protocol/transcode/core_test.go similarity index 98% rename from cmd/msgpacktool/core_test.go rename to protocol/transcode/core_test.go index a6f703a8b7..01ffa4dc8f 100644 --- a/cmd/msgpacktool/core_test.go +++ b/protocol/transcode/core_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package main +package transcode import ( "encoding/base32" @@ -30,7 +30,9 @@ import ( ) func transcodeNoError(t *testing.T, mpToJSON bool, in io.ReadCloser, out io.WriteCloser) { - err := transcode(mpToJSON, in, out) + defer in.Close() + defer out.Close() + err := Transcode(mpToJSON, false, false, in, out) require.NoError(t, err) }