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)
}