{go/src/veyron2/storage,go/src/veyron2/services/store,go/src/veyron/services/store}: new transaction API

Primary description of new API:
https://docs.google.com/a/google.com/document/d/1Zob450G5rQDHmWXmg6UvVy3IH-hSiS8eE5L9IY62sW0/edit

Change-Id: I7272d389414ec27db8d599f0d562938a55e43a97
diff --git a/examples/bank/pbankd/main.go b/examples/bank/pbankd/main.go
index 07d0e4f..76acc17 100644
--- a/examples/bank/pbankd/main.go
+++ b/examples/bank/pbankd/main.go
@@ -26,11 +26,11 @@
 
 	"veyron2"
 	"veyron2/ipc"
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/security"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 )
 
@@ -77,8 +77,8 @@
 	// Pointer to the store
 	store storage.Store
 
-	// Current Transaction
-	transaction storage.Transaction
+	// Current Transaction name; empty if there's no transaction
+	tname string
 
 	// The bank's private ID (used for blessing)
 	ID security.PrivateID
@@ -95,7 +95,11 @@
 
 // InitializeBank bank details in the store; currently only initializes the root.
 func (b *pbankd) initializeBank() {
-	b.newTransaction()
+	if err := b.newTransaction(); err != nil {
+		vlog.Fatal(err)
+	}
+	// NOTE(sadovsky): initializeBankRoot ought to return an error. Currently,
+	// some errors (e.g. failed puts) could slip through unnoticed.
 	b.initializeBankRoot()
 	if err := b.commit(); err != nil {
 		vlog.Fatal(err)
@@ -110,11 +114,11 @@
 	for i, _ := range l {
 		fmt.Println(i)
 		prefix := filepath.Join(l[:i]...)
-		o := b.store.Bind(prefix)
-		if exist, err := o.Exists(runtime.TODOContext(), b.transaction); err != nil {
+		o := b.store.BindObject(naming.Join(b.tname, prefix))
+		if exist, err := o.Exists(runtime.TODOContext()); err != nil {
 			vlog.Infof("Error checking existence at %q: %s", prefix, err)
 		} else if !exist {
-			if _, err := o.Put(runtime.TODOContext(), b.transaction, &schema.Dir{}); err != nil {
+			if _, err := o.Put(runtime.TODOContext(), &schema.Dir{}); err != nil {
 				vlog.Infof("Error creating parent %q: %s", prefix, err)
 			}
 			fmt.Printf("%q was created!\n", prefix)
@@ -124,11 +128,11 @@
 	}
 
 	// Add the bank schema to the store at BANK_ROOT, if necessary
-	o := b.store.Bind(BANK_ROOT)
-	if exist, err := o.Exists(runtime.TODOContext(), b.transaction); err != nil {
+	o := b.store.BindObject(naming.Join(b.tname, BANK_ROOT))
+	if exist, err := o.Exists(runtime.TODOContext()); err != nil {
 		vlog.Infof("Error checking existence at %q: %s", BANK_ROOT, err)
 	} else if !exist {
-		_, err := o.Put(runtime.TODOContext(), b.transaction, &schema.Bank{})
+		_, err := o.Put(runtime.TODOContext(), &schema.Bank{})
 		if err != nil {
 			vlog.Infof("Error creating bank at %q: %s", BANK_ROOT, err)
 		}
@@ -146,7 +150,9 @@
 	} else {
 		fmt.Println("This client isn't blessed. Let's bless them!")
 		// Use the store
-		b.newTransaction()
+		if err := b.newTransaction(); err != nil {
+			vlog.Fatal(err)
+		}
 
 		// Keep rolling until we get an unseen number
 		randID := rand.Int63n(MAX_ACCOUNT_NUMBER-MIN_ACCOUNT_NUMBER) + MIN_ACCOUNT_NUMBER
@@ -194,7 +200,9 @@
 	if user == 0 {
 		return fmt.Errorf("couldn't retrieve account number")
 	}
-	b.newTransaction()
+	if err := b.newTransaction(); err != nil {
+		return err
+	}
 	if !b.isUser(user) {
 		return fmt.Errorf("user isn't registered")
 	} else if amount < 0 {
@@ -210,7 +218,9 @@
 	if user == 0 {
 		return fmt.Errorf("couldn't retrieve account number")
 	}
-	b.newTransaction()
+	if err := b.newTransaction(); err != nil {
+		return err
+	}
 	if !b.isUser(user) {
 		return fmt.Errorf("user isn't registered")
 	} else if amount < 0 {
@@ -228,7 +238,9 @@
 	if user == 0 {
 		return fmt.Errorf("couldn't retrieve account number")
 	}
-	b.newTransaction()
+	if err := b.newTransaction(); err != nil {
+		return err
+	}
 	if !b.isUser(user) {
 		return fmt.Errorf("user isn't registered")
 	} else if !b.isUser(accountNumber) {
@@ -261,14 +273,23 @@
 */
 
 // newTransaction starts a new transaction.
-func (b *pbankd) newTransaction() {
-	b.transaction = primitives.NewTransaction(runtime.TODOContext())
+func (b *pbankd) newTransaction() error {
+	tid, err := b.store.BindTransactionRoot("").CreateTransaction(runtime.TODOContext())
+	if err != nil {
+		b.tname = ""
+		return err
+	}
+	b.tname = tid // Transaction is rooted at "", so tname == tid.
+	return nil
 }
 
 // commit commits the current transaction.
 func (b *pbankd) commit() error {
-	err := b.transaction.Commit(runtime.TODOContext())
-	b.transaction = nil
+	if b.tname == "" {
+		return errors.New("No transaction to commit")
+	}
+	err := b.store.BindTransaction(b.tname).Commit(runtime.TODOContext())
+	b.tname = ""
 	if err != nil {
 		return fmt.Errorf("Failed to commit transaction: %s", err)
 	}
@@ -279,8 +300,8 @@
 func (b *pbankd) isUser(accountNumber int64) bool {
 	// If this is a user, their location in the store should exist.
 	prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", accountNumber))
-	o := b.store.Bind(prefix)
-	exist, err := o.Exists(runtime.TODOContext(), b.transaction)
+	o := b.store.BindObject(naming.Join(b.tname, prefix))
+	exist, err := o.Exists(runtime.TODOContext())
 	if err != nil {
 		vlog.Infof("Error checking existence at %s: %s", prefix, err)
 		return false
@@ -319,8 +340,8 @@
 func (b *pbankd) registerNewUser(user int64) {
 	// Create the user's account
 	prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
-	o := b.store.Bind(prefix)
-	if _, err := o.Put(runtime.TODOContext(), b.transaction, int64(0)); err != nil {
+	o := b.store.BindObject(naming.Join(b.tname, prefix))
+	if _, err := o.Put(runtime.TODOContext(), int64(0)); err != nil {
 		vlog.Infof("Error creating %s: %s", prefix, err)
 	}
 }
@@ -328,8 +349,8 @@
 // checkBalance gets the user's balance from the store
 func (b *pbankd) checkBalance(user int64) int64 {
 	prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
-	o := b.store.Bind(prefix)
-	e, err := o.Get(runtime.TODOContext(), b.transaction)
+	o := b.store.BindObject(naming.Join(b.tname, prefix))
+	e, err := o.Get(runtime.TODOContext())
 	if err != nil {
 		vlog.Infof("Error getting %s: %s", prefix, err)
 	}
@@ -340,12 +361,12 @@
 // changeBalance modifies the user's balance in the store
 func (b *pbankd) changeBalance(user int64, amount int64) {
 	prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
-	o := b.store.Bind(prefix)
-	e, err := o.Get(runtime.TODOContext(), b.transaction)
+	o := b.store.BindObject(naming.Join(b.tname, prefix))
+	e, err := o.Get(runtime.TODOContext())
 	if err != nil {
 		vlog.Infof("Error getting %s: %s", prefix, err)
 	}
-	if _, err := o.Put(runtime.TODOContext(), b.transaction, e.Value.(int64)+amount); err != nil {
+	if _, err := o.Put(runtime.TODOContext(), e.Value.(int64)+amount); err != nil {
 		vlog.Infof("Error changing %s: %s", prefix, err)
 	}
 }
diff --git a/examples/boxes/android/src/boxesp2p/main.go b/examples/boxes/android/src/boxesp2p/main.go
index 67e7475..7580fff 100644
--- a/examples/boxes/android/src/boxesp2p/main.go
+++ b/examples/boxes/android/src/boxesp2p/main.go
@@ -80,7 +80,6 @@
 	iwatch "veyron2/services/watch"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vom"
 )
 
@@ -195,7 +194,8 @@
 	if err != nil {
 		panic(fmt.Errorf("Failed to init veyron store:%v", err))
 	}
-	root := vst.Bind("/")
+	rootName := "/"
+	root := vst.BindObject(rootName)
 
 	// Watch for any box updates from the store
 	go func() {
@@ -224,21 +224,29 @@
 	}()
 
 	// Send any box updates to the store
-	tr := primitives.NewTransaction(ctx)
-	if _, err := root.Put(ctx, tr, ""); err != nil {
-		panic(fmt.Errorf("Put for %s failed:%v", root, err))
+	tid, err := vst.BindTransactionRoot(rootName).CreateTransaction(ctx)
+	if err != nil {
+		panic(fmt.Errorf("CreateTransaction for %s failed:%v", rootName, err))
 	}
-	if err := tr.Commit(ctx); err != nil {
+	tname := naming.Join(rootName, tid)
+	if _, err := vst.BindObject(tname).Put(ctx, ""); err != nil {
+		panic(fmt.Errorf("Put for %s failed:%v", tname, err))
+	}
+	if err := vst.BindTransaction(tname).Commit(ctx); err != nil {
 		panic(fmt.Errorf("Commit transaction failed:%v", err))
 	}
 	for {
 		box := <-gs.boxList
-		boxes := vst.Bind("/" + box.BoxId)
-		tr = primitives.NewTransaction(ctx)
-		if _, err := boxes.Put(ctx, tr, box); err != nil {
-			panic(fmt.Errorf("Put for %s failed:%v", boxes, err))
+		boxesName := "/" + box.BoxId
+		tid, err = vst.BindTransactionRoot(boxesName).CreateTransaction(ctx)
+		if err != nil {
+			panic(fmt.Errorf("CreateTransaction for %s failed:%v", boxesName, err))
 		}
-		if err := tr.Commit(ctx); err != nil {
+		tname := naming.Join(boxesName, tid)
+		if _, err := vst.BindObject(tname).Put(ctx, box); err != nil {
+			panic(fmt.Errorf("Put for %s failed:%v", tname, err))
+		}
+		if err := vst.BindTransaction(tname).Commit(ctx); err != nil {
 			panic(fmt.Errorf("Commit transaction failed:%v", err))
 		}
 	}
diff --git a/examples/stfortune/stfortune/main.go b/examples/stfortune/stfortune/main.go
index 1de9047..500b27d 100644
--- a/examples/stfortune/stfortune/main.go
+++ b/examples/stfortune/stfortune/main.go
@@ -24,7 +24,6 @@
 	iwatch "veyron2/services/watch"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vom"
 )
 
@@ -58,7 +57,7 @@
 	paths := []string{appPath, fortunePath(""), userPath("")}
 	for _, path := range paths {
 		req := iwatch.GlobRequest{Pattern: ""}
-		stream, err := store.Bind(path).WatchGlob(ctx, req)
+		stream, err := store.BindObject(path).WatchGlob(ctx, req)
 		if err != nil {
 			log.Fatalf("WatchGlob %s failed: %v", path, err)
 		}
@@ -89,7 +88,7 @@
 	fmt.Printf("Running as a Watcher monitoring new fortunes under %s...\n", path)
 
 	req := iwatch.GlobRequest{Pattern: "*"}
-	stream, err := store.Bind(path).WatchGlob(ctx, req)
+	stream, err := store.BindObject(path).WatchGlob(ctx, req)
 	if err != nil {
 		log.Fatalf("watcher WatchGlob %s failed: %v", path, err)
 	}
@@ -123,27 +122,32 @@
 // pickFortuneGlob uses Glob to find all available fortunes under the input
 // path and then it chooses one randomly.
 func pickFortuneGlob(store storage.Store, ctx context.T, path string) (string, error) {
-	tr := primitives.NewTransaction(ctx)
-	defer tr.Abort(ctx)
-
-	results, err := store.Bind(path).GlobT(ctx, tr, "*")
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := store.BindTransactionRoot("").CreateTransaction(ctx)
 	if err != nil {
 		return "", err
 	}
+
+	// This transaction is read-only, so we always abort it at the end.
+	defer store.BindTransaction(tname).Abort(ctx)
+
+	trPath := func(path string) string {
+		return naming.Join(tname, path)
+	}
+
+	results := store.BindObject(trPath(path)).Glob(ctx, "*")
 	var names []string
 	for results.Advance() {
-		name := results.Value()
-		names = append(names, name)
+		names = append(names, results.Value())
 	}
-	results.Finish()
-	if names == nil || len(names) < 1 {
-		return "", nil
+	if err := results.Err(); err != nil || len(names) == 0 {
+		return "", err
 	}
 
 	// Get a random fortune using the glob results.
 	random := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
 	p := fortunePath(names[random.Intn(len(names))])
-	f, err := store.Bind(p).Get(ctx, tr)
+	f, err := store.BindObject(trPath(p)).Get(ctx)
 	if err != nil {
 		return "", err
 	}
@@ -157,7 +161,7 @@
 // pickFortuneQuery uses a query to find all available fortunes under the input
 // path and choose one randomly.
 func pickFortuneQuery(store storage.Store, ctx context.T, path string) (string, error) {
-	results := store.Bind(path).Query(ctx, nil,
+	results := store.BindObject(path).Query(ctx,
 		query.Query{
 			"* |" + // Inspect all children of path.
 				"type FortuneData |" + // Include only objects of type FortuneData.
@@ -208,17 +212,27 @@
 // user is created.
 func addFortune(store storage.Store, fortune string, userName string) error {
 	ctx := rt.R().NewContext()
-	tr := primitives.NewTransaction(ctx)
+
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := store.BindTransactionRoot("").CreateTransaction(ctx)
+	if err != nil {
+		return err
+	}
+
 	committed := false
 	defer func() {
 		if !committed {
-			tr.Abort(ctx)
+			store.BindTransaction(tname).Abort(ctx)
 		}
 	}()
 
+	trPath := func(path string) string {
+		return naming.Join(tname, path)
+	}
+
 	// Check if this fortune already exists. If yes, return.
 	hash := getMD5Hash(naming.Join(fortune, userName))
-	exists, err := store.Bind(fortunePath(hash)).Exists(ctx, tr)
+	exists, err := store.BindObject(trPath(fortunePath(hash))).Exists(ctx)
 	if err != nil {
 		return err
 	}
@@ -227,21 +241,21 @@
 	}
 
 	// Check if the UserName exists. If yes, get its OID. If not, create a new user.
-	o := store.Bind(userPath(userName))
-	exists, err = o.Exists(ctx, tr)
+	o := store.BindObject(trPath(userPath(userName)))
+	exists, err = o.Exists(ctx)
 	if err != nil {
 		return err
 	}
 	var userid storage.ID
 	if !exists {
 		u := schema.User{Name: userName}
-		stat, err := o.Put(ctx, tr, u)
+		stat, err := o.Put(ctx, u)
 		if err != nil {
 			return err
 		}
 		userid = stat.ID
 	} else {
-		u, err := o.Get(ctx, tr)
+		u, err := o.Get(ctx)
 		if err != nil {
 			return err
 		}
@@ -250,14 +264,14 @@
 
 	// Create a new fortune entry.
 	f := schema.FortuneData{Fortune: fortune, UserName: userid}
-	s, err := store.Bind(fortunePath(hash)).Put(ctx, tr, f)
+	s, err := store.BindObject(trPath(fortunePath(hash))).Put(ctx, f)
 	if err != nil {
 		return err
 	}
 
 	// Link the new fortune to UserName.
 	p := userPath(naming.Join(userName, hash))
-	if _, err = store.Bind(p).Put(ctx, tr, s.ID); err != nil {
+	if _, err = store.BindObject(trPath(p)).Put(ctx, s.ID); err != nil {
 		return err
 	}
 
@@ -267,7 +281,7 @@
 	// locking. When the error for this scenario is
 	// exposed via the Commit API, one could retry the
 	// transaction.
-	if err := tr.Commit(ctx); err != nil {
+	if err := store.BindTransaction(tname).Commit(ctx); err != nil {
 		return err
 	}
 	committed = true
diff --git a/examples/storage/mdb/mdb_init/main.go b/examples/storage/mdb/mdb_init/main.go
index 4cc4a83..28f8166 100644
--- a/examples/storage/mdb/mdb_init/main.go
+++ b/examples/storage/mdb/mdb_init/main.go
@@ -1,5 +1,6 @@
 // mdb_init is a tool to initialize the store with an initial database.  This is
-// really for demo purposes; in a real database, the contents would be persistant.
+// really for demo purposes; in a real database, the contents would be
+// persistent.
 //
 // The contents are loaded from JSON format.  See mdb/templates/contents.json
 // for the actual input.
@@ -22,10 +23,10 @@
 	"time"
 
 	"veyron/examples/storage/mdb/schema"
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 )
 
@@ -81,9 +82,9 @@
 
 // state is the initial store state.
 type state struct {
-	store       storage.Store
-	transaction storage.Transaction
-	idTable     map[string]*value
+	store   storage.Store
+	tname   string // Current transaction name; empty if there's no transaction.
+	idTable map[string]*value
 }
 
 // value holds the ID and name of a stored value.
@@ -148,7 +149,7 @@
 func (st *state) put(path string, v interface{}) {
 	vlog.Infof("Storing %q = %+v", path, v)
 	st.makeParentDirs(path)
-	if _, err := st.store.Bind(path).Put(rt.R().TODOContext(), st.transaction, v); err != nil {
+	if _, err := st.store.BindObject(naming.Join(st.tname, path)).Put(rt.R().TODOContext(), v); err != nil {
 		vlog.Infof("put failed: %s: %s", path, err)
 		return
 	}
@@ -159,7 +160,7 @@
 func (st *state) putNamed(name, path string, v interface{}) {
 	vlog.Infof("Storing %s: %q = %+v", name, path, v)
 	st.makeParentDirs(path)
-	s, err := st.store.Bind(path).Put(rt.R().TODOContext(), st.transaction, v)
+	s, err := st.store.BindObject(naming.Join(st.tname, path)).Put(rt.R().TODOContext(), v)
 	if err != nil {
 		vlog.Infof("Put failed: %s: %s", path, err)
 		return
@@ -173,11 +174,11 @@
 	l := strings.Split(path, "/")
 	for i, _ := range l {
 		prefix := filepath.Join(l[:i]...)
-		o := st.store.Bind(prefix)
-		if exist, err := o.Exists(rt.R().TODOContext(), st.transaction); err != nil {
+		o := st.store.BindObject(naming.Join(st.tname, prefix))
+		if exist, err := o.Exists(rt.R().TODOContext()); err != nil {
 			vlog.Infof("Error checking existence at %q: %s", prefix, err)
 		} else if !exist {
-			if _, err := o.Put(rt.R().TODOContext(), st.transaction, &schema.Dir{}); err != nil {
+			if _, err := o.Put(rt.R().TODOContext(), &schema.Dir{}); err != nil {
 				vlog.Infof("Error creating parent %q: %s", prefix, err)
 			}
 		}
@@ -186,15 +187,23 @@
 
 // newTransaction starts a new transaction.
 func (st *state) newTransaction() {
-	st.transaction = primitives.NewTransaction(rt.R().TODOContext())
+	tid, err := st.store.BindTransactionRoot("").CreateTransaction(rt.R().TODOContext())
+	if err != nil {
+		vlog.Fatalf("Failed to create transaction: %s", err)
+	}
+	st.tname = tid // Transaction is rooted at "", so tname == tid.
 }
 
 // commit commits the current transaction.
 func (st *state) commit() {
-	if err := st.transaction.Commit(rt.R().TODOContext()); err != nil {
-		vlog.Infof("Failed to commit transaction: %s", err)
+	if st.tname == "" {
+		vlog.Fatalf("No transaction to commit")
 	}
-	st.transaction = nil
+	err := st.store.BindTransaction(st.tname).Commit(rt.R().TODOContext())
+	st.tname = ""
+	if err != nil {
+		vlog.Errorf("Failed to commit transaction: %s", err)
+	}
 }
 
 // storeContents saves each of the values in the Contents to the store.
diff --git a/examples/storage/viewer/value.go b/examples/storage/viewer/value.go
index d3cb79d..279df7a 100644
--- a/examples/storage/viewer/value.go
+++ b/examples/storage/viewer/value.go
@@ -21,15 +21,13 @@
 
 // glob performs a glob expansion of the pattern.  The results are sorted.
 func glob(st storage.Store, path, pattern string) ([]string, error) {
-	results, err := st.Bind(path).GlobT(rt.R().TODOContext(), nil, pattern)
-	if err != nil {
-		return nil, err
-	}
-	defer results.Finish()
+	results := st.BindObject(path).Glob(rt.R().TODOContext(), pattern)
 	names := []string{}
 	for results.Advance() {
-		name := results.Value()
-		names = append(names, "/"+name)
+		names = append(names, "/"+results.Value())
+	}
+	if err := results.Err(); err != nil {
+		return nil, err
 	}
 	sort.Strings(names)
 	return names, nil
@@ -68,7 +66,7 @@
 // exist.
 func (v *Value) Get(path string) interface{} {
 	path = v.fullpath(path)
-	e, err := v.store.Bind(path).Get(rt.R().TODOContext(), nil)
+	e, err := v.store.BindObject(path).Get(rt.R().TODOContext())
 	if err != nil {
 		return nil
 	}
diff --git a/examples/storage/viewer/viewer.go b/examples/storage/viewer/viewer.go
index b97d499..f7cee5b 100644
--- a/examples/storage/viewer/viewer.go
+++ b/examples/storage/viewer/viewer.go
@@ -73,7 +73,7 @@
 // is based on the type of the value, under /template/<pkgPath>/<typeName>.
 func (s *server) loadTemplate(v interface{}) *template.Template {
 	path := templatePath(v)
-	e, err := s.store.Bind(path).Get(rt.R().TODOContext(), nil)
+	e, err := s.store.BindObject(path).Get(rt.R().TODOContext())
 	if err != nil {
 		return nil
 	}
@@ -125,7 +125,7 @@
 // ServeHTTP is the main HTTP handler.
 func (s *server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	path := req.URL.Path
-	e, err := s.store.Bind(path).Get(rt.R().TODOContext(), nil)
+	e, err := s.store.BindObject(path).Get(rt.R().TODOContext())
 	if err != nil {
 		msg := fmt.Sprintf("<html><body><h1>%s</h1><h2>Error: %s</h2></body></html>",
 			html.EscapeString(path),
diff --git a/examples/todos/test/store_test.go b/examples/todos/test/store_test.go
index f1736cf..136c025 100644
--- a/examples/todos/test/store_test.go
+++ b/examples/todos/test/store_test.go
@@ -20,9 +20,9 @@
 
 	bb "veyron/lib/testutil/blackbox"
 
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/storage"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vom"
 )
 
@@ -95,9 +95,9 @@
 ////////////////////////////////////////////////////////////////////////////////
 // Type-specific helpers
 
-func getList(t *testing.T, st storage.Store, tr storage.Transaction, path string) *List {
+func getList(t *testing.T, st storage.Store, path string) *List {
 	_, file, line, _ := runtime.Caller(1)
-	v := get(t, st, tr, path)
+	v := get(t, st, path)
 	res, ok := v.(*List)
 	if !ok {
 		t.Fatalf("%s(%d): %s: not a List: %v", file, line, path, v)
@@ -105,9 +105,9 @@
 	return res
 }
 
-func getTodo(t *testing.T, st storage.Store, tr storage.Transaction, path string) *Todo {
+func getTodo(t *testing.T, st storage.Store, path string) *Todo {
 	_, file, line, _ := runtime.Caller(1)
-	v := get(t, st, tr, path)
+	v := get(t, st, path)
 	res, ok := v.(*Todo)
 	if !ok {
 		t.Fatalf("%s(%d): %s: not a Todo: %v", file, line, path, v)
@@ -120,42 +120,41 @@
 
 func testTodos(t *testing.T, st storage.Store) {
 	ctx := rt.R().NewContext()
+
 	// Create lists.
 	{
 		// NOTE(sadovsky): Currently, we can't put /x/y until we put / and /x.
-		tr := primitives.NewTransaction(ctx)
-		put(t, st, tr, "/", newDir())
-		put(t, st, tr, "/lists", newDir())
-		put(t, st, tr, "/lists/drinks", newList())
-		put(t, st, tr, "/lists/snacks", newList())
-		commit(t, tr)
+		tname := createTransaction(t, st, "")
+		put(t, st, tname, newDir())
+		put(t, st, naming.Join(tname, "lists"), newDir())
+		put(t, st, naming.Join(tname, "lists/drinks"), newList())
+		put(t, st, naming.Join(tname, "lists/snacks"), newList())
+		commit(t, st, tname)
 	}
 
 	// Add some todos.
 	{
-		tr := primitives.NewTransaction(ctx)
+		tname := createTransaction(t, st, "")
 		// NOTE(sadovsky): It feels awkward to create my own names (ids) for these
 		// Todo objects. I'd like some way to create them in some "directory"
 		// without explicitly naming them. I.e. in this case I want to think of the
 		// directory as a list, not a map.
-		put(t, st, tr, "/lists/drinks/Todos/@", newTodo("milk"))
-		put(t, st, tr, "/lists/drinks/Todos/@", newTodo("beer"))
-		put(t, st, tr, "/lists/snacks/Todos/@", newTodo("chips"))
-		commit(t, tr)
+		put(t, st, naming.Join(tname, "lists/drinks/Todos/@"), newTodo("milk"))
+		put(t, st, naming.Join(tname, "lists/drinks/Todos/@"), newTodo("beer"))
+		put(t, st, naming.Join(tname, "lists/snacks/Todos/@"), newTodo("chips"))
+		commit(t, st, tname)
 	}
 
 	// Verify some of the photos.
 	{
-		tr := primitives.NewTransaction(ctx)
-		todo := getTodo(t, st, tr, "/lists/drinks/Todos/0")
+		todo := getTodo(t, st, "/lists/drinks/Todos/0")
 		if todo.Text != "milk" {
 			t.Errorf("Expected %q, got %q", "milk", todo.Text)
 		}
 	}
 
 	{
-		tr := primitives.NewTransaction(ctx)
-		todo := getTodo(t, st, tr, "/lists/snacks/Todos/0")
+		todo := getTodo(t, st, "/lists/snacks/Todos/0")
 		if todo.Text != "chips" {
 			t.Errorf("Expected %q, got %q", "chips", todo.Text)
 		}
@@ -163,27 +162,24 @@
 
 	// Move a todo item from one list to another.
 	{
-		tr := primitives.NewTransaction(ctx)
-		todo := getTodo(t, st, tr, "/lists/drinks/Todos/1")
+		tname := createTransaction(t, st, "")
+		todo := getTodo(t, st, naming.Join(tname, "lists/drinks/Todos/1"))
 		// NOTE(sadovsky): Remove works for map entries, but not yet for slices.
 		// Instead, we read the list, prune it, and write it back.
 		//remove(t, st, tr, "/lists/drinks/Todos/1")
-		list := getList(t, st, tr, "/lists/drinks")
+		list := getList(t, st, naming.Join(tname, "lists/drinks"))
 		list.Todos = list.Todos[:1]
-		put(t, st, tr, "lists/drinks", list)
-		put(t, st, tr, "/lists/snacks/Todos/@", todo)
-		commit(t, tr)
+		put(t, st, naming.Join(tname, "lists/drinks"), list)
+		put(t, st, naming.Join(tname, "lists/snacks/Todos/@"), todo)
+		commit(t, st, tname)
 	}
 
 	// Verify that the original todo is no longer there.
 	// TODO(sadovsky): Use queries to verify that both lists have changed.
 	{
-		tr := primitives.NewTransaction(ctx)
-		// Note, this will be much prettier in veyron2.
-		_, file, line, _ := runtime.Caller(1)
 		path := "/lists/drinks/1"
-		if _, err := st.Bind(path).Get(ctx, tr); err == nil {
-			t.Fatalf("%s(%d): got removed object %s", file, line, path)
+		if _, err := st.BindObject(path).Get(ctx); err == nil {
+			t.Fatalf("Got removed object: %s", path)
 		}
 	}
 }
diff --git a/examples/todos/test/store_util.go b/examples/todos/test/store_util.go
index aff92b3..e08f711 100644
--- a/examples/todos/test/store_util.go
+++ b/examples/todos/test/store_util.go
@@ -4,37 +4,48 @@
 	"runtime"
 	"testing"
 
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/storage"
 )
 
-func get(t *testing.T, st storage.Store, tr storage.Transaction, path string) interface{} {
+func createTransaction(t *testing.T, st storage.Store, name string) string {
 	_, file, line, _ := runtime.Caller(1)
-	e, err := st.Bind(path).Get(rt.R().NewContext(), tr)
+	tid, err := st.BindTransactionRoot(name).CreateTransaction(rt.R().NewContext())
 	if err != nil {
-		t.Fatalf("%s(%d): can't get %s: %s", file, line, path, err)
+		t.Fatalf("%s(%d): can't create transaction %s: %s", file, line, name, err)
+	}
+	return naming.Join(name, tid)
+}
+
+func get(t *testing.T, st storage.Store, name string) interface{} {
+	_, file, line, _ := runtime.Caller(1)
+	e, err := st.BindObject(name).Get(rt.R().NewContext())
+	if err != nil {
+		t.Fatalf("%s(%d): can't get %s: %s", file, line, name, err)
 	}
 	return e.Value
 }
 
-func put(t *testing.T, st storage.Store, tr storage.Transaction, path string, v interface{}) storage.ID {
+func put(t *testing.T, st storage.Store, name string, v interface{}) storage.ID {
 	_, file, line, _ := runtime.Caller(1)
-	stat, err := st.Bind(path).Put(rt.R().NewContext(), tr, v)
+	stat, err := st.BindObject(name).Put(rt.R().NewContext(), v)
 	if err != nil || !stat.ID.IsValid() {
-		t.Fatalf("%s(%d): can't put %s: %s", file, line, path, err)
+		t.Fatalf("%s(%d): can't put %s: %s", file, line, name, err)
 	}
 	return stat.ID
 }
 
-func remove(t *testing.T, st storage.Store, tr storage.Transaction, path string) {
-	if err := st.Bind(path).Remove(rt.R().NewContext(), tr); err != nil {
-		_, file, line, _ := runtime.Caller(1)
-		t.Errorf("%s(%d): can't remove %s: %s", file, line, path, err)
+func remove(t *testing.T, st storage.Store, name string) {
+	_, file, line, _ := runtime.Caller(1)
+	if err := st.BindObject(name).Remove(rt.R().NewContext()); err != nil {
+		t.Errorf("%s(%d): can't remove %s: %s", file, line, name, err)
 	}
 }
 
-func commit(t *testing.T, tr storage.Transaction) {
-	if err := tr.Commit(rt.R().NewContext()); err != nil {
-		t.Fatalf("Transaction aborted: %s", err)
+func commit(t *testing.T, st storage.Store, name string) {
+	_, file, line, _ := runtime.Caller(1)
+	if err := st.BindTransaction(name).Commit(rt.R().NewContext()); err != nil {
+		t.Fatalf("%s(%d): commit failed: %s", file, line, err)
 	}
 }
diff --git a/examples/todos/todos_init/main.go b/examples/todos/todos_init/main.go
index 44db7c2..7013a0c 100644
--- a/examples/todos/todos_init/main.go
+++ b/examples/todos/todos_init/main.go
@@ -1,6 +1,6 @@
 // todos_init is a tool to initialize the store with an initial database. This
 // is really for demo purposes; in a real database, the contents would be
-// persistant.
+// persistent.
 //
 // The data is loaded from a JSON file, todos_init/data.json.
 //
@@ -20,11 +20,11 @@
 
 	"veyron/examples/todos/schema"
 	"veyron2"
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/security"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 )
 
@@ -60,8 +60,8 @@
 
 // state is the initial store state.
 type state struct {
-	store       storage.Store
-	transaction storage.Transaction
+	store storage.Store
+	tname string // Current transaction name; nil if there's no transaction.
 }
 
 // newState returns a fresh state.
@@ -74,7 +74,7 @@
 func (st *state) put(path string, v interface{}) {
 	vlog.Infof("Storing %q = %+v", path, v)
 	st.makeParentDirs(path)
-	if _, err := st.store.Bind(path).Put(rt.R().TODOContext(), st.transaction, v); err != nil {
+	if _, err := st.store.BindObject(naming.Join(st.tname, path)).Put(rt.R().TODOContext(), v); err != nil {
 		vlog.Errorf("put failed: %s: %s", path, err)
 		return
 	}
@@ -86,9 +86,9 @@
 	l := strings.Split(path, "/")
 	for i, _ := range l {
 		prefix := filepath.Join(l[:i]...)
-		o := st.store.Bind(prefix)
-		if _, err := o.Get(rt.R().TODOContext(), st.transaction); err != nil {
-			if _, err := o.Put(rt.R().TODOContext(), st.transaction, &schema.Dir{}); err != nil {
+		o := st.store.BindObject(naming.Join(st.tname, prefix))
+		if _, err := o.Get(rt.R().TODOContext()); err != nil {
+			if _, err := o.Put(rt.R().TODOContext(), &schema.Dir{}); err != nil {
 				vlog.Errorf("Error creating parent %q: %s", prefix, err)
 			}
 		}
@@ -97,15 +97,23 @@
 
 // newTransaction starts a new transaction.
 func (st *state) newTransaction() {
-	st.transaction = primitives.NewTransaction(rt.R().TODOContext())
+	tid, err := st.store.BindTransactionRoot("").CreateTransaction(rt.R().TODOContext())
+	if err != nil {
+		vlog.Fatalf("Failed to create transaction: %s", err)
+	}
+	st.tname = tid // Transaction is rooted at "", so tname == tid.
 }
 
 // commit commits the current transaction.
 func (st *state) commit() {
-	if err := st.transaction.Commit(rt.R().TODOContext()); err != nil {
+	if st.tname == "" {
+		vlog.Fatalf("No transaction to commit")
+	}
+	err := st.store.BindTransaction(st.tname).Commit(rt.R().TODOContext())
+	st.tname = ""
+	if err != nil {
 		vlog.Errorf("Failed to commit transaction: %s", err)
 	}
-	st.transaction = nil
 }
 
 // storeList saves a schema.List to the store with name /lists/<Name>, and also
diff --git a/services/mgmt/application/impl/invoker.go b/services/mgmt/application/impl/invoker.go
index 38e1c28..b0d376b 100644
--- a/services/mgmt/application/impl/invoker.go
+++ b/services/mgmt/application/impl/invoker.go
@@ -6,9 +6,9 @@
 	"strings"
 
 	"veyron2/ipc"
+	"veyron2/naming"
 	"veyron2/services/mgmt/application"
 	"veyron2/storage"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 )
 
@@ -51,13 +51,13 @@
 type dir struct{}
 
 // makeParentNodes creates the parent nodes if they do not already exist.
-func makeParentNodes(context ipc.ServerContext, store storage.Store, transaction storage.Transaction, path string) error {
+func makeParentNodes(context ipc.ServerContext, store storage.Store, path string) error {
 	pathComponents := storage.ParsePath(path)
 	for i := 0; i < len(pathComponents); i++ {
 		name := pathComponents[:i].String()
-		object := store.Bind(name)
-		if _, err := object.Get(context, transaction); err != nil {
-			if _, err := object.Put(context, transaction, &dir{}); err != nil {
+		object := store.BindObject(name)
+		if _, err := object.Get(context); err != nil {
+			if _, err := object.Put(context, &dir{}); err != nil {
 				return errOperationFailed
 			}
 		}
@@ -77,8 +77,7 @@
 	}
 	for _, profile := range profiles {
 		path := path.Join("/applications", name, profile, version)
-		object := i.store.Bind(path)
-		entry, err := object.Get(context, nil)
+		entry, err := i.store.BindObject(path).Get(context)
 		if err != nil {
 			continue
 		}
@@ -100,25 +99,29 @@
 	if version == "" {
 		return errInvalidSuffix
 	}
-	transaction := primitives.NewTransaction(context)
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := i.store.BindTransactionRoot("").CreateTransaction(context)
+	if err != nil {
+		return err
+	}
 	var entry storage.Stat
 	for _, profile := range profiles {
-		path := path.Join("/applications", name, profile, version)
-		if err := makeParentNodes(context, i.store, transaction, path); err != nil {
+		path := naming.Join(tname, path.Join("/applications", name, profile, version))
+		if err := makeParentNodes(context, i.store, path); err != nil {
 			return err
 		}
-		object := i.store.Bind(path)
+		object := i.store.BindObject(path)
 		if !entry.ID.IsValid() {
-			if entry, err = object.Put(context, transaction, envelope); err != nil {
+			if entry, err = object.Put(context, envelope); err != nil {
 				return errOperationFailed
 			}
 		} else {
-			if _, err := object.Put(context, transaction, entry.ID); err != nil {
+			if _, err := object.Put(context, entry.ID); err != nil {
 				return errOperationFailed
 			}
 		}
 	}
-	if err := transaction.Commit(context); err != nil {
+	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
 		return errOperationFailed
 	}
 	return nil
@@ -130,23 +133,27 @@
 	if err != nil {
 		return err
 	}
-	transaction := primitives.NewTransaction(context)
-	path := path.Join("/applications", name, profile)
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := i.store.BindTransactionRoot("").CreateTransaction(context)
+	if err != nil {
+		return err
+	}
+	path := naming.Join(tname, path.Join("/applications", name, profile))
 	if version != "" {
 		path += "/" + version
 	}
-	object := i.store.Bind(path)
-	found, err := object.Exists(context, transaction)
+	object := i.store.BindObject(path)
+	found, err := object.Exists(context)
 	if err != nil {
 		return errOperationFailed
 	}
 	if !found {
 		return errNotFound
 	}
-	if err := object.Remove(context, transaction); err != nil {
+	if err := object.Remove(context); err != nil {
 		return errOperationFailed
 	}
-	if err := transaction.Commit(context); err != nil {
+	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
 		return errOperationFailed
 	}
 	return nil
diff --git a/services/mgmt/profile/impl/invoker.go b/services/mgmt/profile/impl/invoker.go
index aa95dcb..e1e2b4d 100644
--- a/services/mgmt/profile/impl/invoker.go
+++ b/services/mgmt/profile/impl/invoker.go
@@ -7,8 +7,8 @@
 	"veyron/services/mgmt/profile"
 
 	"veyron2/ipc"
+	"veyron2/naming"
 	"veyron2/storage"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 )
 
@@ -38,13 +38,13 @@
 type dir struct{}
 
 // makeParentNodes creates the parent nodes if they do not already exist.
-func makeParentNodes(context ipc.ServerContext, store storage.Store, transaction storage.Transaction, path string) error {
+func makeParentNodes(context ipc.ServerContext, store storage.Store, path string) error {
 	pathComponents := storage.ParsePath(path)
 	for i := 0; i < len(pathComponents); i++ {
 		name := pathComponents[:i].String()
-		object := store.Bind(name)
-		if _, err := object.Get(context, transaction); err != nil {
-			if _, err := object.Put(context, transaction, &dir{}); err != nil {
+		object := store.BindObject(name)
+		if _, err := object.Get(context); err != nil {
+			if _, err := object.Put(context, &dir{}); err != nil {
 				return errOperationFailed
 			}
 		}
@@ -54,16 +54,20 @@
 
 func (i *invoker) Put(context ipc.ServerContext, profile profile.Specification) error {
 	vlog.VI(0).Infof("%v.Put(%v)", i.suffix, profile)
-	transaction := primitives.NewTransaction(context)
-	path := path.Join("/profiles", i.suffix)
-	if err := makeParentNodes(context, i.store, transaction, path); err != nil {
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := i.store.BindTransactionRoot("").CreateTransaction(context)
+	if err != nil {
 		return err
 	}
-	object := i.store.Bind(path)
-	if _, err := object.Put(context, transaction, profile); err != nil {
+	path := naming.Join(tname, path.Join("/profiles", i.suffix))
+	if err := makeParentNodes(context, i.store, path); err != nil {
+		return err
+	}
+	object := i.store.BindObject(path)
+	if _, err := object.Put(context, profile); err != nil {
 		return errOperationFailed
 	}
-	if err := transaction.Commit(context); err != nil {
+	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
 		return errOperationFailed
 	}
 	return nil
@@ -71,20 +75,24 @@
 
 func (i *invoker) Remove(context ipc.ServerContext) error {
 	vlog.VI(0).Infof("%v.Remove()", i.suffix)
-	transaction := primitives.NewTransaction(context)
-	path := path.Join("/profiles", i.suffix)
-	object := i.store.Bind(path)
-	found, err := object.Exists(context, transaction)
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := i.store.BindTransactionRoot("").CreateTransaction(context)
+	if err != nil {
+		return err
+	}
+	path := naming.Join(tname, path.Join("/profiles", i.suffix))
+	object := i.store.BindObject(path)
+	found, err := object.Exists(context)
 	if err != nil {
 		return errOperationFailed
 	}
 	if !found {
 		return errNotFound
 	}
-	if err := object.Remove(context, transaction); err != nil {
+	if err := object.Remove(context); err != nil {
 		return errOperationFailed
 	}
-	if err := transaction.Commit(context); err != nil {
+	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
 		return errOperationFailed
 	}
 	return nil
@@ -95,8 +103,7 @@
 func (i *invoker) lookup(context ipc.ServerContext) (profile.Specification, error) {
 	empty := profile.Specification{}
 	path := path.Join("/profiles", i.suffix)
-	object := i.store.Bind(path)
-	entry, err := object.Get(context, nil)
+	entry, err := i.store.BindObject(path).Get(context)
 	if err != nil {
 		return empty, errNotFound
 	}
diff --git a/services/security/discharger/revoker.go b/services/security/discharger/revoker.go
index 4f32574..e945a37 100644
--- a/services/security/discharger/revoker.go
+++ b/services/security/discharger/revoker.go
@@ -16,7 +16,6 @@
 	"veyron2/security"
 	"veyron2/storage"
 	"veyron2/storage/vstore"
-	"veyron2/storage/vstore/primitives"
 	"veyron2/vlog"
 	"veyron2/vom"
 )
@@ -40,11 +39,9 @@
 func (cav revocationCaveat) Validate(security.Context) error {
 	// TODO(ashankar,mattr): Figure out how to get the context of an existing RPC here
 	rctx := rt.R().NewContext()
-	revocation := revocationService.store.Bind(naming.Join(revocationService.pathInStore,
-		hex.EncodeToString(cav[:])))
-	tx := primitives.NewTransaction(rctx)
-	defer tx.Abort(rctx)
-	exists, err := revocation.Exists(rctx, tx)
+	revocation := revocationService.store.BindObject(
+		naming.Join(revocationService.pathInStore, hex.EncodeToString(cav[:])))
+	exists, err := revocation.Exists(rctx)
 	if err != nil {
 		return err
 	}
@@ -69,13 +66,11 @@
 
 func (revoceationService *revocationServiceT) Revoke(ctx ipc.ServerContext, caveatPreimage ssecurity.RevocationToken) error {
 	caveatNonce := sha256.Sum256(caveatPreimage[:])
-	tx := primitives.NewTransaction(ctx)
-	revocation := revocationService.store.Bind(naming.Join(revocationService.pathInStore, hex.EncodeToString(caveatNonce[:])))
-	if _, err := revocation.Put(ctx, tx, caveatPreimage[:]); err != nil {
-		tx.Abort(ctx)
+	revocation := revocationService.store.BindObject(naming.Join(revocationService.pathInStore, hex.EncodeToString(caveatNonce[:])))
+	if _, err := revocation.Put(ctx, caveatPreimage[:]); err != nil {
 		return err
 	}
-	return tx.Commit(ctx)
+	return nil
 }
 
 // NewRevoker returns a new revoker service that can be passed to a dispatcher.
@@ -94,7 +89,11 @@
 	}
 
 	rctx := rt.R().NewContext()
-	tx := primitives.NewTransaction(rctx)
+	// Transaction is rooted at "", so tname == tid.
+	tname, err := revocationService.store.BindTransactionRoot("").CreateTransaction(rctx)
+	if err != nil {
+		return nil, err
+	}
 
 	// Create parent directories for the revoker root, if necessary
 	// TODO(tilaks,andreser): provide a `mkdir -p` equivalent in store
@@ -103,17 +102,17 @@
 	for i := 0; i <= len(l); i++ {
 		fmt.Println(i, filepath.Join(l[:i]...))
 		prefix := filepath.Join(l[:i]...)
-		o := revocationService.store.Bind(prefix)
-		if exist, err := o.Exists(rctx, tx); err != nil {
+		o := revocationService.store.BindObject(naming.Join(tname, prefix))
+		if exist, err := o.Exists(rctx); err != nil {
 			vlog.Infof("Error checking existence at %q: %s", prefix, err)
 		} else if !exist {
-			if _, err := o.Put(rctx, tx, &Dir{}); err != nil {
+			if _, err := o.Put(rctx, &Dir{}); err != nil {
 				vlog.Infof("Error creating directory %q: %s", prefix, err)
 			}
 		}
 	}
-	if err := tx.Commit(rctx); err != nil {
-		vlog.Fatalf("Commit creation of revocer root et %s: %s", pathInStore, err)
+	if err := revocationService.store.BindTransaction(tname).Commit(rctx); err != nil {
+		vlog.Fatalf("Failed to commit creation of revoker root at %s: %s", pathInStore, err)
 	}
 	return ssecurity.NewServerRevoker(revocationService.revocationServiceT), nil
 }
diff --git a/services/store/memstore/object.go b/services/store/memstore/object.go
index f130c7d..3c7d0a8 100644
--- a/services/store/memstore/object.go
+++ b/services/store/memstore/object.go
@@ -14,9 +14,9 @@
 // path.  This means that in different transactions, the object may refer to
 // different values with different store.IDs.
 //
-// TODO(jyh): Perhaps a more sensible alternative is to resolve the path at
-// Bind time, and have the object then refer to the value by storage.ID.  However,
-// there are two problems with that,
+// TODO(jyh): Perhaps a more sensible alternative is to resolve the path at Bind
+// time, and have the object then refer to the value by storage.ID. However,
+// there are two problems with that:
 //
 //    - We can't resolve the path for objects that don't yet exist.
 //    - Bind is transactionless, so the path resolution wouldn't be consistent.
@@ -107,7 +107,7 @@
 	return stream, nil
 }
 
-// Glob returns the sequence of names that match the given pattern.
+// Glob returns names that match the given pattern.
 func (o *object) Glob(pid security.PublicID, trans service.Transaction, pattern string) (service.GlobStream, error) {
 	tr, _, err := o.store.getTransaction(trans)
 	if err != nil {
diff --git a/services/store/raw/service.vdl b/services/store/raw/service.vdl
index de0d6b4..3384837 100644
--- a/services/store/raw/service.vdl
+++ b/services/store/raw/service.vdl
@@ -1,7 +1,8 @@
-/*
-Package raw defines a raw interface for the Veyron store.
-The raw interface supports synchronizing with remote stores by transporting Mutations.
-*/
+// Package raw defines a raw interface for the Veyron store.
+//
+// The raw interface supports synchronizing with remote stores by transporting
+// Mutations.
+
 package raw
 
 import (
diff --git a/services/store/raw/service.vdl.go b/services/store/raw/service.vdl.go
index e178cb9..d669a23 100644
--- a/services/store/raw/service.vdl.go
+++ b/services/store/raw/service.vdl.go
@@ -1,10 +1,6 @@
 // This file was auto-generated by the veyron vdl tool.
 // Source: service.vdl
 
-/*
-Package raw defines a raw interface for the Veyron store.
-The raw interface supports synchronizing with remote stores by transporting Mutations.
-*/
 package raw
 
 import (
diff --git a/services/store/server/object.go b/services/store/server/object.go
index dd33bb6..5f0c8a8 100644
--- a/services/store/server/object.go
+++ b/services/store/server/object.go
@@ -1,5 +1,8 @@
 package server
 
+// This file defines object, which implements the server-side Object API from
+// veyron2/services/store/service.vdl.
+
 import (
 	"veyron/services/store/service"
 
@@ -14,8 +17,9 @@
 )
 
 type object struct {
-	name   string
+	name   string // will never contain a transaction id
 	obj    service.Object
+	tid    transactionID // may be nullTransactionID
 	server *Server
 }
 
@@ -84,9 +88,25 @@
 	return uattrs
 }
 
+// CreateTransaction creates a transaction.
+func (o *object) CreateTransaction(ctx ipc.ServerContext, opts []vdlutil.Any) (string, error) {
+	if o.tid != nullTransactionID {
+		return "", errNestedTransaction
+	}
+	return o.server.createTransaction(ctx, o.name)
+}
+
+func (o *object) Commit(ctx ipc.ServerContext) error {
+	return o.server.commitTransaction(ctx, o.tid)
+}
+
+func (o *object) Abort(ctx ipc.ServerContext) error {
+	return o.server.abortTransaction(ctx, o.tid)
+}
+
 // Exists returns true iff the Entry has a value.
-func (o *object) Exists(ctx ipc.ServerContext, tid store.TransactionID) (bool, error) {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Exists(ctx ipc.ServerContext) (bool, error) {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return false, err
 	}
@@ -96,8 +116,8 @@
 // Get returns the value for the Object.  The value returned is from the
 // most recent mutation of the entry in the Transaction, or from the
 // Transaction's snapshot if there is no mutation.
-func (o *object) Get(ctx ipc.ServerContext, tid store.TransactionID) (store.Entry, error) {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Get(ctx ipc.ServerContext) (store.Entry, error) {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return nullEntry, err
 	}
@@ -109,8 +129,8 @@
 }
 
 // Put modifies the value of the Object.
-func (o *object) Put(ctx ipc.ServerContext, tid store.TransactionID, val vdlutil.Any) (store.Stat, error) {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Put(ctx ipc.ServerContext, val vdlutil.Any) (store.Stat, error) {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return nullStat, err
 	}
@@ -122,8 +142,8 @@
 }
 
 // Remove removes the Object.
-func (o *object) Remove(ctx ipc.ServerContext, tid store.TransactionID) error {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Remove(ctx ipc.ServerContext) error {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return err
 	}
@@ -133,8 +153,8 @@
 // SetAttr changes the attributes of the entry, such as permissions and
 // replication groups.  Attributes are associated with the value, not the
 // path.
-func (o *object) SetAttr(ctx ipc.ServerContext, tid store.TransactionID, attrs []vdlutil.Any) error {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) SetAttr(ctx ipc.ServerContext, attrs []vdlutil.Any) error {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return err
 	}
@@ -146,8 +166,8 @@
 }
 
 // Stat returns entry info.
-func (o *object) Stat(ctx ipc.ServerContext, tid store.TransactionID) (store.Stat, error) {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Stat(ctx ipc.ServerContext) (store.Stat, error) {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return nullStat, err
 	}
@@ -159,8 +179,8 @@
 }
 
 // Query returns a sequence of objects that match the given query.
-func (o *object) Query(ctx ipc.ServerContext, tid store.TransactionID, q query.Query, stream store.ObjectServiceQueryStream) error {
-	t, err := o.server.findTransaction(ctx, tid)
+func (o *object) Query(ctx ipc.ServerContext, q query.Query, stream store.ObjectServiceQueryStream) error {
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return err
 	}
@@ -189,12 +209,7 @@
 
 // Glob streams a series of names that match the given pattern.
 func (o *object) Glob(ctx ipc.ServerContext, pattern string, stream mounttable.GlobableServiceGlobStream) error {
-	return o.GlobT(ctx, nullTransactionID, pattern, &globStreamAdapter{stream})
-}
-
-// Glob streams a series of names that match the given pattern.
-func (o *object) GlobT(ctx ipc.ServerContext, tid store.TransactionID, pattern string, stream store.ObjectServiceGlobTStream) error {
-	t, err := o.server.findTransaction(ctx, tid)
+	t, err := o.server.findTransaction(ctx, o.tid)
 	if err != nil {
 		return err
 	}
@@ -202,11 +217,12 @@
 	if err != nil {
 		return err
 	}
+	gsa := &globStreamAdapter{stream}
 	for ; it.IsValid(); it.Next() {
 		if ctx.IsClosed() {
 			break
 		}
-		if err := stream.Send(it.Name()); err != nil {
+		if err := gsa.Send(it.Name()); err != nil {
 			return err
 		}
 	}
diff --git a/services/store/server/server.go b/services/store/server/server.go
index 8eb4478..8b72fc4 100644
--- a/services/store/server/server.go
+++ b/services/store/server/server.go
@@ -1,8 +1,15 @@
 // Package server implements a storage service.
+
 package server
 
+// This file defines Server, which implements the server-side Store API from
+// veyron2/services/store/service.vdl.
+
 import (
+	"fmt"
+	"math/rand"
 	"reflect"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -15,12 +22,11 @@
 	"veyron2/ipc"
 	"veyron2/security"
 	"veyron2/services/store"
-	"veyron2/vdl/vdlutil"
 	"veyron2/verror"
 )
 
 const (
-	// transactionMAxLifetime is the maximum duration before a transaction will
+	// transactionMaxLifetime is the maximum duration before a transaction will
 	// be garbage collected.
 	//
 	// TODO(jyh): This should probably be a configuration parameter.
@@ -28,28 +34,28 @@
 )
 
 var (
-	// Server implements the StoreService interface.
 	_ store.StoreService = (*Server)(nil)
 
-	nullTransactionID store.TransactionID
-
-	errTransactionAlreadyExists = verror.Existsf("transaction already exists")
-	errTransactionDoesNotExist  = verror.NotFoundf("transaction does not exist")
+	errNestedTransaction = verror.BadArgf("cannot create a nested Transaction")
+	// Note, this can happen e.g. due to expiration.
+	errTransactionDoesNotExist = verror.NotFoundf("transaction does not exist")
 	// Transaction exists, but may not be used by the caller.
 	errPermissionDenied = verror.NotAuthorizedf("permission denied")
+
+	rng = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+
+	nullTransactionID transactionID
 )
 
-// Server stores the dictionary of all media items.  It has a scanner.Scanner
-// for collecting files from the filesystem.  For each file, a FileService is
-// registered to serve the file.
+// Server implements Store and uses memstore internally.
 type Server struct {
 	mutex sync.RWMutex
 
 	// store is the actual store implementation.
 	store service.Store
 
-	// transactions are the set of active transactions.
-	transactions map[store.TransactionID]*transaction
+	// transactions is the set of active transactions.
+	transactions map[transactionID]*transaction
 
 	// Transaction garbage collection.
 	pending sync.WaitGroup
@@ -60,6 +66,13 @@
 	watcher service.Watcher
 }
 
+// transactionID is an internal transaction identifier chosen by the server.
+//
+// TODO(jyh): Consider using a larger identifier space to reduce chance of
+// collisions. (Note, createTransaction handles collisions when generating
+// transactionIDs.)
+type transactionID uint64
+
 // transactionContext defines the context in which a transaction is used. A
 // transaction may be used only in the context that created it.
 // transactionContext weakly identifies a session by the local and remote
@@ -67,9 +80,11 @@
 // TODO(tilaks): Use the local and remote addresses to identify the session.
 // Does a session with a mobile device break if the remote address changes?
 type transactionContext interface {
-	// LocalID returns the PublicID of the principal at the local end of the request.
+	// LocalID returns the PublicID of the principal at the local end of the
+	// request.
 	LocalID() security.PublicID
-	// RemoteID returns the PublicID of the principal at the remote end of the request.
+	// RemoteID returns the PublicID of the principal at the remote end of the
+	// request.
 	RemoteID() security.PublicID
 }
 
@@ -97,7 +112,7 @@
 	}
 	s := &Server{
 		store:        mstore,
-		transactions: make(map[store.TransactionID]*transaction),
+		transactions: make(map[transactionID]*transaction),
 		ticker:       time.NewTicker(time.Second),
 		closed:       make(chan struct{}),
 		watcher:      mwatcher,
@@ -127,14 +142,82 @@
 	}
 }
 
-// findTransaction returns the transaction for the TransactionID.
-func (s *Server) findTransaction(ctx transactionContext, id store.TransactionID) (service.Transaction, error) {
+// findTransactionComponent returns the (begin, end) offsets of the "$tid.*"
+// component in the given object name, or (-1, -1) if oname does not contain a
+// transaction component.
+func findTransactionComponent(oname string) (int, int) {
+	begin := 0
+	if !strings.HasPrefix(oname, "$tid") {
+		begin = strings.Index(oname, "/$tid")
+	}
+	if begin == -1 {
+		return -1, -1
+	}
+	end := strings.Index(oname[begin+1:], "/")
+	if end == -1 {
+		end = len(oname)
+	} else {
+		end += begin + 1
+	}
+	return begin, end
+}
+
+// TODO(sadovsky): One of the following:
+// - Reserve prefix string "$tid." for internal use.
+// - Reserve prefix char "$" for internal use.
+// - Require users to escape prefix char "$" when they are referring to their
+//   own data, e.g. "\$foo".
+func makeTransactionComponent(id transactionID) string {
+	return fmt.Sprintf("$tid.%d", id)
+}
+
+// stripTransactionComponent returns the given object name with its "$tid.*"
+// component removed, and also returns the stripped transactionID.
+// Examples:
+//   "/foo/$tid.123/bar" => {"/foo/bar", transactionID(123)}
+//   "/foo/bar"          => {"/foo/bar", nullTransactionID}
+func stripTransactionComponent(oname string) (string, transactionID, error) {
+	begin, end := findTransactionComponent(oname)
+	if begin == -1 {
+		return oname, nullTransactionID, nil
+	}
+	tc := oname[begin:end]
+	id, err := strconv.ParseInt(tc[strings.LastIndex(tc, ".")+1:], 10, 64)
+	if err != nil {
+		return "", nullTransactionID, fmt.Errorf("Failed to extract id from %q", tc)
+	}
+	return oname[:begin] + oname[end:], transactionID(id), nil
+}
+
+func (s *Server) createTransaction(ctx transactionContext, oname string) (string, error) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	var id transactionID
+	for {
+		id = transactionID(rng.Int63())
+		_, ok := s.transactions[id]
+		if !ok {
+			break
+		}
+	}
+	info := &transaction{
+		trans:      memstore.NewTransaction(),
+		expires:    time.Now().Add(transactionMaxLifetime),
+		creatorCtx: ctx,
+	}
+	s.transactions[id] = info
+	return makeTransactionComponent(id), nil
+}
+
+// findTransaction returns the transaction for the given transaction ID.
+func (s *Server) findTransaction(ctx transactionContext, id transactionID) (service.Transaction, error) {
 	s.mutex.RLock()
 	defer s.mutex.RUnlock()
 	return s.findTransactionLocked(ctx, id)
 }
 
-func (s *Server) findTransactionLocked(ctx transactionContext, id store.TransactionID) (service.Transaction, error) {
+func (s *Server) findTransactionLocked(ctx transactionContext, id transactionID) (service.Transaction, error) {
 	if id == nullTransactionID {
 		return nil, nil
 	}
@@ -150,6 +233,43 @@
 	return info.trans, nil
 }
 
+// Commit commits the changes in the transaction to the store.  The
+// operation is atomic, so all mutations are performed, or none.  Returns an
+// error if the transaction aborted.
+func (s *Server) commitTransaction(ctx transactionContext, id transactionID) error {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+	t, err := s.findTransactionLocked(ctx, id)
+	if err != nil {
+		return err
+	}
+	if t == nil {
+		return errTransactionDoesNotExist
+	}
+	err = t.Commit()
+	delete(s.transactions, id)
+	return err
+}
+
+// Abort discards a transaction.  This is an optimization; transactions
+// eventually time out and get discarded.  However, live transactions
+// consume resources, so if you know that you won't be using a transaction
+// anymore, you should discard it explicitly.
+func (s *Server) abortTransaction(ctx transactionContext, id transactionID) error {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+	t, err := s.findTransactionLocked(ctx, id)
+	if err != nil {
+		return err
+	}
+	if t == nil {
+		return errTransactionDoesNotExist
+	}
+	err = t.Abort()
+	delete(s.transactions, id)
+	return err
+}
+
 func (t *transaction) matchesContext(ctx transactionContext) bool {
 	creatorCtx := t.creatorCtx
 	return membersEqual(creatorCtx.LocalID().Names(), ctx.LocalID().Names()) &&
@@ -193,60 +313,6 @@
 	}
 }
 
-// CreateTransaction creates a transaction.
-func (s *Server) CreateTransaction(ctx ipc.ServerContext, id store.TransactionID, opts []vdlutil.Any) error {
-	s.mutex.Lock()
-	defer s.mutex.Unlock()
-
-	info, ok := s.transactions[id]
-	if ok {
-		return errTransactionAlreadyExists
-	}
-	info = &transaction{
-		trans:      memstore.NewTransaction(),
-		expires:    time.Now().Add(transactionMaxLifetime),
-		creatorCtx: ctx,
-	}
-	s.transactions[id] = info
-	return nil
-}
-
-// Commit commits the changes in the transaction to the store.  The
-// operation is atomic, so all mutations are performed, or none.  Returns an
-// error if the transaction aborted.
-func (s *Server) Commit(ctx ipc.ServerContext, id store.TransactionID) error {
-	s.mutex.Lock()
-	defer s.mutex.Unlock()
-
-	t, err := s.findTransactionLocked(ctx, id)
-	if err != nil {
-		return err
-	}
-	if t == nil {
-		return errTransactionDoesNotExist
-	}
-	err = t.Commit()
-	delete(s.transactions, id)
-	return err
-}
-
-// Abort discards a transaction.
-func (s *Server) Abort(ctx ipc.ServerContext, id store.TransactionID) error {
-	s.mutex.Lock()
-	defer s.mutex.Unlock()
-
-	t, err := s.findTransactionLocked(ctx, id)
-	if err != nil {
-		return err
-	}
-	if t == nil {
-		return errTransactionDoesNotExist
-	}
-	err = t.Abort()
-	delete(s.transactions, id)
-	return err
-}
-
 // Watch returns a stream of all changes.
 func (s *Server) Watch(ctx ipc.ServerContext, req raw.Request, stream raw.StoreServiceWatchStream) error {
 	return s.watcher.WatchRaw(ctx, req, stream)
@@ -276,20 +342,37 @@
 }
 
 func (d *storeDispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
-	return ipc.ReflectInvoker(d.lookupServer(suffix)), d.auth, nil
+	serv, err := d.lookupServer(suffix)
+	if err != nil {
+		return nil, nil, err
+	}
+	return ipc.ReflectInvoker(serv), d.auth, nil
 }
 
-func (d *storeDispatcher) lookupServer(suffix string) interface{} {
+func (d *storeDispatcher) lookupServer(suffix string) (interface{}, error) {
+	// Strip leading "/" if present so that server internals can reliably use
+	// naming.Join(suffix, "foo").
+	suffix = strings.TrimPrefix(suffix, "/")
 	if strings.HasSuffix(suffix, store.StoreSuffix) {
-		return store.NewServerStore(d.s)
+		return store.NewServerStore(d.s), nil
 	} else if strings.HasSuffix(suffix, raw.RawStoreSuffix) {
-		return raw.NewServerStore(d.s)
+		return raw.NewServerStore(d.s), nil
 	} else {
-		o := d.s.lookupObject(suffix)
-		return store.NewServerObject(o)
+		// TODO(sadovsky): Create Object, Transaction, and TransactionRoot stubs,
+		// merge them, and return the result. See TODO in
+		// veyron2/services/store/service.vdl.
+		o, err := d.s.lookupObject(suffix)
+		if err != nil {
+			return nil, err
+		}
+		return store.NewServerObject(o), nil
 	}
 }
 
-func (s *Server) lookupObject(name string) *object {
-	return &object{name: name, obj: s.store.Bind(name), server: s}
+func (s *Server) lookupObject(name string) (*object, error) {
+	oname, tid, err := stripTransactionComponent(name)
+	if err != nil {
+		return nil, err
+	}
+	return &object{name: oname, obj: s.store.Bind(oname), tid: tid, server: s}, nil
 }
diff --git a/services/store/server/server_test.go b/services/store/server/server_test.go
index e7bd4f7..b509acb 100644
--- a/services/store/server/server_test.go
+++ b/services/store/server/server_test.go
@@ -6,9 +6,11 @@
 	"log"
 	"os"
 	"reflect"
+	"runtime"
 	"testing"
 	"time"
 
+	_ "veyron/lib/testutil" // initialize vlog
 	watchtesting "veyron/services/store/memstore/testing"
 	"veyron/services/store/raw"
 
@@ -18,6 +20,7 @@
 	"veyron2/services/store"
 	"veyron2/services/watch"
 	"veyron2/storage"
+	_ "veyron2/vlog"
 	"veyron2/vom"
 )
 
@@ -26,8 +29,6 @@
 	rootName                          = fmt.Sprintf("%s", rootPublicID)
 	blessedPublicId security.PublicID = security.FakePublicID("root/blessed")
 
-	nextTransactionID store.TransactionID = 1
-
 	rootCtx    ipc.ServerContext = &testContext{rootPublicID}
 	blessedCtx ipc.ServerContext = &testContext{blessedPublicId}
 )
@@ -105,11 +106,6 @@
 	return &Dir{}
 }
 
-func newTransaction() store.TransactionID {
-	nextTransactionID++
-	return nextTransactionID
-}
-
 func closeTest(config ServerConfig, s *Server) {
 	s.Close()
 	os.Remove(config.DBName)
@@ -132,12 +128,56 @@
 	return s, closer
 }
 
+func lookupObjectOrDie(s *Server, name string) *object {
+	o, err := s.lookupObject(name)
+	if err != nil {
+		panic(err)
+	}
+	return o
+}
+
+// createTransaction creates a new transaction and returns its store-relative
+// name.
+func createTransaction(t *testing.T, s *Server, ctx ipc.ServerContext, name string) string {
+	_, file, line, _ := runtime.Caller(1)
+	tid, err := lookupObjectOrDie(s, name).CreateTransaction(ctx, nil)
+	if err != nil {
+		t.Fatalf("%s(%d): can't create transaction %s: %s", file, line, name, err)
+	}
+	return naming.Join(name, tid)
+}
+
+func TestLookupInvalidTransactionName(t *testing.T) {
+	s, c := newServer()
+	defer c()
+
+	_, err := s.lookupObject("/$tid.bad/foo")
+	if err == nil {
+		t.Errorf("lookupObject should've failed, but didn't")
+	}
+}
+
+func TestNestedTransactionError(t *testing.T) {
+	s, c := newServer()
+	defer c()
+	tname := createTransaction(t, s, rootCtx, "/")
+	if _, err := lookupObjectOrDie(s, tname).CreateTransaction(rootCtx, nil); err == nil {
+		t.Fatalf("creating nested transaction at %s should've failed, but didn't", tname)
+	}
+	// Try again with a valid object in between the two $tid components;
+	// CreateTransaction should still fail.
+	lookupObjectOrDie(s, tname).Put(rootCtx, newValue())
+	foo := naming.Join(tname, "foo")
+	if _, err := lookupObjectOrDie(s, foo).CreateTransaction(rootCtx, nil); err == nil {
+		t.Fatalf("creating nested transaction at %s should've failed, but didn't", foo)
+	}
+}
+
 func TestPutGetRemoveRoot(t *testing.T) {
 	s, c := newServer()
 	defer c()
 
-	o := s.lookupObject("/")
-	testPutGetRemove(t, s, o)
+	testPutGetRemove(t, s, "/")
 }
 
 func TestPutGetRemoveChild(t *testing.T) {
@@ -146,199 +186,158 @@
 
 	{
 		// Create a root.
-		o := s.lookupObject("/")
+		name := "/"
 		value := newValue()
-		tr1 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr1, nil); err != nil {
+
+		tobj1 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if _, err := tobj1.Put(rootCtx, value); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
-		if _, err := o.Put(rootCtx, tr1, value); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if err := s.Commit(rootCtx, tr1); err != nil {
+		if err := tobj1.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 
-		tr2 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr2, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr2); !ok || err != nil {
+		tobj2 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj2.Exists(rootCtx); !ok || err != nil {
 			t.Errorf("Should exist: %s", err)
 		}
-		if _, err := o.Get(rootCtx, tr2); err != nil {
+		if _, err := tobj2.Get(rootCtx); err != nil {
 			t.Errorf("Object should exist: %s", err)
 		}
-		if err := s.Abort(rootCtx, tr2); err != nil {
+		if err := tobj2.Abort(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
 
-	o := s.lookupObject("/Entries/a")
-	testPutGetRemove(t, s, o)
+	testPutGetRemove(t, s, "/Entries/a")
 }
 
-func testPutGetRemove(t *testing.T, s *Server, o *object) {
+func testPutGetRemove(t *testing.T, s *Server, name string) {
 	value := newValue()
 	{
 		// Check that the object does not exist.
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr); ok || err != nil {
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj.Exists(rootCtx); ok || err != nil {
 			t.Errorf("Should not exist: %s", err)
 		}
-		if v, err := o.Get(rootCtx, tr); v.Stat.ID.IsValid() && err == nil {
+		if v, err := tobj.Get(rootCtx); v.Stat.ID.IsValid() && err == nil {
 			t.Errorf("Should not exist: %v, %s", v, err)
 		}
 	}
 
 	{
 		// Add the object.
-		tr1 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr1, nil); err != nil {
+		tobj1 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if _, err := tobj1.Put(rootCtx, value); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
-		if _, err := o.Put(rootCtx, tr1, value); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr1); !ok || err != nil {
+		if ok, err := tobj1.Exists(rootCtx); !ok || err != nil {
 			t.Errorf("Should exist: %s", err)
 		}
-		if _, err := o.Get(rootCtx, tr1); err != nil {
+		if _, err := tobj1.Get(rootCtx); err != nil {
 			t.Errorf("Object should exist: %s", err)
 		}
 
 		// Transactions are isolated.
-		tr2 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr2, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr2); ok || err != nil {
+		tobj2 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj2.Exists(rootCtx); ok || err != nil {
 			t.Errorf("Should not exist: %s", err)
 		}
-		if v, err := o.Get(rootCtx, tr2); v.Stat.ID.IsValid() && err == nil {
+		if v, err := tobj2.Get(rootCtx); v.Stat.ID.IsValid() && err == nil {
 			t.Errorf("Should not exist: %v, %s", v, err)
 		}
 
-		// Apply tr1.
-		if err := s.Commit(rootCtx, tr1); err != nil {
+		// Apply tobj1.
+		if err := tobj1.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 
-		// tr2 is still isolated.
-		if ok, err := o.Exists(rootCtx, tr2); ok || err != nil {
+		// tobj2 is still isolated.
+		if ok, err := tobj2.Exists(rootCtx); ok || err != nil {
 			t.Errorf("Should not exist: %s", err)
 		}
-		if v, err := o.Get(rootCtx, tr2); v.Stat.ID.IsValid() && err == nil {
+		if v, err := tobj2.Get(rootCtx); v.Stat.ID.IsValid() && err == nil {
 			t.Errorf("Should not exist: %v, %s", v, err)
 		}
 
-		// tr3 observes the commit.
-		tr3 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr3, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr3); !ok || err != nil {
+		// tobj3 observes the commit.
+		tobj3 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj3.Exists(rootCtx); !ok || err != nil {
 			t.Errorf("Should exist: %s", err)
 		}
-		if _, err := o.Get(rootCtx, tr3); err != nil {
+		if _, err := tobj3.Get(rootCtx); err != nil {
 			t.Errorf("Object should exist: %s", err)
 		}
 	}
 
 	{
 		// Remove the object.
-		tr1 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr1, nil); err != nil {
+		tobj1 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if err := tobj1.Remove(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
-		if err := o.Remove(rootCtx, tr1); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr1); ok || err != nil {
+		if ok, err := tobj1.Exists(rootCtx); ok || err != nil {
 			t.Errorf("Should not exist: %s", err)
 		}
-		if v, err := o.Get(rootCtx, tr1); v.Stat.ID.IsValid() || err == nil {
+		if v, err := tobj1.Get(rootCtx); v.Stat.ID.IsValid() || err == nil {
 			t.Errorf("Object should not exist: %T, %v, %s", v, v, err)
 		}
 
 		// The removal is isolated.
-		tr2 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr2, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr2); !ok || err != nil {
+		tobj2 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj2.Exists(rootCtx); !ok || err != nil {
 			t.Errorf("Should exist: %s", err)
 		}
-		if _, err := o.Get(rootCtx, tr2); err != nil {
+		if _, err := tobj2.Get(rootCtx); err != nil {
 			t.Errorf("Object should exist: %s", err)
 		}
 
-		// Apply tr1.
-		if err := s.Commit(rootCtx, tr1); err != nil {
+		// Apply tobj1.
+		if err := tobj1.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 
 		// The removal is isolated.
-		if ok, err := o.Exists(rootCtx, tr2); !ok || err != nil {
+		if ok, err := tobj2.Exists(rootCtx); !ok || err != nil {
 			t.Errorf("Should exist: %s", err)
 		}
-		if _, err := o.Get(rootCtx, tr2); err != nil {
+		if _, err := tobj2.Get(rootCtx); err != nil {
 			t.Errorf("Object should exist: %s", err)
 		}
 	}
 
 	{
 		// Check that the object does not exist.
-		tr1 := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr1, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if ok, err := o.Exists(rootCtx, tr1); ok || err != nil {
+		tobj1 := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+		if ok, err := tobj1.Exists(rootCtx); ok || err != nil {
 			t.Errorf("Should not exist")
 		}
-		if v, err := o.Get(rootCtx, tr1); v.Stat.ID.IsValid() && err == nil {
+		if v, err := tobj1.Get(rootCtx); v.Stat.ID.IsValid() && err == nil {
 			t.Errorf("Should not exist: %v, %s", v, err)
 		}
 	}
 }
 
-func TestNilTransaction(t *testing.T) {
-	s, c := newServer()
-	defer c()
-
-	if err := s.Commit(rootCtx, nullTransactionID); err != errTransactionDoesNotExist {
-		t.Errorf("Unexpected error: %v", err)
-	}
-
-	if err := s.Abort(rootCtx, nullTransactionID); err != errTransactionDoesNotExist {
-		t.Errorf("Unexpected error: %v", err)
-	}
-}
+// TODO(sadovsky): Add test cases for committing and aborting an expired
+// transaction. The client should get back errTransactionDoesNotExist.
 
 func TestWatch(t *testing.T) {
 	s, c := newServer()
 	defer c()
 
-	path1 := "/"
+	name1 := "/"
 	value1 := "v1"
 	var id1 storage.ID
 
 	// Before the watch request has been made, commit a transaction that puts /.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		o := s.lookupObject(path1)
-		st, err := o.Put(rootCtx, tr, value1)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name1))
+		st, err := tobj.Put(rootCtx, value1)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id1 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -361,23 +360,19 @@
 		watchtesting.ExpectMutationExistsNoVersionCheck(t, changes, id1, value1)
 	}
 
-	path2 := "/a"
+	name2 := "/a"
 	value2 := "v2"
 	var id2 storage.ID
 
 	// Commit a second transaction that puts /a.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		o := s.lookupObject(path2)
-		st, err := o.Put(rootCtx, tr, value2)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name2))
+		st, err := tobj.Put(rootCtx, value2)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id2 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -409,21 +404,18 @@
 	value1 := "v1"
 	var id1 storage.ID
 
-	o1 := s.lookupObject("/")
-	o2 := s.lookupObject("/a")
+	name1, name2 := "/", "/a"
+	o1, o2 := lookupObjectOrDie(s, name1), lookupObjectOrDie(s, name2)
 
 	// Before the watch request has been made, commit a transaction that puts /.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		st, err := o1.Put(rootCtx, tr, value1)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name1))
+		st, err := tobj.Put(rootCtx, value1)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id1 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -454,16 +446,13 @@
 
 	// Commit a second transaction that puts /a.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		st, err := o2.Put(rootCtx, tr, value2)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name2))
+		st, err := tobj.Put(rootCtx, value2)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id2 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -505,23 +494,19 @@
 	s, c := newServer()
 	defer c()
 
-	path1 := "/"
+	name1 := "/"
 	value1 := "v1"
 	var id1 storage.ID
 
 	// Before the watch request has been made, commit a transaction that puts /.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		o := s.lookupObject(path1)
-		st, err := o.Put(rootCtx, tr, value1)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name1))
+		st, err := tobj.Put(rootCtx, value1)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id1 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -544,23 +529,19 @@
 		watchtesting.ExpectMutationExistsNoVersionCheck(t, changes, id1, value1)
 	}
 
-	path2 := "/a"
+	name2 := "/a"
 	value2 := "v2"
 	var id2 storage.ID
 
 	// Commit a second transaction that puts /a.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		o := s.lookupObject(path2)
-		st, err := o.Put(rootCtx, tr, value2)
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name2))
+		st, err := tobj.Put(rootCtx, value2)
 		if err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 		id2 = st.ID
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -586,15 +567,11 @@
 
 	// Commit a third transaction that removes /a.
 	{
-		tr := newTransaction()
-		if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
+		tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, "/a"))
+		if err := tobj.Remove(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
-		o := s.lookupObject("/a")
-		if err := o.Remove(rootCtx, tr); err != nil {
-			t.Errorf("Unexpected error: %s", err)
-		}
-		if err := s.Commit(rootCtx, tr); err != nil {
+		if err := tobj.Commit(rootCtx); err != nil {
 			t.Errorf("Unexpected error: %s", err)
 		}
 	}
@@ -633,62 +610,52 @@
 	defer c()
 
 	// Create a root.
-	o := s.lookupObject("/")
+	name := "/"
 	value := newValue()
 
 	// Create a transaction in the root's session.
-	tr := newTransaction()
-	if err := s.CreateTransaction(rootCtx, tr, nil); err != nil {
-		t.Errorf("Unexpected error: %s", err)
-	}
-	// Check that the transaction cannot be created or accessed by the blessee.
-	if err := s.CreateTransaction(blessedCtx, tr, nil); err != errTransactionAlreadyExists {
+	tobj := lookupObjectOrDie(s, createTransaction(t, s, rootCtx, name))
+
+	// Check that the transaction cannot be accessed by the blessee.
+	if _, err := tobj.Exists(blessedCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Exists(blessedCtx, tr); err != errPermissionDenied {
+	if _, err := tobj.Get(blessedCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Get(blessedCtx, tr); err != errPermissionDenied {
+	if _, err := tobj.Put(blessedCtx, value); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Put(blessedCtx, tr, value); err != errPermissionDenied {
+	if err := tobj.Remove(blessedCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if err := o.Remove(blessedCtx, tr); err != errPermissionDenied {
+	if err := tobj.Abort(blessedCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if err := s.Abort(blessedCtx, tr); err != errPermissionDenied {
-		t.Errorf("Unexpected error: %v", err)
-	}
-	if err := s.Commit(blessedCtx, tr); err != errPermissionDenied {
+	if err := tobj.Commit(blessedCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
 
 	// Create a transaction in the blessee's session.
-	tr = newTransaction()
-	if err := s.CreateTransaction(blessedCtx, tr, nil); err != nil {
-		t.Errorf("Unexpected error: %s", err)
-	}
-	// Check that the transaction cannot be created or accessed by the root.
-	if err := s.CreateTransaction(rootCtx, tr, nil); err != errTransactionAlreadyExists {
+	tobj = lookupObjectOrDie(s, createTransaction(t, s, blessedCtx, name))
+
+	// Check that the transaction cannot be accessed by the root.
+	if _, err := tobj.Exists(rootCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Exists(rootCtx, tr); err != errPermissionDenied {
+	if _, err := tobj.Get(rootCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Get(rootCtx, tr); err != errPermissionDenied {
+	if _, err := tobj.Put(rootCtx, value); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if _, err := o.Put(rootCtx, tr, value); err != errPermissionDenied {
+	if err := tobj.Remove(rootCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if err := o.Remove(rootCtx, tr); err != errPermissionDenied {
+	if err := tobj.Abort(rootCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
-	if err := s.Abort(rootCtx, tr); err != errPermissionDenied {
-		t.Errorf("Unexpected error: %v", err)
-	}
-	if err := s.Commit(rootCtx, tr); err != errPermissionDenied {
+	if err := tobj.Commit(rootCtx); err != errPermissionDenied {
 		t.Errorf("Unexpected error: %v", err)
 	}
 }
@@ -716,12 +683,16 @@
 	s, c := newServer()
 	defer c()
 
-	// TODO(bprosnitz) Switch this to use just exported methods (using signature) once signature stabilizes.
+	// TODO(bprosnitz): Switch this to use just exported methods (using signature)
+	// once signature stabilizes.
 	d := NewStoreDispatcher(s, nil).(*storeDispatcher)
 	for _, test := range tests {
-		srvr := d.lookupServer(test.name)
-		if reflect.TypeOf(srvr) != test.t {
-			t.Errorf("error looking up %s. got %T, expected %v", test.name, srvr, test.t)
+		serv, err := d.lookupServer(test.name)
+		if err != nil {
+			t.Errorf("error looking up %s: %s", test.name, err)
+		}
+		if reflect.TypeOf(serv) != test.t {
+			t.Errorf("error looking up %s. got %T, expected %v", test.name, serv, test.t)
 		}
 	}
 }
diff --git a/services/store/service/service.go b/services/store/service/service.go
index 4f9758f..f91f206 100644
--- a/services/store/service/service.go
+++ b/services/store/service/service.go
@@ -1,5 +1,9 @@
 package service
 
+// Defines the store server Go API.
+// NOTE(sadovsky): See comments in go/vcl/3292 for why we have this API. It may
+// no longer be necessary.
+
 import (
 	"veyron/services/store/raw"
 
@@ -11,8 +15,8 @@
 	"veyron2/storage"
 )
 
-// Transaction is like storage.Transaction, but doesn't include extra client-side
-// parameters.
+// Transaction is like storage.Transaction, but doesn't include extra
+// client-side parameters.
 type Transaction interface {
 	// Commit commits the changes (the Set and Delete operations) in the
 	// transaction to the store.  The operation is atomic, so all Set/Delete
@@ -30,7 +34,7 @@
 }
 
 // Object is like storage.Object, but it include extra server-side parameters.
-// In perticular, each method takes the identity of the client.
+// In particular, each method takes the identity of the client.
 type Object interface {
 	// Exists returns true iff the Entry has a value.
 	Exists(clientID security.PublicID, t Transaction) (bool, error)
@@ -60,7 +64,7 @@
 	// Query returns entries matching the given query.
 	Query(clientID security.PublicID, t Transaction, q query.Query) (QueryStream, error)
 
-	// Glob returns the sequence of names that match the given pattern.
+	// Glob returns names that match the given pattern.
 	Glob(clientID security.PublicID, t Transaction, pattern string) (GlobStream, error)
 }
 
diff --git a/services/wsprd/lib/remove_this.go b/services/wsprd/lib/remove_this.go
index 716da94..a135df7 100644
--- a/services/wsprd/lib/remove_this.go
+++ b/services/wsprd/lib/remove_this.go
@@ -15,7 +15,6 @@
 	vom.Register(store.NestedResult(0))
 	vom.Register(store.QueryResult{})
 	vom.Register(store.Stat{})
-	vom.Register(store.TransactionID(0))
 	vom.Register(watch.GlobRequest{})
 	vom.Register(watch.QueryRequest{})
 	vom.Register(watch.ChangeBatch{})