diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index cce770c1f57dd97557249326b4c6482b0ea3402d..54cb4a063928a18413ff964f48c810c97a7fa17a 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -36,6 +36,10 @@
 			"ImportPath": "github.com/ActiveState/tail",
 			"Rev": "068b72961a6bc5b4a82cf4fc14ccc724c0cfa73a"
 		},
+		{
+			"ImportPath":"github.com/whyrusleeping/iptb",
+			"Rev": "5ee5bc0bb43502dfc798786a78df2448c91dd764"
+		},
 		{
 			"ImportPath": "github.com/Sirupsen/logrus",
 			"Comment": "v0.7.1",
diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/iptb/LICENSE b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..1a976e72e7d626d74ed9310e54a4b537d2e8012a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Jeromy Johnson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/iptb/README.md b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b1d176a88729f41289ffb814c7663e5dc971601c
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/README.md
@@ -0,0 +1,20 @@
+#Ipfs Testbed
+
+##commands:
+
+### init -n=[number of nodes]
+creates and initializes 'n' repos
+
+### start 
+starts up all testbed nodes
+
+### stop 
+kills all testbed nodes
+
+### restart
+kills, then restarts all testbed nodes
+
+### shell [n]
+execs your shell with environment variables set as follows:
+- IPFS_PATH - set to testbed node n's IPFS_PATH
+- NODE[x] - set to the peer ID of node x
diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/iptb/main.go b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..46ff882392407b567bbd6a064b8bf3b16848f449
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/whyrusleeping/iptb/main.go
@@ -0,0 +1,403 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	serial "github.com/ipfs/go-ipfs/repo/fsrepo/serialize"
+	"io/ioutil"
+	"log"
+	"net"
+	"os"
+	"os/exec"
+	"path"
+	"strconv"
+	"sync"
+	"syscall"
+	"time"
+)
+
+// GetNumNodes returns the number of testbed nodes configured in the testbed directory
+func GetNumNodes() int {
+	for i := 0; i < 2000; i++ {
+		_, err := os.Stat(IpfsDirN(i))
+		if os.IsNotExist(err) {
+			return i
+		}
+	}
+	panic("i dont know whats going on")
+}
+
+func TestBedDir() string {
+	tbd := os.Getenv("IPTB_ROOT")
+	if len(tbd) != 0 {
+		return tbd
+	}
+
+	home := os.Getenv("HOME")
+	if len(home) == 0 {
+		panic("could not find home")
+	}
+
+	return path.Join(home, "testbed")
+}
+
+func IpfsDirN(n int) string {
+	return path.Join(TestBedDir(), fmt.Sprint(n))
+}
+
+func YesNoPrompt(prompt string) bool {
+	var s string
+	for {
+		fmt.Println(prompt)
+		fmt.Scanf("%s", &s)
+		switch s {
+		case "y", "Y":
+			return true
+		case "n", "N":
+			return false
+		}
+		fmt.Println("Please press either 'y' or 'n'")
+	}
+}
+
+type initCfg struct {
+	Count     int
+	Force     bool
+	Bootstrap string
+}
+
+func IpfsInit(cfg *initCfg) error {
+	p := IpfsDirN(0)
+	if _, err := os.Stat(p); !os.IsNotExist(err) {
+		if !cfg.Force && !YesNoPrompt("testbed nodes already exist, overwrite? [y/n]") {
+			return nil
+		}
+		err := os.RemoveAll(TestBedDir())
+		if err != nil {
+			return err
+		}
+	}
+	wait := sync.WaitGroup{}
+	for i := 0; i < cfg.Count; i++ {
+		wait.Add(1)
+		go func(v int) {
+			defer wait.Done()
+			dir := IpfsDirN(v)
+			err := os.MkdirAll(dir, 0777)
+			if err != nil {
+				log.Println("ERROR: ", err)
+				return
+			}
+
+			cmd := exec.Command("ipfs", "init", "-b=1024")
+			cmd.Env = append(cmd.Env, "IPFS_PATH="+dir)
+			out, err := cmd.CombinedOutput()
+			if err != nil {
+				log.Println("ERROR: ", err)
+				log.Println(string(out))
+			}
+		}(i)
+	}
+	wait.Wait()
+
+	// Now setup bootstrapping
+	switch cfg.Bootstrap {
+	case "star":
+		err := starBootstrap(cfg)
+		if err != nil {
+			return err
+		}
+	case "none":
+		err := clearBootstrapping(cfg)
+		if err != nil {
+			return err
+		}
+	default:
+		return fmt.Errorf("unrecognized bootstrapping option: %s", cfg.Bootstrap)
+	}
+
+	return nil
+}
+
+func starBootstrap(cfg *initCfg) error {
+	// '0' node is the bootstrap node
+	cfgpath := path.Join(IpfsDirN(0), "config")
+	bcfg, err := serial.Load(cfgpath)
+	if err != nil {
+		return err
+	}
+	bcfg.Bootstrap = nil
+	bcfg.Addresses.Swarm = []string{"/ip4/127.0.0.1/tcp/4002"}
+	bcfg.Addresses.API = "/ip4/127.0.0.1/tcp/5002"
+	bcfg.Addresses.Gateway = ""
+	err = serial.WriteConfigFile(cfgpath, bcfg)
+	if err != nil {
+		return err
+	}
+
+	for i := 1; i < cfg.Count; i++ {
+		cfgpath := path.Join(IpfsDirN(i), "config")
+		cfg, err := serial.Load(cfgpath)
+		if err != nil {
+			return err
+		}
+
+		cfg.Bootstrap = []string{fmt.Sprintf("%s/ipfs/%s", bcfg.Addresses.Swarm[0], bcfg.Identity.PeerID)}
+		cfg.Addresses.Gateway = ""
+		cfg.Addresses.Swarm = []string{
+			fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", 4002+i),
+		}
+		cfg.Addresses.API = fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 5002+i)
+		err = serial.WriteConfigFile(cfgpath, cfg)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func clearBootstrapping(cfg *initCfg) error {
+	for i := 0; i < cfg.Count; i++ {
+		cfgpath := path.Join(IpfsDirN(i), "config")
+		cfg, err := serial.Load(cfgpath)
+		if err != nil {
+			return err
+		}
+
+		cfg.Bootstrap = nil
+		cfg.Addresses.Gateway = ""
+		cfg.Addresses.Swarm = []string{
+			fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", 4002+i),
+		}
+		cfg.Addresses.API = fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 5002+i)
+		err = serial.WriteConfigFile(cfgpath, cfg)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func IpfsPidOf(n int) (int, error) {
+	dir := IpfsDirN(n)
+	b, err := ioutil.ReadFile(path.Join(dir, "daemon.pid"))
+	if err != nil {
+		return -1, err
+	}
+
+	return strconv.Atoi(string(b))
+}
+
+func IpfsKill() error {
+	n := GetNumNodes()
+	for i := 0; i < n; i++ {
+		pid, err := IpfsPidOf(i)
+		if err != nil {
+			fmt.Printf("error killing daemon %d: %s\n", i, err)
+			continue
+		}
+
+		p, err := os.FindProcess(pid)
+		if err != nil {
+			fmt.Printf("error killing daemon %d: %s\n", i, err)
+			continue
+		}
+		err = p.Kill()
+		if err != nil {
+			fmt.Printf("error killing daemon %d: %s\n", i, err)
+			continue
+		}
+
+		p.Wait()
+
+		err = os.Remove(path.Join(IpfsDirN(i), "daemon.pid"))
+		if err != nil {
+			fmt.Printf("error removing pid file for daemon %d: %s\n", i, err)
+			continue
+		}
+	}
+	return nil
+}
+
+func IpfsStart(waitall bool) error {
+	n := GetNumNodes()
+	for i := 0; i < n; i++ {
+		dir := IpfsDirN(i)
+		cmd := exec.Command("ipfs", "daemon")
+		cmd.Dir = dir
+		cmd.Env = []string{"IPFS_PATH=" + dir}
+
+		cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
+
+		stdout, err := os.Create(path.Join(dir, "daemon.stdout"))
+		if err != nil {
+			return err
+		}
+
+		stderr, err := os.Create(path.Join(dir, "daemon.stderr"))
+		if err != nil {
+			return err
+		}
+
+		cmd.Stdout = stdout
+		cmd.Stderr = stderr
+
+		err = cmd.Start()
+		if err != nil {
+			return err
+		}
+		pid := cmd.Process.Pid
+
+		fmt.Printf("Started daemon %d, pid = %d\n", i, pid)
+		err = ioutil.WriteFile(path.Join(dir, "daemon.pid"), []byte(fmt.Sprint(pid)), 0666)
+		if err != nil {
+			return err
+		}
+
+		// Make sure node 0 is up before starting the rest so
+		// bootstrapping works properly
+		if i == 0 || waitall {
+			err := waitForLive(fmt.Sprintf("localhost:%d", 5002+i))
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// waitForLive polls the given endpoint until it is up, or until
+// a timeout
+func waitForLive(addr string) error {
+	for i := 0; i < 50; i++ {
+		c, err := net.Dial("tcp", addr)
+		if err == nil {
+			c.Close()
+			return nil
+		}
+		time.Sleep(time.Millisecond * 200)
+	}
+	return fmt.Errorf("node at %s failed to come online in given time period", addr)
+}
+
+// GetPeerID reads the config of node 'n' and returns its peer ID
+func GetPeerID(n int) (string, error) {
+	cfg, err := serial.Load(path.Join(IpfsDirN(n), "config"))
+	if err != nil {
+		return "", err
+	}
+	return cfg.Identity.PeerID, nil
+}
+
+// IpfsShell sets up environment variables for a new shell to more easily
+// control the given daemon
+func IpfsShell(n int) error {
+	shell := os.Getenv("SHELL")
+	if shell == "" {
+		return fmt.Errorf("couldnt find shell!")
+	}
+
+	dir := IpfsDirN(n)
+	nenvs := []string{"IPFS_PATH=" + dir}
+
+	nnodes := GetNumNodes()
+	for i := 0; i < nnodes; i++ {
+		peerid, err := GetPeerID(i)
+		if err != nil {
+			return err
+		}
+		nenvs = append(nenvs, fmt.Sprintf("NODE%d=%s", i, peerid))
+	}
+	nenvs = append(os.Environ(), nenvs...)
+
+	return syscall.Exec(shell, []string{shell}, nenvs)
+}
+
+var helptext = `Ipfs Testbed
+
+Commands:
+	init 
+	    creates and initializes 'n' repos
+
+		Options:
+			-n=[number of nodes]
+			-f - force overwriting of existing nodes
+			-bootstrap - select bootstrapping style for cluster
+				choices: star, none
+
+	start 
+	    starts up all testbed nodes
+
+		Options:
+			-wait - wait until daemons are fully initialized
+	stop 
+	    kills all testbed nodes
+	restart
+	    kills, then restarts all testbed nodes
+
+	shell [n]
+	    execs your shell with environment variables set as follows:
+	        IPFS_PATH - set to testbed node n's IPFS_PATH
+	        NODE[x] - set to the peer ID of node x
+
+Env Vars:
+
+IPTB_ROOT:
+	Used to specify the directory that nodes will be created in.
+`
+
+func handleErr(s string, err error) {
+	if err != nil {
+		fmt.Println(s, err)
+		os.Exit(1)
+	}
+}
+
+func main() {
+	cfg := new(initCfg)
+	flag.IntVar(&cfg.Count, "n", 0, "number of ipfs nodes to initialize")
+	flag.BoolVar(&cfg.Force, "f", false, "force initialization (overwrite existing configs)")
+	flag.StringVar(&cfg.Bootstrap, "bootstrap", "star", "select bootstrapping style for cluster")
+
+	wait := flag.Bool("wait", false, "wait for nodes to come fully online before exiting")
+	flag.Usage = func() {
+		fmt.Println(helptext)
+	}
+
+	flag.Parse()
+
+	switch flag.Arg(0) {
+	case "init":
+		if cfg.Count == 0 {
+			fmt.Printf("please specify number of nodes: '%s -n=10 init'\n", os.Args[0])
+			os.Exit(1)
+		}
+		err := IpfsInit(cfg)
+		handleErr("ipfs init err: ", err)
+	case "start":
+		err := IpfsStart(*wait)
+		handleErr("ipfs start err: ", err)
+	case "stop", "kill":
+		err := IpfsKill()
+		handleErr("ipfs kill err: ", err)
+	case "restart":
+		err := IpfsKill()
+		handleErr("ipfs kill err: ", err)
+
+		err = IpfsStart(*wait)
+		handleErr("ipfs start err: ", err)
+	case "shell":
+		if len(flag.Args()) < 2 {
+			fmt.Println("please specify which node you want a shell for")
+			os.Exit(1)
+		}
+		n, err := strconv.Atoi(flag.Arg(1))
+		handleErr("parse err: ", err)
+
+		err = IpfsShell(n)
+		handleErr("ipfs shell err: ", err)
+	default:
+		flag.Usage()
+		os.Exit(1)
+	}
+}
diff --git a/test/Makefile b/test/Makefile
index 8974c552bb8f7c0825709e2683f89593dae64fd3..6f86b12327d06d41099d2da1b3693d8488503d58 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -4,6 +4,7 @@ IPFS_ROOT = ../
 IPFS_CMD = ../cmd/ipfs
 RANDOM_SRC = ../Godeps/_workspace/src/github.com/jbenet/go-random
 MULTIHASH_SRC = ../Godeps/_workspace/src/github.com/jbenet/go-multihash
+IPTB_SRC = ../Godeps/_workspace/src/github.com/whyrusleeping/iptb
 POLLENDPOINT_SRC= ../thirdparty/pollEndpoint
 
 # User might want to override those on the command line
@@ -36,6 +37,10 @@ bin/pollEndpoint: $(call find_go_files, $(POLLENDPOINT_SRC)) IPFS-BUILD-OPTIONS
 	@echo "*** installing $@ ***"
 	go build $(GOFLAGS) -o bin/pollEndpoint $(POLLENDPOINT_SRC)
 
+bin/iptb: $(call find_go_files, $(IPTB_SRC)) IPFS-BUILD-OPTIONS
+	@echo "*** installing $@ ***"
+	go build $(GOFLAGS) -o bin/iptb $(IPTB_SRC)
+
 test: test_expensive
 
 test_expensive:
diff --git a/test/sharness/Makefile b/test/sharness/Makefile
index bc07c0170d9b179806c9f662cad9ac226827bd02..f21e02971c207903805b36cc5d3548a5cb26834c 100644
--- a/test/sharness/Makefile
+++ b/test/sharness/Makefile
@@ -7,7 +7,8 @@
 # NOTE: run with TEST_VERBOSE=1 for verbose sharness tests.
 
 T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))
-BINS = bin/random bin/multihash bin/ipfs bin/pollEndpoint
+BINS = bin/random bin/multihash bin/ipfs bin/pollEndpoint \
+	   bin/iptb
 SHARNESS = lib/sharness/sharness.sh
 IPFS_ROOT = ../..
 
diff --git a/test/sharness/t0130-multinode.sh b/test/sharness/t0130-multinode.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0fb96c6cc2822fa25d96d0d44864b1e04d2a5fce
--- /dev/null
+++ b/test/sharness/t0130-multinode.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+#
+# Copyright (c) 2015 Jeromy Johnson
+# MIT Licensed; see the LICENSE file in this repository.
+#
+
+test_description="Test multiple ipfs nodes"
+
+. lib/test-lib.sh
+
+export IPTB_ROOT="`pwd`/.iptb"
+
+test_expect_success "set up a few nodes" '
+	iptb -n=3 init &&
+	iptb -wait start
+'
+
+test_expect_success "add a file on node1" '
+	export IPFS_PATH="$IPTB_ROOT/1"
+	random 1000000 > filea &&
+	FILEA_HASH=`ipfs add -q filea`
+'
+
+test_expect_success "cat that file on node2" '
+	export IPFS_PATH="$IPTB_ROOT/2"
+	ipfs cat $FILEA_HASH | multihash > actual1
+'
+
+test_expect_success "verify files match" '
+	multihash filea > expected1 &&
+	test_cmp actual1 expected1
+'
+
+test_expect_success "shut down nodes" '
+	iptb stop
+'
+
+test_done