diff --git a/Gopkg.lock b/Gopkg.lock
index ab8a14a5b6a15522ff240dea6a783264b227c6ed..2190ec995b6c854d86538b67b108ef32bae39402 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -470,6 +470,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "2a58c6e6ec7d2792452873c5cb57032cc6d1a4750345647f562a8c9e8bc1e0b4"
+  inputs-digest = "53146785a49a1d6940ce537262f1ff17196fed4244188f33c2f14342fadac88a"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/cmd/burrow/commands/dump.go b/cmd/burrow/commands/dump.go
new file mode 100644
index 0000000000000000000000000000000000000000..07ddb156f5e98993c1b4786fee018bf2e9fb5e08
--- /dev/null
+++ b/cmd/burrow/commands/dump.go
@@ -0,0 +1,83 @@
+package commands
+
+import (
+	"encoding/json"
+
+	"github.com/hyperledger/burrow/forensics"
+	"github.com/hyperledger/burrow/txs"
+	cli "github.com/jawher/mow.cli"
+	"github.com/tendermint/tmlibs/db"
+)
+
+func Dump(output Output) func(cmd *cli.Cmd) {
+	return func(dump *cli.Cmd) {
+		configOpt := dump.StringOpt("c config", "", "Use the a specified burrow config file")
+
+		var explorer *forensics.BlockExplorer
+
+		dump.Before = func() {
+			conf, err := obtainBurrowConfig(*configOpt, "")
+			if err != nil {
+				output.Fatalf("Could not obtain config: %v", err)
+			}
+			tmConf := conf.Tendermint.TendermintConfig()
+
+			explorer = forensics.NewBlockExplorer(db.DBBackendType(tmConf.DBBackend), tmConf.DBDir())
+		}
+
+		dump.Command("blocks", "dump blocks to stdout", func(cmd *cli.Cmd) {
+			rangeArg := cmd.StringArg("RANGE", "", "Range as START_HEIGHT:END_HEIGHT where omitting "+
+				"either endpoint implicitly describes the start/end and a negative index counts back from the last block")
+
+			cmd.Spec = "[RANGE]"
+
+			cmd.Action = func() {
+				start, end, err := parseRange(*rangeArg)
+
+				_, err = explorer.Blocks(start, end,
+					func(block *forensics.Block) (stop bool) {
+						bs, err := json.Marshal(block)
+						if err != nil {
+							output.Fatalf("Could not serialise block: %v", err)
+						}
+						output.Printf(string(bs))
+						return false
+					})
+				if err != nil {
+					output.Fatalf("Error iterating over blocks: %v", err)
+				}
+			}
+		})
+
+		dump.Command("txs", "dump transactions to stdout", func(cmd *cli.Cmd) {
+			rangeArg := cmd.StringArg("RANGE", "", "Range as START_HEIGHT:END_HEIGHT where omitting "+
+				"either endpoint implicitly describes the start/end and a negative index counts back from the last block")
+
+			cmd.Spec = "[RANGE]"
+
+			cmd.Action = func() {
+				start, end, err := parseRange(*rangeArg)
+
+				_, err = explorer.Blocks(start, end,
+					func(block *forensics.Block) (stop bool) {
+						stopped, err := block.Transactions(func(tx txs.Tx) (stop bool) {
+							bs, err := json.Marshal(tx)
+							if err != nil {
+								output.Fatalf("Could not deserialise transaction: %v", err)
+							}
+							output.Printf(string(bs))
+							return false
+						})
+						if err != nil {
+							output.Fatalf("Error iterating over transactions: %v", err)
+						}
+						// If we stopped transactions stop everything
+						return stopped
+					})
+				if err != nil {
+					output.Fatalf("Error iterating over blocks: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/cmd/burrow/commands/helpers.go b/cmd/burrow/commands/helpers.go
index 6c2a1b211dc8b26a7d726342b9d213a74f02aa7f..ee8807e085a957b469568307b7a239ef44fe754d 100644
--- a/cmd/burrow/commands/helpers.go
+++ b/cmd/burrow/commands/helpers.go
@@ -3,9 +3,13 @@ package commands
 import (
 	"fmt"
 
+	"strconv"
+	"strings"
+
 	"github.com/hyperledger/burrow/config"
 	"github.com/hyperledger/burrow/config/source"
 	"github.com/hyperledger/burrow/genesis"
+	logging_config "github.com/hyperledger/burrow/logging/config"
 )
 
 type Output interface {
@@ -14,6 +18,29 @@ type Output interface {
 	Fatalf(format string, args ...interface{})
 }
 
+func obtainBurrowConfig(configFile, genesisDocFile string) (*config.BurrowConfig, error) {
+	// We need to reflect on whether this obscures where values are coming from
+	conf := config.DefaultBurrowConfig()
+	// We treat logging a little differently in that if anything is set for logging we will not
+	// set default outputs
+	conf.Logging = nil
+	err := source.EachOf(
+		burrowConfigProvider(configFile),
+		source.FirstOf(
+			genesisDocProvider(genesisDocFile, false),
+			// Try working directory
+			genesisDocProvider(config.DefaultGenesisDocJSONFileName, true)),
+	).Apply(conf)
+	if err != nil {
+		return nil, err
+	}
+	// If no logging config was provided use the default
+	if conf.Logging == nil {
+		conf.Logging = logging_config.DefaultNodeLoggingConfig()
+	}
+	return conf, nil
+}
+
 func burrowConfigProvider(configFile string) source.ConfigProvider {
 	return source.FirstOf(
 		// Will fail if file doesn't exist, but still skipped it configFile == ""
@@ -45,3 +72,32 @@ func genesisDocProvider(genesisFile string, skipNonExistent bool) source.ConfigP
 			return nil
 		})
 }
+
+func parseRange(rangeString string) (start int64, end int64, err error) {
+	start = 0
+	end = -1
+
+	if rangeString == "" {
+		return
+	}
+
+	bounds := strings.Split(rangeString, ":")
+	if len(bounds) == 1 {
+		startString := bounds[0]
+		start, err = strconv.ParseInt(startString, 10, 64)
+		return
+	}
+	if len(bounds) == 2 {
+		if bounds[0] != "" {
+			start, err = strconv.ParseInt(bounds[0], 10, 64)
+			if err != nil {
+				return
+			}
+		}
+		if bounds[1] != "" {
+			end, err = strconv.ParseInt(bounds[1], 10, 64)
+		}
+		return
+	}
+	return 0, 0, fmt.Errorf("could not parse range from %s", rangeString)
+}
diff --git a/cmd/burrow/commands/helpers_test.go b/cmd/burrow/commands/helpers_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e12fc26c528973b7ba5e29173b78d86eff18ade7
--- /dev/null
+++ b/cmd/burrow/commands/helpers_test.go
@@ -0,0 +1,40 @@
+package commands
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseRange(t *testing.T) {
+	start, end, err := parseRange("")
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), start)
+	assert.Equal(t, int64(-1), end)
+
+	start, end, err = parseRange(":")
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), start)
+	assert.Equal(t, int64(-1), end)
+
+	start, end, err = parseRange("0:")
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), start)
+	assert.Equal(t, int64(-1), end)
+
+	start, end, err = parseRange(":-1")
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), start)
+	assert.Equal(t, int64(-1), end)
+
+	start, end, err = parseRange("0:-1")
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), start)
+	assert.Equal(t, int64(-1), end)
+
+	start, end, err = parseRange("123123:-123")
+	require.NoError(t, err)
+	assert.Equal(t, int64(123123), start)
+	assert.Equal(t, int64(-123), end)
+}
diff --git a/cmd/burrow/commands/start.go b/cmd/burrow/commands/start.go
index 67b43bb0e78f9911e93791313435810edcd85b1f..e3d11c1309f25bcdefce95a73de4a76be745b1a3 100644
--- a/cmd/burrow/commands/start.go
+++ b/cmd/burrow/commands/start.go
@@ -3,10 +3,7 @@ package commands
 import (
 	"context"
 
-	"github.com/hyperledger/burrow/config"
-	"github.com/hyperledger/burrow/config/source"
 	"github.com/hyperledger/burrow/crypto"
-	logging_config "github.com/hyperledger/burrow/logging/config"
 	"github.com/jawher/mow.cli"
 )
 
@@ -47,24 +44,7 @@ func Start(output Output) func(cmd *cli.Cmd) {
 			"[--genesis=<genesis json file>]"
 
 		cmd.Action = func() {
-
-			// We need to reflect on whether this obscures where values are coming from
-			conf := config.DefaultBurrowConfig()
-			// We treat logging a little differently in that if anything is set for logging we will not
-			// set default outputs
-			conf.Logging = nil
-			err := source.EachOf(
-				burrowConfigProvider(*configOpt),
-				source.FirstOf(
-					genesisDocProvider(*genesisOpt, false),
-					// Try working directory
-					genesisDocProvider(config.DefaultGenesisDocJSONFileName, true)),
-			).Apply(conf)
-
-			// If no logging config was provided use the default
-			if conf.Logging == nil {
-				conf.Logging = logging_config.DefaultNodeLoggingConfig()
-			}
+			conf, err := obtainBurrowConfig(*configOpt, *genesisOpt)
 			if err != nil {
 				output.Fatalf("could not obtain config: %v", err)
 			}
diff --git a/cmd/burrow/main.go b/cmd/burrow/main.go
index 28930dee9825417e16a5ef3d5a7af338fe7f4bd4..0bc40136364d7fcf5b66de2c3c394cf43745f2ef 100644
--- a/cmd/burrow/main.go
+++ b/cmd/burrow/main.go
@@ -39,6 +39,9 @@ func burrow(output commands.Output) *cli.Cli {
 	app.Command("keys", "A tool for doing a bunch of cool stuff with keys",
 		commands.Keys(output))
 
+	app.Command("dump", "Dump objects from an offline Burrow .burrow directory",
+		commands.Dump(output))
+
 	return app
 }
 
diff --git a/consensus/tendermint/tendermint.go b/consensus/tendermint/tendermint.go
index a83dbfa97b7b5752112a96263ba584e4b43ecbc6..cfd986ab90af96a6b3c1d68c250e5f63f06ce361 100644
--- a/consensus/tendermint/tendermint.go
+++ b/consensus/tendermint/tendermint.go
@@ -33,9 +33,13 @@ type Node struct {
 	}
 }
 
+func DBProvider(ID string, backendType dbm.DBBackendType, dbDir string) dbm.DB {
+	return dbm.NewDB(ID, backendType, dbDir)
+}
+
 // Since Tendermint doesn't close its DB connections
 func (n *Node) DBProvider(ctx *node.DBContext) (dbm.DB, error) {
-	db := dbm.NewDB(ctx.ID, dbm.DBBackendType(ctx.Config.DBBackend), ctx.Config.DBDir())
+	db := DBProvider(ctx.ID, dbm.DBBackendType(ctx.Config.DBBackend), ctx.Config.DBDir())
 	n.closers = append(n.closers, db)
 	return db, nil
 }
diff --git a/forensics/block.go b/forensics/block.go
new file mode 100644
index 0000000000000000000000000000000000000000..c6055790e3d90eb69a4fee43b31911a78355bb49
--- /dev/null
+++ b/forensics/block.go
@@ -0,0 +1,31 @@
+package forensics
+
+import (
+	"github.com/hyperledger/burrow/txs"
+	"github.com/tendermint/tendermint/types"
+)
+
+type Block struct {
+	txDecoder txs.Decoder
+	*types.Block
+}
+
+func NewBlock(txDecoder txs.Decoder, block *types.Block) *Block {
+	return &Block{
+		txDecoder: txDecoder,
+		Block:     block,
+	}
+}
+
+func (b *Block) Transactions(iter func(txs.Tx) (stop bool)) (stopped bool, err error) {
+	for i := 0; i < len(b.Txs); i++ {
+		tx, err := b.txDecoder.DecodeTx(b.Txs[i])
+		if err != nil {
+			return false, err
+		}
+		if iter(tx) {
+			return true, nil
+		}
+	}
+	return false, nil
+}
diff --git a/forensics/block_explorer.go b/forensics/block_explorer.go
new file mode 100644
index 0000000000000000000000000000000000000000..be8541135ba73253c07f9b830b1266ab570f3b22
--- /dev/null
+++ b/forensics/block_explorer.go
@@ -0,0 +1,63 @@
+package forensics
+
+import (
+	"fmt"
+
+	"github.com/hyperledger/burrow/consensus/tendermint"
+	"github.com/hyperledger/burrow/txs"
+	"github.com/tendermint/tendermint/blockchain"
+	"github.com/tendermint/tmlibs/db"
+)
+
+type BlockExplorer struct {
+	txDecoder txs.Decoder
+	*blockchain.BlockStore
+}
+
+func NewBlockExplorer(dbBackendType db.DBBackendType, dbDir string) *BlockExplorer {
+	return &BlockExplorer{
+		txDecoder:  txs.NewGoWireCodec(),
+		BlockStore: blockchain.NewBlockStore(tendermint.DBProvider("blockstore", dbBackendType, dbDir)),
+	}
+}
+
+func (be *BlockExplorer) Block(height int64) (block *Block, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("could not get block at height %v: %v", height, r)
+		}
+	}()
+
+	tmBlock := be.LoadBlock(height)
+	if tmBlock == nil {
+		return nil, fmt.Errorf("could not pull block at height: %v", height)
+	}
+	return NewBlock(be.txDecoder, tmBlock), nil
+}
+
+// Iterate over blocks between start (inclusive) and end (exclusive)
+func (be *BlockExplorer) Blocks(start, end int64, iter func(*Block) (stop bool)) (stopped bool, err error) {
+	if end > 0 && start >= end {
+		return false, fmt.Errorf("end height must be strictly greater than start height")
+	}
+	if start <= 0 {
+		// From first block
+		start = 1
+	}
+	if end < 0 {
+		// -1 means include the very last block so + 1 for offset
+		end = be.Height() + end + 1
+	}
+
+	for height := start; height <= end; height++ {
+		block, err := be.Block(height)
+		if err != nil {
+			return false, err
+		}
+		if iter(block) {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}