syncbase: replace veyron/ with x/ref/

Change-Id: I246fa092651c95b64d5ed43a0f48d40050b46190
diff --git a/services/syncbase/server/database.go b/services/syncbase/server/database.go
new file mode 100644
index 0000000..5a0c4ae
--- /dev/null
+++ b/services/syncbase/server/database.go
@@ -0,0 +1,202 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+)
+
+// TODO(sadovsky): Decide whether to use the same version-aware data layout that
+// we use for Items. Relatedly, decide whether the Database Permissions should get
+// synced. (If so, that suggests we should indeed use the same version-aware
+// data layout here, and perhaps everywhere.)
+
+type database struct {
+	name string
+	u    *universe
+}
+
+var _ syncbase.DatabaseServerMethods = (*database)(nil)
+
+func (d *database) Create(call rpc.ServerCall, acl access.Permissions) error {
+	return store.RunInTransaction(d.u.s.st, func(st store.Store) error {
+		// Update universeData.
+		var uData *universeData
+		if err := d.u.update(call, st, true, func(data *universeData) error {
+			if _, ok := data.Databases[d.name]; ok {
+				return verror.New(verror.ErrExist, call.Context(), d.name)
+			}
+			// https://github.com/veyron/release-issues/issues/1145
+			if data.Databases == nil {
+				data.Databases = map[string]struct{}{}
+			}
+			data.Databases[d.name] = struct{}{}
+			uData = data
+			return nil
+		}); err != nil {
+			return err
+		}
+
+		// Blind-write databaseData.
+		if acl == nil {
+			acl = uData.Permissions
+		}
+		data := &databaseData{
+			Name:        d.name,
+			Permissions: acl,
+		}
+		return d.put(call, st, data)
+	})
+}
+
+func (d *database) Delete(call rpc.ServerCall) error {
+	return store.RunInTransaction(d.u.s.st, func(st store.Store) error {
+		// Read-check-delete databaseData.
+		if _, err := d.get(call, st, true); err != nil {
+			return err
+		}
+		// TODO(sadovsky): Delete all Tables (and Items) in this Database.
+		if err := d.del(call, st); err != nil {
+			return err
+		}
+
+		// Update universeData.
+		return d.u.update(call, st, false, func(data *universeData) error {
+			delete(data.Databases, d.name)
+			return nil
+		})
+	})
+}
+
+func (d *database) UpdateSchema(call rpc.ServerCall, schema syncbase.Schema, version string) error {
+	if schema == nil {
+		return verror.New(verror.ErrInternal, nil, "schema must be specified")
+	}
+
+	return store.RunInTransaction(d.u.s.st, func(st store.Store) error {
+		return d.update(call, st, true, func(data *databaseData) error {
+			if err := checkVersion(call, version, data.Version); err != nil {
+				return err
+			}
+			// TODO(sadovsky): Delete all Tables (and Items) that are no longer part
+			// of the schema.
+			data.Schema = schema
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (d *database) GetSchema(call rpc.ServerCall) (schema syncbase.Schema, version string, err error) {
+	data, err := d.get(call, d.u.s.st, true)
+	if err != nil {
+		return nil, "", err
+	}
+	return data.Schema, formatVersion(data.Version), nil
+}
+
+func (d *database) SetPermissions(call rpc.ServerCall, acl access.Permissions, version string) error {
+	return store.RunInTransaction(d.u.s.st, func(st store.Store) error {
+		return d.update(call, st, true, func(data *databaseData) error {
+			if err := checkVersion(call, version, data.Version); err != nil {
+				return err
+			}
+			data.Permissions = acl
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (d *database) GetPermissions(call rpc.ServerCall) (acl access.Permissions, version string, err error) {
+	data, err := d.get(call, d.u.s.st, true)
+	if err != nil {
+		return nil, "", err
+	}
+	return data.Permissions, formatVersion(data.Version), nil
+}
+
+// TODO(sadovsky): Implement Glob.
+
+////////////////////////////////////////
+// Internal helpers
+
+func (d *database) keyPart() string {
+	return joinKeyParts(d.u.keyPart(), d.name)
+}
+
+func (d *database) key() string {
+	return joinKeyParts("$database", d.keyPart())
+}
+
+// Note, the methods below use "x" as the receiver name to make find-replace
+// easier across the different levels of syncbase hierarchy.
+//
+// TODO(sadovsky): Is there any better way to share this code despite the lack
+// of generics?
+
+// Reads data from the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+func (x *database) get(call rpc.ServerCall, st store.Store, checkAuth bool) (*databaseData, error) {
+	// TODO(kash): Get this to compile.
+	return nil, nil
+	// data := &databaseData{}
+	// if err := getObject(st, x.key(), data); err != nil {
+	// 	if _, ok := err.(*store.ErrUnknownKey); ok {
+	// 		// TODO(sadovsky): Return ErrNoExist if appropriate.
+	// 		return nil, verror.NewErrNoExistOrNoAccess(call.Context())
+	// 	}
+	// 	return nil, verror.New(verror.ErrInternal, call.Context(), err)
+	// }
+	// if checkAuth {
+	// 	auth, _ := access.PermissionsAuthorizer(data.Permissions, access.TypicalTagType())
+	// 	if err := auth.Authorize(call); err != nil {
+	// 		// TODO(sadovsky): Return ErrNoAccess if appropriate.
+	// 		return nil, verror.NewErrNoExistOrNoAccess(call.Context())
+	// 	}
+	// }
+	// return data, nil
+}
+
+// Writes data to the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, use update().
+func (x *database) put(call rpc.ServerCall, st store.Store, data *databaseData) error {
+	if err := putObject(st, x.key(), data); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Deletes data from the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, call get() first.
+func (x *database) del(call rpc.ServerCall, st store.Store) error {
+	if err := st.Delete(x.key()); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Updates data in the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+// fn should perform the "modify" part of "read, modify, write", and should
+// return a VDL-compatible error.
+func (x *database) update(call rpc.ServerCall, st store.Store, checkAuth bool, fn func(data *databaseData) error) error {
+	data, err := x.get(call, st, checkAuth)
+	if err != nil {
+		return err
+	}
+	if err := fn(data); err != nil {
+		return err
+	}
+	return x.put(call, st, data)
+}
diff --git a/services/syncbase/server/dispatcher.go b/services/syncbase/server/dispatcher.go
new file mode 100644
index 0000000..5ea36d6
--- /dev/null
+++ b/services/syncbase/server/dispatcher.go
@@ -0,0 +1,78 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"strings"
+
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+)
+
+type dispatcher struct {
+	s *service
+}
+
+var _ rpc.Dispatcher = (*dispatcher)(nil)
+
+func NewDispatcher(s *service) *dispatcher {
+	return &dispatcher{s: s}
+}
+
+// TODO(sadovsky): Return a real authorizer in various places below.
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	suffix = strings.TrimPrefix(suffix, "/")
+	parts := strings.Split(suffix, "/")
+
+	// Validate all key atoms up front, so that we can avoid doing so in all our
+	// method implementations.
+	for _, s := range parts {
+		if !validKeyAtom(s) {
+			// TODO(sadovsky): Is it okay to pass a nil context to verror?
+			return nil, nil, syncbase.NewErrInvalidName(nil, suffix)
+		}
+	}
+
+	if len(parts) == 0 {
+		return syncbase.ServiceServer(d.s), nil, nil
+	}
+
+	universe := &universe{
+		name: parts[0],
+		s:    d.s,
+	}
+	if len(parts) == 1 {
+		return syncbase.UniverseServer(universe), nil, nil
+	}
+
+	database := &database{
+		name: parts[1],
+		u:    universe,
+	}
+	if len(parts) == 2 {
+		return syncbase.DatabaseServer(database), nil, nil
+	}
+
+	table := &table{
+		name: parts[2],
+		d:    database,
+	}
+	if len(parts) == 3 {
+		return syncbase.TableServer(table), nil, nil
+	}
+
+	item := &item{
+		encodedKey: parts[3],
+		t:          table,
+	}
+	if len(parts) == 4 {
+		return syncbase.ItemServer(item), nil, nil
+	}
+
+	// TODO(sadovsky): Is it okay to pass a nil context to verror?
+	return nil, nil, verror.NewErrNoExist(nil)
+}
diff --git a/services/syncbase/server/item.go b/services/syncbase/server/item.go
new file mode 100644
index 0000000..94f5df4
--- /dev/null
+++ b/services/syncbase/server/item.go
@@ -0,0 +1,115 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/rpc"
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+)
+
+// TODO(sadovsky): Extend data layout to support version tracking for sync.
+// See go/vanadium-local-structured-store.
+
+type item struct {
+	encodedKey string
+	t          *table
+}
+
+var _ syncbase.ItemServerMethods = (*item)(nil)
+
+func (i *item) Get(call rpc.ServerCall) (*vdl.Value, error) {
+	var value *vdl.Value
+	if err := store.RunInTransaction(i.t.d.u.s.st, func(st store.Store) error {
+		var err error
+		value, err = i.get(call, st)
+		return err
+	}); err != nil {
+		return nil, err
+	}
+	return value, nil
+}
+
+func (i *item) Put(call rpc.ServerCall, value *vdl.Value) error {
+	return store.RunInTransaction(i.t.d.u.s.st, func(st store.Store) error {
+		return i.put(call, st, value)
+	})
+}
+
+func (i *item) Delete(call rpc.ServerCall) error {
+	return store.RunInTransaction(i.t.d.u.s.st, func(st store.Store) error {
+		return i.del(call, st)
+	})
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (i *item) keyPart() string {
+	return joinKeyParts(i.t.keyPart(), i.encodedKey)
+}
+
+func (i *item) key() string {
+	return joinKeyParts("$item", i.keyPart())
+}
+
+// Performs authorization check (against the database Permissions) and checks that this
+// item's table exists in the database schema. Returns the schema and a
+// VDL-compatible error.
+func (i *item) checkAccess(call rpc.ServerCall, st store.Store) (syncbase.Schema, error) {
+	data, err := i.t.d.get(call, st, true)
+	if err != nil {
+		return nil, err
+	}
+	if _, ok := data.Schema[i.t.name]; !ok {
+		return nil, verror.NewErrNoExist(call.Context())
+	}
+	return data.Schema, nil
+}
+
+// Reads data from the storage engine.
+// Performs authorization check. Returns a VDL-compatible error.
+func (i *item) get(call rpc.ServerCall, st store.Store) (*vdl.Value, error) {
+	if _, err := i.checkAccess(call, st); err != nil {
+		return nil, err
+	}
+	value := &vdl.Value{}
+	if err := getObject(st, i.key(), value); err != nil {
+		if _, ok := err.(*store.ErrUnknownKey); ok {
+			// We've already done an auth check, so here we can safely return NoExist
+			// rather than NoExistOrNoAccess.
+			return nil, verror.NewErrNoExist(call.Context())
+		}
+		return nil, verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return value, nil
+}
+
+// Writes data to the storage engine.
+// Performs authorization check. Returns a VDL-compatible error.
+func (i *item) put(call rpc.ServerCall, st store.Store, value *vdl.Value) error {
+	// TODO(sadovsky): Check that value's primary key field matches i.encodedKey.
+	if _, err := i.checkAccess(call, st); err != nil {
+		return err
+	}
+	if err := putObject(st, i.key(), &value); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Deletes data from the storage engine.
+// Performs authorization check. Returns a VDL-compatible error.
+func (i *item) del(call rpc.ServerCall, st store.Store) error {
+	if _, err := i.checkAccess(call, st); err != nil {
+		return err
+	}
+	if err := st.Delete(i.key()); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
diff --git a/services/syncbase/server/key_util.go b/services/syncbase/server/key_util.go
new file mode 100644
index 0000000..7506b38
--- /dev/null
+++ b/services/syncbase/server/key_util.go
@@ -0,0 +1,22 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"regexp"
+	"strings"
+)
+
+// TODO(sadovsky): Consider loosening. Perhaps model after MySQL:
+// http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
+var keyAtomRegexp *regexp.Regexp = regexp.MustCompile("^[a-zA-Z0-9_.-]+$")
+
+func validKeyAtom(s string) bool {
+	return keyAtomRegexp.MatchString(s)
+}
+
+func joinKeyParts(parts ...string) string {
+	return strings.Join(parts, ":")
+}
diff --git a/services/syncbase/server/server_test.go b/services/syncbase/server/server_test.go
new file mode 100644
index 0000000..e12f396
--- /dev/null
+++ b/services/syncbase/server/server_test.go
@@ -0,0 +1,106 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server_test
+
+// Note: core/veyron/services/security/groups/server/server_test.go has some
+// helpful code snippets to model after.
+
+import (
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/x/lib/vlog"
+
+	"v.io/syncbase/x/ref/services/syncbase/server"
+	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
+	_ "v.io/x/ref/profiles"
+	tsecurity "v.io/x/ref/test/testutil"
+)
+
+func defaultPermissions() access.Permissions {
+	acl := access.Permissions{}
+	for _, tag := range access.AllTypicalTags() {
+		acl.Add(security.BlessingPattern("server/client"), string(tag))
+	}
+	return acl
+}
+
+func newServer(ctx *context.T, acl access.Permissions) (string, func()) {
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatal("v23.NewServer() failed: ", err)
+	}
+	eps, err := s.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		vlog.Fatal("s.Listen() failed: ", err)
+	}
+
+	service := server.NewService(memstore.New())
+	if acl == nil {
+		acl = defaultPermissions()
+	}
+	if err := service.Create(acl); err != nil {
+		vlog.Fatal("service.Create() failed: ", err)
+	}
+	d := server.NewDispatcher(service)
+
+	if err := s.ServeDispatcher("", d); err != nil {
+		vlog.Fatal("s.ServeDispatcher() failed: ", err)
+	}
+
+	name := naming.JoinAddressName(eps[0].String(), "")
+	return name, func() {
+		s.Stop()
+	}
+}
+
+func setupOrDie(acl access.Permissions) (clientCtx *context.T, serverName string, cleanup func()) {
+	ctx, shutdown := v23.Init()
+	cp, sp := tsecurity.NewPrincipal("client"), tsecurity.NewPrincipal("server")
+
+	// Have the server principal bless the client principal as "client".
+	blessings, err := sp.Bless(cp.PublicKey(), sp.BlessingStore().Default(), "client", security.UnconstrainedUse())
+	if err != nil {
+		vlog.Fatal("sp.Bless() failed: ", err)
+	}
+	// Have the client present its "client" blessing when talking to the server.
+	if _, err := cp.BlessingStore().Set(blessings, "server"); err != nil {
+		vlog.Fatal("cp.BlessingStore().Set() failed: ", err)
+	}
+	// Have the client treat the server's public key as an authority on all
+	// blessings that match the pattern "server".
+	if err := cp.AddToRoots(blessings); err != nil {
+		vlog.Fatal("cp.AddToRoots() failed: ", err)
+	}
+
+	clientCtx, err = v23.SetPrincipal(ctx, cp)
+	if err != nil {
+		vlog.Fatal("v23.SetPrincipal() failed: ", err)
+	}
+	serverCtx, err := v23.SetPrincipal(ctx, sp)
+	if err != nil {
+		vlog.Fatal("v23.SetPrincipal() failed: ", err)
+	}
+
+	serverName, stopServer := newServer(serverCtx, acl)
+	cleanup = func() {
+		stopServer()
+		shutdown()
+	}
+	return
+}
+
+////////////////////////////////////////
+// Test cases
+
+// TODO(sadovsky): Write some tests.
+func TestSomething(t *testing.T) {
+	_, _, cleanup := setupOrDie(nil)
+	defer cleanup()
+}
diff --git a/services/syncbase/server/service.go b/services/syncbase/server/service.go
new file mode 100644
index 0000000..ef30019
--- /dev/null
+++ b/services/syncbase/server/service.go
@@ -0,0 +1,144 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+)
+
+type service struct {
+	st store.TransactableStore
+}
+
+var _ syncbase.ServiceServerMethods = (*service)(nil)
+
+func NewService(st store.TransactableStore) *service {
+	return &service{st: st}
+}
+
+// Returns a VDL-compatible error.
+// Note, this is not an RPC method.
+func (s *service) Create(acl access.Permissions) error {
+	if acl == nil {
+		return verror.New(verror.ErrInternal, nil, "acl must be specified")
+	}
+
+	return store.RunInTransaction(s.st, func(st store.Store) error {
+		// TODO(sadovsky): Maybe add "has" method to storage engine.
+		data := &serviceData{}
+		if err := getObject(st, s.key(), data); err == nil {
+			return verror.NewErrExist(nil)
+		}
+
+		data = &serviceData{
+			Permissions: acl,
+		}
+		if err := putObject(st, s.key(), data); err != nil {
+			return verror.New(verror.ErrInternal, nil, "put failed")
+		}
+
+		return nil
+	})
+}
+
+func (s *service) SetPermissions(call rpc.ServerCall, acl access.Permissions, version string) error {
+	return store.RunInTransaction(s.st, func(st store.Store) error {
+		return s.update(call, st, true, func(data *serviceData) error {
+			if err := checkVersion(call, version, data.Version); err != nil {
+				return err
+			}
+			data.Permissions = acl
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (s *service) GetPermissions(call rpc.ServerCall) (acl access.Permissions, version string, err error) {
+	data, err := s.get(call, s.st, true)
+	if err != nil {
+		return nil, "", err
+	}
+	return data.Permissions, formatVersion(data.Version), nil
+}
+
+// TODO(sadovsky): Implement Glob.
+
+////////////////////////////////////////
+// Internal helpers
+
+func (s *service) key() string {
+	return "$service"
+}
+
+// Note, the methods below use "x" as the receiver name to make find-replace
+// easier across the different levels of syncbase hierarchy.
+//
+// TODO(sadovsky): Is there any better way to share this code despite the lack
+// of generics?
+
+// Reads data from the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+func (x *service) get(call rpc.ServerCall, st store.Store, checkAuth bool) (*serviceData, error) {
+	// TODO(kash): Get this to compile.
+	return nil, nil
+	// data := &serviceData{}
+	// if err := getObject(st, x.key(), data); err != nil {
+	// 	if _, ok := err.(*store.ErrUnknownKey); ok {
+	// 		// TODO(sadovsky): Return ErrNoExist if appropriate.
+	// 		return nil, verror.New(verror.ErrNoExistOrNoAccess, call.Context())
+	// 	}
+	// 	return nil, verror.New(verror.ErrInternal, call.Context(), err)
+	// }
+	// if checkAuth {
+	// 	auth, _ := access.PermissionsAuthorizer(data.Permissions, access.TypicalTagType())
+	// 	if err := auth.Authorize(call); err != nil {
+	// 		// TODO(sadovsky): Return ErrNoAccess if appropriate.
+	// 		return nil, verror.New(verror.ErrNoExistOrNoAccess, call.Context(), err)
+	// 	}
+	// }
+	// return data, nil
+}
+
+// Writes data to the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, use update().
+func (x *service) put(call rpc.ServerCall, st store.Store, data *serviceData) error {
+	if err := putObject(st, x.key(), data); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Deletes data from the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, call get() first.
+func (x *service) del(call rpc.ServerCall, st store.Store) error {
+	if err := st.Delete(x.key()); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Updates data in the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+// fn should perform the "modify" part of "read, modify, write", and should
+// return a VDL-compatible error.
+func (x *service) update(call rpc.ServerCall, st store.Store, checkAuth bool, fn func(data *serviceData) error) error {
+	data, err := x.get(call, st, checkAuth)
+	if err != nil {
+		return err
+	}
+	if err := fn(data); err != nil {
+		return err
+	}
+	return x.put(call, st, data)
+}
diff --git a/services/syncbase/server/store_util.go b/services/syncbase/server/store_util.go
new file mode 100644
index 0000000..fb09ad0
--- /dev/null
+++ b/services/syncbase/server/store_util.go
@@ -0,0 +1,42 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"strconv"
+
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+func getObject(st store.Store, k string, v interface{}) error {
+	bytes, err := st.Get(k)
+	if err != nil {
+		return err
+	}
+	return vom.Decode(bytes, v)
+}
+
+func putObject(st store.Store, k string, v interface{}) error {
+	bytes, err := vom.Encode(v)
+	if err != nil {
+		return err
+	}
+	return st.Put(k, bytes)
+}
+
+func formatVersion(version uint64) string {
+	return strconv.FormatUint(version, 10)
+}
+
+func checkVersion(call rpc.ServerCall, presented string, actual uint64) error {
+	if presented != "" && presented != formatVersion(actual) {
+		return verror.NewErrBadVersion(call.Context())
+	}
+	return nil
+}
diff --git a/services/syncbase/server/table.go b/services/syncbase/server/table.go
new file mode 100644
index 0000000..2c1af6a
--- /dev/null
+++ b/services/syncbase/server/table.go
@@ -0,0 +1,23 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import "v.io/syncbase/v23/services/syncbase"
+
+type table struct {
+	name string
+	d    *database
+}
+
+var _ syncbase.TableServerMethods = (*table)(nil)
+
+// TODO(sadovsky): Implement Glob.
+
+////////////////////////////////////////
+// Internal helpers
+
+func (t *table) keyPart() string {
+	return joinKeyParts(t.d.keyPart(), t.name)
+}
diff --git a/services/syncbase/server/types.vdl b/services/syncbase/server/types.vdl
new file mode 100644
index 0000000..8e7a751
--- /dev/null
+++ b/services/syncbase/server/types.vdl
@@ -0,0 +1,33 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/v23/security/access"
+)
+
+// serviceData represents the persistent state of a Service.
+type serviceData struct {
+	Universes   set[string]
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+}
+
+// universeData represents the persistent state of a Universe.
+type universeData struct {
+	Name        string
+	Databases   set[string]
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+}
+
+// databaseData represents the persistent state of a Database.
+type databaseData struct {
+	Name        string
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+	Schema      syncbase.Schema
+}
diff --git a/services/syncbase/server/types.vdl.go b/services/syncbase/server/types.vdl.go
new file mode 100644
index 0000000..d029dd1
--- /dev/null
+++ b/services/syncbase/server/types.vdl.go
@@ -0,0 +1,61 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package server
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/v23/security/access"
+)
+
+// serviceData represents the persistent state of a Service.
+type serviceData struct {
+	Universes   map[string]struct{}
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+}
+
+func (serviceData) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/server.serviceData"
+}) {
+}
+
+// universeData represents the persistent state of a Universe.
+type universeData struct {
+	Name        string
+	Databases   map[string]struct{}
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+}
+
+func (universeData) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/server.universeData"
+}) {
+}
+
+// databaseData represents the persistent state of a Database.
+type databaseData struct {
+	Name        string
+	Version     uint64 // covers the fields below
+	Permissions access.Permissions
+	Schema      syncbase.Schema
+}
+
+func (databaseData) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/server.databaseData"
+}) {
+}
+
+func init() {
+	vdl.Register((*serviceData)(nil))
+	vdl.Register((*universeData)(nil))
+	vdl.Register((*databaseData)(nil))
+}
diff --git a/services/syncbase/server/universe.go b/services/syncbase/server/universe.go
new file mode 100644
index 0000000..8f0f9b7
--- /dev/null
+++ b/services/syncbase/server/universe.go
@@ -0,0 +1,170 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/v23/services/syncbase"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+)
+
+type universe struct {
+	name string
+	s    *service
+}
+
+var _ syncbase.UniverseServerMethods = (*universe)(nil)
+
+func (u *universe) Create(call rpc.ServerCall, acl access.Permissions) error {
+	return store.RunInTransaction(u.s.st, func(st store.Store) error {
+		// Update serviceData.
+		var sData *serviceData
+		if err := u.s.update(call, st, true, func(data *serviceData) error {
+			if _, ok := data.Universes[u.name]; ok {
+				return verror.New(verror.ErrExist, call.Context(), u.name)
+			}
+			// https://github.com/veyron/release-issues/issues/1145
+			if data.Universes == nil {
+				data.Universes = map[string]struct{}{}
+			}
+			data.Universes[u.name] = struct{}{}
+			sData = data
+			return nil
+		}); err != nil {
+			return err
+		}
+
+		// Blind-write universeData.
+		if acl == nil {
+			acl = sData.Permissions
+		}
+		data := &universeData{
+			Name:        u.name,
+			Permissions: acl,
+		}
+		return u.put(call, st, data)
+	})
+}
+
+func (u *universe) Delete(call rpc.ServerCall) error {
+	return store.RunInTransaction(u.s.st, func(st store.Store) error {
+		// Read-check-delete universeData.
+		if _, err := u.get(call, st, true); err != nil {
+			return err
+		}
+		// TODO(sadovsky): Delete all Databases in this Universe.
+		if err := u.del(call, st); err != nil {
+			return err
+		}
+
+		// Update serviceData.
+		return u.s.update(call, st, false, func(data *serviceData) error {
+			delete(data.Universes, u.name)
+			return nil
+		})
+	})
+}
+
+func (u *universe) SetPermissions(call rpc.ServerCall, acl access.Permissions, version string) error {
+	return store.RunInTransaction(u.s.st, func(st store.Store) error {
+		return u.update(call, st, true, func(data *universeData) error {
+			if err := checkVersion(call, version, data.Version); err != nil {
+				return err
+			}
+			data.Permissions = acl
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (u *universe) GetPermissions(call rpc.ServerCall) (acl access.Permissions, version string, err error) {
+	data, err := u.get(call, u.s.st, true)
+	if err != nil {
+		return nil, "", err
+	}
+	return data.Permissions, formatVersion(data.Version), nil
+}
+
+// TODO(sadovsky): Implement Glob.
+
+////////////////////////////////////////
+// Internal helpers
+
+func (u *universe) keyPart() string {
+	return u.name
+}
+
+func (u *universe) key() string {
+	return joinKeyParts("$universe", u.keyPart())
+}
+
+// Note, the methods below use "x" as the receiver name to make find-replace
+// easier across the different levels of syncbase hierarchy.
+//
+// TODO(sadovsky): Is there any better way to share this code despite the lack
+// of generics?
+
+// Reads data from the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+func (x *universe) get(call rpc.ServerCall, st store.Store, checkAuth bool) (*universeData, error) {
+	// TODO(kash): Get this to compile.
+	return nil, nil
+	// data := &universeData{}
+	// if err := getObject(st, x.key(), data); err != nil {
+	// 	if _, ok := err.(*store.ErrUnknownKey); ok {
+	// 		// TODO(sadovsky): Return ErrNoExist if appropriate.
+	// 		return nil, verror.NewErrNoExistOrNoAccess(call.Context())
+	// 	}
+	// 	return nil, verror.New(verror.ErrInternal, call.Context(), err)
+	// }
+	// if checkAuth {
+	// 	auth, _ := access.PermissionsAuthorizer(data.Permissions, access.TypicalTagType())
+	// 	if err := auth.Authorize(call); err != nil {
+	// 		// TODO(sadovsky): Return ErrNoAccess if appropriate.
+	// 		return nil, verror.NewErrNoExistOrNoAccess(call.Context())
+	// 	}
+	// }
+	// return data, nil
+}
+
+// Writes data to the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, use update().
+func (x *universe) put(call rpc.ServerCall, st store.Store, data *universeData) error {
+	if err := putObject(st, x.key(), data); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Deletes data from the storage engine.
+// Returns a VDL-compatible error.
+// If you need to perform an authorization check, call get() first.
+func (x *universe) del(call rpc.ServerCall, st store.Store) error {
+	if err := st.Delete(x.key()); err != nil {
+		return verror.New(verror.ErrInternal, call.Context(), err)
+	}
+	return nil
+}
+
+// Updates data in the storage engine.
+// Returns a VDL-compatible error.
+// checkAuth specifies whether to perform an authorization check.
+// fn should perform the "modify" part of "read, modify, write", and should
+// return a VDL-compatible error.
+func (x *universe) update(call rpc.ServerCall, st store.Store, checkAuth bool, fn func(data *universeData) error) error {
+	data, err := x.get(call, st, checkAuth)
+	if err != nil {
+		return err
+	}
+	if err := fn(data); err != nil {
+		return err
+	}
+	return x.put(call, st, data)
+}
diff --git a/services/syncbase/store/memstore/memstore.go b/services/syncbase/store/memstore/memstore.go
new file mode 100644
index 0000000..f9de50a
--- /dev/null
+++ b/services/syncbase/store/memstore/memstore.go
@@ -0,0 +1,179 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package memstore provides a simple, in-memory implementation of
+// store.TransactableStore. Since it's a prototype implementation, it makes no
+// attempt to be performant.
+package memstore
+
+import (
+	"errors"
+	"sync"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+var (
+	txnTimeout       = time.Duration(5) * time.Second
+	errExpiredTxn    = errors.New("expired transaction")
+	errCommittedTxn  = errors.New("committed transaction")
+	errAbortedTxn    = errors.New("aborted transaction")
+	errConcurrentTxn = errors.New("concurrent transaction")
+)
+
+type transaction struct {
+	st *memstore
+	// The following fields are used to determine whether method calls should
+	// error out.
+	err         error
+	seq         uint64
+	createdTime time.Time
+	// The following fields track writes performed against this transaction.
+	puts    map[string][]byte
+	deletes map[string]struct{}
+}
+
+var _ store.Transaction = (*transaction)(nil)
+
+type memstore struct {
+	mu   sync.Mutex
+	data map[string][]byte
+	// Most recent sequence number handed out.
+	lastSeq uint64
+	// Value of lastSeq at the time of the most recent commit.
+	lastCommitSeq uint64
+}
+
+var _ store.TransactableStore = (*memstore)(nil)
+
+func New() store.TransactableStore {
+	return &memstore{data: map[string][]byte{}}
+}
+
+////////////////////////////////////////
+// transaction methods
+
+func newTxn(st *memstore, seq uint64) *transaction {
+	return &transaction{
+		st:          st,
+		seq:         seq,
+		createdTime: time.Now(),
+		puts:        map[string][]byte{},
+		deletes:     map[string]struct{}{},
+	}
+}
+
+func (tx *transaction) expired() bool {
+	return time.Now().After(tx.createdTime.Add(txnTimeout))
+}
+
+func (tx *transaction) checkError() error {
+	if tx.err != nil {
+		return tx.err
+	}
+	if tx.expired() {
+		return errExpiredTxn
+	}
+	if tx.seq <= tx.st.lastCommitSeq {
+		return errConcurrentTxn
+	}
+	return nil
+}
+
+func (tx *transaction) Get(k string) ([]byte, error) {
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	if err := tx.checkError(); err != nil {
+		return nil, err
+	}
+	v, ok := tx.st.data[k]
+	if !ok {
+		return nil, &store.ErrUnknownKey{Key: k}
+	}
+	return v, nil
+}
+
+func (tx *transaction) Put(k string, v []byte) error {
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	if err := tx.checkError(); err != nil {
+		return err
+	}
+	delete(tx.deletes, k)
+	tx.puts[k] = v
+	return nil
+}
+
+func (tx *transaction) Delete(k string) error {
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	if err := tx.checkError(); err != nil {
+		return err
+	}
+	delete(tx.puts, k)
+	tx.deletes[k] = struct{}{}
+	return nil
+}
+
+func (tx *transaction) Commit() error {
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	if err := tx.checkError(); err != nil {
+		return err
+	}
+	tx.err = errCommittedTxn
+	for k, v := range tx.puts {
+		tx.st.data[k] = v
+	}
+	for k := range tx.deletes {
+		delete(tx.st.data, k)
+	}
+	tx.st.lastCommitSeq = tx.st.lastSeq
+	return nil
+}
+
+func (tx *transaction) Abort() error {
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	if err := tx.checkError(); err != nil {
+		return err
+	}
+	tx.err = errAbortedTxn
+	return nil
+}
+
+////////////////////////////////////////
+// memstore methods
+
+func (st *memstore) Get(k string) ([]byte, error) {
+	var v []byte
+	if err := store.RunInTransaction(st, func(st store.Store) error {
+		var err error
+		v, err = st.Get(k)
+		return err
+	}); err != nil {
+		return nil, err
+	}
+	return v, nil
+}
+
+func (st *memstore) Put(k string, v []byte) error {
+	return store.RunInTransaction(st, func(st store.Store) error {
+		return st.Put(k, v)
+	})
+}
+
+func (st *memstore) Delete(k string) error {
+	return store.RunInTransaction(st, func(st store.Store) error {
+		return st.Delete(k)
+	})
+}
+
+func (st *memstore) CreateTransaction() store.Transaction {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	st.lastSeq++
+	return newTxn(st, st.lastSeq)
+}
diff --git a/services/syncbase/store/model.go b/services/syncbase/store/model.go
new file mode 100644
index 0000000..ce18640
--- /dev/null
+++ b/services/syncbase/store/model.go
@@ -0,0 +1,81 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package store defines the API for the syncbase storage engine.
+// Currently, this API and its implementations are meant to be internal.
+package store
+
+// Store is a CRUD-capable storage engine.
+// TODO(sadovsky): For convenience, we use string (rather than []byte) as the
+// key type. Maybe revisit this.
+// TODO(sadovsky): Maybe separate Insert/Update/Upsert for implementation
+// convenience. Or, provide these as helpers on top of the API below.
+type Store interface {
+	// Get returns the value for the given key.
+	// Fails if the given key is unknown (ErrUnknownKey).
+	Get(k string) ([]byte, error)
+
+	// Put writes the given value for the given key.
+	Put(k string, v []byte) error
+
+	// Delete deletes the entry for the given key.
+	// Succeeds (no-op) if the given key is unknown.
+	Delete(k string) error
+}
+
+// TransactableStore is a Store that supports transactions.
+// It should be possible to implement using LevelDB, SQLite, or similar.
+type TransactableStore interface {
+	Store
+
+	// CreateTransaction creates a transaction.
+	CreateTransaction() Transaction
+
+	// TODO(sadovsky): Figure out how sync's "PutMutations" fits in here. In
+	// theory it avoids a read (since it knows the version, i.e. the etag, up
+	// front), but OTOH that version still must be read at commit time (for
+	// verification), so reading it in a transaction (and thus adding it to the
+	// transaction's read-set) incurs no extra cost if we use transaction
+	// implementation strategy #1 from the doc. That said, strategy #1 is not
+	// tenable in a cloud environment. Perhaps Transaction should provide some
+	// mechanism for directly adding to its read-set (without actually reading)?
+}
+
+// Transaction provides a mechanism for atomic reads and writes.
+// Reads don't reflect writes performed inside this transaction. (This
+// limitation is imposed for API parity with Spanner.)
+// Once a transaction has been committed or aborted, subsequent method calls
+// will fail with no effect.
+type Transaction interface {
+	Store
+
+	// Commit commits the transaction.
+	Commit() error
+
+	// Abort aborts the transaction.
+	Abort() error
+}
+
+// TODO(sadovsky): Maybe put this elsewhere.
+// TODO(sadovsky): Add retry loop.
+func RunInTransaction(st TransactableStore, fn func(st Store) error) error {
+	tx := st.CreateTransaction()
+	if err := fn(tx); err != nil {
+		tx.Abort()
+		return err
+	}
+	if err := tx.Commit(); err != nil {
+		tx.Abort()
+		return err
+	}
+	return nil
+}
+
+type ErrUnknownKey struct {
+	Key string
+}
+
+func (err *ErrUnknownKey) Error() string {
+	return "unknown key: " + err.Key
+}
diff --git a/services/syncbase/sync/dag.go b/services/syncbase/sync/dag.go
new file mode 100644
index 0000000..69f467b
--- /dev/null
+++ b/services/syncbase/sync/dag.go
@@ -0,0 +1,1183 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Veyron Sync DAG (directed acyclic graph) utility functions.
+// The DAG is used to track the version history of objects in order to
+// detect and resolve conflicts (concurrent changes on different devices).
+//
+// Terminology:
+// * An object is a unique value in the Veyron Store represented by its UID.
+// * As an object mutates, its version number is updated by the Store.
+// * Each (object, version) tuple is represented by a node in the Sync DAG.
+// * The previous version of an object is its parent in the DAG, i.e. the
+//   new version is derived from that parent.
+// * When there are no conflicts, the node has a single reference back to
+//   a parent node.
+// * When a conflict between two concurrent object versions is resolved,
+//   the new version has references back to each of the two parents to
+//   indicate that it is derived from both nodes.
+// * During a sync operation from a source device to a target device, the
+//   target receives a DAG fragment from the source.  That fragment has to
+//   be incorporated (grafted) into the target device's DAG.  It may be a
+//   continuation of the DAG of an object, with the attachment (graft) point
+//   being the current head of DAG, in which case there are no conflicts.
+//   Or the graft point(s) may be older nodes, which means the new fragment
+//   is a divergence in the graph causing a conflict that must be resolved
+//   in order to re-converge the two DAG fragments.
+//
+// In the diagrams below:
+// (h) represents the head node in the local device.
+// (nh) represents the new head node received from the remote device.
+// (g) represents a graft node, where new nodes attach to the existing DAG.
+// <- represents a derived-from mutation, i.e. a child-to-parent pointer
+//
+// a- No-conflict example: the new nodes (v3, v4) attach to the head node (v2).
+//    In this case the new head becomes the head node, the new DAG fragment
+//    being a continuation of the existing DAG.
+//
+//    Before:
+//    v0 <- v1 <- v2(h)
+//
+//    Sync updates applied, no conflict detected:
+//    v0 <- v1 <- v2(h,g) <- v3 <- v4 (nh)
+//
+//    After:
+//    v0 <- v1 <- v2 <- v3 <- v4 (h)
+//
+// b- Conflict example: the new nodes (v3, v4) attach to an old node (v1).
+//    The current head node (v2) and the new head node (v4) are divergent
+//    (concurrent) mutations that need to be resolved.  The conflict
+//    resolution function is passed the old head (v2), new head (v4), and
+//    the common ancestor (v1) and resolves the conflict with (v5) which
+//    is represented in the DAG as derived from both v2 and v4 (2 parents).
+//
+//    Before:
+//    v0 <- v1 <- v2(h)
+//
+//    Sync updates applied, conflict detected (v2 not a graft node):
+//    v0 <- v1(g) <- v2(h)
+//                <- v3 <- v4 (nh)
+//
+//    After, conflict resolver creates v5 having 2 parents (v2, v4):
+//    v0 <- v1(g) <- v2 <------- v5(h)
+//                <- v3 <- v4 <-
+//
+// Note: the DAG does not grow indefinitely.  During a sync operation each
+// device learns what the other device already knows -- where it's at in
+// the version history for the objects.  When a device determines that all
+// devices that sync an object (as per the definitions of replication groups
+// in the Veyron Store) have moved past some version for that object, the
+// DAG for that object can be pruned, deleting all prior (ancestor) nodes.
+//
+// The DAG DB contains four tables persisted to disk (nodes, heads, trans,
+// priv) and three in-memory (ephemeral) maps (graft, txSet, txGC):
+//   * nodes: one entry per (object, version) with references to the
+//            parent node(s) it is derived from, a reference to the
+//            log record identifying that change, a reference to its
+//            transaction set (or NoTxId if none), and a boolean to
+//            indicate whether this change was a deletion of the object.
+//   * heads: one entry per object pointing to its most recent version
+//            in the nodes table
+//   * trans: one entry per transaction ID containing the set of objects
+//            that forms the transaction and their versions.
+//   * priv:  one entry per object ID for objects that are private to the
+//            store, not shared through any SyncGroup.
+//   * graft: during a sync operation, it tracks the nodes where the new
+//            DAG fragments are attached to the existing graph for each
+//            mutated object.  This map is used to determine whether a
+//            conflict happened for an object and, if yes, select the most
+//            recent common ancestor from these graft points to use when
+//            resolving the conflict.  At the end of a sync operation the
+//            graft map is destroyed.
+//   * txSet: used to incrementally construct the transaction sets that
+//            are stored in the "trans" table once all the nodes of a
+//            transaction have been added.  Multiple transaction sets
+//            can be constructed to support the concurrency between the
+//            Sync Initiator and Watcher threads.
+//   * txGC:  used to track the transactions impacted by objects being
+//            pruned.  At the end of the pruning operation the records
+//            of the "trans" table are updated from the txGC information.
+//
+// Note: for regular (no-conflict) changes, a node has a reference to
+// one parent from which it was derived.  When a conflict is resolved,
+// the new node has references to the two concurrent parents that triggered
+// the conflict.  The states of the parents[] array are:
+//   * []            The earliest/first version of an object
+//   * [XYZ]         Regular non-conflict version derived from XYZ
+//   * [XYZ, ABC]    Resolution version caused by XYZ-vs-ABC conflict
+
+import (
+	"container/list"
+	"errors"
+	"fmt"
+	"math/rand"
+	"strconv"
+	"time"
+
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/stats"
+)
+
+const (
+	NoTxId = TxId(0)
+)
+
+var (
+	errBadDAG = errors.New("invalid DAG")
+)
+
+type dagTxMap map[ObjId]Version
+
+// dagTxState tracks the state of a transaction.
+type dagTxState struct {
+	TxMap   dagTxMap
+	TxCount uint32
+}
+
+type dag struct {
+	fname string               // file pathname
+	store *kvdb                // underlying K/V store
+	heads *kvtable             // pointer to "heads" table in the store
+	nodes *kvtable             // pointer to "nodes" table in the store
+	trans *kvtable             // pointer to "trans" table in the store
+	priv  *kvtable             // pointer to "priv" table in the store
+	graft map[ObjId]*graftInfo // in-memory state of DAG object grafting
+	txSet map[TxId]*dagTxState // in-memory construction of transaction sets
+	txGC  map[TxId]dagTxMap    // in-memory tracking of transaction sets to cleanup
+	txGen *rand.Rand           // transaction ID random number generator
+
+	// DAG stats
+	numObj  *stats.Integer // number of objects
+	numNode *stats.Integer // number of versions across all objects
+	numTx   *stats.Integer // number of transactions tracked
+	numPriv *stats.Integer // number of private objects
+}
+
+type dagNode struct {
+	Level   uint64    // node distance from root
+	Parents []Version // references to parent versions
+	Logrec  string    // reference to log record change
+	TxId    TxId      // ID of a transaction set
+	Deleted bool      // true if the change was a delete
+}
+
+type graftInfo struct {
+	newNodes   map[Version]struct{} // set of newly added nodes during a sync
+	graftNodes map[Version]uint64   // set of graft nodes and their level
+	newHeads   map[Version]struct{} // set of candidate new head nodes
+}
+
+type privNode struct {
+	//Mutation *raw.Mutation // most recent store mutation for a private (unshared) object
+	PathIDs  []ObjId // store IDs in the path from the object to the root of the store
+	SyncTime int64   // SyncTime is the timestamp of the mutation when it arrives at the Sync server.
+	TxId     TxId    // ID of the transaction in which this mutation was done
+	TxCount  uint32  // total number of object mutations in that transaction
+}
+
+// openDAG opens or creates a DAG for the given filename.
+func openDAG(filename string) (*dag, error) {
+	// Open the file and create it if it does not exist.
+	// Also initialize the store and its tables.
+	db, tbls, err := kvdbOpen(filename, []string{"heads", "nodes", "trans", "priv"})
+	if err != nil {
+		return nil, err
+	}
+
+	d := &dag{
+		fname:   filename,
+		store:   db,
+		heads:   tbls[0],
+		nodes:   tbls[1],
+		trans:   tbls[2],
+		priv:    tbls[3],
+		txGen:   rand.New(rand.NewSource(time.Now().UTC().UnixNano())),
+		txSet:   make(map[TxId]*dagTxState),
+		numObj:  stats.NewInteger(statsNumDagObj),
+		numNode: stats.NewInteger(statsNumDagNode),
+		numTx:   stats.NewInteger(statsNumDagTx),
+		numPriv: stats.NewInteger(statsNumDagPrivNode),
+	}
+
+	// Initialize the stats counters from the tables.
+	d.numObj.Set(int64(d.heads.getNumKeys()))
+	d.numNode.Set(int64(d.nodes.getNumKeys()))
+	d.numTx.Set(int64(d.trans.getNumKeys()))
+	d.numPriv.Set(int64(d.priv.getNumKeys()))
+
+	d.clearGraft()
+	d.clearTxGC()
+
+	return d, nil
+}
+
+// close closes the DAG and invalidates its structure.
+func (d *dag) close() {
+	if d.store != nil {
+		d.store.close() // this also closes the tables
+		stats.Delete(statsNumDagObj)
+		stats.Delete(statsNumDagNode)
+		stats.Delete(statsNumDagTx)
+		stats.Delete(statsNumDagPrivNode)
+	}
+	*d = dag{} // zero out the DAG struct
+}
+
+// flush flushes the DAG store to disk.
+func (d *dag) flush() {
+	if d.store != nil {
+		d.store.flush()
+	}
+}
+
+// clearGraft clears the temporary in-memory grafting maps.
+func (d *dag) clearGraft() {
+	if d.store != nil {
+		d.graft = make(map[ObjId]*graftInfo)
+	}
+}
+
+// clearTxGC clears the temporary in-memory transaction garbage collection maps.
+func (d *dag) clearTxGC() {
+	if d.store != nil {
+		d.txGC = make(map[TxId]dagTxMap)
+	}
+}
+
+// getObjectGraft returns the graft structure for an object ID.
+// The graftInfo struct for an object is ephemeral (in-memory) and it
+// tracks the following information:
+// - newNodes:   the set of newly added nodes used to detect the type of
+//               edges between nodes (new-node to old-node or vice versa).
+// - newHeads:   the set of new candidate head nodes used to detect conflicts.
+// - graftNodes: the set of nodes used to find common ancestors between
+//               conflicting nodes.
+//
+// After the received Sync logs are applied, if there are two new heads in
+// the newHeads set, there is a conflict to be resolved for this object.
+// Otherwise if there is only one head, no conflict was triggered and the
+// new head becomes the current version for the object.
+//
+// In case of conflict, the graftNodes set is used to select the common
+// ancestor to pass to the conflict resolver.
+//
+// Note: if an object's graft structure does not exist only create it
+// if the "create" parameter is set to true.
+func (d *dag) getObjectGraft(oid ObjId, create bool) *graftInfo {
+	graft := d.graft[oid]
+	if graft == nil && create {
+		graft = &graftInfo{
+			newNodes:   make(map[Version]struct{}),
+			graftNodes: make(map[Version]uint64),
+			newHeads:   make(map[Version]struct{}),
+		}
+
+		// If a current head node exists for this object, initialize
+		// the set of candidate new heads to include it.
+		head, err := d.getHead(oid)
+		if err == nil {
+			graft.newHeads[head] = struct{}{}
+		}
+
+		d.graft[oid] = graft
+	}
+	return graft
+}
+
+// addNodeTxStart generates a transaction ID and returns it to the
+// caller if a TxId is not specified.  This transaction ID is stored
+// as part of each log record. If a TxId is specified by the caller,
+// state corresponding to that TxId is instantiated. TxId is used to
+// track DAG nodes that are part of the same transaction.
+func (d *dag) addNodeTxStart(tid TxId) TxId {
+	if d.store == nil {
+		return NoTxId
+	}
+
+	// Check if "tid" already exists.
+	if tid != NoTxId {
+		if _, ok := d.txSet[tid]; ok {
+			return tid
+		}
+		txSt, err := d.getTransaction(tid)
+		if err == nil {
+			d.txSet[tid] = txSt
+			return tid
+		}
+	} else {
+		// Generate a random 64-bit transaction ID different than NoTxId.
+		// Also make sure the ID is not already being used.
+		for (tid == NoTxId) || (d.txSet[tid] != nil) {
+			// Generate an unsigned 64-bit random value by combining a
+			// random 63-bit value and a random 1-bit value.
+			tid = (TxId(d.txGen.Int63()) << 1) | TxId(d.txGen.Int63n(2))
+		}
+	}
+
+	// Initialize the in-memory object/version map for that transaction ID.
+	d.txSet[tid] = &dagTxState{TxMap: make(dagTxMap), TxCount: 0}
+	return tid
+}
+
+// addNodeTxEnd marks the end of a given transaction.
+// The DAG completes its internal tracking of the transaction information.
+func (d *dag) addNodeTxEnd(tid TxId, count uint32) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	if tid == NoTxId || count == 0 {
+		return fmt.Errorf("invalid TxState: %v %v", tid, count)
+	}
+
+	txSt, ok := d.txSet[tid]
+	if !ok {
+		return fmt.Errorf("unknown transaction ID: %v", tid)
+	}
+
+	// The first time a transaction (TxId) is ended, TxCount is
+	// zero while "count" is not. Subsequently if this TxId is
+	// started and ended, TxCount should be the same as the
+	// incoming "count".
+	if txSt.TxCount != 0 && txSt.TxCount != count {
+		return fmt.Errorf("incorrect counts for transaction: %v (%v %v)", tid, txSt.TxCount, count)
+	}
+
+	// Only save non-empty transactions, i.e. those that have at least
+	// one mutation on a shared (non-private) object.
+	if len(txSt.TxMap) > 0 {
+		txSt.TxCount = count
+		if err := d.setTransaction(tid, txSt); err != nil {
+			return err
+		}
+	}
+
+	delete(d.txSet, tid)
+	return nil
+}
+
+// addNode adds a new node for an object in the DAG, linking it to its parent nodes.
+// It verifies that this node does not exist and that its parent nodes are valid.
+// It also determines the DAG level of the node from its parent nodes (max() + 1).
+//
+// If the node is due to a local change (from the Watcher API), no need to
+// update the grafting structure.  Otherwise the node is due to a remote change
+// (from the Sync protocol) being grafted on the DAG:
+// - If a parent node is not new, mark it as a DAG graft point.
+// - Mark this version as a new node.
+// - Update the new head node pointer of the grafted DAG.
+//
+// If the transaction ID is set to NoTxId, this node is not part of a transaction.
+// Otherwise, track its membership in the given transaction ID.
+func (d *dag) addNode(oid ObjId, version Version, remote, deleted bool,
+	parents []Version, logrec string, tid TxId) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+
+	if parents != nil {
+		if len(parents) > 2 {
+			return fmt.Errorf("cannot have more than 2 parents, not %d", len(parents))
+		}
+		if len(parents) == 0 {
+			// Replace an empty array with a nil.
+			parents = nil
+		}
+	}
+
+	// The new node must not exist.
+	if d.hasNode(oid, version) {
+		return fmt.Errorf("node %d:%d already exists in the DAG", oid, version)
+	}
+
+	// A new root node (no parents) is allowed only for new objects.
+	if parents == nil {
+		_, err := d.getHead(oid)
+		if err == nil {
+			return fmt.Errorf("cannot add another root node %d:%d for this object in the DAG", oid, version)
+		}
+	}
+
+	// For a remote change, make sure the object has a graft info entry.
+	// During a sync operation, each mutated object gets new nodes added
+	// in its DAG.  These new nodes are either derived from nodes that
+	// were previously known on this device (i.e. their parent nodes are
+	// pre-existing), or they are derived from other new DAG nodes being
+	// discovered during this sync (i.e. their parent nodes were also
+	// just added to the DAG).
+	//
+	// To detect a conflict and find the most recent common ancestor to
+	// pass to the conflict resolver callback, the DAG keeps track of the
+	// new nodes that have old parent nodes.  These old-to-new edges are
+	// the points where new DAG fragments are attached (grafted) onto the
+	// existing DAG.  The old nodes are the "graft nodes" and they form
+	// the set of possible common ancestors to use in case of conflict:
+	// 1- A conflict happens when the current "head node" for an object
+	//    is not in the set of graft nodes.  It means the object mutations
+	//    were not derived from what the device knows, but where divergent
+	//    changes from a prior point (from one of the graft nodes).
+	// 2- The most recent common ancestor to use in resolving the conflict
+	//    is the object graft node with the deepest level (furthest from
+	//    the origin root node), representing the most up-to-date common
+	//    knowledge between this device and the divergent changes.
+	//
+	// Note: at the end of a sync operation between 2 devices, the whole
+	// graft info is cleared (Syncd calls clearGraft()) to prepare it for
+	// the new pairwise sync operation.
+	graft := d.getObjectGraft(oid, remote)
+
+	// Verify the parents and determine the node level.
+	// Update the graft info in the DAG for this object.
+	var level uint64
+	for _, parent := range parents {
+		node, err := d.getNode(oid, parent)
+		if err != nil {
+			return err
+		}
+		if level <= node.Level {
+			level = node.Level + 1
+		}
+		if remote {
+			// If this parent is an old node, it's a graft point in the DAG
+			// and may be a common ancestor used during conflict resolution.
+			if _, ok := graft.newNodes[parent]; !ok {
+				graft.graftNodes[parent] = node.Level
+			}
+
+			// The parent nodes can no longer be candidates for new head versions.
+			if _, ok := graft.newHeads[parent]; ok {
+				delete(graft.newHeads, parent)
+			}
+		}
+	}
+
+	if remote {
+		// This new node is a candidate for new head version.
+		graft.newNodes[version] = struct{}{}
+		graft.newHeads[version] = struct{}{}
+	}
+
+	// If this node is part of a transaction, add it to that set.
+	if tid != NoTxId {
+		txSt, ok := d.txSet[tid]
+		if !ok {
+			return fmt.Errorf("unknown transaction ID: %v", tid)
+		}
+
+		txSt.TxMap[oid] = version
+	}
+
+	// Insert the new node in the kvdb.
+	node := &dagNode{Level: level, Parents: parents, Logrec: logrec, TxId: tid, Deleted: deleted}
+	if err := d.setNode(oid, version, node); err != nil {
+		return err
+	}
+
+	d.numNode.Incr(1)
+	if parents == nil {
+		d.numObj.Incr(1)
+	}
+	return nil
+}
+
+// hasNode returns true if the node (oid, version) exists in the DAG DB.
+func (d *dag) hasNode(oid ObjId, version Version) bool {
+	if d.store == nil {
+		return false
+	}
+	key := objNodeKey(oid, version)
+	return d.nodes.hasKey(key)
+}
+
+// addParent adds to the DAG node (oid, version) linkage to this parent node.
+// If the parent linkage is due to a local change (from conflict resolution
+// by blessing an existing version), no need to update the grafting structure.
+// Otherwise a remote change (from the Sync protocol) updates the graft.
+//
+// TODO(rdaoud): recompute the levels of reachable child-nodes if the new
+// parent's level is greater or equal to the node's current level.
+func (d *dag) addParent(oid ObjId, version, parent Version, remote bool) error {
+	if version == parent {
+		return fmt.Errorf("addParent: object %v: node %d cannot be its own parent", oid, version)
+	}
+
+	node, err := d.getNode(oid, version)
+	if err != nil {
+		return err
+	}
+
+	pnode, err := d.getNode(oid, parent)
+	if err != nil {
+		vlog.VI(1).Infof("addParent: object %v, node %d, parent %d: parent node not found", oid, version, parent)
+		return err
+	}
+
+	// Check if the parent is already linked to this node.
+	found := false
+	for i := range node.Parents {
+		if node.Parents[i] == parent {
+			found = true
+			break
+		}
+	}
+
+	// If the parent is not yet linked (local or remote) add it.
+	if !found {
+		// Make sure that adding the link does not create a cycle in the DAG.
+		// This is done by verifying that the node is not an ancestor of the
+		// parent that it is being linked to.
+		err = d.ancestorIter(oid, pnode.Parents, func(oid ObjId, v Version, nd *dagNode) error {
+			if v == version {
+				return fmt.Errorf("addParent: cycle on object %v: node %d is an ancestor of parent node %d",
+					oid, version, parent)
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+		node.Parents = append(node.Parents, parent)
+		err = d.setNode(oid, version, node)
+		if err != nil {
+			return err
+		}
+	}
+
+	// For local changes we are done, the grafting structure is not updated.
+	if !remote {
+		return nil
+	}
+
+	// If the node and its parent are new/old or old/new then add
+	// the parent as a graft point (a potential common ancestor).
+	graft := d.getObjectGraft(oid, true)
+
+	_, nodeNew := graft.newNodes[version]
+	_, parentNew := graft.newNodes[parent]
+	if (nodeNew && !parentNew) || (!nodeNew && parentNew) {
+		graft.graftNodes[parent] = pnode.Level
+	}
+
+	// The parent node can no longer be a candidate for a new head version.
+	// The addParent() function only removes candidates from newHeads that
+	// have become parents.  It does not add the child nodes to newHeads
+	// because they are not necessarily new-head candidates.  If they are
+	// new nodes, the addNode() function handles adding them to newHeads.
+	// For old nodes, only the current head could be a candidate and it is
+	// added to newHeads when the graft struct is initialized.
+	if _, ok := graft.newHeads[parent]; ok {
+		delete(graft.newHeads, parent)
+	}
+
+	return nil
+}
+
+// moveHead moves the object head node in the DAG.
+func (d *dag) moveHead(oid ObjId, head Version) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+
+	// Verify that the node exists.
+	if !d.hasNode(oid, head) {
+		return fmt.Errorf("node %d:%d does not exist in the DAG", oid, head)
+	}
+
+	return d.setHead(oid, head)
+}
+
+// hasConflict determines if there is a conflict for this object between its
+// new and old head nodes.
+// - Yes: return (true, newHead, oldHead, ancestor)
+// - No:  return (false, newHead, oldHead, NoVersion)
+// A conflict exists when there are two new-head nodes.  It means the newly
+// added object versions are not derived in part from this device's current
+// knowledge.  If there is a single new-head, the object changes were applied
+// without triggering a conflict.
+func (d *dag) hasConflict(oid ObjId) (isConflict bool, newHead, oldHead, ancestor Version, err error) {
+	oldHead = NoVersion
+	newHead = NoVersion
+	ancestor = NoVersion
+	if d.store == nil {
+		err = errBadDAG
+		return
+	}
+
+	graft := d.graft[oid]
+	if graft == nil {
+		err = fmt.Errorf("node %d has no DAG graft information", oid)
+		return
+	}
+
+	numHeads := len(graft.newHeads)
+	if numHeads < 1 || numHeads > 2 {
+		err = fmt.Errorf("node %d has invalid number of new head candidates %d: %v", oid, numHeads, graft.newHeads)
+		return
+	}
+
+	// Fetch the current head for this object if it exists.  The error from getHead()
+	// is ignored because a newly received object is not yet known on this device and
+	// will not trigger a conflict.
+	oldHead, _ = d.getHead(oid)
+
+	// If there is only one new head node there is no conflict.
+	// The new head is that single one, even if it might also be the same old node.
+	if numHeads == 1 {
+		for k := range graft.newHeads {
+			newHead = k
+		}
+		return
+	}
+
+	// With two candidate head nodes, the new one is the node that is
+	// not the current (old) head node.
+	for k := range graft.newHeads {
+		if k != oldHead {
+			newHead = k
+			break
+		}
+	}
+
+	// There is a conflict: the best choice ancestor is the graft point
+	// node with the largest level (farthest from the root).  It is
+	// possible in some corner cases to have multiple graft nodes at
+	// the same level.  This would still be a single conflict, but the
+	// multiple same-level graft points representing equivalent conflict
+	// resolutions on different devices that are now merging their
+	// resolutions.  In such a case it does not matter which node is
+	// chosen as the ancestor because the conflict resolver function
+	// is assumed to be convergent.  However it's nicer to make that
+	// selection deterministic so all devices see the same choice.
+	// For this the version number is used as a tie-breaker.
+	isConflict = true
+	var maxLevel uint64
+	for node, level := range graft.graftNodes {
+		if maxLevel < level ||
+			(maxLevel == level && ancestor < node) {
+			maxLevel = level
+			ancestor = node
+		}
+	}
+	return
+}
+
+// ancestorIter iterates over the DAG ancestor nodes for an object in a
+// breadth-first traversal starting from given version node(s).  In its
+// traversal it invokes the callback function once for each node, passing
+// the object ID, version number and a pointer to the dagNode.
+func (d *dag) ancestorIter(oid ObjId, startVersions []Version,
+	cb func(ObjId, Version, *dagNode) error) error {
+	visited := make(map[Version]bool)
+	queue := list.New()
+	for _, version := range startVersions {
+		queue.PushBack(version)
+		visited[version] = true
+	}
+
+	for queue.Len() > 0 {
+		version := queue.Remove(queue.Front()).(Version)
+		node, err := d.getNode(oid, version)
+		if err != nil {
+			// Ignore it, the parent was previously pruned.
+			continue
+		}
+		for _, parent := range node.Parents {
+			if !visited[parent] {
+				queue.PushBack(parent)
+				visited[parent] = true
+			}
+		}
+		if err = cb(oid, version, node); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// hasDeletedDescendant returns true if the node (oid, version) exists in the
+// DAG DB and one of its descendants is a deleted node (i.e. has its "Deleted"
+// flag set true).  This means that at some object mutation after this version,
+// the object was deleted.
+func (d *dag) hasDeletedDescendant(oid ObjId, version Version) bool {
+	if d.store == nil {
+		return false
+	}
+	if !d.hasNode(oid, version) {
+		return false
+	}
+
+	// Do a breadth-first traversal from the object's head node back to
+	// the given version.  Along the way, track whether a deleted node is
+	// traversed.  Return true only if a traversal reaches the given version
+	// and had seen a deleted node along the way.
+
+	// nodeStep tracks a step along a traversal.  It stores the node to visit
+	// when taking that step and a boolean tracking whether a deleted node
+	// was seen so far along that trajectory.
+	head, err := d.getHead(oid)
+	if err != nil {
+		return false
+	}
+
+	type nodeStep struct {
+		node    Version
+		deleted bool
+	}
+
+	visited := make(map[nodeStep]struct{})
+	queue := list.New()
+
+	step := nodeStep{node: head, deleted: false}
+	queue.PushBack(&step)
+	visited[step] = struct{}{}
+
+	for queue.Len() > 0 {
+		step := queue.Remove(queue.Front()).(*nodeStep)
+		if step.node == version {
+			if step.deleted {
+				return true
+			}
+			continue
+		}
+		node, err := d.getNode(oid, step.node)
+		if err != nil {
+			// Ignore it, the parent was previously pruned.
+			continue
+		}
+		nextDel := step.deleted || node.Deleted
+
+		for _, parent := range node.Parents {
+			nextStep := nodeStep{node: parent, deleted: nextDel}
+			if _, ok := visited[nextStep]; !ok {
+				queue.PushBack(&nextStep)
+				visited[nextStep] = struct{}{}
+			}
+		}
+	}
+
+	return false
+}
+
+// prune trims the DAG of an object at a given version (node) by deleting
+// all its ancestor nodes, making it the new root node.  For each deleted
+// node it calls the given callback function to delete its log record.
+// This function should only be called when Sync determines that all devices
+// that know about the object have gotten past this version.
+// Also track any transaction sets affected by deleting DAG objects that
+// have transaction IDs.  This is later used to do garbage collection
+// on transaction sets when pruneDone() is called.
+func (d *dag) prune(oid ObjId, version Version, delLogRec func(logrec string) error) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+
+	// Get the node at the pruning point and set its parents to nil.
+	// It will become the oldest DAG node (root) for the object.
+	node, err := d.getNode(oid, version)
+	if err != nil {
+		return err
+	}
+	if node.Parents == nil {
+		// Nothing to do, this node is already the root.
+		return nil
+	}
+
+	iterVersions := node.Parents
+
+	node.Parents = nil
+	if err = d.setNode(oid, version, node); err != nil {
+		return err
+	}
+
+	// Delete all ancestor nodes and their log records.
+	// Delete as many as possible and track the error counts.
+	// Keep track of objects deleted from transaction in order
+	// to cleanup transaction sets when pruneDone() is called.
+	numNodeErrs, numLogErrs := 0, 0
+	err = d.ancestorIter(oid, iterVersions, func(oid ObjId, v Version, node *dagNode) error {
+		nodeErrs, logErrs, err := d.removeNode(oid, v, node, delLogRec)
+		numNodeErrs += nodeErrs
+		numLogErrs += logErrs
+		return err
+	})
+	if err != nil {
+		return err
+	}
+	if numNodeErrs != 0 || numLogErrs != 0 {
+		return fmt.Errorf("prune failed to delete %d nodes and %d log records", numNodeErrs, numLogErrs)
+	}
+	return nil
+}
+
+// removeNode removes the state associated with a DAG node.
+func (d *dag) removeNode(oid ObjId, v Version, node *dagNode, delLogRec func(logrec string) error) (int, int, error) {
+	numNodeErrs, numLogErrs := 0, 0
+	if tid := node.TxId; tid != NoTxId {
+		if d.txGC[tid] == nil {
+			d.txGC[tid] = make(dagTxMap)
+		}
+		d.txGC[tid][oid] = v
+	}
+
+	if err := delLogRec(node.Logrec); err != nil {
+		numLogErrs++
+	}
+	if err := d.delNode(oid, v); err != nil {
+		numNodeErrs++
+	}
+	d.numNode.Incr(-1)
+	return numNodeErrs, numLogErrs, nil
+}
+
+// pruneAll prunes the entire DAG state corresponding to an object,
+// including the head.
+func (d *dag) pruneAll(oid ObjId, delLogRec func(logrec string) error) error {
+	vers, err := d.getHead(oid)
+	if err != nil {
+		return err
+	}
+	node, err := d.getNode(oid, vers)
+	if err != nil {
+		return err
+	}
+
+	if err := d.prune(oid, vers, delLogRec); err != nil {
+		return err
+	}
+
+	// Clean up the head.
+	numNodeErrs, numLogErrs, err := d.removeNode(oid, vers, node, delLogRec)
+	if err != nil {
+		return err
+	}
+	if numNodeErrs != 0 || numLogErrs != 0 {
+		return fmt.Errorf("pruneAll failed to delete %d nodes and %d log records", numNodeErrs, numLogErrs)
+	}
+
+	return d.delHead(oid)
+}
+
+// pruneDone is called when object pruning is finished within a single pass
+// of the Sync garbage collector.  It updates the transaction sets affected
+// by the objects deleted by the prune() calls.
+func (d *dag) pruneDone() error {
+	if d.store == nil {
+		return errBadDAG
+	}
+
+	// Update transaction sets by removing from them the objects that
+	// were pruned.  If the resulting set is empty, delete it.
+	for tid, txMapGC := range d.txGC {
+		txSt, err := d.getTransaction(tid)
+		if err != nil {
+			return err
+		}
+
+		for oid := range txMapGC {
+			delete(txSt.TxMap, oid)
+		}
+
+		if len(txSt.TxMap) > 0 {
+			err = d.setTransaction(tid, txSt)
+		} else {
+			err = d.delTransaction(tid)
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	d.clearTxGC()
+	return nil
+}
+
+// getLogrec returns the log record information for a given object version.
+func (d *dag) getLogrec(oid ObjId, version Version) (string, error) {
+	node, err := d.getNode(oid, version)
+	if err != nil {
+		return "", err
+	}
+	return node.Logrec, nil
+}
+
+// objNodeKey returns the key used to access the object node (oid, version)
+// in the DAG DB.
+func objNodeKey(oid ObjId, version Version) string {
+	return fmt.Sprintf("%s:%d", oid, version)
+}
+
+// setNode stores the dagNode structure for the object node (oid, version)
+// in the DAG DB.
+func (d *dag) setNode(oid ObjId, version Version, node *dagNode) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objNodeKey(oid, version)
+	return d.nodes.set(key, node)
+}
+
+// getNode retrieves the dagNode structure for the object node (oid, version)
+// from the DAG DB.
+func (d *dag) getNode(oid ObjId, version Version) (*dagNode, error) {
+	if d.store == nil {
+		return nil, errBadDAG
+	}
+	var node dagNode
+	key := objNodeKey(oid, version)
+	if err := d.nodes.get(key, &node); err != nil {
+		return nil, err
+	}
+	return &node, nil
+}
+
+// delNode deletes the object node (oid, version) from the DAG DB.
+func (d *dag) delNode(oid ObjId, version Version) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objNodeKey(oid, version)
+	return d.nodes.del(key)
+}
+
+// objHeadKey returns the key used to access the object head in the DAG DB.
+func objHeadKey(oid ObjId) string {
+	return oid.String()
+}
+
+// setHead stores version as the object head in the DAG DB.
+func (d *dag) setHead(oid ObjId, version Version) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objHeadKey(oid)
+	return d.heads.set(key, version)
+}
+
+// getHead retrieves the object head from the DAG DB.
+func (d *dag) getHead(oid ObjId) (Version, error) {
+	var version Version
+	if d.store == nil {
+		return version, errBadDAG
+	}
+	key := objHeadKey(oid)
+	err := d.heads.get(key, &version)
+	if err != nil {
+		version = NoVersion
+	}
+	return version, err
+}
+
+// delHead deletes the object head from the DAG DB.
+func (d *dag) delHead(oid ObjId) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objHeadKey(oid)
+	if err := d.heads.del(key); err != nil {
+		return err
+	}
+	d.numObj.Incr(-1)
+	return nil
+}
+
+// dagTransactionKey returns the key used to access the transaction in the DAG DB.
+func dagTransactionKey(tid TxId) string {
+	return fmt.Sprintf("%v", tid)
+}
+
+// setTransaction stores the transaction object/version map in the DAG DB.
+func (d *dag) setTransaction(tid TxId, txSt *dagTxState) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	if tid == NoTxId {
+		return fmt.Errorf("invalid TxId: %v", tid)
+	}
+	key := dagTransactionKey(tid)
+	exists := d.trans.hasKey(key)
+
+	if err := d.trans.set(key, txSt); err != nil {
+		return err
+	}
+
+	if !exists {
+		d.numTx.Incr(1)
+	}
+	return nil
+}
+
+// getTransaction retrieves the transaction object/version map from the DAG DB.
+func (d *dag) getTransaction(tid TxId) (*dagTxState, error) {
+	if d.store == nil {
+		return nil, errBadDAG
+	}
+	if tid == NoTxId {
+		return nil, fmt.Errorf("invalid TxId: %v", tid)
+	}
+	var txSt dagTxState
+	key := dagTransactionKey(tid)
+	if err := d.trans.get(key, &txSt); err != nil {
+		return nil, err
+	}
+	return &txSt, nil
+}
+
+// delTransaction deletes the transation object/version map from the DAG DB.
+func (d *dag) delTransaction(tid TxId) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	if tid == NoTxId {
+		return fmt.Errorf("invalid TxId: %v", tid)
+	}
+	key := dagTransactionKey(tid)
+	if err := d.trans.del(key); err != nil {
+		return err
+	}
+	d.numTx.Incr(-1)
+	return nil
+}
+
+// objPrivNodeKey returns the key used to access a private (unshared) node in the DAG DB.
+func objPrivNodeKey(oid ObjId) string {
+	return oid.String()
+}
+
+// setPrivNode stores the privNode structure for a private (unshared) object in the DAG DB.
+func (d *dag) setPrivNode(oid ObjId, priv *privNode) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objPrivNodeKey(oid)
+	exists := d.priv.hasKey(key)
+
+	if err := d.priv.set(key, priv); err != nil {
+		return err
+	}
+
+	if !exists {
+		d.numPriv.Incr(1)
+	}
+	return nil
+}
+
+// getPrivNode retrieves the privNode structure for a private (unshared) object from the DAG DB.
+func (d *dag) getPrivNode(oid ObjId) (*privNode, error) {
+	if d.store == nil {
+		return nil, errBadDAG
+	}
+	var priv privNode
+	key := objPrivNodeKey(oid)
+	if err := d.priv.get(key, &priv); err != nil {
+		return nil, err
+	}
+	return &priv, nil
+}
+
+// delPrivNode deletes a private (unshared) object from the DAG DB.
+func (d *dag) delPrivNode(oid ObjId) error {
+	if d.store == nil {
+		return errBadDAG
+	}
+	key := objPrivNodeKey(oid)
+	if err := d.priv.del(key); err != nil {
+		return err
+	}
+	d.numPriv.Incr(-1)
+	return nil
+}
+
+// getParentMap is a testing and debug helper function that returns for
+// an object a map of all the object version in the DAG and their parents.
+// The map represents the graph of the object version history.
+func (d *dag) getParentMap(oid ObjId) map[Version][]Version {
+	parentMap := make(map[Version][]Version)
+	var iterVersions []Version
+
+	if head, err := d.getHead(oid); err == nil {
+		iterVersions = append(iterVersions, head)
+	}
+	if graft := d.graft[oid]; graft != nil {
+		for k := range graft.newHeads {
+			iterVersions = append(iterVersions, k)
+		}
+	}
+
+	// Breadth-first traversal starting from the object head.
+	d.ancestorIter(oid, iterVersions, func(oid ObjId, v Version, node *dagNode) error {
+		parentMap[v] = node.Parents
+		return nil
+	})
+
+	return parentMap
+}
+
+// getGraftNodes is a testing and debug helper function that returns for
+// an object the graft information built and used during a sync operation.
+// The newHeads map identifies the candidate head nodes based on the data
+// reported by the other device during a sync operation.  The graftNodes map
+// identifies the set of old nodes where the new DAG fragments were attached
+// and their depth level in the DAG.
+func (d *dag) getGraftNodes(oid ObjId) (map[Version]struct{}, map[Version]uint64) {
+	if d.store != nil {
+		if ginfo := d.graft[oid]; ginfo != nil {
+			return ginfo.newHeads, ginfo.graftNodes
+		}
+	}
+	return nil, nil
+}
+
+// strToTxId converts from a string to a transaction ID.
+func strToTxId(txStr string) (TxId, error) {
+	tx, err := strconv.ParseUint(txStr, 10, 64)
+	if err != nil {
+		return NoTxId, err
+	}
+	return TxId(tx), nil
+}
+
+// dump writes to the log file information on all DAG entries.
+func (d *dag) dump() {
+	if d.store == nil {
+		return
+	}
+
+	// Dump the head and ancestor information for DAG objects.
+	d.heads.keyIter(func(oidStr string) {
+		oid, err := strToObjId(oidStr)
+		if err != nil {
+			return
+		}
+
+		head, err := d.getHead(oid)
+		if err != nil {
+			return
+		}
+
+		vlog.VI(1).Infof("DUMP: DAG oid %v: head %v", oid, head)
+		start := []Version{head}
+		d.ancestorIter(oid, start, func(oid ObjId, v Version, node *dagNode) error {
+			vlog.VI(1).Infof("DUMP: DAG node %v:%v: tx %v, del %t, logrec %s --> %v",
+				oid, v, node.TxId, node.Deleted, node.Logrec, node.Parents)
+			return nil
+		})
+	})
+
+	// Dump the transactions.
+	d.trans.keyIter(func(tidStr string) {
+		tid, err := strToTxId(tidStr)
+		if err != nil {
+			return
+		}
+
+		txSt, err := d.getTransaction(tid)
+		if err != nil {
+			return
+		}
+
+		vlog.VI(1).Infof("DUMP: DAG tx %v: count %d, elem %v", tid, txSt.TxCount, txSt.TxMap)
+	})
+}
diff --git a/services/syncbase/sync/dag_test.go b/services/syncbase/sync/dag_test.go
new file mode 100644
index 0000000..a8ee7ee
--- /dev/null
+++ b/services/syncbase/sync/dag_test.go
@@ -0,0 +1,2128 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Veyron Sync DAG component.
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/x/ref/lib/stats"
+)
+
+// dagFilename generates a filename for a temporary (per unit test) DAG file.
+// Do not replace this function with TempFile because TempFile creates the new
+// file and the tests must verify that the DAG can create a non-existing file.
+func dagFilename() string {
+	return fmt.Sprintf("%s/sync_dag_test_%d_%d", os.TempDir(), os.Getpid(), time.Now().UnixNano())
+}
+
+// TestDAGOpen tests the creation of a DAG, closing and re-opening it.  It also
+// verifies that its backing file is created and that a 2nd close is safe.
+func TestDAGOpen(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	fsize := getFileSize(dagfile)
+	if fsize < 0 {
+		//t.Fatalf("DAG file %s not created", dagfile)
+	}
+
+	dag.flush()
+	oldfsize := fsize
+	fsize = getFileSize(dagfile)
+	if fsize <= oldfsize {
+		//t.Fatalf("DAG file %s not flushed", dagfile)
+	}
+
+	dag.close()
+
+	dag, err = openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot re-open existing DAG file %s", dagfile)
+	}
+
+	oldfsize = fsize
+	fsize = getFileSize(dagfile)
+	if fsize != oldfsize {
+		t.Fatalf("DAG file %s size changed across re-open", dagfile)
+	}
+
+	dag.close()
+	dag.close() // multiple closes should be a safe NOP
+
+	fsize = getFileSize(dagfile)
+	if fsize != oldfsize {
+		t.Fatalf("DAG file %s size changed across close", dagfile)
+	}
+
+	// Fail opening a DAG in a non-existent directory.
+	_, err = openDAG("/not/really/there/junk.dag")
+	if err == nil {
+		//t.Fatalf("openDAG() did not fail when using a bad pathname")
+	}
+}
+
+// TestInvalidDAG tests using DAG methods on an invalid (closed) DAG.
+func TestInvalidDAG(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	dag.close()
+
+	oid, err := strToObjId("6789")
+	if err != nil {
+		t.Error(err)
+	}
+
+	validateError := func(err error, funcName string) {
+		if err == nil || err.Error() != "invalid DAG" {
+			t.Errorf("%s() did not fail on a closed DAG: %v", funcName, err)
+		}
+	}
+
+	err = dag.addNode(oid, 4, false, false, []Version{2, 3}, "foobar", NoTxId)
+	validateError(err, "addNode")
+
+	err = dag.moveHead(oid, 4)
+	validateError(err, "moveHead")
+
+	_, _, _, _, err = dag.hasConflict(oid)
+	validateError(err, "hasConflict")
+
+	_, err = dag.getLogrec(oid, 4)
+	validateError(err, "getLogrec")
+
+	err = dag.prune(oid, 4, func(lr string) error {
+		return nil
+	})
+	validateError(err, "prune")
+
+	err = dag.pruneAll(oid, func(lr string) error {
+		return nil
+	})
+	validateError(err, "pruneAll")
+
+	err = dag.pruneDone()
+	validateError(err, "pruneDone")
+
+	node := &dagNode{Level: 15, Parents: []Version{444, 555}, Logrec: "logrec-23"}
+	err = dag.setNode(oid, 4, node)
+	validateError(err, "setNode")
+
+	_, err = dag.getNode(oid, 4)
+	validateError(err, "getNode")
+
+	err = dag.delNode(oid, 4)
+	validateError(err, "delNode")
+
+	err = dag.addParent(oid, 4, 2, true)
+	validateError(err, "addParent")
+
+	err = dag.setHead(oid, 4)
+	validateError(err, "setHead")
+
+	_, err = dag.getHead(oid)
+	validateError(err, "getHead")
+
+	err = dag.delHead(oid)
+	validateError(err, "delHead")
+
+	if tid := dag.addNodeTxStart(NoTxId); tid != NoTxId {
+		t.Errorf("addNodeTxStart() did not fail on a closed DAG: TxId %v", tid)
+	}
+
+	err = dag.addNodeTxEnd(1, 1)
+	validateError(err, "addNodeTxEnd")
+
+	err = dag.setTransaction(1, nil)
+	validateError(err, "setTransaction")
+
+	_, err = dag.getTransaction(1)
+	validateError(err, "getTransaction")
+
+	err = dag.delTransaction(1)
+	validateError(err, "delTransaction")
+
+	err = dag.setPrivNode(oid, nil)
+	validateError(err, "setPrivNode")
+
+	_, err = dag.getPrivNode(oid)
+	validateError(err, "getPrivNode")
+
+	err = dag.delPrivNode(oid)
+	validateError(err, "delPrivNode")
+
+	// These calls should be harmless NOPs.
+	dag.clearGraft()
+	dag.clearTxGC()
+	dag.dump()
+	dag.flush()
+	dag.close()
+	if dag.hasNode(oid, 4) {
+		t.Errorf("hasNode() found an object on a closed DAG")
+	}
+	if dag.hasDeletedDescendant(oid, 3) {
+		t.Errorf("hasDeletedDescendant() returned true on a closed DAG")
+	}
+	if pmap := dag.getParentMap(oid); len(pmap) != 0 {
+		t.Errorf("getParentMap() found data on a closed DAG: %v", pmap)
+	}
+	if hmap, gmap := dag.getGraftNodes(oid); hmap != nil || gmap != nil {
+		t.Errorf("getGraftNodes() found data on a closed DAG: head map: %v, graft map: %v", hmap, gmap)
+	}
+}
+
+// TestSetNode tests setting and getting a DAG node across DAG open/close/reopen.
+func TestSetNode(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	version := Version(0)
+	oid, err := strToObjId("1111")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	node, err := dag.getNode(oid, version)
+	if err == nil || node != nil {
+		t.Errorf("Found non-existent object %d:%d in DAG file %s: %v", oid, version, dagfile, node)
+	}
+
+	if dag.hasNode(oid, version) {
+		t.Errorf("hasNode() found non-existent object %d:%d in DAG file %s", oid, version, dagfile)
+	}
+
+	if logrec, err := dag.getLogrec(oid, version); err == nil || logrec != "" {
+		t.Errorf("Non-existent object %d:%d has a logrec in DAG file %s: %v", oid, version, dagfile, logrec)
+	}
+
+	node = &dagNode{Level: 15, Parents: []Version{444, 555}, Logrec: "logrec-23"}
+	if err = dag.setNode(oid, version, node); err != nil {
+		t.Fatalf("Cannot set object %d:%d (%v) in DAG file %s", oid, version, node, dagfile)
+	}
+
+	for i := 0; i < 2; i++ {
+		node2, err := dag.getNode(oid, version)
+		if err != nil || node2 == nil {
+			t.Errorf("Cannot find stored object %d:%d (i=%d) in DAG file %s", oid, version, i, dagfile)
+		}
+
+		if !dag.hasNode(oid, version) {
+			t.Errorf("hasNode() did not find object %d:%d (i=%d) in DAG file %s", oid, version, i, dagfile)
+		}
+
+		if !reflect.DeepEqual(node, node2) {
+			t.Errorf("Object %d:%d has wrong data (i=%d) in DAG file %s: %v instead of %v",
+				oid, version, i, dagfile, node2, node)
+		}
+
+		if logrec, err := dag.getLogrec(oid, version); err != nil || logrec != "logrec-23" {
+			t.Errorf("Object %d:%d has wrong logrec (i=%d) in DAG file %s: %v",
+				oid, version, i, dagfile, logrec)
+		}
+
+		if i == 0 {
+			dag.flush()
+			dag.close()
+			dag, err = openDAG(dagfile)
+			if err != nil {
+				t.Fatalf("Cannot re-open DAG file %s", dagfile)
+			}
+		}
+	}
+
+	dag.close()
+}
+
+// TestDelNode tests deleting a DAG node across DAG open/close/reopen.
+func TestDelNode(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	version := Version(1)
+	oid, err := strToObjId("2222")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	node := &dagNode{Level: 123, Parents: []Version{333}, Logrec: "logrec-789"}
+	if err = dag.setNode(oid, version, node); err != nil {
+		t.Fatalf("Cannot set object %d:%d (%v) in DAG file %s", oid, version, node, dagfile)
+	}
+
+	dag.flush()
+
+	err = dag.delNode(oid, version)
+	if err != nil {
+		t.Fatalf("Cannot delete object %d:%d in DAG file %s", oid, version, dagfile)
+	}
+
+	dag.flush()
+
+	for i := 0; i < 2; i++ {
+		node2, err := dag.getNode(oid, version)
+		if err == nil || node2 != nil {
+			t.Errorf("Found deleted object %d:%d (%v) (i=%d) in DAG file %s", oid, version, node2, i, dagfile)
+		}
+
+		if dag.hasNode(oid, version) {
+			t.Errorf("hasNode() found deleted object %d:%d (i=%d) in DAG file %s", oid, version, i, dagfile)
+		}
+
+		if logrec, err := dag.getLogrec(oid, version); err == nil || logrec != "" {
+			t.Errorf("Deleted object %d:%d (i=%d) has logrec in DAG file %s: %v", oid, version, i, dagfile, logrec)
+		}
+
+		if i == 0 {
+			dag.close()
+			dag, err = openDAG(dagfile)
+			if err != nil {
+				t.Fatalf("Cannot re-open DAG file %s", dagfile)
+			}
+		}
+	}
+
+	dag.close()
+}
+
+// TestAddParent tests adding parents to a DAG node.
+func TestAddParent(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	version := Version(7)
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err = dag.addParent(oid, version, 1, true); err == nil {
+		t.Errorf("addParent() did not fail for an unknown object %d:%d in DAG file %s", oid, version, dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	node := &dagNode{Level: 15, Logrec: "logrec-22"}
+	if err = dag.setNode(oid, version, node); err != nil {
+		t.Fatalf("Cannot set object %d:%d (%v) in DAG file %s", oid, version, node, dagfile)
+	}
+
+	if err = dag.addParent(oid, version, version, true); err == nil {
+		t.Errorf("addParent() did not fail on a self-parent for object %d:%d in DAG file %s", oid, version, dagfile)
+	}
+
+	for _, parent := range []Version{4, 5, 6} {
+		if err = dag.addParent(oid, version, parent, true); err == nil {
+			t.Errorf("addParent() did not reject invalid parent %d for object %d:%d in DAG file %s",
+				parent, oid, version, dagfile)
+		}
+
+		pnode := &dagNode{Level: 11, Logrec: fmt.Sprintf("logrec-%d", parent), Parents: []Version{3}}
+		if err = dag.setNode(oid, parent, pnode); err != nil {
+			t.Fatalf("Cannot set parent object %d:%d (%v) in DAG file %s", oid, parent, pnode, dagfile)
+		}
+
+		remote := parent%2 == 0
+		for i := 0; i < 2; i++ {
+			if err = dag.addParent(oid, version, parent, remote); err != nil {
+				t.Errorf("addParent() failed on parent %d, remote %t (i=%d) for object %d:%d in DAG file %s: %v",
+					parent, remote, i, oid, version, dagfile, err)
+			}
+		}
+	}
+
+	node2, err := dag.getNode(oid, version)
+	if err != nil || node2 == nil {
+		t.Errorf("Cannot find stored object %d:%d in DAG file %s", oid, version, dagfile)
+	}
+
+	expParents := []Version{4, 5, 6}
+	if !reflect.DeepEqual(node2.Parents, expParents) {
+		t.Errorf("invalid parents for object %d:%d in DAG file %s: %v instead of %v",
+			oid, version, dagfile, node2.Parents, expParents)
+	}
+
+	// Creating cycles should fail.
+	for v := Version(1); v < version; v++ {
+		if err = dag.addParent(oid, v, version, false); err == nil {
+			t.Errorf("addParent() failed to reject a cycle for object %d: from ancestor %d to node %d in DAG file %s",
+				oid, v, version, dagfile)
+		}
+	}
+
+	dag.close()
+}
+
+// TestSetHead tests setting and getting a DAG head node across DAG open/close/reopen.
+func TestSetHead(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	oid, err := strToObjId("3333")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	version, err := dag.getHead(oid)
+	if err == nil {
+		t.Errorf("Found non-existent object head %d in DAG file %s: %d", oid, dagfile, version)
+	}
+
+	version = 555
+	if err = dag.setHead(oid, version); err != nil {
+		t.Fatalf("Cannot set object head %d (%d) in DAG file %s", oid, version, dagfile)
+	}
+
+	dag.flush()
+
+	for i := 0; i < 3; i++ {
+		version2, err := dag.getHead(oid)
+		if err != nil {
+			t.Errorf("Cannot find stored object head %d (i=%d) in DAG file %s", oid, i, dagfile)
+		}
+		if version != version2 {
+			t.Errorf("Object %d has wrong head data (i=%d) in DAG file %s: %d instead of %d",
+				oid, i, dagfile, version2, version)
+		}
+
+		if i == 0 {
+			dag.close()
+			dag, err = openDAG(dagfile)
+			if err != nil {
+				t.Fatalf("Cannot re-open DAG file %s", dagfile)
+			}
+		} else if i == 1 {
+			version = 888
+			if err = dag.setHead(oid, version); err != nil {
+				t.Fatalf("Cannot set new object head %d (%d) in DAG file %s", oid, version, dagfile)
+			}
+			dag.flush()
+		}
+	}
+
+	dag.close()
+}
+
+// checkEndOfSync simulates and check the end-of-sync operations: clear the
+// node grafting metadata and verify that it is empty and that HasConflict()
+// detects this case and fails, then close the DAG.
+func checkEndOfSync(d *dag, oid ObjId) error {
+	// Clear grafting info; this happens at the end of a sync log replay.
+	d.clearGraft()
+
+	// There should be no grafting or transaction info, and hasConflict() should fail.
+	newHeads, grafts := d.getGraftNodes(oid)
+	if newHeads != nil || grafts != nil {
+		return fmt.Errorf("Object %d: graft info not cleared: newHeads (%v), grafts (%v)", oid, newHeads, grafts)
+	}
+
+	if n := len(d.txSet); n != 0 {
+		return fmt.Errorf("transaction set not empty: %d entries found", n)
+	}
+
+	isConflict, newHead, oldHead, ancestor, errConflict := d.hasConflict(oid)
+	if errConflict == nil {
+		return fmt.Errorf("Object %d: conflict did not fail: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	d.dump()
+	d.close()
+	return nil
+}
+
+// checkDAGStats verifies the DAG stats counters.
+func checkDAGStats(t *testing.T, which string, numObj, numNode, numTx, numPriv int64) {
+	if num, err := stats.Value(statsNumDagObj); err != nil || num != numObj {
+		t.Errorf("num-dag-objects (%s): got %v (err: %v) instead of %v", which, num, err, numObj)
+	}
+	if num, err := stats.Value(statsNumDagNode); err != nil || num != numNode {
+		t.Errorf("num-dag-nodes (%s): got %v (err: %v) instead of %v", which, num, err, numNode)
+	}
+	if num, err := stats.Value(statsNumDagTx); err != nil || num != numTx {
+		t.Errorf("num-dag-tx (%s): got %v (err: %v) instead of %v", which, num, err, numTx)
+	}
+	if num, err := stats.Value(statsNumDagPrivNode); err != nil || num != numPriv {
+		t.Errorf("num-dag-privnodes (%s): got %v (err: %v) instead of %v", which, num, err, numPriv)
+	}
+}
+
+// TestLocalUpdates tests the sync handling of initial local updates: an object
+// is created (v0) and updated twice (v1, v2) on this device.  The DAG should
+// show: v0 -> v1 -> v2 and the head should point to v2.
+func TestLocalUpdates(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must have moved to "v2" and the parent map shows the updated DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 2 {
+		t.Errorf("Invalid object %d head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Make sure an existing node cannot be added again.
+	if err = dag.addNode(oid, 1, false, false, []Version{0, 2}, "foobar", NoTxId); err == nil {
+		t.Errorf("addNode() did not fail when given an existing node")
+	}
+
+	// Make sure a new node cannot have more than 2 parents.
+	if err = dag.addNode(oid, 3, false, false, []Version{0, 1, 2}, "foobar", NoTxId); err == nil {
+		t.Errorf("addNode() did not fail when given 3 parents")
+	}
+
+	// Make sure a new node cannot have an invalid parent.
+	if err = dag.addNode(oid, 3, false, false, []Version{0, 555}, "foobar", NoTxId); err == nil {
+		t.Errorf("addNode() did not fail when using an invalid parent")
+	}
+
+	// Make sure a new root node (no parents) cannot be added once a root exists.
+	// For the parents array, check both the "nil" and the empty array as input.
+	if err = dag.addNode(oid, 6789, false, false, nil, "foobar", NoTxId); err == nil {
+		t.Errorf("Adding a 2nd root node (nil parents) for object %d in DAG file %s did not fail", oid, dagfile)
+	}
+	if err = dag.addNode(oid, 6789, false, false, []Version{}, "foobar", NoTxId); err == nil {
+		t.Errorf("Adding a 2nd root node (empty parents) for object %d in DAG file %s did not fail", oid, dagfile)
+	}
+
+	checkDAGStats(t, "local-update", 1, 3, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteUpdates tests the sync handling of initial remote updates:
+// an object is created (v0) and updated twice (v1, v2) on another device and
+// we learn about it during sync.  The updated DAG should show: v0 -> v1 -> v2
+// and report no conflicts with the new head pointing at v2.
+func TestRemoteUpdates(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "remote-init-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still undefined) and the parent
+	// map shows the newly grafted DAG fragment.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e == nil {
+		t.Errorf("Object %d head found in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{2: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(!isConflict && newHead == 2 && oldHead == 0 && ancestor == 0 && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, e := dag.getLogrec(oid, newHead); e != nil || logrec != "logrec-02" {
+		t.Errorf("Invalid logrec for newhead object %d:%d in DAG file %s: %v", oid, newHead, dagfile, logrec)
+	}
+
+	// Make sure an unknown node cannot become the new head.
+	if err = dag.moveHead(oid, 55); err == nil {
+		t.Errorf("moveHead() did not fail on an invalid node")
+	}
+
+	// Then we can move the head and clear the grafting data.
+	if err = dag.moveHead(oid, newHead); err != nil {
+		t.Errorf("Object %d cannot move head to %d in DAG file %s: %v", oid, newHead, dagfile, err)
+	}
+
+	checkDAGStats(t, "remote-update", 1, 3, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteNoConflict tests sync of remote updates on top of a local initial
+// state without conflict.  An object is created locally and updated twice
+// (v0 -> v1 -> v2).  Another device, having gotten this info, makes 3 updates
+// on top of that (v2 -> v3 -> v4 -> v5) and sends this info in a later sync.
+// The updated DAG should show (v0 -> v1 -> v2 -> v3 -> v4 -> v5) and report
+// no conflicts with the new head pointing at v5.  It should also report v2 as
+// the graft point on which the new fragment (v3 -> v4 -> v5) gets attached.
+func TestRemoteNoConflict(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-noconf-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 2 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}, 3: {2}, 4: {3}, 5: {4}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{5: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{2: 2}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(!isConflict && newHead == 5 && oldHead == 2 && ancestor == 0 && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, e := dag.getLogrec(oid, oldHead); e != nil || logrec != "logrec-02" {
+		t.Errorf("Invalid logrec for oldhead object %d:%d in DAG file %s: %v", oid, oldHead, dagfile, logrec)
+	}
+	if logrec, e := dag.getLogrec(oid, newHead); e != nil || logrec != "logrec-05" {
+		t.Errorf("Invalid logrec for newhead object %d:%d in DAG file %s: %v", oid, newHead, dagfile, logrec)
+	}
+
+	// Then we can move the head and clear the grafting data.
+	if err = dag.moveHead(oid, newHead); err != nil {
+		t.Errorf("Object %d cannot move head to %d in DAG file %s: %v", oid, newHead, dagfile, err)
+	}
+
+	// Clear the grafting data and verify that hasConflict() fails without it.
+	dag.clearGraft()
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %d did not fail w/o graft info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	checkDAGStats(t, "remote-noconf", 1, 6, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteConflict tests sync handling remote updates that build on the
+// local initial state and trigger a conflict.  An object is created locally
+// and updated twice (v0 -> v1 -> v2).  Another device, having only gotten
+// the v0 -> v1 history, makes 3 updates on top of v1 (v1 -> v3 -> v4 -> v5)
+// and sends this info during a later sync.  Separately, the local device
+// makes a conflicting (concurrent) update v1 -> v2.  The updated DAG should
+// show the branches: (v0 -> v1 -> v2) and (v0 -> v1 -> v3 -> v4 -> v5) and
+// report the conflict between v2 and v5 (current and new heads).  It should
+// also report v1 as the graft point and the common ancestor in the conflict.
+// The conflict is resolved locally by creating v6 that is derived from both
+// v2 and v5 and it becomes the new head.
+func TestRemoteConflict(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-conf-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 2 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}, 3: {1}, 4: {3}, 5: {4}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{2: struct{}{}, 5: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{1: 1}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be a conflict between v2 and v5 with v1 as ancestor.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(isConflict && newHead == 5 && oldHead == 2 && ancestor == 1 && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, e := dag.getLogrec(oid, oldHead); e != nil || logrec != "logrec-02" {
+		t.Errorf("Invalid logrec for oldhead object %d:%d in DAG file %s: %v", oid, oldHead, dagfile, logrec)
+	}
+	if logrec, e := dag.getLogrec(oid, newHead); e != nil || logrec != "logrec-05" {
+		t.Errorf("Invalid logrec for newhead object %d:%d in DAG file %s: %v", oid, newHead, dagfile, logrec)
+	}
+	if logrec, e := dag.getLogrec(oid, ancestor); e != nil || logrec != "logrec-01" {
+		t.Errorf("Invalid logrec for ancestor object %d:%d in DAG file %s: %v", oid, ancestor, dagfile, logrec)
+	}
+
+	checkDAGStats(t, "remote-conf-pre", 1, 6, 0, 0)
+
+	// Resolve the conflict by adding a new local v6 derived from v2 and v5 (this replay moves the head).
+	if err = dagReplayCommands(dag, "local-resolve-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that the head moved to v6 and the parent map shows the resolution.
+	if head, e := dag.getHead(oid); e != nil || head != 6 {
+		t.Errorf("Object %d has wrong head after conflict resolution in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	exp[6] = []Version{2, 5}
+	pmap = dag.getParentMap(oid)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map after conflict resolution in DAG file %s: (%v) instead of (%v)",
+			oid, dagfile, pmap, exp)
+	}
+
+	checkDAGStats(t, "remote-conf-post", 1, 7, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteConflictTwoGrafts tests sync handling remote updates that build
+// on the local initial state and trigger a conflict with 2 graft points.
+// An object is created locally and updated twice (v0 -> v1 -> v2).  Another
+// device, first learns about v0 and makes it own conflicting update v0 -> v3.
+// That remote device later learns about v1 and resolves the v1/v3 confict by
+// creating v4.  Then it makes a last v4 -> v5 update -- which will conflict
+// with v2 but it doesn't know that.
+// Now the sync order is reversed and the local device learns all of what
+// happened on the remote device.  The local DAG should get be augmented by
+// a subtree with 2 graft points: v0 and v1.  It receives this new branch:
+// v0 -> v3 -> v4 -> v5.  Note that v4 is also derived from v1 as a remote
+// conflict resolution.  This should report a conflict between v2 and v5
+// (current and new heads), with v0 and v1 as graft points, and v1 as the
+// most-recent common ancestor for that conflict.  The conflict is resolved
+// locally by creating v6, derived from both v2 and v5, becoming the new head.
+func TestRemoteConflictTwoGrafts(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-conf-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 2 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}, 3: {0}, 4: {1, 3}, 5: {4}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{2: struct{}{}, 5: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{0: 0, 1: 1}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be a conflict between v2 and v5 with v1 as ancestor.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(isConflict && newHead == 5 && oldHead == 2 && ancestor == 1 && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, e := dag.getLogrec(oid, oldHead); e != nil || logrec != "logrec-02" {
+		t.Errorf("Invalid logrec for oldhead object %d:%d in DAG file %s: %v", oid, oldHead, dagfile, logrec)
+	}
+	if logrec, e := dag.getLogrec(oid, newHead); e != nil || logrec != "logrec-05" {
+		t.Errorf("Invalid logrec for newhead object %d:%d in DAG file %s: %v", oid, newHead, dagfile, logrec)
+	}
+	if logrec, e := dag.getLogrec(oid, ancestor); e != nil || logrec != "logrec-01" {
+		t.Errorf("Invalid logrec for ancestor object %d:%d in DAG file %s: %v", oid, ancestor, dagfile, logrec)
+	}
+
+	checkDAGStats(t, "remote-conf2-pre", 1, 6, 0, 0)
+
+	// Resolve the conflict by adding a new local v6 derived from v2 and v5 (this replay moves the head).
+	if err = dagReplayCommands(dag, "local-resolve-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that the head moved to v6 and the parent map shows the resolution.
+	if head, e := dag.getHead(oid); e != nil || head != 6 {
+		t.Errorf("Object %d has wrong head after conflict resolution in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	exp[6] = []Version{2, 5}
+	pmap = dag.getParentMap(oid)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map after conflict resolution in DAG file %s: (%v) instead of (%v)",
+			oid, dagfile, pmap, exp)
+	}
+
+	checkDAGStats(t, "remote-conf2-post", 1, 7, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestAncestorIterator checks that the iterator goes over the correct set
+// of ancestor nodes for an object given a starting node.  It should traverse
+// reconvergent DAG branches only visiting each ancestor once:
+// v0 -> v1 -> v2 -> v4 -> v5 -> v7 -> v8
+//        |--> v3 ---|           |
+//        +--> v6 ---------------+
+// - Starting at v0 it should only cover v0.
+// - Starting at v2 it should only cover v0-v2.
+// - Starting at v5 it should only cover v0-v5.
+// - Starting at v8 it should cover all nodes (v0-v8).
+func TestAncestorIterator(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Loop checking the iteration behavior for different starting nodes.
+	for _, start := range []Version{0, 2, 5, 8} {
+		visitCount := make(map[Version]int)
+		err = dag.ancestorIter(oid, []Version{start},
+			func(oid ObjId, v Version, node *dagNode) error {
+				visitCount[v]++
+				return nil
+			})
+
+		// Check that all prior nodes are visited only once.
+		for i := Version(0); i < (start + 1); i++ {
+			if visitCount[i] != 1 {
+				t.Errorf("wrong visit count for iter on object %d node %d starting from node %d: %d instead of 1",
+					oid, i, start, visitCount[i])
+			}
+		}
+	}
+
+	// Make sure an error in the callback is returned through the iterator.
+	cbErr := errors.New("callback error")
+	err = dag.ancestorIter(oid, []Version{8}, func(oid ObjId, v Version, node *dagNode) error {
+		if v == 0 {
+			return cbErr
+		}
+		return nil
+	})
+	if err != cbErr {
+		t.Errorf("wrong error returned from callback: %v instead of %v", err, cbErr)
+	}
+
+	checkDAGStats(t, "ancestor-iter", 1, 9, 0, 0)
+
+	if err = checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestPruning tests sync pruning of the DAG for an object with 3 concurrent
+// updates (i.e. 2 conflict resolution convergent points).  The pruning must
+// get rid of the DAG branches across the reconvergence points:
+// v0 -> v1 -> v2 -> v4 -> v5 -> v7 -> v8
+//        |--> v3 ---|           |
+//        +--> v6 ---------------+
+// By pruning at v0, nothing is deleted.
+// Then by pruning at v1, only v0 is deleted.
+// Then by pruning at v5, v1-v4 are deleted leaving v5 and "v6 -> v7 -> v8".
+// Then by pruning at v7, v5-v6 are deleted leaving "v7 -> v8".
+// Then by pruning at v8, v7 is deleted leaving v8 as the head.
+// Then by pruning again at v8 nothing changes.
+func TestPruning(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "prune-init", 1, 9, 0, 0)
+
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	exp := map[Version][]Version{0: nil, 1: {0}, 2: {1}, 3: {1}, 4: {2, 3}, 5: {4}, 6: {1}, 7: {5, 6}, 8: {7}}
+
+	// Loop pruning at an invalid version (333) then at v0, v5, v8 and again at v8.
+	testVersions := []Version{333, 0, 1, 5, 7, 8, 8}
+	delCounts := []int{0, 0, 1, 4, 2, 1, 0}
+	which := "prune-snip-"
+	remain := 9
+
+	for i, version := range testVersions {
+		del := 0
+		err = dag.prune(oid, version, func(lr string) error {
+			del++
+			return nil
+		})
+
+		if i == 0 && err == nil {
+			t.Errorf("pruning non-existent object %d:%d did not fail in DAG file %s", oid, version, dagfile)
+		} else if i > 0 && err != nil {
+			t.Errorf("pruning object %d:%d failed in DAG file %s: %v", oid, version, dagfile, err)
+		}
+
+		if del != delCounts[i] {
+			t.Errorf("pruning object %d:%d deleted %d log records instead of %d", oid, version, del, delCounts[i])
+		}
+
+		which += "*"
+		remain -= del
+		checkDAGStats(t, which, 1, int64(remain), 0, 0)
+
+		if head, err := dag.getHead(oid); err != nil || head != 8 {
+			t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+		}
+
+		err = dag.pruneDone()
+		if err != nil {
+			t.Errorf("pruneDone() failed in DAG file %s: %v", dagfile, err)
+		}
+
+		// Remove pruned nodes from the expected parent map used to validate
+		// and set the parents of the pruned node to nil.
+		if version < 10 {
+			for j := Version(0); j < version; j++ {
+				delete(exp, j)
+			}
+			exp[version] = nil
+		}
+
+		pmap := dag.getParentMap(oid)
+		if !reflect.DeepEqual(pmap, exp) {
+			t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+		}
+	}
+
+	checkDAGStats(t, "prune-end", 1, 1, 0, 0)
+
+	err = dag.pruneAll(oid, func(lr string) error {
+		return nil
+	})
+	if err != nil {
+		t.Errorf("pruneAll() for object %d failed in DAG file %s: %v", oid, dagfile, err)
+	}
+
+	if err = checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestPruningCallbackError tests sync pruning of the DAG when the callback
+// function returns an error.  The pruning must try to delete as many nodes
+// and log records as possible and properly adjust the parent pointers of
+// the pruning node.  The object DAG is:
+// v0 -> v1 -> v2 -> v4 -> v5 -> v7 -> v8
+//        |--> v3 ---|           |
+//        +--> v6 ---------------+
+// By pruning at v8 and having the callback function fail for v3, all other
+// nodes must be deleted and only v8 remains as the head.
+func TestPruningCallbackError(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "prune-cb-init", 1, 9, 0, 0)
+
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	exp := map[Version][]Version{8: nil}
+
+	// Prune at v8 with a callback function that fails for v3.
+	del, expDel := 0, 8
+	version := Version(8)
+	err = dag.prune(oid, version, func(lr string) error {
+		del++
+		if lr == "logrec-03" {
+			return fmt.Errorf("refuse to delete %s", lr)
+		}
+		return nil
+	})
+
+	if err == nil {
+		t.Errorf("pruning object %d:%d did not fail in DAG file %s", oid, version, dagfile)
+	}
+	if del != expDel {
+		t.Errorf("pruning object %d:%d deleted %d log records instead of %d", oid, version, del, expDel)
+	}
+
+	err = dag.pruneDone()
+	if err != nil {
+		t.Errorf("pruneDone() failed in DAG file %s: %v", dagfile, err)
+	}
+
+	if head, err := dag.getHead(oid); err != nil || head != 8 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	checkDAGStats(t, "prune-cb-end", 1, 1, 0, 0)
+
+	if err = checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteLinkedNoConflictSameHead tests sync of remote updates that contain
+// linked nodes (conflict resolution by selecting an existing version) on top of
+// a local initial state without conflict.  An object is created locally and
+// updated twice (v1 -> v2 -> v3).  Another device has learned about v1, created
+// (v1 -> v4), then learned about (v1 -> v2) and resolved that conflict by selecting
+// v2 over v4.  Now it sends that new info (v4 and the v2/v4 link) back to the
+// original (local) device.  Instead of a v3/v4 conflict, the device sees that
+// v2 was chosen over v4 and resolves it as a no-conflict case.
+func TestRemoteLinkedNoConflictSameHead(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-noconf-link-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "linked-noconf", 1, 4, 0, 0)
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 3 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{1: nil, 2: {1, 4}, 3: {2}, 4: {1}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{3: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{1: 0, 4: 1}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(!isConflict && newHead == 3 && oldHead == 3 && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Clear the grafting data and verify that hasConflict() fails without it.
+	dag.clearGraft()
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %d did not fail w/o graft info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteLinkedConflict tests sync of remote updates that contain linked
+// nodes (conflict resolution by selecting an existing version) on top of a local
+// initial state triggering a local conflict.  An object is created locally and
+// updated twice (v1 -> v2 -> v3).  Another device has along the way learned about v1,
+// created (v1 -> v4), then learned about (v1 -> v2) and resolved that conflict by
+// selecting v4 over v2.  Now it sends that new info (v4 and the v4/v2 link) back
+// to the original (local) device.  The device sees a v3/v4 conflict.
+func TestRemoteLinkedConflict(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-conf-link.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "linked-conf", 1, 4, 0, 0)
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 3 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{1: nil, 2: {1}, 3: {2}, 4: {1, 2}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{3: struct{}{}, 4: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{1: 0, 2: 1}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be a conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(isConflict && newHead == 4 && oldHead == 3 && ancestor == 2 && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Clear the grafting data and verify that hasConflict() fails without it.
+	dag.clearGraft()
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %d did not fail w/o graft info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteLinkedNoConflictNewHead tests sync of remote updates that contain
+// linked nodes (conflict resolution by selecting an existing version) on top of
+// a local initial state without conflict, but moves the head node to a new one.
+// An object is created locally and updated twice (v1 -> v2 -> v3).  Another device
+// has along the way learned about v1, created (v1 -> v4), then learned about
+// (v1 -> v2 -> v3) and resolved that conflict by selecting v4 over v3.  Now it
+// sends that new info (v4 and the v4/v3 link) back to the original (local) device.
+// The device sees that the new head v4 is "derived" from v3 thus no conflict.
+func TestRemoteLinkedConflictNewHead(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-noconf-link-01.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "linked-conf2", 1, 4, 0, 0)
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 3 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{1: nil, 2: {1}, 3: {2}, 4: {1, 3}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{4: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{1: 0, 3: 2}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(!isConflict && newHead == 4 && oldHead == 3 && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Clear the grafting data and verify that hasConflict() fails without it.
+	dag.clearGraft()
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %d did not fail w/o graft info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestRemoteLinkedNoConflictNewHeadOvertake tests sync of remote updates that
+// contain linked nodes (conflict resolution by selecting an existing version)
+// on top of a local initial state without conflict, but moves the head node
+// to a new one that overtook the linked node.
+// An object is created locally and updated twice (v1 -> v2 -> v3).  Another
+// device has along the way learned about v1, created (v1 -> v4), then learned
+// about (v1 -> v2 -> v3) and resolved that conflict by selecting v3 over v4.
+// Then it creates a new update v5 from v3 (v3 -> v5).  Now it sends that new
+// info (v4, the v3/v4 link, and v5) back to the original (local) device.
+// The device sees that the new head v5 is "derived" from v3 thus no conflict.
+func TestRemoteLinkedConflictNewHeadOvertake(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	if err = dagReplayCommands(dag, "remote-noconf-link-02.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "linked-conf3-pre", 1, 5, 0, 0)
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 3 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	pmap := dag.getParentMap(oid)
+
+	exp := map[Version][]Version{1: nil, 2: {1}, 3: {2, 4}, 4: {1}, 5: {3}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("Invalid object %d parent map in DAG file %s: (%v) instead of (%v)", oid, dagfile, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	newHeads, grafts := dag.getGraftNodes(oid)
+
+	expNewHeads := map[Version]struct{}{5: struct{}{}}
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts := map[Version]uint64{1: 0, 3: 2, 4: 1}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := dag.hasConflict(oid)
+	if !(!isConflict && newHead == 5 && oldHead == 3 && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Then we can move the head and clear the grafting data.
+	if err = dag.moveHead(oid, newHead); err != nil {
+		t.Errorf("Object %d cannot move head to %d in DAG file %s: %v", oid, newHead, dagfile, err)
+	}
+
+	// Clear the grafting data and verify that hasConflict() fails without it.
+	dag.clearGraft()
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %d did not fail w/o graft info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Now new info comes from another device repeating the v2/v3 link.
+	// Verify that it is a NOP (no changes).
+	if err = dagReplayCommands(dag, "remote-noconf-link-repeat.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	if head, e := dag.getHead(oid); e != nil || head != 5 {
+		t.Errorf("Object %d has wrong head in DAG file %s: %d", oid, dagfile, head)
+	}
+
+	newHeads, grafts = dag.getGraftNodes(oid)
+	if !reflect.DeepEqual(newHeads, expNewHeads) {
+		t.Errorf("Object %d has invalid newHeads in DAG file %s: (%v) instead of (%v)", oid, dagfile, newHeads, expNewHeads)
+	}
+
+	expgrafts = map[Version]uint64{}
+	if !reflect.DeepEqual(grafts, expgrafts) {
+		t.Errorf("Invalid object %d graft in DAG file %s: (%v) instead of (%v)", oid, dagfile, grafts, expgrafts)
+	}
+
+	isConflict, newHead, oldHead, ancestor, errConflict = dag.hasConflict(oid)
+	if !(!isConflict && newHead == 5 && oldHead == 5 && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("Object %d wrong conflict info: flag %t, newHead %d, oldHead %d, ancestor %d, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	checkDAGStats(t, "linked-conf3-post", 1, 5, 0, 0)
+
+	if err := checkEndOfSync(dag, oid); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestAddNodeTransactional tests adding multiple DAG nodes grouped within a transaction.
+func TestAddNodeTransactional(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-02.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "add-tx-init", 3, 5, 0, 0)
+
+	oid_a, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+	oid_b, err := strToObjId("6789")
+	if err != nil {
+		t.Fatal(err)
+	}
+	oid_c, err := strToObjId("2222")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify NoTxId is reported as an error.
+	if err := dag.addNodeTxEnd(NoTxId, 0); err == nil {
+		t.Errorf("addNodeTxEnd() did not fail for invalid 'NoTxId' value")
+	}
+	if _, err := dag.getTransaction(NoTxId); err == nil {
+		t.Errorf("getTransaction() did not fail for invalid 'NoTxId' value")
+	}
+	if err := dag.setTransaction(NoTxId, nil); err == nil {
+		t.Errorf("setTransaction() did not fail for invalid 'NoTxId' value")
+	}
+	if err := dag.delTransaction(NoTxId); err == nil {
+		t.Errorf("delTransaction() did not fail for invalid 'NoTxId' value")
+	}
+
+	// Mutate 2 objects within a transaction.
+	tid_1 := dag.addNodeTxStart(NoTxId)
+	if tid_1 == NoTxId {
+		t.Fatal("Cannot start 1st DAG addNode() transaction")
+	}
+	if err := dag.addNodeTxEnd(tid_1, 0); err == nil {
+		t.Errorf("addNodeTxEnd() did not fail for a zero-count transaction")
+	}
+
+	txSt, ok := dag.txSet[tid_1]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_1, dagfile)
+	}
+	if n := len(txSt.TxMap); n != 0 {
+		t.Errorf("Transactions map for Tx ID %v has length %d instead of 0 in DAG file %s", tid_1, n, dagfile)
+	}
+
+	if err := dag.addNode(oid_a, 3, false, false, []Version{2}, "logrec-a-03", tid_1); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_a, tid_1, dagfile, err)
+	}
+
+	if tTmp := dag.addNodeTxStart(tid_1); tTmp != tid_1 {
+		t.Fatal("restarting transaction failed")
+	}
+
+	if err := dag.addNode(oid_b, 3, false, false, []Version{2}, "logrec-b-03", tid_1); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_b, tid_1, dagfile, err)
+	}
+
+	// At the same time mutate the 3rd object in another transaction.
+	tid_2 := dag.addNodeTxStart(NoTxId)
+	if tid_2 == NoTxId {
+		t.Fatal("Cannot start 2nd DAG addNode() transaction")
+	}
+
+	txSt, ok = dag.txSet[tid_2]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_2, dagfile)
+	}
+	if n := len(txSt.TxMap); n != 0 {
+		t.Errorf("Transactions map for Tx ID %v has length %d instead of 0 in DAG file %s", tid_2, n, dagfile)
+	}
+
+	if err := dag.addNode(oid_c, 2, false, false, []Version{1}, "logrec-c-02", tid_2); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_c, tid_2, dagfile, err)
+	}
+
+	// Verify the in-memory transaction sets constructed.
+	txSt, ok = dag.txSet[tid_1]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_1, dagfile)
+	}
+	expTxSt := &dagTxState{dagTxMap{oid_a: 3, oid_b: 3}, 0}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_1, dagfile, txSt, expTxSt)
+	}
+
+	txSt, ok = dag.txSet[tid_2]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_2, dagfile)
+	}
+	expTxSt = &dagTxState{dagTxMap{oid_c: 2}, 0}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_2, dagfile, txSt, expTxSt)
+	}
+
+	// Verify failing to use a Tx ID not returned by addNodeTxStart().
+	bad_tid := tid_1 + 1
+	for bad_tid == NoTxId || bad_tid == tid_2 {
+		bad_tid++
+	}
+
+	if err := dag.addNode(oid_c, 3, false, false, []Version{2}, "logrec-c-03", bad_tid); err == nil {
+		t.Errorf("addNode() did not fail on object %d for a bad Tx ID %v in DAG file %s", oid_c, bad_tid, dagfile)
+	}
+	if err := dag.addNodeTxEnd(bad_tid, 1); err == nil {
+		t.Errorf("addNodeTxEnd() did not fail for a bad Tx ID %v in DAG file %s", bad_tid, dagfile)
+	}
+
+	// End the 1st transaction and verify the in-memory and in-DAG data.
+	if err := dag.addNodeTxEnd(tid_1, 2); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_1, dagfile, err)
+	}
+
+	checkDAGStats(t, "add-tx-1", 3, 8, 1, 0)
+
+	if _, ok = dag.txSet[tid_1]; ok {
+		t.Errorf("Transactions state for Tx ID %v still exists in DAG file %s", tid_1, dagfile)
+	}
+
+	txSt, err = dag.getTransaction(tid_1)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_1, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_a: 3, oid_b: 3}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state from DAG storage for Tx ID %v in DAG file %s: %v instead of %v",
+			tid_1, dagfile, txSt, expTxSt)
+	}
+
+	txSt, ok = dag.txSet[tid_2]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_2, dagfile)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_c: 2}, 0}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_2, dagfile, txSt, expTxSt)
+	}
+
+	// End the 2nd transaction and re-verify the in-memory and in-DAG data.
+	if err := dag.addNodeTxEnd(tid_2, 1); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_2, dagfile, err)
+	}
+
+	checkDAGStats(t, "add-tx-2", 3, 8, 2, 0)
+
+	if _, ok = dag.txSet[tid_2]; ok {
+		t.Errorf("Transactions state for Tx ID %v still exists in DAG file %s", tid_2, dagfile)
+	}
+
+	txSt, err = dag.getTransaction(tid_2)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_2, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_c: 2}, 1}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_2, dagfile, txSt, expTxSt)
+	}
+
+	if n := len(dag.txSet); n != 0 {
+		t.Errorf("Transaction sets in-memory: %d entries found, should be empty in DAG file %s", n, dagfile)
+	}
+
+	// Test incrementally filling up a transaction.
+	tid_3 := TxId(100)
+	if _, ok = dag.txSet[tid_3]; ok {
+		t.Errorf("Transactions state for Tx ID %v found in DAG file %s", tid_3, dagfile)
+	}
+
+	if tTmp := dag.addNodeTxStart(tid_3); tTmp != tid_3 {
+		t.Fatalf("Cannot start transaction %v", tid_3)
+	}
+
+	txSt, ok = dag.txSet[tid_3]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_3, dagfile)
+	}
+	if n := len(txSt.TxMap); n != 0 {
+		t.Errorf("Transactions map for Tx ID %v has length %d instead of 0 in DAG file %s", tid_3, n, dagfile)
+	}
+
+	if err := dag.addNode(oid_a, 4, false, false, []Version{3}, "logrec-a-04", tid_3); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_a, tid_3, dagfile, err)
+	}
+
+	if err := dag.addNodeTxEnd(tid_3, 2); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_3, dagfile, err)
+	}
+
+	checkDAGStats(t, "add-tx-3", 3, 9, 3, 0)
+
+	if _, ok = dag.txSet[tid_3]; ok {
+		t.Errorf("Transactions state for Tx ID %v still exists in DAG file %s", tid_3, dagfile)
+	}
+
+	txSt, err = dag.getTransaction(tid_3)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_3, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_a: 4}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state from DAG storage for Tx ID %v in DAG file %s: %v instead of %v",
+			tid_3, dagfile, txSt, expTxSt)
+	}
+
+	if tTmp := dag.addNodeTxStart(tid_3); tTmp != tid_3 {
+		t.Fatalf("Cannot start transaction %v", tid_3)
+	}
+
+	txSt, ok = dag.txSet[tid_3]
+	if !ok {
+		t.Errorf("Transactions state for Tx ID %v not found in DAG file %s", tid_3, dagfile)
+	}
+
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state from DAG storage for Tx ID %v in DAG file %s: %v instead of %v",
+			tid_3, dagfile, txSt, expTxSt)
+	}
+
+	if err := dag.addNode(oid_b, 4, false, false, []Version{3}, "logrec-b-04", tid_3); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_b, tid_3, dagfile, err)
+	}
+
+	if err := dag.addNodeTxEnd(tid_3, 3); err == nil {
+		t.Errorf("addNodeTxEnd() didn't fail for Tx ID %v in DAG file %s: %v", tid_3, dagfile, err)
+	}
+
+	if err := dag.addNodeTxEnd(tid_3, 2); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_3, dagfile, err)
+	}
+
+	checkDAGStats(t, "add-tx-4", 3, 10, 3, 0)
+
+	txSt, err = dag.getTransaction(tid_3)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_3, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_a: 4, oid_b: 4}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state from DAG storage for Tx ID %v in DAG file %s: %v instead of %v",
+			tid_3, dagfile, txSt, expTxSt)
+	}
+
+	// Get the 3 new nodes from the DAG and verify their Tx IDs.
+	node, err := dag.getNode(oid_a, 3)
+	if err != nil {
+		t.Errorf("Cannot find object %d:3 in DAG file %s: %v", oid_a, dagfile, err)
+	}
+	if node.TxId != tid_1 {
+		t.Errorf("Invalid TxId for object %d:3 in DAG file %s: %v instead of %v", oid_a, dagfile, node.TxId, tid_1)
+	}
+	node, err = dag.getNode(oid_a, 4)
+	if err != nil {
+		t.Errorf("Cannot find object %d:4 in DAG file %s: %v", oid_a, dagfile, err)
+	}
+	if node.TxId != tid_3 {
+		t.Errorf("Invalid TxId for object %d:4 in DAG file %s: %v instead of %v", oid_a, dagfile, node.TxId, tid_3)
+	}
+	node, err = dag.getNode(oid_b, 3)
+	if err != nil {
+		t.Errorf("Cannot find object %d:3 in DAG file %s: %v", oid_b, dagfile, err)
+	}
+	if node.TxId != tid_1 {
+		t.Errorf("Invalid TxId for object %d:3 in DAG file %s: %v instead of %v", oid_b, dagfile, node.TxId, tid_1)
+	}
+	node, err = dag.getNode(oid_b, 4)
+	if err != nil {
+		t.Errorf("Cannot find object %d:4 in DAG file %s: %v", oid_b, dagfile, err)
+	}
+	if node.TxId != tid_3 {
+		t.Errorf("Invalid TxId for object %d:4 in DAG file %s: %v instead of %v", oid_b, dagfile, node.TxId, tid_3)
+	}
+	node, err = dag.getNode(oid_c, 2)
+	if err != nil {
+		t.Errorf("Cannot find object %d:2 in DAG file %s: %v", oid_c, dagfile, err)
+	}
+	if node.TxId != tid_2 {
+		t.Errorf("Invalid TxId for object %d:2 in DAG file %s: %v instead of %v", oid_c, dagfile, node.TxId, tid_2)
+	}
+
+	for _, oid := range []ObjId{oid_a, oid_b, oid_c} {
+		if err := checkEndOfSync(dag, oid); err != nil {
+			t.Fatal(err)
+		}
+	}
+}
+
+// TestPruningTransactions tests pruning DAG nodes grouped within transactions.
+func TestPruningTransactions(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-02.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	checkDAGStats(t, "prune-tx-init", 3, 5, 0, 0)
+
+	oid_a, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+	oid_b, err := strToObjId("6789")
+	if err != nil {
+		t.Fatal(err)
+	}
+	oid_c, err := strToObjId("2222")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Mutate objects in 2 transactions then add non-transactional mutations
+	// to act as the pruning points.  Before pruning the DAG is:
+	// a1 -- a2 -- (a3) --- a4
+	// b1 -- b2 -- (b3) -- (b4) -- b5
+	// c1 ---------------- (c2)
+	// Now by pruning at (a4, b5, c2), the new DAG should be:
+	// a4
+	// b5
+	// (c2)
+	// Transaction 1 (a3, b3) gets deleted, but transaction 2 (b4, c2) still
+	// has (c2) dangling waiting for a future pruning.
+	tid_1 := dag.addNodeTxStart(NoTxId)
+	if tid_1 == NoTxId {
+		t.Fatal("Cannot start 1st DAG addNode() transaction")
+	}
+	if err := dag.addNode(oid_a, 3, false, false, []Version{2}, "logrec-a-03", tid_1); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_a, tid_1, dagfile, err)
+	}
+	if err := dag.addNode(oid_b, 3, false, false, []Version{2}, "logrec-b-03", tid_1); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_b, tid_1, dagfile, err)
+	}
+	if err := dag.addNodeTxEnd(tid_1, 2); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_1, dagfile, err)
+	}
+
+	checkDAGStats(t, "prune-tx-1", 3, 7, 1, 0)
+
+	tid_2 := dag.addNodeTxStart(NoTxId)
+	if tid_2 == NoTxId {
+		t.Fatal("Cannot start 2nd DAG addNode() transaction")
+	}
+	if err := dag.addNode(oid_b, 4, false, false, []Version{3}, "logrec-b-04", tid_2); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_b, tid_2, dagfile, err)
+	}
+	if err := dag.addNode(oid_c, 2, false, false, []Version{1}, "logrec-c-02", tid_2); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_c, tid_2, dagfile, err)
+	}
+	if err := dag.addNodeTxEnd(tid_2, 2); err != nil {
+		t.Errorf("Cannot addNodeTxEnd() for Tx ID %v in DAG file %s: %v", tid_2, dagfile, err)
+	}
+
+	checkDAGStats(t, "prune-tx-2", 3, 9, 2, 0)
+
+	if err := dag.addNode(oid_a, 4, false, false, []Version{3}, "logrec-a-04", NoTxId); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_a, tid_1, dagfile, err)
+	}
+	if err := dag.addNode(oid_b, 5, false, false, []Version{4}, "logrec-b-05", NoTxId); err != nil {
+		t.Errorf("Cannot addNode() on object %d and Tx ID %v in DAG file %s: %v", oid_b, tid_2, dagfile, err)
+	}
+
+	if err = dag.moveHead(oid_a, 4); err != nil {
+		t.Errorf("Object %d cannot move head in DAG file %s: %v", oid_a, dagfile, err)
+	}
+	if err = dag.moveHead(oid_b, 5); err != nil {
+		t.Errorf("Object %d cannot move head in DAG file %s: %v", oid_b, dagfile, err)
+	}
+	if err = dag.moveHead(oid_c, 2); err != nil {
+		t.Errorf("Object %d cannot move head in DAG file %s: %v", oid_c, dagfile, err)
+	}
+
+	checkDAGStats(t, "prune-tx-3", 3, 11, 2, 0)
+
+	// Verify the transaction sets.
+	txSt, err := dag.getTransaction(tid_1)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_1, dagfile, err)
+	}
+
+	expTxSt := &dagTxState{dagTxMap{oid_a: 3, oid_b: 3}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state from DAG storage for Tx ID %v in DAG file %s: %v instead of %v",
+			tid_1, dagfile, txSt, expTxSt)
+	}
+
+	txSt, err = dag.getTransaction(tid_2)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_2, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_b: 4, oid_c: 2}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_2, dagfile, txSt, expTxSt)
+	}
+
+	// Prune the 3 objects at their head nodes.
+	for _, oid := range []ObjId{oid_a, oid_b, oid_c} {
+		head, err := dag.getHead(oid)
+		if err != nil {
+			t.Errorf("Cannot getHead() on object %d in DAG file %s: %v", oid, dagfile, err)
+		}
+		err = dag.prune(oid, head, func(lr string) error {
+			return nil
+		})
+		if err != nil {
+			t.Errorf("Cannot prune() on object %d in DAG file %s: %v", oid, dagfile, err)
+		}
+	}
+
+	if err = dag.pruneDone(); err != nil {
+		t.Errorf("pruneDone() failed in DAG file %s: %v", dagfile, err)
+	}
+
+	if n := len(dag.txGC); n != 0 {
+		t.Errorf("Transaction GC map not empty after pruneDone() in DAG file %s: %d", dagfile, n)
+	}
+
+	// Verify that Tx-1 was deleted and Tx-2 still has c2 in it.
+	checkDAGStats(t, "prune-tx-4", 3, 3, 1, 0)
+
+	txSt, err = dag.getTransaction(tid_1)
+	if err == nil {
+		t.Errorf("getTransaction() did not fail for Tx ID %v in DAG file %s: %v", tid_1, dagfile, txSt)
+	}
+
+	txSt, err = dag.getTransaction(tid_2)
+	if err != nil {
+		t.Errorf("Cannot getTransaction() for Tx ID %v in DAG file %s: %v", tid_2, dagfile, err)
+	}
+
+	expTxSt = &dagTxState{dagTxMap{oid_c: 2}, 2}
+	if !reflect.DeepEqual(txSt, expTxSt) {
+		t.Errorf("Invalid transaction state for Tx ID %v in DAG file %s: %v instead of %v", tid_2, dagfile, txSt, expTxSt)
+	}
+
+	// Add c3 as a new head and prune at that point.  This should GC Tx-2.
+	if err := dag.addNode(oid_c, 3, false, false, []Version{2}, "logrec-c-03", NoTxId); err != nil {
+		t.Errorf("Cannot addNode() on object %d in DAG file %s: %v", oid_c, dagfile, err)
+	}
+	if err = dag.moveHead(oid_c, 3); err != nil {
+		t.Errorf("Object %d cannot move head in DAG file %s: %v", oid_c, dagfile, err)
+	}
+
+	checkDAGStats(t, "prune-tx-5", 3, 4, 1, 0)
+
+	err = dag.prune(oid_c, 3, func(lr string) error {
+		return nil
+	})
+	if err != nil {
+		t.Errorf("Cannot prune() on object %d in DAG file %s: %v", oid_c, dagfile, err)
+	}
+	if err = dag.pruneDone(); err != nil {
+		t.Errorf("pruneDone() #2 failed in DAG file %s: %v", dagfile, err)
+	}
+	if n := len(dag.txGC); n != 0 {
+		t.Errorf("Transaction GC map not empty after pruneDone() in DAG file %s: %d", dagfile, n)
+	}
+
+	checkDAGStats(t, "prune-tx-6", 3, 3, 0, 0)
+
+	txSt, err = dag.getTransaction(tid_2)
+	if err == nil {
+		t.Errorf("getTransaction() did not fail for Tx ID %v in DAG file %s: %v", tid_2, dagfile, txSt)
+	}
+
+	for _, oid := range []ObjId{oid_a, oid_b, oid_c} {
+		if err := checkEndOfSync(dag, oid); err != nil {
+			t.Fatal(err)
+		}
+	}
+}
+
+// TestHasDeletedDescendant tests lookup of DAG deleted nodes descending from a given node.
+func TestHasDeletedDescendant(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	if err = dagReplayCommands(dag, "local-init-03.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	oid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Delete node v3 to create a dangling parent link from v7 (increase code coverage).
+	if err = dag.delNode(oid, 3); err != nil {
+		t.Errorf("cannot delete node %d:3 in DAG file %s: %v", oid, dagfile, err)
+	}
+
+	type hasDelDescTest struct {
+		node   Version
+		result bool
+	}
+	tests := []hasDelDescTest{
+		{NoVersion, false},
+		{999, false},
+		{1, true},
+		{2, true},
+		{3, false},
+		{4, false},
+		{5, false},
+		{6, false},
+		{7, false},
+		{8, false},
+	}
+
+	for _, test := range tests {
+		result := dag.hasDeletedDescendant(oid, test.node)
+		if result != test.result {
+			t.Errorf("hasDeletedDescendant() for node %d in DAG file %s: %v instead of %v",
+				test.node, dagfile, result, test.result)
+		}
+	}
+
+	dag.close()
+}
+
+// TestPrivNode tests access to the private nodes table in a DAG.
+func TestPrivNode(t *testing.T) {
+	dagfile := dagFilename()
+	defer os.Remove(dagfile)
+
+	dag, err := openDAG(dagfile)
+	if err != nil {
+		t.Fatalf("Cannot open new DAG file %s", dagfile)
+	}
+
+	oid, err := strToObjId("2222")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	priv, err := dag.getPrivNode(oid)
+	if err == nil || priv != nil {
+		t.Errorf("Found non-existing private object %d in DAG file %s: %v, err %v", oid, dagfile, priv, err)
+	}
+
+	priv = &privNode{
+		//Mutation: &raw.Mutation{ID: oid, PriorVersion: 0x0, Version: 0x55104dc76695721d, Value: "value-foobar"},
+		PathIDs: []ObjId{oid, ObjId("haha"), ObjId("foobar")},
+		TxId:    56789,
+	}
+
+	if err = dag.setPrivNode(oid, priv); err != nil {
+		t.Fatalf("Cannot set private object %d (%v) in DAG file %s: %v", oid, priv, dagfile, err)
+	}
+
+	checkDAGStats(t, "priv-1", 0, 0, 0, 1)
+
+	priv2, err := dag.getPrivNode(oid)
+	if err != nil {
+		t.Fatalf("Cannot get private object %d from DAG file %s: %v", oid, dagfile, err)
+	}
+	if !reflect.DeepEqual(priv2, priv) {
+		t.Errorf("Private object %d has wrong data in DAG file %s: %v instead of %v", oid, dagfile, priv2, priv)
+	}
+
+	//priv.Mutation.PriorVersion = priv.Mutation.Version
+	//priv.Mutation.Version = 0x55555ddddd345abc
+	//priv.Mutation.Value = "value-new"
+	priv.TxId = 98765
+
+	if err = dag.setPrivNode(oid, priv); err != nil {
+		t.Fatalf("Cannot overwrite private object %d (%v) in DAG file %s: %v", oid, priv, dagfile, err)
+	}
+
+	checkDAGStats(t, "priv-1", 0, 0, 0, 1)
+
+	priv2, err = dag.getPrivNode(oid)
+	if err != nil {
+		t.Fatalf("Cannot get updated private object %d from DAG file %s: %v", oid, dagfile, err)
+	}
+	if !reflect.DeepEqual(priv2, priv) {
+		t.Errorf("Private object %d has wrong data post-update in DAG file %s: %v instead of %v", oid, dagfile, priv2, priv)
+	}
+
+	err = dag.delPrivNode(oid)
+	if err != nil {
+		t.Fatalf("Cannot delete private object %d in DAG file %s: %v", oid, dagfile, err)
+	}
+
+	checkDAGStats(t, "priv-1", 0, 0, 0, 0)
+
+	priv3, err := dag.getPrivNode(oid)
+	if err == nil || priv3 != nil {
+		t.Errorf("Found deleted private object %d in DAG file %s: %v, err %v", oid, dagfile, priv3, err)
+	}
+
+	dag.close()
+}
diff --git a/services/syncbase/sync/devtable.go b/services/syncbase/sync/devtable.go
new file mode 100644
index 0000000..9a463a1
--- /dev/null
+++ b/services/syncbase/sync/devtable.go
@@ -0,0 +1,546 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Package vsync provides veyron sync DevTable utility functions.
+// DevTable is indexed by the device id and stores device level
+// information needed by sync.  Main component of a device's info are
+// a set of generation vectors: one per SyncRoot. Generation vector
+// is the version vector for a device's view of a SyncRoot,
+// representing all the different generations (from different devices)
+// seen by that device for that SyncRoot. A generation represents a
+// collection of updates that originated on a device during an
+// interval of time. It serves as a checkpoint when communicating with
+// other devices. Generations do not overlap and all updates belong to
+// a generation.
+//
+// Synchronization for a given SyncRoot between two devices A and B
+// uses generation vectors as follows:
+//                 A                              B
+//                                     <== B's generation vector
+// diff(A's generation vector, B's generation vector)
+// log records of missing generations ==>
+// cache B's generation vector (for space reclamation)
+//
+// Implementation notes: DevTable is stored in a persistent K/V
+// database in the current implementation.  Generation vector is
+// implemented as a map of (Device ID -> Generation ID), one entry for
+// every known device.  If the generation vector contains an entry
+// (Device ID -> Generation ID), it implies that the device has
+// learned of all the generations until and including Generation
+// ID. Generation IDs start from 1.  A generation ID of 0 is a
+// reserved bootstrap value, and indicates the device has no updates.
+import (
+	"errors"
+	"fmt"
+	"sort"
+	"time"
+
+	"v.io/x/lib/vlog"
+)
+
+var (
+	errInvalidDTab = errors.New("invalid devtable db")
+)
+
+// devInfo is the information stored per device.
+type devInfo struct {
+	Vectors map[ObjId]GenVector // device generation vectors.
+	Ts      time.Time           // last communication time stamp.
+}
+
+// devTableHeader contains the header metadata.
+type devTableHeader struct {
+	Resmark []byte // resume marker for watch.
+	// Generation vector for space reclamation. All generations
+	// less than this generation vector are deleted from storage.
+	ReclaimVec GenVector
+}
+
+// devTable contains the metadata for the device table db.
+type devTable struct {
+	fname   string   // file pathname.
+	db      *kvdb    // underlying K/V DB.
+	devices *kvtable // pointer to the "devices" table in the kvdb. Contains device info.
+
+	// Key:"Head" Value:devTableHeader
+	header *kvtable        // pointer to the "header" table in the kvdb. Contains device table header.
+	head   *devTableHeader // devTable head cached in memory.
+
+	s *syncd // pointer to the sync daemon object.
+}
+
+// genOrder represents a generation along with its position in the log.
+type genOrder struct {
+	devID DeviceId
+	srID  ObjId
+	genID GenId
+	order uint32
+}
+
+// byOrder is used to sort the genOrder array.
+type byOrder []*genOrder
+
+func (a byOrder) Len() int {
+	return len(a)
+}
+
+func (a byOrder) Swap(i, j int) {
+	a[i], a[j] = a[j], a[i]
+}
+
+func (a byOrder) Less(i, j int) bool {
+	return a[i].order < a[j].order
+}
+
+// openDevTable opens or creates a devTable for the given filename.
+func openDevTable(filename string, sin *syncd) (*devTable, error) {
+	dtab := &devTable{
+		fname: filename,
+		s:     sin,
+	}
+	// Open the file and create it if it does not exist.
+	// Also initialize the kvdb and its collection.
+	db, tbls, err := kvdbOpen(filename, []string{"devices", "header"})
+	if err != nil {
+		return nil, err
+	}
+
+	dtab.db = db
+	dtab.devices = tbls[0]
+	dtab.header = tbls[1]
+
+	// Initialize the devTable header.
+	dtab.head = &devTableHeader{
+		ReclaimVec: GenVector{
+			dtab.s.id: 0,
+		},
+	}
+	// If header already exists in db, read it back from db.
+	if dtab.hasHead() {
+		if err := dtab.getHead(); err != nil {
+			dtab.db.close() // this also closes the tables.
+			return nil, err
+		}
+	}
+
+	return dtab, nil
+}
+
+// close closes the devTable and invalidates its struct.
+func (dt *devTable) close() error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	// Flush the dirty data.
+	if err := dt.flush(); err != nil {
+		return err
+	}
+	dt.db.close() // this also closes the tables.
+
+	*dt = devTable{} // zero out the devTable struct.
+	return nil
+}
+
+// flush flushes the devTable db to storage.
+func (dt *devTable) flush() error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	// Set the head from memory before flushing.
+	if err := dt.putHead(); err != nil {
+		return err
+	}
+	dt.db.flush()
+	return nil
+}
+
+// initSyncRoot initializes the local generation vector for this SyncRoot.
+func (dt *devTable) initSyncRoot(srid ObjId) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	if _, err := dt.getGenVec(dt.s.id, srid); err == nil {
+		return fmt.Errorf("syncroot already exists %v", srid)
+	}
+	return dt.putGenVec(dt.s.id, srid, GenVector{dt.s.id: 0})
+}
+
+// delSyncRoot deletes the generation vector for this SyncRoot.
+func (dt *devTable) delSyncRoot(srid ObjId) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	info, err := dt.getDevInfo(dt.s.id)
+	if err != nil {
+		return fmt.Errorf("dev doesn't exists %v", dt.s.id)
+	}
+	if _, ok := info.Vectors[srid]; !ok {
+		return fmt.Errorf("syncroot doesn't exist %v", srid)
+	}
+	delete(info.Vectors, srid)
+	if len(info.Vectors) == 0 {
+		return dt.delDevInfo(dt.s.id)
+	}
+	return dt.putDevInfo(dt.s.id, info)
+}
+
+// putHead puts the devTable head into the devTable db.
+func (dt *devTable) putHead() error {
+	return dt.header.set("Head", dt.head)
+}
+
+// getHead gets the devTable head from the devTable db.
+func (dt *devTable) getHead() error {
+	if dt.head == nil {
+		return errors.New("nil devTable header")
+	}
+	return dt.header.get("Head", dt.head)
+}
+
+// hasHead returns true if the devTable db has a devTable head.
+func (dt *devTable) hasHead() bool {
+	return dt.header.hasKey("Head")
+}
+
+// putDevInfo puts a devInfo struct in the devTable db.
+func (dt *devTable) putDevInfo(devid DeviceId, info *devInfo) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	return dt.devices.set(string(devid), info)
+}
+
+// getDevInfo gets a devInfo struct from the devTable db.
+func (dt *devTable) getDevInfo(devid DeviceId) (*devInfo, error) {
+	if dt.db == nil {
+		return nil, errInvalidDTab
+	}
+	var info devInfo
+	if err := dt.devices.get(string(devid), &info); err != nil {
+		return nil, err
+	}
+	if info.Vectors == nil {
+		return nil, errors.New("nil genvectors")
+	}
+	return &info, nil
+}
+
+// hasDevInfo returns true if the device (devid) has any devInfo in the devTable db.
+func (dt *devTable) hasDevInfo(devid DeviceId) bool {
+	if dt.db == nil {
+		return false
+	}
+	return dt.devices.hasKey(string(devid))
+}
+
+// delDevInfo deletes devInfo struct in the devTable db.
+func (dt *devTable) delDevInfo(devid DeviceId) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	return dt.devices.del(string(devid))
+}
+
+// putGenVec puts a generation vector in the devTable db.
+func (dt *devTable) putGenVec(devid DeviceId, srid ObjId, v GenVector) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	var info *devInfo
+	if dt.hasDevInfo(devid) {
+		var err error
+		if info, err = dt.getDevInfo(devid); err != nil {
+			return err
+		}
+	} else {
+		info = &devInfo{
+			Vectors: make(map[ObjId]GenVector),
+		}
+	}
+	info.Vectors[srid] = v
+	info.Ts = time.Now().UTC()
+	return dt.putDevInfo(devid, info)
+}
+
+// getGenVec gets a generation vector from the devTable db.
+func (dt *devTable) getGenVec(devid DeviceId, srid ObjId) (GenVector, error) {
+	if dt.db == nil {
+		return nil, errInvalidDTab
+	}
+	info, err := dt.getDevInfo(devid)
+	if err != nil {
+		return nil, err
+	}
+	v, ok := info.Vectors[srid]
+	if !ok {
+		return nil, fmt.Errorf("srid %s doesn't exist", srid.String())
+	}
+	return v, nil
+}
+
+// populateGenOrderEntry populates a genOrder entry.
+func (dt *devTable) populateGenOrderEntry(e *genOrder, id DeviceId, srid ObjId, gnum GenId) error {
+	e.devID = id
+	e.srID = srid
+	e.genID = gnum
+
+	o, err := dt.s.log.getGenMetadata(id, srid, gnum)
+	if err != nil {
+		return err
+	}
+	e.order = o.Pos
+	return nil
+}
+
+// updateGeneration updates a single generation (upID, upGen) in a device's generation vector for SyncRoot srID.
+func (dt *devTable) updateGeneration(key DeviceId, srID ObjId, upID DeviceId, upGen GenId) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	info, err := dt.getDevInfo(key)
+	if err != nil {
+		return err
+	}
+
+	v, ok := info.Vectors[srID]
+	if !ok {
+		return fmt.Errorf("srid %s doesn't exist", srID.String())
+	}
+	v[upID] = upGen
+	return dt.putDevInfo(key, info)
+}
+
+// updateLocalGenVector updates local generation vector based on the remote generation vector.
+func (dt *devTable) updateLocalGenVector(local, remote GenVector) error {
+	if dt.db == nil {
+		return errInvalidDTab
+	}
+	if local == nil || remote == nil {
+		return errors.New("invalid input args to function")
+	}
+	for rid, rgen := range remote {
+		lgen, ok := local[rid]
+		if !ok || lgen < rgen {
+			local[rid] = rgen
+		}
+	}
+	return nil
+}
+
+// diffGenVectors diffs generation vectors for a given SyncRoot belonging
+// to src and dest and returns the generations known to src and not known to
+// dest. In addition, sync needs to maintain the order in which device
+// generations are created/received. Hence, when two generation
+// vectors are diffed, the differing generations are returned in a
+// sorted order based on their position in the src's log.  genOrder
+// array consists of every generation that is missing between src and
+// dest sorted using its position in the src's log.
+// Example: Generation vector for device A (src) AVec = {A:10, B:5, C:1}
+//          Generation vector for device B (dest) BVec = {A:5, B:10, D:2}
+// Missing generations in unsorted order: {A:6, A:7, A:8, A:9, A:10, C:1}
+//
+// TODO(hpucha): Revisit for the case of a lot of generations to
+// send back (say during bootstrap).
+func (dt *devTable) diffGenVectors(srcVec, destVec GenVector, srid ObjId) ([]*genOrder, error) {
+	if dt.db == nil {
+		return nil, errInvalidDTab
+	}
+
+	// Create an array for the generations that need to be returned.
+	var gens []*genOrder
+
+	// Compute missing generations for devices that are in destination and source vector.
+	for devid, genid := range destVec {
+		srcGenId, ok := srcVec[devid]
+		// Skip since src doesn't know of this device.
+		if !ok {
+			continue
+		}
+		// Need to include all generations in the interval [genid+1, srcGenId],
+		// genid+1 and srcGenId inclusive.
+		// Check against reclaimVec to see if required generations are already GCed.
+		// Starting gen is then max(oldGen, genid+1)
+		startGen := genid + 1
+		oldGen := dt.getOldestGen(devid) + 1
+		if startGen < oldGen {
+			vlog.VI(1).Infof("diffGenVectors:: Adjusting starting generations from %d to %d",
+				startGen, oldGen)
+			startGen = oldGen
+		}
+		for i := startGen; i <= srcGenId; i++ {
+			// Populate the genorder entry.
+			var entry genOrder
+			if err := dt.populateGenOrderEntry(&entry, devid, srid, i); err != nil {
+				return nil, err
+			}
+			gens = append(gens, &entry)
+		}
+	}
+	// Compute missing generations for devices not in destination vector but in source vector.
+	for devid, genid := range srcVec {
+		// Add devices destination does not know about.
+		if _, ok := destVec[devid]; !ok {
+			// Bootstrap generation to oldest available.
+			destGenId := dt.getOldestGen(devid) + 1
+			// Need to include all generations in the interval [destGenId, genid],
+			// destGenId and genid inclusive.
+			for i := destGenId; i <= genid; i++ {
+				// Populate the genorder entry.
+				var entry genOrder
+				if err := dt.populateGenOrderEntry(&entry, devid, srid, i); err != nil {
+					return nil, err
+				}
+				gens = append(gens, &entry)
+			}
+		}
+	}
+
+	// Sort generations in log order.
+	sort.Sort(byOrder(gens))
+	return gens, nil
+}
+
+// getOldestGen returns the most recent gc'ed generation for the device "dev".
+func (dt *devTable) getOldestGen(dev DeviceId) GenId {
+	return dt.head.ReclaimVec[dev]
+}
+
+// // computeReclaimVector computes a generation vector such that the
+// // generations less than or equal to those in the vector can be
+// // garbage collected. Caller holds a lock on s.lock.
+// //
+// // Approach: For each device in the system, we compute its maximum
+// // generation known to all the other devices in the system. This is a
+// // O(N^2) algorithm where N is the number of devices in the system. N
+// // is assumed to be small, of the order of hundreds of devices.
+// func (dt *devTable) computeReclaimVector() (GenVector, error) {
+// 	// Get local generation vector to create the set of devices in
+// 	// the system. Local generation vector is a good bootstrap
+// 	// device set since it contains all the devices whose log
+// 	// records were ever stored locally.
+// 	devSet, err := dt.getGenVec(dt.s.id)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+//
+// 	newReclaimVec := GenVector{}
+// 	for devid := range devSet {
+// 		if !dt.hasDevInfo(devid) {
+// 			// This node knows of devid, but hasn't yet
+// 			// contacted the device. Do not garbage
+// 			// collect any further. For instance, when
+// 			// node A learns of node C's generations from
+// 			// node B, node A may not have an entry for
+// 			// node C yet, but node C will be part of its
+// 			// devSet.
+// 			for dev := range devSet {
+// 				newReclaimVec[dev] = dt.getOldestGen(dev)
+// 			}
+// 			return newReclaimVec, nil
+// 		}
+//
+// 		vec, err := dt.getGenVec(devid)
+// 		if err != nil {
+// 			return nil, err
+// 		}
+// 		for dev := range devSet {
+// 			gen1, ok := vec[dev]
+// 			// Device "devid" does not know about device "dev".
+// 			if !ok {
+// 				newReclaimVec[dev] = dt.getOldestGen(dev)
+// 				continue
+// 			}
+// 			gen2, ok := newReclaimVec[dev]
+// 			if !ok || (gen1 < gen2) {
+// 				newReclaimVec[dev] = gen1
+// 			}
+// 		}
+// 	}
+// 	return newReclaimVec, nil
+// }
+//
+// // addDevice adds a newly learned device to the devTable state.
+// func (dt *devTable) addDevice(newDev DeviceId) error {
+// 	// Create an entry in the device table for the new device.
+// 	vector := GenVector{
+// 		newDev: 0,
+// 	}
+// 	if err := dt.putGenVec(newDev, vector); err != nil {
+// 		return err
+// 	}
+//
+// 	// Update local generation vector with the new device.
+// 	local, err := dt.getDevInfo(dt.s.id)
+// 	if err != nil {
+// 		return err
+// 	}
+// 	if err := dt.updateLocalGenVector(local.Vector, vector); err != nil {
+// 		return err
+// 	}
+// 	if err := dt.putDevInfo(dt.s.id, local); err != nil {
+// 		return err
+// 	}
+// 	return nil
+// }
+//
+// // updateReclaimVec updates the reclaim vector to track gc'ed generations.
+// func (dt *devTable) updateReclaimVec(minGens GenVector) error {
+// 	for dev, min := range minGens {
+// 		gen, ok := dt.head.ReclaimVec[dev]
+// 		if !ok {
+// 			if min < 1 {
+// 				vlog.Errorf("updateReclaimVec:: Received bad generation %s %d",
+// 					dev, min)
+// 				dt.head.ReclaimVec[dev] = 0
+// 			} else {
+// 				dt.head.ReclaimVec[dev] = min - 1
+// 			}
+// 			continue
+// 		}
+//
+// 		// We obtained a generation that is already reclaimed.
+// 		if min <= gen {
+// 			return errors.New("requested gen smaller than GC'ed gen")
+// 		}
+// 	}
+// 	return nil
+// }
+
+// getDeviceIds returns the IDs of all devices that are involved in synchronization.
+func (dt *devTable) getDeviceIds() ([]DeviceId, error) {
+	if dt.db == nil {
+		return nil, errInvalidDTab
+	}
+
+	devIDs := make([]DeviceId, 0)
+	dt.devices.keyIter(func(devStr string) {
+		devIDs = append(devIDs, DeviceId(devStr))
+	})
+
+	return devIDs, nil
+}
+
+// dump writes to the log file information on all device table entries.
+func (dt *devTable) dump() {
+	if dt.db == nil {
+		return
+	}
+
+	vlog.VI(1).Infof("DUMP: Dev: self %v: resmark %v, reclaim %v",
+		dt.s.id, dt.head.Resmark, dt.head.ReclaimVec)
+
+	dt.devices.keyIter(func(devStr string) {
+		info, err := dt.getDevInfo(DeviceId(devStr))
+		if err != nil {
+			return
+		}
+
+		vlog.VI(1).Infof("DUMP: Dev: %s: #SR %d, time %v", devStr, len(info.Vectors), info.Ts)
+		for sr, vec := range info.Vectors {
+			vlog.VI(1).Infof("DUMP: Dev: %s: SR %v, vec %v", devStr, sr, vec)
+		}
+	})
+}
diff --git a/services/syncbase/sync/devtable_test.go b/services/syncbase/sync/devtable_test.go
new file mode 100644
index 0000000..8cf5307
--- /dev/null
+++ b/services/syncbase/sync/devtable_test.go
@@ -0,0 +1,1227 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Veyron Sync devTable component.
+import (
+	"os"
+	"reflect"
+	"runtime"
+	"testing"
+	"time"
+)
+
+// TestDevTabStore tests creating a backing file for devTable.
+func TestDevTabStore(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	fsize := getFileSize(devfile)
+	if fsize < 0 {
+		//t.Errorf("DevTable file %s not created", devfile)
+	}
+
+	if err := dtab.flush(); err != nil {
+		t.Errorf("Cannot flush devTable file %s, err %v", devfile, err)
+	}
+
+	oldfsize := fsize
+	fsize = getFileSize(devfile)
+	if fsize <= oldfsize {
+		//t.Errorf("DevTable file %s not flushed", devfile)
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+
+	oldfsize = getFileSize(devfile)
+
+	dtab, err = openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot re-open existing devTable file %s, err %v", devfile, err)
+	}
+
+	fsize = getFileSize(devfile)
+	if fsize != oldfsize {
+		t.Errorf("DevTable file %s size changed across re-open (%d %d)", devfile, fsize, oldfsize)
+	}
+
+	if err := dtab.flush(); err != nil {
+		t.Errorf("Cannot flush devTable file %s, err %v", devfile, err)
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestInvalidDTab tests devTable methods on an invalid (closed) devTable ptr.
+func TestInvalidDTab(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+
+	validateError := func(err error, funcName string) {
+		_, file, line, _ := runtime.Caller(1)
+		if err == nil || err != errInvalidDTab {
+			t.Errorf("%s:%d %s() did not fail on a closed devTable: %v", file, line, funcName, err)
+		}
+	}
+
+	err = dtab.close()
+	validateError(err, "close")
+
+	err = dtab.flush()
+	validateError(err, "flush")
+
+	srid := ObjId("foo")
+
+	err = dtab.initSyncRoot(srid)
+	validateError(err, "initSyncRoot")
+
+	err = dtab.putDevInfo(s.id, &devInfo{})
+	validateError(err, "putDevInfo")
+
+	_, err = dtab.getDevInfo(s.id)
+	validateError(err, "getDevInfo")
+
+	err = dtab.delDevInfo(s.id)
+	validateError(err, "delDevInfo")
+
+	if dtab.hasDevInfo(s.id) {
+		t.Errorf("hasDevInfo() did not fail on a closed devTable: %v", err)
+	}
+
+	err = dtab.putGenVec(s.id, srid, GenVector{})
+	validateError(err, "putGenVec")
+
+	_, err = dtab.getGenVec(s.id, srid)
+	validateError(err, "getGenVec")
+
+	err = dtab.updateGeneration(s.id, srid, s.id, 0)
+	validateError(err, "updateGeneration")
+
+	err = dtab.updateLocalGenVector(GenVector{}, GenVector{})
+	validateError(err, "updateLocalGenVector")
+
+	_, err = dtab.diffGenVectors(GenVector{}, GenVector{}, srid)
+	validateError(err, "diffGenVectors")
+
+	_, err = dtab.getDeviceIds()
+	validateError(err, "getDeviceIds")
+
+	// Harmless NOP.
+	dtab.dump()
+}
+
+// TestPutGetDevTableHeader tests setting and getting devTable header across devTable open/close/reopen.
+func TestPutGetDevTableHeader(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	// In memory head should be initialized.
+	if dtab.head.Resmark != nil {
+		t.Errorf("First time log create should reset header: %v", dtab.head.Resmark)
+	}
+	expVec := GenVector{dtab.s.id: 0}
+	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+			devfile, dtab.head.ReclaimVec, expVec)
+	}
+
+	// No head should be there in db.
+	if err = dtab.getHead(); err == nil {
+		t.Errorf("getHead() found non-existent head in devTable file %s, err %v", devfile, err)
+	}
+
+	if dtab.hasHead() {
+		t.Errorf("hasHead() found non-existent head in devTable file %s", devfile)
+	}
+
+	expMark := []byte{1, 2, 3}
+	expVec = GenVector{
+		"VeyronTab":   30,
+		"VeyronPhone": 10,
+	}
+	dtab.head = &devTableHeader{
+		Resmark:    expMark,
+		ReclaimVec: expVec,
+	}
+
+	if err := dtab.putHead(); err != nil {
+		t.Errorf("Cannot put head %v in devTable file %s, err %v", dtab.head, devfile, err)
+	}
+
+	// Reset values.
+	dtab.head.Resmark = nil
+	dtab.head.ReclaimVec = GenVector{}
+
+	for i := 0; i < 2; i++ {
+		if err := dtab.getHead(); err != nil {
+			t.Fatalf("getHead() can not find head (i=%d) in devTable file %s, err %v", i, devfile, err)
+		}
+
+		if !dtab.hasHead() {
+			t.Errorf("hasHead() can not find head (i=%d) in devTable file %s", i, devfile)
+		}
+
+		if !reflect.DeepEqual(dtab.head.Resmark, expMark) {
+			t.Errorf("Data mismatch for resmark (i=%d) in devTable file %s: %v instead of %v",
+				i, devfile, dtab.head.Resmark, expMark)
+		}
+		if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+			t.Errorf("Data mismatch for reclaimVec (i=%d) in devTable file %s: %v instead of %v",
+				i, devfile, dtab.head.ReclaimVec, expVec)
+		}
+
+		if i == 0 {
+			if err := dtab.close(); err != nil {
+				t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+			}
+			dtab, err = openDevTable(devfile, s)
+			if err != nil {
+				t.Fatalf("Cannot re-open devTable file %s, err %v", devfile, err)
+			}
+		}
+	}
+
+	dtab.dump()
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestPersistDevTableHeader tests that devTable header is
+// automatically persisted across devTable open/close/reopen.
+func TestPersistDevTableHeader(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	// In memory head should be initialized.
+	if dtab.head.Resmark != nil {
+		t.Errorf("First time log create should reset header: %v", dtab.head.Resmark)
+	}
+	expVec := GenVector{dtab.s.id: 0}
+	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+			devfile, dtab.head.ReclaimVec, expVec)
+	}
+
+	expMark := []byte{0, 2, 255}
+	expVec = GenVector{
+		"VeyronTab":   100,
+		"VeyronPhone": 10000,
+	}
+	dtab.head = &devTableHeader{
+		Resmark:    expMark,
+		ReclaimVec: expVec,
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+
+	dtab, err = openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	// In memory head should be initialized from db.
+	if !reflect.DeepEqual(dtab.head.Resmark, expMark) {
+		t.Errorf("Data mismatch for resmark in devTable file %s: %v instead of %v",
+			devfile, dtab.head.Resmark, expMark)
+	}
+	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+			devfile, dtab.head.ReclaimVec, expVec)
+	}
+
+	expMark = []byte{60, 180, 7}
+	expVec = GenVector{
+		"VeyronTab":   1,
+		"VeyronPhone": 1987,
+	}
+	dtab.head = &devTableHeader{
+		Resmark:    expMark,
+		ReclaimVec: expVec,
+	}
+
+	if err := dtab.flush(); err != nil {
+		t.Errorf("Cannot flush devTable file %s, err %v", devfile, err)
+	}
+
+	// Reset values.
+	dtab.head.Resmark = nil
+	dtab.head.ReclaimVec = GenVector{}
+
+	if err := dtab.getHead(); err != nil {
+		t.Fatalf("getHead() can not find head in devTable file %s, err %v", devfile, err)
+	}
+
+	// In memory head should be initialized from db.
+	if !reflect.DeepEqual(dtab.head.Resmark, expMark) {
+		t.Errorf("Data mismatch for resmark in devTable file %s: %v instead of %v",
+			devfile, dtab.head.Resmark, expMark)
+	}
+	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+			devfile, dtab.head.ReclaimVec, expVec)
+	}
+
+	dtab.dump()
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestDTabInitDelSyncRoot tests initing and deleting a new SyncRoot.
+func TestDTabInitDelSyncRoot(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+	srid1 := ObjId("foo")
+	if err := dtab.initSyncRoot(srid1); err != nil {
+		t.Fatalf("Cannot create new SyncRoot %s, err %v", srid1.String(), err)
+	}
+	srid2 := ObjId("bar")
+	if err := dtab.initSyncRoot(srid2); err != nil {
+		t.Fatalf("Cannot create new SyncRoot %s, err %v", srid2.String(), err)
+	}
+	if err := dtab.initSyncRoot(srid2); err == nil {
+		t.Fatalf("Creating existing SyncRoot didn't fail %s", srid2.String())
+	}
+
+	info, err := dtab.getDevInfo(s.id)
+	if err != nil {
+		t.Errorf("GetDevInfo() can not find device %s in devTable file %s err %v",
+			s.id, devfile, err)
+	}
+	expVec := map[ObjId]GenVector{
+		srid1: {s.id: 0},
+		srid2: {s.id: 0},
+	}
+	if !reflect.DeepEqual(info.Vectors, expVec) {
+		t.Errorf("Data mismatch for device %s %v instead of %v",
+			s.id, info.Vectors, expVec)
+	}
+	if err := dtab.delSyncRoot(srid1); err != nil {
+		t.Fatalf("Cannot delete SyncRoot %s, err %v", srid1.String(), err)
+	}
+	if err := dtab.delSyncRoot(srid1); err == nil {
+		t.Fatalf("Deleting non-existent SyncRoot didn't fail %s", srid1.String())
+	}
+	if err := dtab.delSyncRoot(srid2); err != nil {
+		t.Fatalf("Cannot delete SyncRoot %s, err %v", srid2.String(), err)
+	}
+
+	if _, err = dtab.getDevInfo(s.id); err == nil {
+		t.Errorf("GetDevInfo() found device %s in devTable file %s err %v",
+			s.id, devfile, err)
+	}
+}
+
+// TestPutGetDevInfo tests setting and getting devInfo across devTable open/close/reopen.
+func TestPutGetDevInfo(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+
+	info, err := dtab.getDevInfo(devid)
+	if err == nil || info != nil {
+		t.Errorf("GetDevInfo() found non-existent device %s in devTable file %s: %v, err %v",
+			devid, devfile, info, err)
+	}
+
+	if dtab.hasDevInfo(devid) {
+		t.Errorf("HasDevInfo() found non-existent device %s in devTable file %s",
+			devid, devfile)
+	}
+	info = &devInfo{
+		Vectors: map[ObjId]GenVector{ObjId("haha"): GenVector{"VeyronTab": 0, "VeyronPhone": 10},
+			ObjId("hello"): GenVector{"VeyronLaptop": 20, "VeyronPhone": 30, "VeyronTab": 80}},
+		Ts: time.Now().UTC(),
+	}
+
+	if err := dtab.putDevInfo(devid, info); err != nil {
+		t.Errorf("Cannot put device %s (%v) in devTable file %s, err %v", devid, info, devfile, err)
+	}
+
+	for i := 0; i < 2; i++ {
+		curInfo, err := dtab.getDevInfo(devid)
+		if err != nil || curInfo == nil {
+			t.Fatalf("GetDevInfo() can not find device %s (i=%d) in devTable file %s: %v, err: %v",
+				devid, i, devfile, curInfo, err)
+		}
+
+		if !dtab.hasDevInfo(devid) {
+			t.Errorf("HasDevInfo() can not find device %s (i=%d) in devTable file %s",
+				devid, i, devfile)
+		}
+
+		if !reflect.DeepEqual(curInfo, info) {
+			t.Errorf("Data mismatch for device %s (i=%d) in devTable file %s: %v instead of %v",
+				devid, i, devfile, curInfo, info)
+		}
+
+		if i == 0 {
+			if err := dtab.close(); err != nil {
+				t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+			}
+			dtab, err = openDevTable(devfile, s)
+			if err != nil {
+				t.Fatalf("Cannot re-open devTable file %s, err %v", devfile, err)
+			}
+		}
+	}
+
+	dtab.dump()
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestPutGetGenVec tests setting and getting generation vector across dtab open/close/reopen.
+func TestPutGetGenVec(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srids := []ObjId{"foo", "bar"}
+	vecs := []GenVector{GenVector{"VeyronTab": 0, "VeyronPhone": 10, "VeyronDesktop": 20, "VeyronLaptop": 2},
+		GenVector{"VeyronTab": 400, "VeyronPhone": 100, "VeyronDesktop": 200, "VeyronLaptop": 20}}
+
+	for i, sr := range srids {
+		vec, err := dtab.getGenVec(devid, sr)
+		if err == nil || vec != nil {
+			t.Errorf("GetGenVec() found non-existent device %s in devTable file %s: %v, err %v",
+				devid, devfile, vec, err)
+		}
+
+		if err := dtab.putGenVec(devid, sr, vecs[i]); err != nil {
+			t.Errorf("Cannot put device %s (%s %v) in devTable file %s, err %v", devid, sr.String(),
+				vecs[i], devfile, err)
+		}
+	}
+
+	for k, sr := range srids {
+		for i := 0; i < 2; i++ {
+			// Check for devid.
+			curVec, err := dtab.getGenVec(devid, sr)
+			if err != nil || curVec == nil {
+				t.Fatalf("GetGenVec() can not find device %s (i=%d) in devTable file %s, err %v",
+					devid, i, devfile, err)
+			}
+
+			if !reflect.DeepEqual(curVec, vecs[k]) {
+				t.Errorf("Data mismatch for device %s srid %s (i=%d) in devTable file %s: %v instead of %v",
+					devid, sr.String(), i, devfile, curVec, vecs[k])
+			}
+
+			if i == 0 {
+				if err := dtab.close(); err != nil {
+					t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+				}
+				dtab, err = openDevTable(devfile, s)
+				if err != nil {
+					t.Fatalf("Cannot re-open devTable file %s, err %v", devfile, err)
+				}
+			}
+		}
+	}
+
+	dtab.dump()
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestUpdateGeneration tests updating a generation.
+func TestUpdateGeneration(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("foo")
+
+	err = dtab.updateGeneration(devid, srid, devid, 10)
+	if err == nil {
+		t.Errorf("UpdateGeneration() found non-existent device %s in devTable file %s, err %v",
+			devid, devfile, err)
+	}
+	v := GenVector{
+		"VeyronTab":     0,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+
+	if err := dtab.putGenVec(devid, srid, v); err != nil {
+		t.Errorf("Cannot put device %s (%s %v) in devTable file %s, err %v", devid,
+			srid.String(), v, devfile, err)
+	}
+
+	// Try a non-existent SyncRoot ID.
+	if err = dtab.updateGeneration(devid, ObjId("haha"), devid, 10); err == nil {
+		t.Errorf("UpdateGeneration() did not fail on a wrong SyncRoot for %s in devTable file %s", devid, devfile)
+	}
+
+	err = dtab.updateGeneration(devid, srid, devid, 10)
+	if err != nil {
+		t.Errorf("UpdateGeneration() failed for %s in devTable file %s with error %v",
+			devid, devfile, err)
+	}
+	err = dtab.updateGeneration(devid, srid, "VeyronLaptop", 18)
+	if err != nil {
+		t.Errorf("UpdateGeneration() failed for %s in devTable file %s with error %v",
+			devid, devfile, err)
+	}
+	curVec, err := dtab.getGenVec(devid, srid)
+	if err != nil || curVec == nil {
+		t.Fatalf("GetGenVec() can not find device %s in devTable file %s, err %v",
+			devid, devfile, err)
+	}
+	vExp := GenVector{
+		"VeyronTab":     10,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  18,
+	}
+
+	if !reflect.DeepEqual(curVec, vExp) {
+		t.Errorf("Data mismatch for device %s srid %s in devTable file %s: %v instead of %v",
+			devid, srid.String(), devfile, v, vExp)
+	}
+
+	dtab.dump()
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestUpdateLocalGenVector tests updating a gen vector.
+func TestUpdateLocalGenVector(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	s := &syncd{id: "VeyronPhone"}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	// Test nil args.
+	if err := dtab.updateLocalGenVector(nil, nil); err == nil {
+		t.Errorf("UpdateLocalGenVector() failed in devTable file %s with error %v",
+			devfile, err)
+	}
+
+	// Nothing to update.
+	local := GenVector{
+		"VeyronTab":   0,
+		"VeyronPhone": 1,
+	}
+	remote := GenVector{
+		"VeyronTab":   0,
+		"VeyronPhone": 1,
+	}
+	if err := dtab.updateLocalGenVector(local, remote); err != nil {
+		t.Errorf("UpdateLocalGenVector() failed in devTable file %s with error %v",
+			devfile, err)
+	}
+
+	if !reflect.DeepEqual(local, remote) {
+		t.Errorf("Data mismatch for object %v instead of %v",
+			local, remote)
+	}
+
+	// local is missing a generation.
+	local = GenVector{
+		"VeyronPhone": 1,
+	}
+	if err := dtab.updateLocalGenVector(local, remote); err != nil {
+		t.Errorf("UpdateLocalGenVector() failed in devTable file %s with error %v",
+			devfile, err)
+	}
+	if !reflect.DeepEqual(local, remote) {
+		t.Errorf("Data mismatch for object %v instead of %v",
+			local, remote)
+	}
+
+	// local is stale compared to remote.
+	local = GenVector{
+		"VeyronTab":   0,
+		"VeyronPhone": 0,
+	}
+	remote = GenVector{
+		"VeyronTab":    1,
+		"VeyronPhone":  0,
+		"VeyronLaptop": 2,
+	}
+	if err := dtab.updateLocalGenVector(local, remote); err != nil {
+		t.Errorf("UpdateLocalGenVector() failed in devTable file %s with error %v",
+			devfile, err)
+	}
+	if !reflect.DeepEqual(local, remote) {
+		t.Errorf("Data mismatch for object %v instead of %v",
+			local, remote)
+	}
+
+	// local is partially stale.
+	local = GenVector{
+		"VeyronTab":     0,
+		"VeyronPhone":   0,
+		"VeyronDesktop": 20,
+	}
+	remote = GenVector{
+		"VeyronTab":    1,
+		"VeyronPhone":  10,
+		"VeyronLaptop": 2,
+	}
+	localExp := GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+	if err := dtab.updateLocalGenVector(local, remote); err != nil {
+		t.Errorf("UpdateLocalGenVector() failed in devTable file %s with error %v",
+			devfile, err)
+	}
+	if !reflect.DeepEqual(local, localExp) {
+		t.Errorf("Data mismatch for object %v instead of %v",
+			local, localExp)
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestDiffGenVectors tests diffing gen vectors.
+func TestDiffGenVectors(t *testing.T) {
+	logOrder := []DeviceId{"VeyronTab", "VeyronPhone", "VeyronDesktop", "VeyronLaptop"}
+	var expGens []*genOrder
+	srid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// set reclaimVec such that it doesn't affect diffs.
+	reclaimVec := GenVector{
+		"VeyronTab":     0,
+		"VeyronPhone":   0,
+		"VeyronDesktop": 0,
+		"VeyronLaptop":  0,
+	}
+
+	// src and dest are identical vectors.
+	vec := GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+	setupAndTestDiff(t, vec, vec, reclaimVec, logOrder, expGens)
+
+	// src has no updates.
+	srcVec := GenVector{
+		"VeyronTab": 0,
+	}
+	remoteVec := GenVector{
+		"VeyronTab":     5,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  8,
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, []DeviceId{}, expGens)
+
+	// src and remote have no updates.
+	srcVec = GenVector{
+		"VeyronTab": 0,
+	}
+	remoteVec = GenVector{
+		"VeyronTab": 0,
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, []DeviceId{}, expGens)
+
+	// set reclaimVec such that it doesn't affect diffs.
+	reclaimVec = GenVector{
+		"VeyronTab": 0,
+	}
+
+	// src is staler than remote.
+	srcVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+	remoteVec = GenVector{
+		"VeyronTab":     5,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  8,
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, logOrder, expGens)
+
+	// src is fresher than remote.
+	srcVec = GenVector{
+		"VeyronTab":     5,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+	remoteVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+	}
+	expGens = make([]*genOrder, 4)
+	for i := 0; i < 4; i++ {
+		expGens[i] = &genOrder{
+			devID: "VeyronTab",
+			srID:  srid,
+			genID: GenId(i + 2),
+			order: uint32(i + 1),
+		}
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, logOrder, expGens)
+
+	// src is fresher than remote in all but one device.
+	srcVec = GenVector{
+		"VeyronTab":     5,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 22,
+		"VeyronLaptop":  2,
+	}
+	remoteVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   10,
+		"VeyronDesktop": 20,
+		"VeyronLaptop":  2,
+		"VeyronCloud":   40,
+	}
+	expGens = make([]*genOrder, 6)
+	for i := 0; i < 6; i++ {
+		switch {
+		case i < 4:
+			expGens[i] = &genOrder{
+				devID: "VeyronTab",
+				srID:  srid,
+				genID: GenId(i + 2),
+				order: uint32(i + 1),
+			}
+		default:
+			expGens[i] = &genOrder{
+				devID: "VeyronDesktop",
+				srID:  srid,
+				genID: GenId(i - 4 + 21),
+				order: uint32(i - 4 + 35),
+			}
+		}
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, logOrder, expGens)
+
+	// src is fresher than dest, scramble log order.
+	o := []DeviceId{"VeyronTab", "VeyronLaptop", "VeyronPhone", "VeyronDesktop"}
+	srcVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   2,
+		"VeyronDesktop": 3,
+		"VeyronLaptop":  4,
+	}
+	remoteVec = GenVector{
+		"VeyronTab":     0,
+		"VeyronPhone":   2,
+		"VeyronDesktop": 0,
+	}
+	expGens = make([]*genOrder, 8)
+	for i := 0; i < 8; i++ {
+		switch {
+		case i < 1:
+			expGens[i] = &genOrder{
+				devID: "VeyronTab",
+				srID:  srid,
+				genID: GenId(i + 1),
+				order: uint32(i),
+			}
+		case i >= 1 && i < 5:
+			expGens[i] = &genOrder{
+				devID: "VeyronLaptop",
+				srID:  srid,
+				genID: GenId(i),
+				order: uint32(i),
+			}
+		default:
+			expGens[i] = &genOrder{
+				devID: "VeyronDesktop",
+				srID:  srid,
+				genID: GenId(i - 4),
+				order: uint32(i - 5 + 7),
+			}
+		}
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, o, expGens)
+
+	// remote has no updates.
+	srcVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   2,
+		"VeyronDesktop": 3,
+		"VeyronLaptop":  4,
+	}
+	remoteVec = GenVector{
+		"VeyronPhone": 0,
+	}
+	expGens = make([]*genOrder, 10)
+	for i := 0; i < 10; i++ {
+		switch {
+		case i < 1:
+			expGens[i] = &genOrder{
+				devID: "VeyronTab",
+				srID:  srid,
+				genID: GenId(i + 1),
+				order: uint32(i),
+			}
+		case i >= 1 && i < 3:
+			expGens[i] = &genOrder{
+				devID: "VeyronPhone",
+				srID:  srid,
+				genID: GenId(i),
+				order: uint32(i),
+			}
+		case i >= 3 && i < 6:
+			expGens[i] = &genOrder{
+				devID: "VeyronDesktop",
+				srID:  srid,
+				genID: GenId(i - 2),
+				order: uint32(i),
+			}
+		default:
+			expGens[i] = &genOrder{
+				devID: "VeyronLaptop",
+				srID:  srid,
+				genID: GenId(i - 5),
+				order: uint32(i),
+			}
+		}
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, logOrder, expGens)
+
+	// Test with reclaimVec fast-fwded.
+	reclaimVec = GenVector{
+		"VeyronPhone":  1,
+		"VeyronLaptop": 2,
+	}
+	srcVec = GenVector{
+		"VeyronTab":     1,
+		"VeyronPhone":   2,
+		"VeyronDesktop": 3,
+		"VeyronLaptop":  4,
+	}
+	remoteVec = GenVector{
+		"VeyronPhone": 0,
+	}
+	expGens = make([]*genOrder, 7)
+	for i := 0; i < 7; i++ {
+		switch {
+		case i < 1:
+			expGens[i] = &genOrder{
+				devID: "VeyronTab",
+				srID:  srid,
+				genID: GenId(i + 1),
+				order: uint32(i),
+			}
+		case i == 1:
+			expGens[i] = &genOrder{
+				devID: "VeyronPhone",
+				srID:  srid,
+				genID: GenId(i + 1),
+				order: uint32(i + 1),
+			}
+		case i >= 2 && i < 5:
+			expGens[i] = &genOrder{
+				devID: "VeyronDesktop",
+				srID:  srid,
+				genID: GenId(i - 1),
+				order: uint32(i + 1),
+			}
+		default:
+			expGens[i] = &genOrder{
+				devID: "VeyronLaptop",
+				srID:  srid,
+				genID: GenId(i - 2),
+				order: uint32(i + 3),
+			}
+		}
+	}
+	setupAndTestDiff(t, srcVec, remoteVec, reclaimVec, logOrder, expGens)
+}
+
+// setupAndTestDiff is an utility function to test diffing generation vectors.
+func setupAndTestDiff(t *testing.T, srcVec, remoteVec, reclaimVec GenVector, logOrder []DeviceId, expGens []*genOrder) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	var srcid DeviceId = "VeyronTab"
+	var destid DeviceId = "VeyronPhone"
+
+	var err error
+	s := &syncd{id: srcid}
+	s.log, err = openILog(logfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+	dtab, err := openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+	dtab.head.ReclaimVec = reclaimVec
+
+	srid, err := strToObjId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Populate generations in log order.
+	var order uint32
+	for _, k := range logOrder {
+		v, ok := (srcVec)[k]
+		if !ok {
+			t.Errorf("Cannot find key %s in srcVec %v", k, srcVec)
+		}
+		for i := GenId(1); i <= v; i++ {
+			val := &genMetadata{Pos: order}
+			if err := s.log.putGenMetadata(k, srid, i, val); err != nil {
+				t.Errorf("Cannot put object %s:%d in log file %s, err %v", k, v, logfile, err)
+			}
+			order++
+		}
+	}
+	gens, err := dtab.diffGenVectors(srcVec, remoteVec, srid)
+	if err != nil {
+		t.Fatalf("DiffGenVectors() failed src: %s %v dest: %s %v in devTable file %s, err %v",
+			srcid, srcVec, destid, remoteVec, devfile, err)
+	}
+
+	if !reflect.DeepEqual(gens, expGens) {
+		t.Fatalf("Data mismatch for genorder %v instead of %v, src %v dest %v reclaim %v",
+			gens, expGens, srcVec, remoteVec, reclaimVec)
+	}
+
+	if err := dtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// TestGetOldestGen tests obtaining generations from reclaimVec.
+func TestGetOldestGen(t *testing.T) {
+	devfile := getFileName()
+	defer os.Remove(devfile)
+
+	var srcid DeviceId = "VeyronTab"
+	s := &syncd{id: srcid}
+	var err error
+	s.devtab, err = openDevTable(devfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+	}
+
+	if s.devtab.getOldestGen(srcid) != 0 {
+		t.Errorf("Cannot get generation for device %s in devTable file %s",
+			srcid, devfile)
+	}
+
+	var destid DeviceId = "VeyronPhone"
+	if s.devtab.getOldestGen(destid) != 0 {
+		t.Errorf("Cannot get generation for device %s in devTable file %s",
+			destid, devfile)
+	}
+
+	s.devtab.head.ReclaimVec[srcid] = 10
+	if s.devtab.getOldestGen(srcid) != 10 {
+		t.Errorf("Cannot get generation for device %s in devTable file %s",
+			srcid, devfile)
+	}
+	if s.devtab.getOldestGen(destid) != 0 {
+		t.Errorf("Cannot get generation for device %s in devTable file %s",
+			destid, devfile)
+	}
+
+	if err := s.devtab.close(); err != nil {
+		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+	}
+}
+
+// // TestComputeReclaimVector tests reclaim vector computation.
+// func TestComputeReclaimVector(t *testing.T) {
+// 	devArr := []DeviceId{"VeyronTab", "VeyronPhone", "VeyronDesktop", "VeyronLaptop"}
+// 	genVecArr := make([]GenVector, 4)
+//
+// 	// All devices are up-to-date.
+// 	genVecArr[0] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[2] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[3] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, genVecArr[0])
+//
+// 	// Every device is missing at least one other device. Not possible to gc.
+// 	genVecArr[0] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronLaptop": 4}
+// 	genVecArr[2] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3}
+// 	genVecArr[3] = GenVector{"VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	expReclaimVec := GenVector{"VeyronTab": 0, "VeyronPhone": 0, "VeyronDesktop": 0, "VeyronLaptop": 0}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, expReclaimVec)
+//
+// 	// All devices know at least one generation from other devices.
+// 	genVecArr[0] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 2, "VeyronLaptop": 2}
+// 	genVecArr[2] = GenVector{"VeyronTab": 1, "VeyronPhone": 1, "VeyronDesktop": 3, "VeyronLaptop": 1}
+// 	genVecArr[3] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 1, "VeyronLaptop": 4}
+// 	expReclaimVec = GenVector{"VeyronTab": 1, "VeyronPhone": 1, "VeyronDesktop": 1, "VeyronLaptop": 1}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, expReclaimVec)
+//
+// 	// One device is missing from one other device.
+// 	genVecArr[0] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 2}
+// 	genVecArr[2] = GenVector{"VeyronTab": 1, "VeyronPhone": 1, "VeyronDesktop": 3, "VeyronLaptop": 1}
+// 	genVecArr[3] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 1, "VeyronLaptop": 4}
+// 	expReclaimVec = GenVector{"VeyronTab": 1, "VeyronPhone": 1, "VeyronDesktop": 1, "VeyronLaptop": 0}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, expReclaimVec)
+//
+// 	// All devices know at least "n" generations from other devices.
+// 	var n GenId = 10
+// 	genVecArr[0] = GenVector{"VeyronTab": n + 10, "VeyronPhone": n,
+// 		"VeyronDesktop": n + 8, "VeyronLaptop": n + 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": n + 6, "VeyronPhone": n + 10,
+// 		"VeyronDesktop": n, "VeyronLaptop": n + 3}
+// 	genVecArr[2] = GenVector{"VeyronTab": n, "VeyronPhone": n + 2,
+// 		"VeyronDesktop": n + 10, "VeyronLaptop": n}
+// 	genVecArr[3] = GenVector{"VeyronTab": n + 7, "VeyronPhone": n + 1,
+// 		"VeyronDesktop": n + 5, "VeyronLaptop": n + 10}
+// 	expReclaimVec = GenVector{"VeyronTab": n, "VeyronPhone": n, "VeyronDesktop": n, "VeyronLaptop": n}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, expReclaimVec)
+//
+// 	// Never contacted a device.
+// 	devArr = []DeviceId{"VeyronTab", "VeyronDesktop", "VeyronLaptop"}
+// 	genVecArr[0] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[1] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[2] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	expReclaimVec = GenVector{"VeyronTab": 0, "VeyronPhone": 0, "VeyronDesktop": 0, "VeyronLaptop": 0}
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, nil, expReclaimVec)
+//
+// 	// Start from existing reclaim vector.
+// 	devArr = []DeviceId{"VeyronTab", "VeyronPhone", "VeyronDesktop", "VeyronLaptop"}
+// 	reclaimVec := GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	genVecArr[0] = GenVector{"VeyronTab": 6, "VeyronPhone": 6, "VeyronDesktop": 6, "VeyronLaptop": 6}
+// 	genVecArr[1] = GenVector{"VeyronTab": 6, "VeyronPhone": 6, "VeyronDesktop": 3, "VeyronLaptop": 6}
+// 	genVecArr[2] = GenVector{"VeyronTab": 6, "VeyronPhone": 6, "VeyronDesktop": 6, "VeyronLaptop": 4}
+// 	genVecArr[3] = GenVector{"VeyronTab": 1, "VeyronPhone": 2, "VeyronDesktop": 6, "VeyronLaptop": 6}
+//
+// 	setupAndTestReclaimVector(t, devArr, genVecArr, reclaimVec, reclaimVec)
+// }
+//
+// // setupAndTestReclaimVector is an utility function to test reclaim vector computation.
+// func setupAndTestReclaimVector(t *testing.T, devArr []DeviceId, genVecArr []GenVector, reclaimStart, expReclaimVec GenVector) {
+// 	devfile := getFileName()
+// 	defer os.Remove(devfile)
+//
+// 	s := &syncd{id: "VeyronTab"}
+// 	dtab, err := openDevTable(devfile, s)
+// 	if err != nil {
+// 		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+// 	}
+// 	if reclaimStart != nil {
+// 		dtab.head.ReclaimVec = reclaimStart
+// 	}
+//
+// 	for i := range devArr {
+// 		if err := dtab.putGenVec(devArr[i], genVecArr[i]); err != nil {
+// 			t.Errorf("Cannot put object %s (%v) in devTable file %s, err %v",
+// 				devArr[i], genVecArr[i], devfile, err)
+// 		}
+// 	}
+//
+// 	reclaimVec, err := dtab.computeReclaimVector()
+// 	if err != nil {
+// 		t.Fatalf("computeReclaimVector() failed devices: %v, vectors: %v in devTable file %s, err %v",
+// 			devArr, genVecArr, devfile, err)
+// 	}
+//
+// 	if !reflect.DeepEqual(reclaimVec, expReclaimVec) {
+// 		t.Fatalf("Data mismatch for reclaimVec %v instead of %v",
+// 			reclaimVec, expReclaimVec)
+// 	}
+//
+// 	if err := dtab.close(); err != nil {
+// 		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+// 	}
+// }
+//
+// // TestAddDevice tests adding a device to the devTable.
+// func TestAddDevice(t *testing.T) {
+// 	devfile := getFileName()
+// 	defer os.Remove(devfile)
+//
+// 	s := &syncd{id: "VeyronPhone"}
+// 	dtab, err := openDevTable(devfile, s)
+// 	if err != nil {
+// 		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+// 	}
+//
+// 	var dev DeviceId = "VeyronLaptop"
+// 	if err := dtab.addDevice(dev); err != nil {
+// 		t.Fatalf("Cannot add new device in devTable file %s, err %v", devfile, err)
+// 	}
+//
+// 	vec, err := dtab.getGenVec(dev)
+// 	if err != nil || vec == nil {
+// 		t.Fatalf("GetGenVec() can not find object %s in devTable file %s, err %v",
+// 			dev, devfile, err)
+// 	}
+// 	expVec := GenVector{dev: 0}
+// 	if !reflect.DeepEqual(vec, expVec) {
+// 		t.Errorf("Data mismatch for object %s in devTable file %s: %v instead of %v",
+// 			dev, devfile, vec, expVec)
+// 	}
+//
+// 	vec, err = dtab.getGenVec(dtab.s.id)
+// 	if err != nil || vec == nil {
+// 		t.Fatalf("GetGenVec() can not find object %s in devTable file %s, err %v",
+// 			dtab.s.id, devfile, err)
+// 	}
+// 	expVec = GenVector{dtab.s.id: 0, dev: 0}
+// 	if !reflect.DeepEqual(vec, expVec) {
+// 		t.Errorf("Data mismatch for object %s in devTable file %s: %v instead of %v",
+// 			dtab.s.id, devfile, vec, expVec)
+// 	}
+//
+// 	expVec = GenVector{dtab.s.id: 10, "VeyronDesktop": 40, dev: 80}
+// 	if err := dtab.putGenVec(dtab.s.id, expVec); err != nil {
+// 		t.Fatalf("PutGenVec() can not put object %s in devTable file %s, err %v",
+// 			dtab.s.id, devfile, err)
+// 	}
+// 	dev = "VeyronTab"
+// 	if err := dtab.addDevice(dev); err != nil {
+// 		t.Fatalf("Cannot add new device in devTable file %s, err %v", devfile, err)
+// 	}
+// 	expVec[dev] = 0
+//
+// 	vec, err = dtab.getGenVec(dtab.s.id)
+// 	if err != nil || vec == nil {
+// 		t.Fatalf("GetGenVec() can not find object %s in devTable file %s, err %v",
+// 			dtab.s.id, devfile, err)
+// 	}
+// 	if !reflect.DeepEqual(vec, expVec) {
+// 		t.Errorf("Data mismatch for object %s in devTable file %s: %v instead of %v",
+// 			dtab.s.id, devfile, vec, expVec)
+// 	}
+//
+// 	if err := dtab.close(); err != nil {
+// 		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+// 	}
+// }
+//
+// // TestUpdateReclaimVec tests updating the reclaim vector.
+// func TestUpdateReclaimVec(t *testing.T) {
+// 	devfile := getFileName()
+// 	defer os.Remove(devfile)
+//
+// 	s := &syncd{id: "VeyronPhone"}
+// 	dtab, err := openDevTable(devfile, s)
+// 	if err != nil {
+// 		t.Fatalf("Cannot open new devTable file %s, err %v", devfile, err)
+// 	}
+//
+// 	minGens := GenVector{"VeyronTab": 1, "VeyronDesktop": 3, "VeyronLaptop": 4}
+// 	if err := dtab.updateReclaimVec(minGens); err != nil {
+// 		t.Fatalf("Cannot update reclaimvec in devTable file %s, err %v", devfile, err)
+// 	}
+// 	expVec := GenVector{dtab.s.id: 0, "VeyronTab": 0, "VeyronDesktop": 2, "VeyronLaptop": 3}
+// 	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+// 		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+// 			devfile, dtab.head.ReclaimVec, expVec)
+// 	}
+//
+// 	dtab.head.ReclaimVec[DeviceId("VeyronTab")] = 4
+// 	minGens = GenVector{"VeyronTab": 1}
+// 	if err := dtab.updateReclaimVec(minGens); err == nil {
+// 		t.Fatalf("Update reclaimvec didn't fail in devTable file %s", devfile)
+// 	}
+//
+// 	minGens = GenVector{"VeyronTab": 5}
+// 	if err := dtab.updateReclaimVec(minGens); err != nil {
+// 		t.Fatalf("Cannot update reclaimvec in devTable file %s, err %v", devfile, err)
+// 	}
+// 	expVec = GenVector{dtab.s.id: 0, "VeyronTab": 4, "VeyronDesktop": 2, "VeyronLaptop": 3}
+// 	if !reflect.DeepEqual(dtab.head.ReclaimVec, expVec) {
+// 		t.Errorf("Data mismatch for reclaimVec in devTable file %s: %v instead of %v",
+// 			devfile, dtab.head.ReclaimVec, expVec)
+// 	}
+//
+// 	if err := dtab.close(); err != nil {
+// 		t.Errorf("Cannot close devTable file %s, err %v", devfile, err)
+// 	}
+// }
diff --git a/services/syncbase/sync/ilog.go b/services/syncbase/sync/ilog.go
new file mode 100644
index 0000000..9e35c24
--- /dev/null
+++ b/services/syncbase/sync/ilog.go
@@ -0,0 +1,627 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Package vsync provides veyron sync ILog utility functions.  ILog
+// (Indexed Log) provides log functionality with indexing support.
+// ILog stores log records that are locally generated or obtained over
+// the network.  Indexing is needed since sync needs to selectively
+// retrieve log records that belong to a particular device, syncroot
+// and generation during synchronization.
+//
+// When a device receives a request to send log records, it first
+// computes the missing generations between itself and the incoming
+// request on a per-syncroot basis. It then sends all the log records
+// belonging to each missing generation.  A device that receives log
+// records over the network replays all the records received from
+// another device in a single batch. Each replayed log record adds a
+// new version to the dag of the object contained in the log
+// record. At the end of replaying all the log records, conflict
+// detection and resolution is carried out for all the objects learned
+// during this iteration. Conflict detection and resolution is carried
+// out after a batch of log records are replayed, instead of
+// incrementally after each record is replayed, to avoid repeating
+// conflict resolution already performed by other devices.
+//
+// New log records are created when objects in the local store are
+// created/updated. Local log records are also replayed to keep the
+// per-object dags consistent with the local store state.
+//
+// Note that syncgroups are mainly tracked between syncd/store and the
+// client. Sync-related metadata (log records, generations and
+// generation vectors) is unaware of syncgroups, and uses syncroots
+// instead. Each syncgroup contains a root object and a unique root
+// object ID. In case of peer syncgroups, all the peer syncgroups
+// contain the same root object ID.  Sync translates any given
+// syncgroup to a syncroot rooted at this root object ID and syncs all
+// the syncroots the device is part of.  Thus, although a device may
+// be part of more than 1 peer syncgroup, at the sync level, a single
+// syncroot is synced on behalf of all the peer syncgroups.
+//
+// Implementation notes: ILog records are stored in a persistent K/V
+// database in the current implementation.  ILog db consists of 3
+// tables:
+// ** records: table consists of all the log records indexed
+// by deviceid:srid:genid:lsn referring to
+//         the device that creates the log record
+//         the root oid of the syncroot to which the log record belongs
+//         the generation on the syncroot the log record is part of
+//         the sequence number in that generation
+// Note that lsn in each generation starts from 0 and genid starts from 1.
+// ** gens: table consists of the generation metadata for each
+// generation, and is indexed by deviceid:srid:genid.
+// ** head: table consists of the log header.
+import (
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"v.io/x/lib/vlog"
+)
+
+var (
+	errNoUpdates  = errors.New("no new local updates")
+	errInvalidLog = errors.New("invalid log db")
+)
+
+// curLocalGen contains metadata re. the current local generation for a SyncRoot.
+type curLocalGen struct {
+	CurGenNum GenId  // generation id for a SyncRoot's current generation.
+	CurSeqNum SeqNum // log sequence number for a SyncRoot's current generation.
+}
+
+// iLogHeader contains the log header metadata.
+type iLogHeader struct {
+	CurSRGens map[ObjId]*curLocalGen // local generation info per SyncRoot.
+	Curorder  uint32                 // position in log for the next generation.
+}
+
+// genMetadata contains the metadata for a generation.
+type genMetadata struct {
+	// All generations stored in the log are ordered wrt each
+	// other and this order needs to be preserved.
+	// Position of this generation in the log.
+	Pos uint32
+
+	// Number of log records in this generation still stored in the log.
+	// This count is used during garbage collection.
+	Count uint64
+
+	// Maximum SeqNum that was part of this generation.
+	// This is useful during garbage collection to track any unclaimed log records.
+	MaxSeqNum SeqNum
+}
+
+// iLog contains the metadata for the ILog db.
+type iLog struct {
+	fname string // file pathname.
+	db    *kvdb  // underlying k/v db.
+
+	// Key:deviceid:srid:genid:lsn Value:LogRecord. Pointer to
+	// the "records" table in the kvdb. Contains log records.
+	records *kvtable
+
+	// Key:deviceid:srid:genid Value:genMetadata. Pointer to the
+	// "gens" table in the kvdb. Contains generation metadata for
+	// each generation.
+	gens *kvtable
+
+	// Key:"Head" Value:iLogHeader. Pointer to the "header" table
+	// in the kvdb. Contains logheader.
+	header *kvtable
+
+	head *iLogHeader // log head cached in memory.
+
+	s *syncd // pointer to the sync daemon object.
+}
+
+// openILog opens or creates a ILog for the given filename.
+func openILog(filename string, sin *syncd) (*iLog, error) {
+	ilog := &iLog{
+		fname: filename,
+		head:  &iLogHeader{},
+		s:     sin,
+	}
+	// Open the file and create it if it does not exist.
+	// Also initialize the kvdb and its three collections.
+	db, tbls, err := kvdbOpen(filename, []string{"records", "gens", "header"})
+	if err != nil {
+		return nil, err
+	}
+
+	ilog.db = db
+	ilog.records = tbls[0]
+	ilog.gens = tbls[1]
+	ilog.header = tbls[2]
+
+	// If header already exists in db, read it back from db.
+	if ilog.hasHead() {
+		if err := ilog.getHead(); err != nil {
+			ilog.db.close() // this also closes the tables.
+			return nil, err
+		}
+	} else {
+		ilog.head.CurSRGens = make(map[ObjId]*curLocalGen)
+	}
+
+	return ilog, nil
+}
+
+// close closes the ILog and invalidate its struct.
+func (l *iLog) close() error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	// Flush the dirty data.
+	if err := l.flush(); err != nil {
+		return err
+	}
+
+	l.db.close() // this also closes the tables.
+
+	*l = iLog{} // zero out the ILog struct.
+	return nil
+}
+
+// flush flushes the ILog db to disk.
+func (l *iLog) flush() error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	// Set the head from memory before flushing.
+	if err := l.putHead(); err != nil {
+		return err
+	}
+
+	l.db.flush()
+
+	return nil
+}
+
+// initSyncRoot initializes the log header for this SyncRoot.
+func (l *iLog) initSyncRoot(srid ObjId) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	if _, ok := l.head.CurSRGens[srid]; ok {
+		return fmt.Errorf("syncroot already exists %v", srid)
+	}
+	// First generation to be created is generation 1. A
+	// generation of 0 represents no updates on the device.
+	l.head.CurSRGens[srid] = &curLocalGen{CurGenNum: 1}
+	return nil
+}
+
+// delSyncRoot deletes the log header for this SyncRoot.
+func (l *iLog) delSyncRoot(srid ObjId) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	if _, ok := l.head.CurSRGens[srid]; !ok {
+		return fmt.Errorf("syncroot doesn't exist %v", srid)
+	}
+
+	delete(l.head.CurSRGens, srid)
+	return nil
+}
+
+// putHead puts the log head into the ILog db.
+func (l *iLog) putHead() error {
+	return l.header.set("Head", l.head)
+}
+
+// getHead gets the log head from the ILog db.
+func (l *iLog) getHead() error {
+	if l.head == nil {
+		return errors.New("nil log header")
+	}
+	if err := l.header.get("Head", l.head); err != nil {
+		return err
+	}
+	// When we put an empty map in kvdb, we get back a "nil" map
+	// (due to VOM encoding/decoding). To keep putHead/getHead
+	// symmetrical, we add this additional initialization.
+	if l.head.CurSRGens == nil {
+		l.head.CurSRGens = make(map[ObjId]*curLocalGen)
+	}
+	return nil
+}
+
+// hasHead returns true if the ILog db has a log head.
+func (l *iLog) hasHead() bool {
+	return l.header.hasKey("Head")
+}
+
+// logRecKey creates a key for a log record.
+func logRecKey(devid DeviceId, srid ObjId, gnum GenId, lsn SeqNum) string {
+	return fmt.Sprintf("%s:%s:%d:%d", devid, srid.String(), uint64(gnum), uint64(lsn))
+}
+
+// strToGenId converts a string to GenId.
+func strToGenId(genIDStr string) (GenId, error) {
+	id, err := strconv.ParseUint(genIDStr, 10, 64)
+	if err != nil {
+		return 0, err
+	}
+	return GenId(id), nil
+}
+
+// strToObjId converts a string to ObjId.
+func strToObjId(objIDStr string) (ObjId, error) {
+	return ObjId(objIDStr), nil
+}
+
+// splitLogRecKey splits a : separated logrec key into its components.
+func splitLogRecKey(key string) (DeviceId, ObjId, GenId, SeqNum, error) {
+	args := strings.Split(key, ":")
+	if len(args) != 4 {
+		return "", NoObjId, 0, 0, fmt.Errorf("bad logrec key %s", key)
+	}
+	srid, err := strToObjId(args[1])
+	if err != nil {
+		return "", NoObjId, 0, 0, err
+	}
+	gnum, err := strToGenId(args[2])
+	if err != nil {
+		return "", NoObjId, 0, 0, err
+	}
+	lsn, err := strconv.ParseUint(args[3], 10, 64)
+	if err != nil {
+		return "", NoObjId, 0, 0, err
+	}
+	return DeviceId(args[0]), srid, gnum, SeqNum(lsn), nil
+}
+
+// putLogRec puts the log record into the ILog db.
+func (l *iLog) putLogRec(rec *LogRec) (string, error) {
+	if l.db == nil {
+		return "", errInvalidLog
+	}
+	key := logRecKey(rec.DevId, rec.SyncRootId, rec.GenNum, rec.SeqNum)
+	return key, l.records.set(key, rec)
+}
+
+// getLogRec gets the log record from the ILog db.
+func (l *iLog) getLogRec(devid DeviceId, srid ObjId, gnum GenId, lsn SeqNum) (*LogRec, error) {
+	if l.db == nil {
+		return nil, errInvalidLog
+	}
+	key := logRecKey(devid, srid, gnum, lsn)
+	var rec LogRec
+	if err := l.records.get(key, &rec); err != nil {
+		return nil, err
+	}
+	return &rec, nil
+}
+
+// hasLogRec returns true if the ILog db has a log record matching (devid, srid, gnum, lsn).
+func (l *iLog) hasLogRec(devid DeviceId, srid ObjId, gnum GenId, lsn SeqNum) bool {
+	if l.db == nil {
+		return false
+	}
+	key := logRecKey(devid, srid, gnum, lsn)
+	return l.records.hasKey(key)
+}
+
+// delLogRec deletes the log record matching (devid, srid, gnum, lsn) from the ILog db.
+func (l *iLog) delLogRec(devid DeviceId, srid ObjId, gnum GenId, lsn SeqNum) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	key := logRecKey(devid, srid, gnum, lsn)
+	return l.records.del(key)
+}
+
+// generationKey creates a key for a generation.
+func generationKey(devid DeviceId, srid ObjId, gnum GenId) string {
+	return fmt.Sprintf("%s:%s:%d", devid, srid.String(), gnum)
+}
+
+// splitGenerationKey splits a : separated generation key into its components.
+func splitGenerationKey(key string) (DeviceId, ObjId, GenId, error) {
+	args := strings.Split(key, ":")
+	if len(args) != 3 {
+		return "", NoObjId, 0, fmt.Errorf("bad generation key %s", key)
+	}
+	srid, err := strToObjId(args[1])
+	if err != nil {
+		return "", NoObjId, 0, err
+	}
+	gnum, err := strToGenId(args[2])
+	if err != nil {
+		return "", NoObjId, 0, err
+	}
+	return DeviceId(args[0]), srid, gnum, nil
+}
+
+// putGenMetadata puts the metadata of the generation (devid, srid, gnum) into the ILog db.
+func (l *iLog) putGenMetadata(devid DeviceId, srid ObjId, gnum GenId, val *genMetadata) error {
+	key := generationKey(devid, srid, gnum)
+	return l.gens.set(key, val)
+}
+
+// getGenMetadata gets the metadata of the generation (devid, srid, gnum) from the ILog db.
+func (l *iLog) getGenMetadata(devid DeviceId, srid ObjId, gnum GenId) (*genMetadata, error) {
+	if l.db == nil {
+		return nil, errInvalidLog
+	}
+	key := generationKey(devid, srid, gnum)
+	var val genMetadata
+	if err := l.gens.get(key, &val); err != nil {
+		return nil, err
+	}
+	return &val, nil
+}
+
+// hasGenMetadata returns true if the ILog db has the generation (devid, srid, gnum).
+func (l *iLog) hasGenMetadata(devid DeviceId, srid ObjId, gnum GenId) bool {
+	key := generationKey(devid, srid, gnum)
+	return l.gens.hasKey(key)
+}
+
+// delGenMetadata deletes the generation (devid, srid, gnum) metadata from the ILog db.
+func (l *iLog) delGenMetadata(devid DeviceId, srid ObjId, gnum GenId) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+	key := generationKey(devid, srid, gnum)
+	return l.gens.del(key)
+}
+
+// getLocalGenInfo gets the local generation info for the given syncroot.
+func (l *iLog) getLocalGenInfo(srid ObjId) (*curLocalGen, error) {
+	gen, ok := l.head.CurSRGens[srid]
+	if !ok {
+		return nil, fmt.Errorf("no gen info found for srid %s", srid.String())
+	}
+	return gen, nil
+}
+
+// createLocalLogRec creates a new local log record of type NodeRec.
+func (l *iLog) createLocalLogRec(obj ObjId, vers Version,
+	par []Version, val *LogValue, srid ObjId) (*LogRec, error) {
+	gen, err := l.getLocalGenInfo(srid)
+	if err != nil {
+		return nil, err
+	}
+	rec := &LogRec{
+		DevId:      l.s.id,
+		SyncRootId: srid,
+		GenNum:     gen.CurGenNum,
+		SeqNum:     gen.CurSeqNum,
+		RecType:    NodeRec,
+
+		ObjId:   obj,
+		CurVers: vers,
+		Parents: par,
+		Value:   *val,
+	}
+
+	// Increment the SeqNum for the local log.
+	gen.CurSeqNum++
+
+	return rec, nil
+}
+
+// createLocalLinkLogRec creates a new local log record of type LinkRec.
+func (l *iLog) createLocalLinkLogRec(obj ObjId, vers, par Version, srid ObjId) (*LogRec, error) {
+	gen, err := l.getLocalGenInfo(srid)
+	if err != nil {
+		return nil, err
+	}
+	rec := &LogRec{
+		DevId:      l.s.id,
+		SyncRootId: srid,
+		GenNum:     gen.CurGenNum,
+		SeqNum:     gen.CurSeqNum,
+		RecType:    LinkRec,
+
+		ObjId:   obj,
+		CurVers: vers,
+		Parents: []Version{par},
+		Value:   LogValue{},
+	}
+
+	// Increment the SeqNum for the local log.
+	gen.CurSeqNum++
+
+	return rec, nil
+}
+
+// createRemoteGeneration adds a new remote generation.
+func (l *iLog) createRemoteGeneration(dev DeviceId, srid ObjId, gnum GenId, gen *genMetadata) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+
+	if gen.Count != uint64(gen.MaxSeqNum+1) {
+		return errors.New("mismatch in count and lsn")
+	}
+
+	vlog.VI(2).Infof("createRemoteGeneration:: dev %s srid %s gen %d %v", dev, srid.String(), gnum, gen)
+
+	gen.Pos = l.head.Curorder
+	l.head.Curorder++
+
+	return l.putGenMetadata(dev, srid, gnum, gen)
+}
+
+// createLocalGeneration creates a new local generation.
+func (l *iLog) createLocalGeneration(srid ObjId) (GenId, error) {
+	if l.db == nil {
+		return 0, errInvalidLog
+	}
+
+	g, err := l.getLocalGenInfo(srid)
+	if err != nil {
+		return 0, err
+	}
+
+	gnum := g.CurGenNum
+
+	// If there are no updates, there will be no new generation.
+	if g.CurSeqNum == 0 {
+		return gnum - 1, errNoUpdates
+	}
+
+	// Add the current generation to the db.
+	val := &genMetadata{
+		Pos:       l.head.Curorder,
+		Count:     uint64(g.CurSeqNum),
+		MaxSeqNum: g.CurSeqNum - 1,
+	}
+
+	err = l.putGenMetadata(l.s.id, srid, gnum, val)
+
+	vlog.VI(2).Infof("createLocalGeneration:: created srid %s gen %d %v", srid.String(), gnum, val)
+	// Move to the next generation irrespective of err.
+	l.head.Curorder++
+	g.CurGenNum++
+	g.CurSeqNum = 0
+
+	return gnum, err
+}
+
+// processWatchRecord processes new object versions obtained from the local store.
+func (l *iLog) processWatchRecord(objID ObjId, vers, parent Version, val *LogValue, srid ObjId) error {
+	if l.db == nil {
+		return errInvalidLog
+	}
+
+	vlog.VI(2).Infof("processWatchRecord:: adding for object %v %v (srid %v)", objID, vers, srid)
+
+	if !srid.IsValid() {
+		return errors.New("invalid syncroot id")
+	}
+
+	// Filter out the echo from the watcher. When syncd puts mutations into store,
+	// it hears back these mutations once again on the watch stream. We need to
+	// filter out these echoes and only process brand new watch changes.
+	if vers != NoVersion {
+		// Check if the object's vers already exists in the DAG.
+		if l.s.dag.hasNode(objID, vers) {
+			return nil
+		}
+
+		// When we successfully join a SyncGroup, Store
+		// creates an empty directory with object ID as the
+		// rootObjId of the SyncGroup joined.  We do not care
+		// about this version of the directory. We will start
+		// accepting local mutations on this object only after
+		// we hear about it remotely first.
+		//
+		// NOTE: Since this new directory is protected from
+		// any local updates via an Permissions until after the first
+		// sync, waiting to hear remotely first works without
+		// dropping any updates. However, we cache in the
+		// "priv" table its version in the Store so that we
+		// can correctly specify prior version when we put
+		// mutations into the store.
+		if _, err := l.s.dag.getHead(objID); err != nil && objID == srid {
+			priv := &privNode{ /*Mutation: &val.Mutation, */ SyncTime: val.SyncTime, TxId: val.TxId, TxCount: val.TxCount}
+			return l.s.dag.setPrivNode(objID, priv)
+		}
+	} else {
+		// Check if the parent version has a deleted
+		// descendant already in the DAG.
+		if l.s.dag.hasDeletedDescendant(objID, parent) {
+			return nil
+		}
+	}
+
+	var pars []Version
+	if parent != NoVersion {
+		pars = []Version{parent}
+	}
+
+	// If the current version is a deletion, generate a new version number.
+	if val.Delete {
+		if vers != NoVersion {
+			return fmt.Errorf("deleted vers is %v", vers)
+		}
+		vers = NewVersion()
+		//val.Mutation.Version = vers
+	}
+
+	// Create a log record from Watch's Change Record.
+	rec, err := l.createLocalLogRec(objID, vers, pars, val, srid)
+	if err != nil {
+		return err
+	}
+
+	// Insert the new log record into the log.
+	logKey, err := l.putLogRec(rec)
+	if err != nil {
+		return err
+	}
+
+	// Insert the new log record into dag.
+	if err = l.s.dag.addNode(rec.ObjId, rec.CurVers, false, val.Delete, rec.Parents, logKey, val.TxId); err != nil {
+		return err
+	}
+
+	// Move the head.
+	return l.s.dag.moveHead(rec.ObjId, rec.CurVers)
+}
+
+// dump writes to the log file information on ILog internals.
+func (l *iLog) dump() {
+	if l.db == nil {
+		return
+	}
+
+	vlog.VI(1).Infof("DUMP: ILog: #SR %d, cur %d", len(l.head.CurSRGens), l.head.Curorder)
+	for sr, gen := range l.head.CurSRGens {
+		vlog.VI(1).Infof("DUMP: ILog: SR %v: gen %v, lsn %v", sr, gen.CurGenNum, gen.CurSeqNum)
+	}
+
+	// Find for each (devid, srid) pair its lowest and highest generation numbers.
+	type genInfo struct{ min, max GenId }
+	devs := make(map[DeviceId]map[ObjId]*genInfo)
+
+	l.gens.keyIter(func(genKey string) {
+		devid, srid, gnum, err := splitGenerationKey(genKey)
+		if err != nil {
+			return
+		}
+
+		srids, ok := devs[devid]
+		if !ok {
+			srids = make(map[ObjId]*genInfo)
+			devs[devid] = srids
+		}
+
+		info, ok := srids[srid]
+		if ok {
+			if gnum > info.max {
+				info.max = gnum
+			}
+			if gnum < info.min {
+				info.min = gnum
+			}
+		} else {
+			srids[srid] = &genInfo{min: gnum, max: gnum}
+		}
+	})
+
+	// For each (devid, srid) pair dump its generation info in order from
+	// min to max generation numbers, inclusive.
+	for devid, srids := range devs {
+		for srid, info := range srids {
+			for gnum := info.min; gnum <= info.max; gnum++ {
+				meta, err := l.getGenMetadata(devid, srid, gnum)
+				if err == nil {
+					vlog.VI(1).Infof(
+						"DUMP: ILog: gen (%v, %v, %v): pos %v, count %v, maxlsn %v",
+						devid, srid, gnum, meta.Pos, meta.Count, meta.MaxSeqNum)
+				} else {
+					vlog.VI(1).Infof("DUMP: ILog: gen (%v, %v, %v): missing generation",
+						devid, srid, gnum)
+				}
+			}
+		}
+	}
+}
diff --git a/services/syncbase/sync/ilog_test.go b/services/syncbase/sync/ilog_test.go
new file mode 100644
index 0000000..ba12bf0
--- /dev/null
+++ b/services/syncbase/sync/ilog_test.go
@@ -0,0 +1,1024 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Veyron Sync ILog component.
+import (
+	"fmt"
+	"math/rand"
+	"os"
+	"reflect"
+	"runtime"
+	"testing"
+)
+
+// TestILogStore tests creating a backing file for ILog.
+func TestILogStore(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new ILog file %s, err %v", logfile, err)
+	}
+
+	fsize := getFileSize(logfile)
+	if fsize < 0 {
+		//t.Errorf("Log file %s not created", logfile)
+	}
+
+	if err := log.flush(); err != nil {
+		t.Errorf("Cannot flush ILog file %s, err %v", logfile, err)
+	}
+
+	oldfsize := fsize
+	fsize = getFileSize(logfile)
+	if fsize <= oldfsize {
+		//t.Errorf("Log file %s not flushed", logfile)
+	}
+
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close ILog file %s, err %v", logfile, err)
+	}
+
+	oldfsize = getFileSize(logfile)
+
+	log, err = openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot re-open existing log file %s, err %v", logfile, err)
+	}
+
+	fsize = getFileSize(logfile)
+	if fsize != oldfsize {
+		t.Errorf("Log file %s size changed across re-open (%d %d)", logfile, fsize, oldfsize)
+	}
+
+	if err := log.flush(); err != nil {
+		t.Errorf("Cannot flush ILog file %s, err %v", logfile, err)
+	}
+
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close ILog file %s, err %v", logfile, err)
+	}
+}
+
+// TestInvalidLog tests log methods on an invalid (closed) log ptr.
+func TestInvalidLog(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new ILog file %s, err %v", logfile, err)
+	}
+
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close ILog file %s, err %v", logfile, err)
+	}
+
+	validateError := func(err error, funcName string) {
+		_, file, line, _ := runtime.Caller(1)
+		if err == nil || err != errInvalidLog {
+			t.Errorf("%s:%d %s() did not fail on a closed log: %v", file, line, funcName, err)
+		}
+	}
+
+	err = log.close()
+	validateError(err, "close")
+
+	err = log.flush()
+	validateError(err, "flush")
+
+	srid := ObjId("haha")
+
+	err = log.initSyncRoot(srid)
+	validateError(err, "initSyncRoot")
+
+	_, err = log.putLogRec(&LogRec{})
+	validateError(err, "putLogRec")
+
+	var devid DeviceId = "VeyronPhone"
+	var gnum GenId = 1
+	var lsn SeqNum
+
+	_, err = log.getLogRec(devid, srid, gnum, lsn)
+	validateError(err, "getLogRec")
+
+	if log.hasLogRec(devid, srid, gnum, lsn) {
+		t.Errorf("hasLogRec() did not fail on a closed log: %v", err)
+	}
+
+	err = log.delLogRec(devid, srid, gnum, lsn)
+	validateError(err, "delLogRec")
+
+	_, err = log.getGenMetadata(devid, srid, gnum)
+	validateError(err, "getGenMetadata")
+
+	err = log.delGenMetadata(devid, srid, gnum)
+	validateError(err, "delGenMetadata")
+
+	err = log.createRemoteGeneration(devid, srid, gnum, &genMetadata{})
+	validateError(err, "createRemoteGeneration")
+
+	_, err = log.createLocalGeneration(srid)
+	validateError(err, "createLocalGeneration")
+
+	err = log.processWatchRecord(ObjId("foobar"), 2, Version(999), &LogValue{}, srid)
+	validateError(err, "processWatchRecord")
+
+	// Harmless NOP.
+	log.dump()
+}
+
+// TestPutGetLogHeader tests setting and getting log header across log open/close/reopen.
+func TestPutGetLogHeader(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	// In memory head should be initialized.
+	if len(log.head.CurSRGens) != 0 || log.head.Curorder != 0 {
+		t.Errorf("First time log create should reset header")
+	}
+
+	// No head should be there in db.
+	if err = log.getHead(); err == nil {
+		t.Errorf("getHead() found non-existent head in log file %s, err %v", logfile, err)
+	}
+
+	if log.hasHead() {
+		t.Errorf("hasHead() found non-existent head in log file %s", logfile)
+	}
+
+	expVal := map[ObjId]*curLocalGen{ObjId("bla"): &curLocalGen{CurGenNum: 10, CurSeqNum: 100},
+		ObjId("qwerty"): &curLocalGen{CurGenNum: 40, CurSeqNum: 80}}
+
+	log.head = &iLogHeader{
+		CurSRGens: expVal,
+		Curorder:  1000,
+	}
+
+	if err := log.putHead(); err != nil {
+		t.Errorf("Cannot put head %v in log file %s, err %v", log.head, logfile, err)
+	}
+
+	// Reset values.
+	log.head.CurSRGens = nil
+	log.head.Curorder = 0
+
+	for i := 0; i < 2; i++ {
+		if err := log.getHead(); err != nil {
+			t.Fatalf("getHead() can not find head (i=%d) in log file %s, err %v", i, logfile, err)
+		}
+
+		if !log.hasHead() {
+			t.Errorf("hasHead() can not find head (i=%d) in log file %s", i, logfile)
+		}
+
+		if !reflect.DeepEqual(log.head.CurSRGens, expVal) || log.head.Curorder != 1000 {
+			t.Errorf("Data mismatch for head (i=%d) in log file %s: %v",
+				i, logfile, log.head)
+		}
+
+		if i == 0 {
+			if err := log.close(); err != nil {
+				t.Errorf("Cannot close log file %s, err %v", logfile, err)
+			}
+			log, err = openILog(logfile, nil)
+			if err != nil {
+				t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+			}
+		}
+	}
+
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestPersistLogHeader tests that log header is automatically persisted across log open/close/reopen.
+func TestPersistLogHeader(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	// In memory head should be initialized.
+	if len(log.head.CurSRGens) != 0 || log.head.Curorder != 0 {
+		t.Errorf("First time log create should reset header")
+	}
+
+	expVal := map[ObjId]*curLocalGen{ObjId("blabla"): &curLocalGen{CurGenNum: 10, CurSeqNum: 100},
+		ObjId("xyz"): &curLocalGen{CurGenNum: 40, CurSeqNum: 80}}
+	log.head = &iLogHeader{
+		CurSRGens: expVal,
+		Curorder:  1000,
+	}
+
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+
+	log, err = openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	// In memory head should be initialized from db.
+	if !reflect.DeepEqual(log.head.CurSRGens, expVal) || log.head.Curorder != 1000 {
+		t.Errorf("Data mismatch for head in log file %s: %v", logfile, log.head)
+	}
+
+	for sr := range expVal {
+		expVal[sr] = &curLocalGen{CurGenNum: 1000, CurSeqNum: 10}
+		break
+	}
+	log.head = &iLogHeader{
+		CurSRGens: expVal,
+		Curorder:  100,
+	}
+
+	if err := log.flush(); err != nil {
+		t.Errorf("Cannot flush ILog file %s, err %v", logfile, err)
+	}
+
+	// Reset values.
+	log.head.CurSRGens = nil
+	log.head.Curorder = 0
+
+	if err := log.getHead(); err != nil {
+		t.Fatalf("getHead() can not find head in log file %s, err %v", logfile, err)
+	}
+
+	// In memory head should be initialized from db.
+	if !reflect.DeepEqual(log.head.CurSRGens, expVal) || log.head.Curorder != 100 {
+		t.Errorf("Data mismatch for head in log file %s: %v", logfile, log.head)
+	}
+
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestLogInitDelSyncRoot tests initing and deleting a new SyncRoot.
+func TestLogInitDelSyncRoot(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	srid1 := ObjId("haha")
+	if err := log.initSyncRoot(srid1); err != nil {
+		t.Fatalf("Cannot create new SyncRoot %s, err %v", srid1.String(), err)
+	}
+	srid2 := ObjId("asdf")
+	if err := log.initSyncRoot(srid2); err != nil {
+		t.Fatalf("Cannot create new SyncRoot %s, err %v", srid2.String(), err)
+	}
+	if err := log.initSyncRoot(srid2); err == nil {
+		t.Fatalf("Creating existing SyncRoot didn't fail %s, err %v", srid2.String(), err)
+	}
+	expGen := &curLocalGen{CurGenNum: 1, CurSeqNum: 0}
+	expSRGens := map[ObjId]*curLocalGen{
+		srid1: expGen,
+		srid2: expGen,
+	}
+	if !reflect.DeepEqual(log.head.CurSRGens, expSRGens) {
+		t.Errorf("Data mismatch for head in log file %s: %v instead of %v",
+			logfile, log.head.CurSRGens, expSRGens)
+	}
+
+	if err := log.delSyncRoot(srid1); err != nil {
+		t.Fatalf("Cannot delete SyncRoot %s, err %v", srid1.String(), err)
+	}
+	if err := log.delSyncRoot(srid2); err != nil {
+		t.Fatalf("Cannot delete SyncRoot %s, err %v", srid1.String(), err)
+	}
+	if err := log.delSyncRoot(srid1); err == nil {
+		t.Fatalf("Deleting non-existent SyncRoot didn't fail %s", srid1.String())
+	}
+	expSRGens = map[ObjId]*curLocalGen{}
+	if !reflect.DeepEqual(log.head.CurSRGens, expSRGens) {
+		t.Errorf("Data mismatch for head in log file %s: %v instead of %v",
+			logfile, log.head.CurSRGens, expSRGens)
+	}
+
+	if err := log.putHead(); err != nil {
+		t.Errorf("Cannot put head %v in log file %s, err %v", log.head, logfile, err)
+	}
+
+	// Reset values.
+	log.head.CurSRGens = nil
+	log.head.Curorder = 0
+
+	if err := log.getHead(); err != nil {
+		t.Fatalf("getHead() can not find head in log file %s, err %v", logfile, err)
+	}
+
+	if !reflect.DeepEqual(log.head.CurSRGens, expSRGens) {
+		t.Errorf("Data mismatch for head in log file %s: %v instead of %v",
+			logfile, log.head.CurSRGens, expSRGens)
+	}
+}
+
+// TestStringAndParseKeys tests conversion to/from string for log record and generation keys.
+func TestStringAndParseKeys(t *testing.T) {
+	var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+	randString := func(n int) string {
+		b := make([]rune, n)
+		for i := range b {
+			b[i] = letters[rand.Intn(len(letters))]
+		}
+		return string(b)
+	}
+
+	for i := 0; i < 10; i++ {
+		devid := DeviceId(randString(rand.Intn(20) + 1))
+		srid := ObjId(fmt.Sprintf("haha-%d", i))
+		gnum := GenId(rand.Int63())
+		lsn := SeqNum(rand.Int63())
+
+		devid1, srid1, gnum1, lsn1, err := splitLogRecKey(logRecKey(devid, srid, gnum, lsn))
+		if err != nil || devid1 != devid || srid1 != srid || gnum1 != gnum || lsn1 != lsn {
+			t.Fatalf("LogRec conversion failed in iter %d: got %s %v %v %v want %s %v %v %v, err %v",
+				i, devid1, srid1, gnum1, lsn1, devid, srid, gnum, lsn, err)
+		}
+
+		devid2, srid2, gnum2, err := splitGenerationKey(generationKey(devid, srid, gnum))
+		if err != nil || devid2 != devid || srid2 != srid || gnum2 != gnum {
+			t.Fatalf("Gen conversion failed in iter %d: got %s %v %v want %s %v %v, err %v",
+				i, devid2, srid2, gnum2, devid, srid, gnum, err)
+		}
+	}
+
+	//badStrs := []string{"abc:3:4", "abc:123:4:5", "abc:1234:-1:10", "ijk:7890:1000:-1", "abc:3:4:5:6", "abc::3:4:5"}
+	badStrs := []string{"abc:3:4", "abc:1234:-1:10", "ijk:7890:1000:-1", "abc:3:4:5:6", "abc::3:4:5"}
+	for _, s := range badStrs {
+		if _, _, _, _, err := splitLogRecKey(s); err == nil {
+			t.Fatalf("LogRec conversion didn't fail for str %s", s)
+		}
+	}
+
+	//badStrs = []string{"abc:3", "abc:123:4", "abc:1234:-1", "abc:3:4:5", "abc:4::5"}
+	badStrs = []string{"abc:3", "abc:1234:-1", "abc:3:4:5", "abc:4::5"}
+	for _, s := range badStrs {
+		if _, _, _, err := splitGenerationKey(s); err == nil {
+			t.Fatalf("Gen conversion didn't fail for str %s", s)
+		}
+	}
+}
+
+// TestPutGetLogRec tests setting and getting a log record across log open/close/reopen.
+func TestPutGetLogRec(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("haha")
+	var gnum GenId = 100
+	var lsn SeqNum = 1000
+
+	rec, err := log.getLogRec(devid, srid, gnum, lsn)
+	if err == nil || rec != nil {
+		t.Errorf("GetLogRec() found non-existent object %s:%s:%d:%d in log file %s: %v, err %v",
+			devid, srid.String(), gnum, lsn, logfile, rec, err)
+	}
+
+	if log.hasLogRec(devid, srid, gnum, lsn) {
+		t.Errorf("HasLogRec() found non-existent object %s:%s:%d:%d in log file %s",
+			devid, srid.String(), gnum, lsn, logfile)
+	}
+
+	rec = &LogRec{
+		DevId:      devid,
+		SyncRootId: srid,
+		GenNum:     gnum,
+		SeqNum:     lsn,
+		ObjId:      ObjId("bla"),
+		CurVers:    2,
+		Parents:    []Version{0, 1},
+		Value:      LogValue{},
+	}
+
+	if _, err := log.putLogRec(rec); err != nil {
+		t.Errorf("Cannot put object %s:%s:%d:%d (%v) in log file %s, err %v", devid, srid.String(), gnum, lsn, rec, logfile, err)
+	}
+
+	for i := 0; i < 2; i++ {
+		curRec, err := log.getLogRec(devid, srid, gnum, lsn)
+		if err != nil || curRec == nil {
+			t.Fatalf("GetLogRec() can not find object %s:%s:%d:%d (i=%d) in log file %s, err %v",
+				devid, srid.String(), gnum, lsn, i, logfile, err)
+		}
+
+		if !log.hasLogRec(devid, srid, gnum, lsn) {
+			t.Errorf("HasLogRec() can not find object %s:%s:%d:%d (i=%d) in log file %s",
+				devid, srid.String(), gnum, lsn, i, logfile)
+		}
+
+		if !reflect.DeepEqual(curRec, rec) {
+			t.Errorf("Data mismatch for object %s:%d:%d (i=%d) in log file %s: %v instead of %v",
+				devid, gnum, lsn, i, logfile, curRec, rec)
+		}
+
+		if i == 0 {
+			if err := log.close(); err != nil {
+				t.Errorf("Cannot close log file %s, err %v", logfile, err)
+			}
+			log, err = openILog(logfile, nil)
+			if err != nil {
+				t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+			}
+		}
+	}
+
+	log.dump()
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestDelLogRec tests deleting a log record across log open/close/reopen.
+func TestDelLogRec(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("haha")
+	var gnum GenId = 100
+	var lsn SeqNum = 1000
+
+	rec := &LogRec{
+		DevId:      devid,
+		SyncRootId: srid,
+		GenNum:     gnum,
+		SeqNum:     lsn,
+		ObjId:      ObjId("bla"),
+		CurVers:    2,
+		Parents:    []Version{0, 1},
+		Value:      LogValue{},
+	}
+
+	if _, err := log.putLogRec(rec); err != nil {
+		t.Errorf("Cannot put object %s:%s:%d:%d (%v) in log file %s, err %v", devid, srid.String(), gnum, lsn, rec, logfile, err)
+	}
+
+	curRec, err := log.getLogRec(devid, srid, gnum, lsn)
+	if err != nil || curRec == nil {
+		t.Fatalf("GetLogRec() can not find object %s:%s:%d:%d in log file %s, err %v",
+			devid, srid.String(), gnum, lsn, logfile, err)
+	}
+
+	if err := log.delLogRec(devid, srid, gnum, lsn); err != nil {
+		t.Fatalf("DelLogRec() can not delete object %s:%s:%d:%d in log file %s, err %v",
+			devid, srid.String(), gnum, lsn, logfile, err)
+	}
+
+	for i := 0; i < 2; i++ {
+		curRec, err = log.getLogRec(devid, srid, gnum, lsn)
+		if err == nil || curRec != nil {
+			t.Fatalf("GetLogRec() finds deleted object %s:%s:%d:%d (i=%d) in log file %s, err %v",
+				devid, srid.String(), gnum, lsn, i, logfile, err)
+		}
+
+		if log.hasLogRec(devid, srid, gnum, lsn) {
+			t.Errorf("HasLogRec() finds deleted object %s:%s:%d:%d (i=%d) in log file %s",
+				devid, srid.String(), gnum, lsn, i, logfile)
+		}
+
+		if i == 0 {
+			if err := log.close(); err != nil {
+				t.Errorf("Cannot close log file %s, err %v", logfile, err)
+			}
+			log, err = openILog(logfile, nil)
+			if err != nil {
+				t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+			}
+		}
+	}
+
+	log.dump()
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+
+}
+
+// TestPutGetGenMetadata tests setting and getting generation metadata across log open/close/reopen.
+func TestPutGetGenMetadata(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("haha")
+	var gnum GenId = 100
+
+	val, err := log.getGenMetadata(devid, srid, gnum)
+	if err == nil || val != nil {
+		t.Errorf("GetGenMetadata() found non-existent object %s:%s:%d in log file %s: %v, err %v",
+			devid, srid.String(), gnum, logfile, val, err)
+	}
+
+	if log.hasGenMetadata(devid, srid, gnum) {
+		t.Errorf("hasGenMetadata() found non-existent object %s:%s:%d in log file %s",
+			devid, srid.String(), gnum, logfile)
+	}
+
+	val = &genMetadata{Pos: 40, Count: 100, MaxSeqNum: 99}
+	if err := log.putGenMetadata(devid, srid, gnum, val); err != nil {
+		t.Errorf("Cannot put object %s:%s:%d in log file %s, err %v", devid, srid.String(), gnum, logfile, err)
+	}
+
+	for i := 0; i < 2; i++ {
+		curVal, err := log.getGenMetadata(devid, srid, gnum)
+		if err != nil || curVal == nil {
+			t.Fatalf("GetGenMetadata() can not find object %s:%s:%d (i=%d) in log file %s, err %v",
+				devid, srid.String(), gnum, i, logfile, err)
+		}
+
+		if !log.hasGenMetadata(devid, srid, gnum) {
+			t.Errorf("hasGenMetadata() can not find object %s:%s:%d (i=%d) in log file %s",
+				devid, srid.String(), gnum, i, logfile)
+		}
+
+		if !reflect.DeepEqual(curVal, val) {
+			t.Errorf("Data mismatch for object %s:%s:%d (i=%d) in log file %s: %v instead of %v",
+				devid, srid.String(), gnum, i, logfile, curVal, val)
+		}
+
+		if i == 0 {
+			if err := log.close(); err != nil {
+				t.Errorf("Cannot close log file %s, err %v", logfile, err)
+			}
+			log, err = openILog(logfile, nil)
+			if err != nil {
+				t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+			}
+		}
+	}
+
+	log.dump()
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestDelGenMetadata tests deleting generation metadata across log open/close/reopen.
+func TestDelGenMetadata(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("haha")
+	var gnum GenId = 100
+
+	val := &genMetadata{Pos: 40, Count: 100, MaxSeqNum: 99}
+	if err := log.putGenMetadata(devid, srid, gnum, val); err != nil {
+		t.Errorf("Cannot put object %s:%s:%d in log file %s, err %v", devid, srid.String(), gnum, logfile, err)
+	}
+
+	curVal, err := log.getGenMetadata(devid, srid, gnum)
+	if err != nil || curVal == nil {
+		t.Fatalf("GetGenMetadata() can not find object %s:%s:%d in log file %s, err %v",
+			devid, srid.String(), gnum, logfile, err)
+	}
+
+	if err := log.delGenMetadata(devid, srid, gnum); err != nil {
+		t.Fatalf("DelGenMetadata() can not delete object %s:%s:%d in log file %s, err %v",
+			devid, srid.String(), gnum, logfile, err)
+	}
+
+	for i := 0; i < 2; i++ {
+		curVal, err := log.getGenMetadata(devid, srid, gnum)
+		if err == nil || curVal != nil {
+			t.Fatalf("GetGenMetadata() finds deleted object %s:%s:%d (i=%d) in log file %s, err %v",
+				devid, srid.String(), gnum, i, logfile, err)
+		}
+
+		if log.hasGenMetadata(devid, srid, gnum) {
+			t.Errorf("hasGenMetadata() finds deleted object %s:%s:%d (i=%d) in log file %s",
+				devid, srid.String(), gnum, i, logfile)
+		}
+
+		if i == 0 {
+			if err := log.close(); err != nil {
+				t.Errorf("Cannot close log file %s, err %v", logfile, err)
+			}
+			log, err = openILog(logfile, nil)
+			if err != nil {
+				t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+			}
+		}
+	}
+
+	log.dump()
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestPersistLogState tests that generation metadata and record state
+// is persisted across log open/close/reopen.
+func TestPersistLogState(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	log, err := openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	var devid DeviceId = "VeyronTab"
+	srid := ObjId("haha")
+
+	// Add several generations.
+	for i := uint32(0); i < 10; i++ {
+		val := &genMetadata{Pos: i}
+		if err := log.putGenMetadata(devid, srid, GenId(i+10), val); err != nil {
+			t.Errorf("Cannot put object %s:%s:%d in log file %s, err %v", devid, srid.String(),
+				i, logfile, err)
+		}
+	}
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+	log, err = openILog(logfile, nil)
+	if err != nil {
+		t.Fatalf("Cannot re-open log file %s, err %v", logfile, err)
+	}
+	for i := uint32(0); i < 10; i++ {
+		curVal, err := log.getGenMetadata(devid, srid, GenId(i+10))
+		if err != nil || curVal == nil {
+			t.Fatalf("GetGenMetadata() can not find object %s:%s:%d in log file %s, err %v",
+				devid, srid.String(), i, logfile, err)
+		}
+		if curVal.Pos != i {
+			t.Errorf("Data mismatch for object %s:%s:%d in log file %s: %v",
+				devid, srid.String(), i, logfile, curVal)
+		}
+		// Should safely overwrite the same keys.
+		curVal.Pos = i + 10
+		if err := log.putGenMetadata(devid, srid, GenId(i+10), curVal); err != nil {
+			t.Errorf("Cannot put object %s:%s:%d in log file %s, err %v", devid, srid.String(),
+				i, logfile, err)
+		}
+	}
+	for i := uint32(0); i < 10; i++ {
+		curVal, err := log.getGenMetadata(devid, srid, GenId(i+10))
+		if err != nil || curVal == nil {
+			t.Fatalf("GetGenMetadata() can not find object %s:%s:%d in log file %s, err %v",
+				devid, srid.String(), i, logfile, err)
+		}
+		if curVal.Pos != (i + 10) {
+			t.Errorf("Data mismatch for object %s:%s:%d in log file %s: %v, err %v",
+				devid, srid.String(), i, logfile, curVal, err)
+		}
+	}
+
+	log.dump()
+	if err := log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// fillFakeLogRecords fills fake log records for testing purposes.
+func (l *iLog) fillFakeLogRecords(srid ObjId) {
+	const num = 10
+	var parvers []Version
+	id := ObjId("haha")
+	for i := int(0); i < num; i++ {
+		// Create a local log record.
+		curvers := Version(i)
+		rec, err := l.createLocalLogRec(id, curvers, parvers, &LogValue{}, srid)
+		if err != nil {
+			return
+		}
+		// Insert the new log record into the log.
+		_, err = l.putLogRec(rec)
+		if err != nil {
+			return
+		}
+		parvers = []Version{curvers}
+	}
+}
+
+// TestCreateGeneration tests that local log records and local
+// generations are created uniquely and remote generations are
+// correctly inserted in log order.
+func TestCreateGeneration(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	s := &syncd{id: "VeyronTab"}
+	log, err := openILog(logfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	// Try using a wrong SyncRoot ID.
+	bad_srid := ObjId("xyz")
+	if _, err := log.createLocalLogRec(ObjId("asdf"), NoVersion, nil, &LogValue{}, bad_srid); err == nil {
+		t.Errorf("createLocalLogRec did not fail when using an invalid SyncRoot ID %v", bad_srid)
+	}
+	if _, err := log.createLocalLinkLogRec(ObjId("qwerty"), NoVersion, NoVersion, bad_srid); err == nil {
+		t.Errorf("createLocalLinkLogRec did not fail when using an invalid SyncRoot ID %v", bad_srid)
+	}
+	if _, err := log.createLocalGeneration(bad_srid); err == nil {
+		t.Errorf("createLocalGeneration did not fail when using an invalid SyncRoot ID %v", bad_srid)
+	}
+
+	srids := []ObjId{ObjId("foo"), ObjId("bar")}
+	num := []uint64{10, 20}
+	for pos, sr := range srids {
+		if err := log.initSyncRoot(sr); err != nil {
+			t.Fatalf("Cannot create new SyncRoot %s, err %v", sr.String(), err)
+		}
+		if g, err := log.createLocalGeneration(sr); err != errNoUpdates {
+			t.Errorf("Should not find local updates gen %d with error %v", g, err)
+		}
+
+		var parvers []Version
+		id := ObjId(fmt.Sprintf("haha-%d", pos))
+		for i := uint64(0); i < num[pos]; i++ {
+			// Create a local log record.
+			curvers := Version(i)
+			rec, err := log.createLocalLogRec(id, curvers, parvers, &LogValue{}, sr)
+			if err != nil {
+				t.Fatalf("Cannot create local log rec ObjId: %v Current: %v Parents: %v Error: %v",
+					id, curvers, parvers, err)
+			}
+
+			temprec := &LogRec{
+				DevId:      log.s.id,
+				SyncRootId: sr,
+				GenNum:     GenId(1),
+				SeqNum:     SeqNum(i),
+				ObjId:      id,
+				CurVers:    curvers,
+				Parents:    parvers,
+				Value:      LogValue{},
+			}
+			// Verify that the log record has the right values.
+			if !reflect.DeepEqual(rec, temprec) {
+				t.Errorf("Data mismtach in log record %v instead of %v", rec, temprec)
+			}
+
+			// Insert the new log record into the log.
+			_, err = log.putLogRec(rec)
+			if err != nil {
+				t.Errorf("Cannot put log record:: failed with err %v", err)
+			}
+
+			parvers = []Version{curvers}
+		}
+	}
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+
+	log, err = openILog(logfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	for pos, sr := range srids {
+		if log.head.CurSRGens[sr].CurGenNum != 1 || log.head.CurSRGens[sr].CurSeqNum != SeqNum(num[pos]) {
+			t.Errorf("Data mismatch in log header %v (pos=%d)", log.head, pos)
+		}
+
+		g, err := log.createLocalGeneration(sr)
+		if g != 1 || err != nil {
+			t.Errorf("Could not create local generation srid %s gen %d (pos=%d) with error %v",
+				sr.String(), g, pos, err)
+		}
+		curVal, err := log.getGenMetadata(log.s.id, sr, g)
+		if err != nil || curVal == nil {
+			t.Fatalf("GetGenMetadata() can not find object %s:%s:%d (pos=%d) in log file %s, err %v",
+				log.s.id, sr.String(), g, pos, logfile, err)
+		}
+		expVal := &genMetadata{Pos: uint32(pos), Count: num[pos], MaxSeqNum: SeqNum(num[pos] - 1)}
+		if !reflect.DeepEqual(curVal, expVal) {
+			t.Errorf("Data mismatch for object %s:%s:%d (pos=%d) in log file %s: %v instead of %v",
+				log.s.id, sr.String(), g, pos, logfile, curVal, expVal)
+		}
+		if log.head.CurSRGens[sr].CurGenNum != 2 || log.head.CurSRGens[sr].CurSeqNum != 0 ||
+			log.head.Curorder != uint32(pos+1) {
+			t.Errorf("Data mismatch in log header (pos=%d) %v %v",
+				pos, log.head.CurSRGens[sr], log.head.Curorder)
+		}
+
+		if g, err := log.createLocalGeneration(sr); err != errNoUpdates {
+			t.Errorf("Should not find local updates sr %s gen %d (pos=%d) with error %v",
+				sr.String(), g, pos, err)
+		}
+	}
+
+	// Populate one more generation for only one syncroot.
+	log.fillFakeLogRecords(srids[0])
+
+	if g, err := log.createLocalGeneration(srids[0]); g != 2 || err != nil {
+		t.Errorf("Could not create local generation sgid %s gen %d with error %v",
+			srids[0].String(), g, err)
+	}
+
+	if g, err := log.createLocalGeneration(srids[1]); err != errNoUpdates {
+		t.Errorf("Should not find local updates sr %s gen %d with error %v", srids[1].String(), g, err)
+	}
+
+	// Create a remote generation.
+	expGen := &genMetadata{Count: 1, MaxSeqNum: 50}
+	if err = log.createRemoteGeneration("VeyronPhone", srids[0], 1, expGen); err == nil {
+		t.Errorf("Remote generation create should have failed")
+	}
+	expGen.MaxSeqNum = 0
+	if err = log.createRemoteGeneration("VeyronPhone", srids[0], 1, expGen); err != nil {
+		t.Errorf("createRemoteGeneration failed with err %v", err)
+	}
+	if expGen.Pos != 3 {
+		t.Errorf("createRemoteGeneration created incorrect log order %d", expGen.Pos)
+	}
+
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+
+	// Reopen the log and ensure that all log records for generations exist.
+	// Also ensure that generation metadata is accurate.
+	log, err = openILog(logfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	gens := []GenId{2, 1}
+	order := map[ObjId][]uint32{srids[0]: []uint32{0, 2},
+		srids[1]: []uint32{1}}
+	for pos, sr := range srids {
+		for g := GenId(1); g <= gens[pos]; g++ {
+			// Check all log records.
+			for i := SeqNum(0); i < SeqNum(num[pos]); i++ {
+				curRec, err := log.getLogRec(log.s.id, sr, g, i)
+				if err != nil || curRec == nil {
+					t.Fatalf("GetLogRec() can not find object %s:%s:%d:%d in log file %s, err %v",
+						log.s.id, sr.String(), g, i, logfile, err)
+				}
+			}
+			// Check generation metadata.
+			curVal, err := log.getGenMetadata(log.s.id, sr, g)
+			if err != nil || curVal == nil {
+				t.Fatalf("GetGenMetadata() can not find object %s:%s:%d in log file %s, err %v",
+					log.s.id, sr.String(), g, logfile, err)
+			}
+			expVal := &genMetadata{Count: num[pos], MaxSeqNum: SeqNum(num[pos] - 1), Pos: order[sr][g-1]}
+			if !reflect.DeepEqual(curVal, expVal) {
+				t.Errorf("Data mismatch for object %s:%s:%d in log file %s: %v instead of %v",
+					log.s.id, sr.String(), g, logfile, curVal, expVal)
+			}
+		}
+	}
+
+	// Check remote generation metadata.
+	curVal, err := log.getGenMetadata("VeyronPhone", srids[0], 1)
+	if err != nil || curVal == nil {
+		t.Fatalf("GetGenMetadata() can not find object in log file %s, err %v",
+			logfile, err)
+	}
+	if !reflect.DeepEqual(curVal, expGen) {
+		t.Errorf("Data mismatch for object in log file %s: %v instead of %v",
+			logfile, curVal, expGen)
+	}
+
+	expSRGens := map[ObjId]*curLocalGen{srids[0]: &curLocalGen{3, 0},
+		srids[1]: &curLocalGen{2, 0}}
+
+	if !reflect.DeepEqual(log.head.CurSRGens, expSRGens) || log.head.Curorder != 4 {
+		t.Errorf("Data mismatch in log header %v %v", log.head.CurSRGens, log.head.Curorder)
+	}
+
+	log.dump()
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+
+// TestProcessWatchRecord tests that local updates are correctly handled.
+// Commands are in file testdata/<local-init-00.log.sync, local-init-02.log.sync>.
+/*
+func TestProcessWatchRecord(t *testing.T) {
+	logfile := getFileName()
+	defer os.Remove(logfile)
+
+	dagfile := getFileName()
+	defer os.Remove(dagfile)
+
+	var err error
+	s := &syncd{id: "VeyronTab"}
+
+	if s.dag, err = openDAG(dagfile); err != nil {
+		t.Fatalf("Cannot open new dag file %s, err %v", logfile, err)
+	}
+	log, err := openILog(logfile, s)
+	if err != nil {
+		t.Fatalf("Cannot open new log file %s, err %v", logfile, err)
+	}
+
+	srids := []ObjId{"foo", "bar"}
+	fnames := []string{"local-init-00.log.sync", "local-init-02.log.sync"}
+	num := []uint32{3, 5}
+
+	// Try using an invalid SyncRoot ID.
+	if err = log.processWatchRecord(ObjId("haha"), 2, Version(999), &LogValue{}, NoObjId); err == nil {
+		t.Errorf("processWatchRecord did not fail when using an invalid SyncRoot ID")
+	}
+
+	// Test echo suppression on JoinSyncGroup.
+	if err := log.processWatchRecord(srids[0], 5, NoVersion, &LogValue{}, srids[0]); err != nil {
+		t.Fatalf("Echo suppression on JoinSyncGroup failed with err %v", err)
+	}
+
+	for pos, sr := range srids {
+		if err := log.initSyncRoot(sr); err != nil {
+			t.Fatalf("Cannot create new SyncRoot %s, err %v", sr.String(), err)
+		}
+
+		if err := logReplayCommands(log, fnames[pos], sr); err != nil {
+			t.Error(err)
+		}
+	}
+
+	checkObject := func(obj string, expHead Version) {
+		_, file, line, _ := runtime.Caller(1)
+		objid, err := strToObjId(obj)
+		if err != nil {
+			t.Errorf("%s:%d Could not create objid %v", file, line, err)
+		}
+
+		// Verify DAG state.
+		if head, err := log.s.dag.getHead(objid); err != nil || head != expHead {
+			t.Errorf("%s:%d Invalid object %d head in DAG %v want %v, err %v", file, line,
+				objid, head, expHead, err)
+		}
+	}
+	for pos, sr := range srids {
+		if log.head.CurSRGens[sr].CurGenNum != 1 || log.head.CurSRGens[sr].CurSeqNum != SeqNum(num[pos]) ||
+			log.head.Curorder != 0 {
+			t.Errorf("Data mismatch in log header %v", log.head)
+		}
+
+		// Check all log records.
+		for i := SeqNum(0); i < SeqNum(num[pos]); i++ {
+			curRec, err := log.getLogRec(log.s.id, sr, GenId(1), i)
+			if err != nil || curRec == nil {
+				t.Fatalf("GetLogRec() can not find object %s:%s:1:%d in log file %s, err %v",
+					log.s.id, sr.String(), i, logfile, err)
+			}
+		}
+
+		if pos == 0 {
+			checkObject("1234", 3)
+		} else if pos == 1 {
+			checkObject("12", 3)
+			checkObject("45", 20)
+		}
+	}
+
+	s.dag.flush()
+	s.dag.close()
+
+	log.dump()
+	if err = log.close(); err != nil {
+		t.Errorf("Cannot close log file %s, err %v", logfile, err)
+	}
+}
+*/
diff --git a/services/syncbase/sync/initiator.go b/services/syncbase/sync/initiator.go
new file mode 100644
index 0000000..057a2e8
--- /dev/null
+++ b/services/syncbase/sync/initiator.go
@@ -0,0 +1,943 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"errors"
+	"fmt"
+	"math/rand"
+	"os"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/vtrace"
+	"v.io/x/lib/vlog"
+)
+
+// Policies to pick a peer to sync with.
+const (
+	// Picks a peer at random from the available set.
+	selectRandom = iota
+
+	// TODO(hpucha): implement other policies.
+	// Picks a peer with most differing generations.
+	selectMostDiff
+
+	// Picks a peer that was synced with the furthest in the past.
+	selectOldest
+)
+
+// Policies for conflict resolution.
+const (
+	// Resolves conflicts by picking the mutation with the most recent timestamp.
+	useTime = iota
+
+	// TODO(hpucha): implement other policies.
+	// Resolves conflicts by using the app conflict resolver callbacks via store.
+	useCallback
+)
+
+var (
+	// peerSyncInterval is the duration between two consecutive
+	// sync events.  In every sync event, the initiator contacts
+	// one of its peers to obtain any pending updates.
+	peerSyncInterval = 50 * time.Millisecond
+
+	// peerSelectionPolicy is the policy used to select a peer when
+	// the initiator gets a chance to sync.
+	peerSelectionPolicy = selectRandom
+
+	// conflictResolutionPolicy is the policy used to resolve conflicts.
+	conflictResolutionPolicy = useTime
+
+	errNoUsefulPeer = errors.New("no useful peer to contact")
+)
+
+// syncInitiator contains the metadata and state for the initiator thread.
+type syncInitiator struct {
+	syncd *syncd
+
+	// State accumulated during an initiation round.
+	iState *initiationState
+}
+
+// initiationState accumulated during an initiation round.
+type initiationState struct {
+
+	// Veyron name of peer being synced with.
+	peer string
+
+	// Local generation vector.
+	local map[ObjId]GenVector
+
+	// SyncGroups being requested in the initiation round.
+	syncGroups map[ObjId]GroupIdSet
+
+	// Map to track new generations received in the RPC reply.
+	newGens map[string]*genMetadata
+
+	// Array to track order of arrival for the generations.
+	// We need to preserve this order for the replay.
+	orderGens []string
+
+	// Generation vector to track the oldest generation received
+	// in the RPC reply per device, for garbage collection.
+	minGens map[ObjId]GenVector
+
+	// Generation vector from the remote peer.
+	remote map[ObjId]GenVector
+
+	// Tmp kvdb state.
+	tmpFile string
+	tmpDB   *kvdb
+	tmpTbl  *kvtable
+
+	// State to track updated objects during a log replay.
+	updObjects map[ObjId]*objConflictState
+
+	// State to delete objects from the "priv" table.
+	delPrivObjs map[ObjId]struct{}
+}
+
+// objConflictState contains the conflict state for objects that are
+// updated during an initiator run.
+type objConflictState struct {
+	isConflict bool
+	newHead    Version
+	oldHead    Version
+	ancestor   Version
+	resolvVal  *LogValue
+	srID       ObjId
+}
+
+// newInitiator creates a new initiator instance attached to the given syncd instance.
+func newInitiator(syncd *syncd, syncTick time.Duration) *syncInitiator {
+	i := &syncInitiator{syncd: syncd}
+
+	// Override the default peerSyncInterval value if syncTick is specified.
+	if syncTick > 0 {
+		peerSyncInterval = syncTick
+	}
+
+	vlog.VI(1).Infof("newInitiator: My device ID: %s", i.syncd.id)
+	vlog.VI(1).Infof("newInitiator: Sync interval: %v", peerSyncInterval)
+
+	return i
+}
+
+// contactPeers wakes up every peerSyncInterval to contact peers and get deltas from them.
+func (i *syncInitiator) contactPeers() {
+	ticker := time.NewTicker(peerSyncInterval)
+	for {
+		select {
+		case <-i.syncd.closed:
+			ticker.Stop()
+			i.syncd.pending.Done()
+			return
+		case <-ticker.C:
+		}
+
+		peerRelName, err := i.pickPeer()
+		if err != nil {
+			continue
+		}
+
+		i.getDeltasFromPeer(peerRelName)
+	}
+}
+
+// pickPeer picks a sync endpoint to sync with.
+func (i *syncInitiator) pickPeer() (string, error) {
+	myID := string(i.syncd.relName)
+
+	switch peerSelectionPolicy {
+	case selectRandom:
+		// TODO(hpucha): Eliminate reaching into syncd's lock.
+		i.syncd.lock.RLock()
+		// neighbors, err := i.syncd.sgtab.getMembers()
+		neighbors := make(map[string]uint32)
+		var err error
+		i.syncd.lock.RUnlock()
+
+		if err != nil {
+			return "", err
+		}
+
+		// Remove myself from the set so only neighbors are counted.
+		delete(neighbors, myID)
+
+		if len(neighbors) == 0 {
+			return "", errNoUsefulPeer
+		}
+
+		// Pick a neighbor at random.
+		ind := rand.Intn(len(neighbors))
+		for k := range neighbors {
+			if ind == 0 {
+				return k, nil
+			}
+			ind--
+		}
+		return "", fmt.Errorf("random selection didn't succeed")
+	default:
+		return "", fmt.Errorf("unknown peer selection policy")
+	}
+}
+
+// getDeltasFromPeer performs an initiation round to the specified
+// peer. An initiation round consists of:
+// * Creating local generation for syncroots that are going to be requested in this round.
+// * Contacting the peer to receive all the deltas based on the local gen vector.
+// * Processing those deltas to discover objects which have been updated.
+// * Processing updated objects to detect and resolve any conflicts if needed.
+// * Communicating relevant object updates to the store.
+func (i *syncInitiator) getDeltasFromPeer(peerRelName string) {
+
+	vlog.VI(2).Infof("getDeltasFromPeer:: %s", peerRelName)
+	// Initialize initiation state.
+	i.newInitiationState()
+	defer i.clearInitiationState()
+
+	// Freeze the most recent batch of local changes
+	// before fetching remote changes from a peer.
+	//
+	// We only allow an initiator to create new local
+	// generations (not responders/watcher) in order to
+	// maintain a static baseline for the duration of a
+	// sync. This addresses the following race condition:
+	// If we allow responders to create new local
+	// generations while the initiator is in progress,
+	// they may beat the initiator and send these new
+	// generations to remote devices.  These remote
+	// devices in turn can send these generations back to
+	// the initiator in progress which was started with
+	// older generation information.
+	err := i.updateLocalGeneration(peerRelName)
+	if err != nil {
+		vlog.Fatalf("getDeltasFromPeer:: error updating local generation: err %v", err)
+	}
+
+	// Obtain deltas from the peer over the network. These deltas
+	// are stored in a tmp kvdb.
+	if err := i.getDeltas(); err != nil {
+		vlog.Errorf("getDeltasFromPeer:: error getting deltas: err %v", err)
+		return
+	}
+
+	i.syncd.sgOp.Lock()
+	defer i.syncd.sgOp.Unlock()
+
+	if err := i.processDeltas(); err != nil {
+		vlog.Fatalf("getDeltasFromPeer:: error processing logs: err %v", err)
+	}
+
+	if err := i.processUpdatedObjects(); err != nil {
+		vlog.Fatalf("getDeltasFromPeer:: error processing objects: err %v", err)
+	}
+}
+
+// newInitiationState creates new initiation state.
+func (i *syncInitiator) newInitiationState() {
+	st := &initiationState{}
+	st.local = make(map[ObjId]GenVector)
+	st.syncGroups = make(map[ObjId]GroupIdSet)
+	st.newGens = make(map[string]*genMetadata)
+	st.minGens = make(map[ObjId]GenVector)
+	st.remote = make(map[ObjId]GenVector)
+	st.updObjects = make(map[ObjId]*objConflictState)
+	st.delPrivObjs = make(map[ObjId]struct{})
+
+	i.iState = st
+}
+
+// clearInitiationState cleans up the state from the current initiation round.
+func (i *syncInitiator) clearInitiationState() {
+	if i.iState.tmpDB != nil {
+		i.iState.tmpDB.close()
+	}
+	if i.iState.tmpFile != "" {
+		os.Remove(i.iState.tmpFile)
+	}
+
+	for o := range i.iState.delPrivObjs {
+		i.syncd.dag.delPrivNode(o)
+	}
+
+	i.syncd.dag.clearGraft()
+
+	i.iState = nil
+}
+
+// updateLocalGeneration creates a new local generation if needed and
+// populates the newest local generation vector.
+func (i *syncInitiator) updateLocalGeneration(peerRelName string) error {
+	// TODO(hpucha): Eliminate reaching into syncd's lock.
+	i.syncd.lock.Lock()
+	defer i.syncd.lock.Unlock()
+
+	// peerInfo, err := i.syncd.sgtab.getMemberInfo(peerRelName)
+	// if err != nil {
+	// 	return err
+	// }
+
+	// Re-construct all mount table possibilities.
+	mtTables := make(map[string]struct{})
+
+	// for sg := range peerInfo.gids {
+	// 	sgData, err := i.syncd.sgtab.getSyncGroupByID(sg)
+	// 	if err != nil {
+	// 		return err
+	// 	}
+	// 	sr := ObjId(sgData.SrvInfo.RootObjId)
+	// 	for _, mt := range sgData.SrvInfo.Config.MountTables {
+	// 		mtTables[mt] = struct{}{}
+	// 	}
+	// 	i.iState.syncGroups[sr] = append(i.iState.syncGroups[sr], sg)
+	// }
+
+	// Create a new local generation if there are any local updates
+	// for every syncroot that is common with the "peer" to be
+	// contacted.
+	for sr := range i.iState.syncGroups {
+		gen, err := i.syncd.log.createLocalGeneration(sr)
+		if err != nil && err != errNoUpdates {
+			return err
+		}
+
+		if err == nil {
+			vlog.VI(2).Infof("updateLocalGeneration:: created gen for sr %s at %d", sr.String(), gen)
+
+			// Update local generation vector in devTable.
+			if err = i.syncd.devtab.updateGeneration(i.syncd.id, sr, i.syncd.id, gen); err != nil {
+				return err
+			}
+		}
+
+		v, err := i.syncd.devtab.getGenVec(i.syncd.id, sr)
+		if err != nil {
+			return err
+		}
+
+		i.iState.local[sr] = v
+	}
+
+	// Check if the name is absolute, and if so, use the name as-is.
+	if naming.Rooted(peerRelName) {
+		i.iState.peer = peerRelName
+		return nil
+	}
+
+	// Pick any global name to contact the peer.
+	for mt := range mtTables {
+		i.iState.peer = naming.Join(mt, peerRelName)
+		vlog.VI(2).Infof("updateLocalGeneration:: contacting peer %s", i.iState.peer)
+		return nil
+	}
+
+	return fmt.Errorf("no mounttables found")
+}
+
+// getDeltas obtains the deltas from the peer and stores them in a tmp kvdb.
+func (i *syncInitiator) getDeltas() error {
+	// As log records are streamed, they are saved
+	// in a tmp kvdb so that they can be processed in one batch.
+	if err := i.initTmpKVDB(); err != nil {
+		return err
+	}
+
+	ctx, _ := vtrace.SetNewTrace(i.syncd.ctx)
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+
+	vlog.VI(1).Infof("getDeltas:: From peer with DeviceId %s at %v", i.iState.peer, time.Now().UTC())
+
+	// Construct a new stub that binds to peer endpoint.
+	c := SyncClient(naming.JoinAddressName(i.iState.peer, SyncSuffix))
+
+	vlog.VI(1).Infof("GetDeltasFromPeer:: Sending local information: %v", i.iState.local)
+
+	// Issue a GetDeltas() rpc.
+	stream, err := c.GetDeltas(ctx, i.iState.local, i.iState.syncGroups, i.syncd.id)
+	if err != nil {
+		return err
+	}
+
+	return i.recvLogStream(stream)
+}
+
+// initTmpKVDB initializes the tmp kvdb to store the log record stream.
+func (i *syncInitiator) initTmpKVDB() error {
+	i.iState.tmpFile = fmt.Sprintf("%s/tmp_%d", i.syncd.kvdbPath, time.Now().UnixNano())
+	tmpDB, tbls, err := kvdbOpen(i.iState.tmpFile, []string{"tmprec"})
+	if err != nil {
+		return err
+	}
+	i.iState.tmpDB = tmpDB
+	i.iState.tmpTbl = tbls[0]
+	return nil
+}
+
+// recvLogStream receives the log records from the GetDeltas stream
+// and puts them in tmp kvdb for later processing.
+func (i *syncInitiator) recvLogStream(stream SyncGetDeltasClientCall) error {
+	rStream := stream.RecvStream()
+	for rStream.Advance() {
+		rec := rStream.Value()
+
+		// Insert log record in tmpTbl.
+		if err := i.iState.tmpTbl.set(logRecKey(rec.DevId, rec.SyncRootId, rec.GenNum, rec.SeqNum), &rec); err != nil {
+			// TODO(hpucha): do we need to cancel stream?
+			return err
+		}
+
+		// Populate the generation metadata.
+		genKey := generationKey(rec.DevId, rec.SyncRootId, rec.GenNum)
+		if gen, ok := i.iState.newGens[genKey]; !ok {
+			// New generation in the stream.
+			i.iState.orderGens = append(i.iState.orderGens, genKey)
+			i.iState.newGens[genKey] = &genMetadata{
+				Count:     1,
+				MaxSeqNum: rec.SeqNum,
+			}
+			if _, ok := i.iState.minGens[rec.SyncRootId]; !ok {
+				i.iState.minGens[rec.SyncRootId] = GenVector{}
+			}
+			g, ok := i.iState.minGens[rec.SyncRootId][rec.DevId]
+			if !ok || g > rec.GenNum {
+				i.iState.minGens[rec.SyncRootId][rec.DevId] = rec.GenNum
+			}
+		} else {
+			gen.Count++
+			if rec.SeqNum > gen.MaxSeqNum {
+				gen.MaxSeqNum = rec.SeqNum
+			}
+		}
+	}
+
+	if err := rStream.Err(); err != nil {
+		return err
+	}
+
+	var err error
+	if i.iState.remote, err = stream.Finish(); err != nil {
+		return err
+	}
+	vlog.VI(1).Infof("recvLogStream:: Local vector %v", i.iState.local)
+	vlog.VI(1).Infof("recvLogStream:: Remote vector %v", i.iState.remote)
+	vlog.VI(2).Infof("recvLogStream:: orderGens %v", i.iState.orderGens)
+	return nil
+}
+
+// processDeltas replays an entire log stream spanning multiple
+// generations from different devices received from a single GetDeltas
+// call. It does not perform any conflict resolution during replay.
+// This avoids resolving conflicts that have already been resolved by
+// other devices.
+func (i *syncInitiator) processDeltas() error {
+	// TODO(hpucha): Eliminate reaching into syncd's lock.
+	i.syncd.lock.Lock()
+	defer i.syncd.lock.Unlock()
+
+	// Track received transactions.
+	txMap := make(map[TxId]uint32)
+
+	// Loop through each received generation in order.
+	for _, key := range i.iState.orderGens {
+		gen := i.iState.newGens[key]
+		dev, sr, gnum, err := splitGenerationKey(key)
+		if err != nil {
+			return err
+		}
+
+		// If "sr" has been left since getDeltas, skip processing.
+		// if !i.syncd.sgtab.isSyncRoot(sr) {
+		// 	continue
+		// }
+
+		for l := SeqNum(0); l <= gen.MaxSeqNum; l++ {
+			var rec LogRec
+			if err := i.iState.tmpTbl.get(logRecKey(dev, sr, gnum, l), &rec); err != nil {
+				return err
+			}
+
+			// Begin a new transaction if needed.
+			curTx := rec.Value.TxId
+			if curTx != NoTxId {
+				if cnt, ok := txMap[curTx]; !ok {
+					if i.syncd.dag.addNodeTxStart(curTx) != curTx {
+						return fmt.Errorf("failed to create transaction")
+					}
+					txMap[curTx] = rec.Value.TxCount
+					vlog.VI(2).Infof("processDeltas:: Begin Tx %v, count %d", curTx, rec.Value.TxCount)
+				} else if cnt != rec.Value.TxCount {
+					return fmt.Errorf("inconsistent counts for tid %v %d, %d", curTx, cnt, rec.Value.TxCount)
+				}
+			}
+
+			if err := i.insertRecInLogAndDag(&rec, curTx); err != nil {
+				return err
+			}
+
+			// Mark object dirty.
+			i.iState.updObjects[rec.ObjId] = &objConflictState{
+				srID: rec.SyncRootId,
+			}
+		}
+		// Insert the generation metadata.
+		if err := i.syncd.log.createRemoteGeneration(dev, sr, gnum, gen); err != nil {
+			return err
+		}
+	}
+
+	// End the started transactions if any.
+	for t, cnt := range txMap {
+		if err := i.syncd.dag.addNodeTxEnd(t, cnt); err != nil {
+			return err
+		}
+		vlog.VI(2).Infof("processLogStream:: End Tx %v %v", t, cnt)
+	}
+
+	return nil
+}
+
+// insertRecInLogAndDag adds a new log record to log and dag data structures.
+func (i *syncInitiator) insertRecInLogAndDag(rec *LogRec, txID TxId) error {
+	logKey, err := i.syncd.log.putLogRec(rec)
+	if err != nil {
+		return err
+	}
+
+	vlog.VI(2).Infof("insertRecInLogAndDag:: Adding log record %v, Tx %v", rec, txID)
+	switch rec.RecType {
+	case NodeRec:
+		return i.syncd.dag.addNode(rec.ObjId, rec.CurVers, true, rec.Value.Delete, rec.Parents, logKey, txID)
+	case LinkRec:
+		return i.syncd.dag.addParent(rec.ObjId, rec.CurVers, rec.Parents[0], true)
+	default:
+		return fmt.Errorf("unknown log record type")
+	}
+}
+
+// processUpdatedObjects processes all the updates received by the
+// initiator, one object at a time. For each updated object, we first
+// check if the object has any conflicts, resulting in three
+// possibilities:
+//
+// * There is no conflict, and no updates are needed to the store
+// (isConflict=false, newHead == oldHead). All changes received convey
+// information that still keeps the local head as the most recent
+// version. This occurs when conflicts are resolved by blessings.
+//
+// * There is no conflict, but a remote version is discovered that
+// builds on the local head (isConflict=false, newHead != oldHead). In
+// this case, we generate a store mutation to simply update the store
+// to the latest value.
+//
+// * There is a conflict and we call into the app or the system to
+// resolve the conflict, resulting in three possibilties: (a) conflict
+// was resolved by blessing the local version. In this case, store
+// need not be updated, but a link is added to record the
+// blessing. (b) conflict was resolved by blessing the remote
+// version. In this case, store is updated with the remote version and
+// a link is added as well. (c) conflict was resolved by generating a
+// new store mutation. In this case, store is updated with the new
+// version.
+//
+// We then put all these mutations in the store. If the put succeeds,
+// we update the log and dag state suitably (move the head ptr of the
+// object in the dag to the latest version, and create a new log
+// record reflecting conflict resolution if any). Puts to store can
+// fail since preconditions on the objects may have been violated. In
+// this case, we wait to get the latest versions of objects from the
+// store, and recheck if the object has any conflicts and repeat the
+// above steps, until put to store succeeds.
+func (i *syncInitiator) processUpdatedObjects() error {
+	for {
+		if err := i.detectConflicts(); err != nil {
+			return err
+		}
+
+		if err := i.resolveConflicts(); err != nil {
+			return err
+		}
+
+		err := i.updateStoreAndSync()
+		if err == nil {
+			break
+		}
+
+		vlog.Errorf("PutMutations failed %v. Will retry", err)
+		// TODO(hpucha): Sleeping and retrying is a temporary
+		// solution. Next iteration will have coordination
+		// with watch thread to intelligently retry. Hence
+		// this value is not a config param.
+		time.Sleep(1 * time.Second)
+	}
+
+	return nil
+}
+
+// detectConflicts iterates through all the updated objects to detect
+// conflicts.
+func (i *syncInitiator) detectConflicts() error {
+	// TODO(hpucha): Eliminate reaching into syncd's lock.
+	i.syncd.lock.RLock()
+	defer i.syncd.lock.RUnlock()
+
+	for obj, st := range i.iState.updObjects {
+		// Check if object has a conflict.
+		var err error
+		st.isConflict, st.newHead, st.oldHead, st.ancestor, err = i.syncd.dag.hasConflict(obj)
+		vlog.VI(2).Infof("detectConflicts:: object %v state %v err %v",
+			obj, st, err)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// resolveConflicts resolves conflicts for updated objects. Conflicts
+// may be resolved by adding new versions or blessing either the local
+// or the remote version.
+func (i *syncInitiator) resolveConflicts() error {
+	for obj, st := range i.iState.updObjects {
+		if !st.isConflict {
+			continue
+		}
+
+		res, err := i.resolveObjConflict(obj, st.oldHead, st.newHead, st.ancestor)
+		if err != nil {
+			return err
+		}
+
+		st.resolvVal = res
+	}
+
+	return nil
+}
+
+// resolveObjConflict resolves a conflict for an object given its ID and
+// the 3 versions that express the conflict: the object's local version, its
+// remote version (from the device contacted), and the common ancestor from
+// which both versions branched away.  The function returns the new object
+// value according to the conflict resolution policy.
+func (i *syncInitiator) resolveObjConflict(oid ObjId,
+	local, remote, ancestor Version) (*LogValue, error) {
+
+	// Fetch the log records of the 3 object versions.
+	versions := []Version{local, remote, ancestor}
+	lrecs, err := i.getLogRecsBatch(oid, versions)
+	if err != nil {
+		return nil, err
+	}
+
+	// Resolve the conflict according to the resolution policy.
+	var res *LogValue
+
+	switch conflictResolutionPolicy {
+	case useTime:
+		res, err = i.resolveObjConflictByTime(oid, lrecs[0], lrecs[1], lrecs[2])
+	default:
+		err = fmt.Errorf("unknown conflict resolution policy: %v", conflictResolutionPolicy)
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	resCopy := *res
+	// resCopy.Mutation.Version = NewVersion()
+	// resCopy.Mutation.Dir = resDir
+	resCopy.SyncTime = time.Now().UnixNano()
+	resCopy.TxId = NoTxId
+	resCopy.TxCount = 0
+	return &resCopy, nil
+}
+
+// resolveObjConflictByTime resolves conflicts using the timestamps
+// of the conflicting mutations.  It picks a mutation with the larger
+// timestamp, i.e. the most recent update.  If the timestamps are equal,
+// it uses the mutation version numbers as a tie-breaker, picking the
+// mutation with the larger version.
+// Instead of creating a new version that resolves the conflict, we are
+// blessing an existing version as the conflict resolution.
+func (i *syncInitiator) resolveObjConflictByTime(oid ObjId,
+	local, remote, ancestor *LogRec) (*LogValue, error) {
+
+	var res *LogValue
+	switch {
+	case local.Value.SyncTime > remote.Value.SyncTime:
+		res = &local.Value
+	case local.Value.SyncTime < remote.Value.SyncTime:
+		res = &remote.Value
+		// case local.Value.Mutation.Version > remote.Value.Mutation.Version:
+		// 	res = &local.Value
+		// case local.Value.Mutation.Version < remote.Value.Mutation.Version:
+		// 	res = &remote.Value
+	}
+
+	return res, nil
+}
+
+// getLogRecsBatch gets the log records for an array of versions.
+func (i *syncInitiator) getLogRecsBatch(obj ObjId, versions []Version) ([]*LogRec, error) {
+	// TODO(hpucha): Eliminate reaching into syncd's lock.
+	i.syncd.lock.RLock()
+	defer i.syncd.lock.RUnlock()
+
+	lrecs := make([]*LogRec, len(versions))
+	var err error
+	for p, v := range versions {
+		lrecs[p], err = i.getLogRec(obj, v)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return lrecs, nil
+}
+
+// updateStoreAndSync updates the store, and if that is successful,
+// updates log and dag data structures.
+func (i *syncInitiator) updateStoreAndSync() error {
+
+	// TODO(hpucha): Eliminate reaching into syncd's lock.
+	i.syncd.lock.Lock()
+	defer i.syncd.lock.Unlock()
+
+	// var m []raw.Mutation
+	// for obj, st := range i.iState.updObjects {
+	// 	if !st.isConflict {
+	// 		rec, err := i.getLogRec(obj, st.newHead)
+	// 		if err != nil {
+	// 			return err
+	// 		}
+	// 		st.resolvVal = &rec.Value
+	// 		// Sanity check.
+	// 		if st.resolvVal.Mutation.Version != st.newHead {
+	// 			return fmt.Errorf("bad mutation %d %d",
+	// 				st.resolvVal.Mutation.Version, st.newHead)
+	// 		}
+	// 	}
+
+	// 	// If the local version is picked, no further updates
+	// 	// to the store are needed. If the remote version is
+	// 	// picked, we put it in the store.
+	// 	if st.resolvVal.Mutation.Version != st.oldHead {
+	// 		st.resolvVal.Mutation.PriorVersion = st.oldHead
+
+	// 		// Convert resolvVal.Mutation into a mutation for the Store.
+	// 		stMutation, err := i.storeMutation(obj, st.resolvVal)
+	// 		if err != nil {
+	// 			return err
+	// 		}
+
+	// 		vlog.VI(2).Infof("updateStoreAndSync:: Try to append mutation %v (%v) for obj %v (nh %v, oh %v)",
+	// 			st.resolvVal.Mutation, stMutation, obj, st.newHead, st.oldHead)
+
+	// 		// Append to mutations, skipping a delete following a delete mutation.
+	// 		if stMutation.Version != NoVersion ||
+	// 			stMutation.PriorVersion != NoVersion {
+	// 			vlog.VI(2).Infof("updateStoreAndSync:: appending mutation %v for obj %v",
+	// 				stMutation, obj)
+	// 			m = append(m, stMutation)
+	// 		}
+	// 	}
+	// }
+
+	// TODO(hpucha): We will hold the lock across PutMutations rpc
+	// to prevent a race with watcher. The next iteration will
+	// clean up this coordination.
+	// if store := i.syncd.store; store != nil && len(m) > 0 {
+	// 	ctx, _ := vtrace.SetNewTrace(i.syncd.ctx)
+	// 	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	// 	defer cancel()
+
+	// 	stream, err := store.PutMutations(ctx)
+	// 	if err != nil {
+	// 		vlog.Errorf("updateStoreAndSync:: putmutations err %v", err)
+	// 		return err
+	// 	}
+	// 	sender := stream.SendStream()
+	// 	for i := range m {
+	// 		if err := sender.Send(m[i]); err != nil {
+	// 			vlog.Errorf("updateStoreAndSync:: send err %v", err)
+	// 			return err
+	// 		}
+	// 	}
+	// 	if err := sender.Close(); err != nil {
+	// 		vlog.Errorf("updateStoreAndSync:: closesend err %v", err)
+	// 		return err
+	// 	}
+	// 	if err := stream.Finish(); err != nil {
+	// 		vlog.Errorf("updateStoreAndSync:: finish err %v", err)
+	// 		return err
+	// 	}
+	// }
+
+	vlog.VI(2).Infof("updateStoreAndSync:: putmutations succeeded")
+	if err := i.updateLogAndDag(); err != nil {
+		return err
+	}
+
+	if err := i.updateGenVecs(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// storeMutation converts a resolved mutation generated by syncd to
+// one that can be sent to the store. To send to the store, it
+// converts the version numbers corresponding to object deletions to
+// NoVersion when required. It also converts the version number
+// appropriately to handle SyncGroup join.
+// func (i *syncInitiator) storeMutation(obj ObjId, resolvVal *LogValue) (raw.Mutation, error) {
+// 	curDelete := resolvVal.Delete
+// 	priorDelete := false
+// 	if resolvVal.Mutation.PriorVersion != raw.NoVersion {
+// 		oldRec, err := i.getLogRec(obj, resolvVal.Mutation.PriorVersion)
+// 		if err != nil {
+// 			return raw.Mutation{}, err
+// 		}
+// 		priorDelete = oldRec.Value.Delete
+// 	}
+
+// 	// Handle the versioning of a SyncGroup's root ObjId during join.
+// 	if resolvVal.Mutation.PriorVersion == raw.NoVersion {
+// 		if i.syncd.sgtab.isSyncRoot(obj) {
+// 			node, err := i.syncd.dag.getPrivNode(obj)
+// 			if err != nil {
+// 				return raw.Mutation{}, err
+// 			}
+// 			resolvVal.Mutation.PriorVersion = node.Mutation.Version
+// 			i.iState.delPrivObjs[obj] = struct{}{}
+// 		}
+// 	}
+
+// 	// Current version and prior versions are not deletes.
+// 	if !curDelete && !priorDelete {
+// 		return resolvVal.Mutation, nil
+// 	}
+
+// 	// Creating a new copy of the mutation to adjust version
+// 	// numbers when handling deletions.
+// 	stMutation := resolvVal.Mutation
+// 	// Adjust the current version if this a deletion.
+// 	if curDelete {
+// 		stMutation.Version = NoVersion
+// 	}
+// 	// Adjust the prior version if it is a deletion.
+// 	if priorDelete {
+// 		stMutation.PriorVersion = NoVersion
+// 	}
+
+// 	return stMutation, nil
+// }
+
+// getLogRec returns the log record corresponding to a given object and its version.
+func (i *syncInitiator) getLogRec(obj ObjId, vers Version) (*LogRec, error) {
+	logKey, err := i.syncd.dag.getLogrec(obj, vers)
+	vlog.VI(2).Infof("getLogRec:: logkey from dag is %s", logKey)
+	if err != nil {
+		return nil, err
+	}
+	dev, sg, gen, lsn, err := splitLogRecKey(logKey)
+	if err != nil {
+		return nil, err
+	}
+	vlog.VI(2).Infof("getLogRec:: splitting logkey %s to %s %v %v %v", logKey, dev, sg, gen, lsn)
+	rec, err := i.syncd.log.getLogRec(dev, sg, gen, lsn)
+	if err != nil {
+		return nil, err
+	}
+	return rec, nil
+}
+
+// updateLogAndDag updates the log and dag data structures on a successful store put.
+func (i *syncInitiator) updateLogAndDag() error {
+	for obj, st := range i.iState.updObjects {
+		if st.isConflict {
+			// Object had a conflict, which was resolved successfully.
+			// Put is successful, create a log record.
+			var err error
+			var rec *LogRec
+
+			// switch {
+			// case st.resolvVal.Mutation.Version == st.oldHead:
+			// 	// Local version was blessed as the conflict resolution.
+			// 	rec, err = i.syncd.log.createLocalLinkLogRec(obj, st.oldHead, st.newHead, st.srID)
+			// case st.resolvVal.Mutation.Version == st.newHead:
+			// 	// Remote version was blessed as the conflict resolution.
+			// 	rec, err = i.syncd.log.createLocalLinkLogRec(obj, st.newHead, st.oldHead, st.srID)
+			// default:
+			// 	// New version was created to resolve the conflict.
+			// 	parents := []Version{st.newHead, st.oldHead}
+			// 	rec, err = i.syncd.log.createLocalLogRec(obj, st.resolvVal.Mutation.Version, parents, st.resolvVal, st.srID)
+			// }
+			if err != nil {
+				return err
+			}
+			logKey, err := i.syncd.log.putLogRec(rec)
+			if err != nil {
+				return err
+			}
+			// Add a new DAG node.
+			switch rec.RecType {
+			case NodeRec:
+				// TODO(hpucha): addNode operations arising out of conflict resolution
+				// may need to be part of a transaction when app-driven resolution
+				// is introduced.
+				err = i.syncd.dag.addNode(obj, rec.CurVers, false, rec.Value.Delete, rec.Parents, logKey, NoTxId)
+			case LinkRec:
+				err = i.syncd.dag.addParent(obj, rec.CurVers, rec.Parents[0], false)
+			default:
+				return fmt.Errorf("unknown log record type")
+			}
+			if err != nil {
+				return err
+			}
+		}
+
+		// Move the head. This should be idempotent. We may
+		// move head to the local head in some cases.
+		// if err := i.syncd.dag.moveHead(obj, st.resolvVal.Mutation.Version); err != nil {
+		// 	return err
+		// }
+	}
+	return nil
+}
+
+// updateGenVecs updates local, reclaim and remote vectors at the end of an initiator cycle.
+func (i *syncInitiator) updateGenVecs() error {
+	// Update the local gen vector and put it in kvdb only if we have new updates.
+	if len(i.iState.updObjects) > 0 {
+		// remote can be a subset of local.
+		for sr, rVec := range i.iState.remote {
+			lVec := i.iState.local[sr]
+
+			if err := i.syncd.devtab.updateLocalGenVector(lVec, rVec); err != nil {
+				return err
+			}
+
+			if err := i.syncd.devtab.putGenVec(i.syncd.id, sr, lVec); err != nil {
+				return err
+			}
+
+			// if err := i.syncd.devtab.updateReclaimVec(minGens); err != nil {
+			// 	return err
+			// }
+		}
+	}
+
+	for sr, rVec := range i.iState.remote {
+		// Cache the remote generation vector for space reclamation.
+		if err := i.syncd.devtab.putGenVec(i.syncd.nameToDevId(i.iState.peer), sr, rVec); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/services/syncbase/sync/kvdb.go b/services/syncbase/sync/kvdb.go
new file mode 100644
index 0000000..a15542c
--- /dev/null
+++ b/services/syncbase/sync/kvdb.go
@@ -0,0 +1,137 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Helpful wrappers to a persistent key/value (K/V) DB used by Veyron Sync.
+// The current underlying DB is an in-memory map.
+
+import (
+	"bytes"
+	"fmt"
+	"sort"
+
+	"v.io/v23/vom"
+)
+
+var memStore map[string]*kvdb
+
+type kvdb struct {
+	tables map[string]*kvtable
+}
+
+type kvtable struct {
+	data map[string][]byte
+}
+
+// kvdbOpen opens or creates a K/V DB for the given filename and table names
+// within the DB.  It returns the DB handler and handlers for each table.
+func kvdbOpen(filename string, tables []string) (*kvdb, []*kvtable, error) {
+	if memStore == nil {
+		memStore = make(map[string]*kvdb)
+	}
+
+	db := memStore[filename]
+	if db == nil {
+		db = &kvdb{tables: make(map[string]*kvtable)}
+		memStore[filename] = db
+	}
+
+	tbls := make([]*kvtable, len(tables))
+
+	for i, table := range tables {
+		t := db.tables[table]
+		if t == nil {
+			t = &kvtable{data: make(map[string][]byte)}
+			db.tables[table] = t
+		}
+		tbls[i] = t
+	}
+
+	return db, tbls, nil
+}
+
+// close closes the given K/V DB.
+func (db *kvdb) close() {
+}
+
+// flush flushes the given K/V DB to disk.
+func (db *kvdb) flush() {
+}
+
+// set stores (or overwrites) the given key/value pair in the DB table.
+func (t *kvtable) set(key string, value interface{}) error {
+	var val bytes.Buffer
+	enc, err := vom.NewEncoder(&val)
+	if err != nil {
+		return err
+	}
+	if enc.Encode(value); err != nil {
+		return err
+	}
+	t.data[key] = val.Bytes()
+	return nil
+}
+
+// create stores the given key/value pair in the DB table only if
+// the key does not already exist.  Otherwise it returns an error.
+func (t *kvtable) create(key string, value interface{}) error {
+	if t.hasKey(key) {
+		return fmt.Errorf("key %s exists", key)
+	}
+	return t.set(key, value)
+}
+
+// update stores the given key/value pair in the DB table only if
+// the key already exists.  Otherwise it returns an error.
+func (t *kvtable) update(key string, value interface{}) error {
+	if !t.hasKey(key) {
+		return fmt.Errorf("key %s does not exist", key)
+	}
+	return t.set(key, value)
+}
+
+// get retrieves the value of a key from the DB table.
+func (t *kvtable) get(key string, value interface{}) error {
+	val := t.data[key]
+	if val == nil {
+		return fmt.Errorf("entry %s not found in the K/V DB table", key)
+	}
+	dec, err := vom.NewDecoder(bytes.NewBuffer(val))
+	if err != nil {
+		return err
+	}
+	return dec.Decode(value)
+}
+
+// del deletes the entry in the DB table given its key.
+func (t *kvtable) del(key string) error {
+	delete(t.data, key)
+	return nil
+}
+
+// hasKey returns true if the given key exists in the DB table.
+func (t *kvtable) hasKey(key string) bool {
+	val, ok := t.data[key]
+	return ok && val != nil
+}
+
+// keyIter iterates over all keys in a DB table invoking the given callback
+// function for each one.  The key iterator callback is passed the item key.
+func (t *kvtable) keyIter(keyIterCB func(key string)) error {
+	keys := make([]string, 0, len(t.data))
+	for k := range t.data {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	for _, k := range keys {
+		keyIterCB(k)
+	}
+	return nil
+}
+
+// getNumKeys returns the number of keys (entries) in the DB table.
+func (t *kvtable) getNumKeys() uint64 {
+	return uint64(len(t.data))
+}
diff --git a/services/syncbase/sync/kvdb_test.go b/services/syncbase/sync/kvdb_test.go
new file mode 100644
index 0000000..69116e3
--- /dev/null
+++ b/services/syncbase/sync/kvdb_test.go
@@ -0,0 +1,407 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Veyron Sync K/V DB component.
+
+import (
+	"fmt"
+	"os"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// A user structure stores info in the "users" table.
+type user struct {
+	Username string
+	Drinks   []string
+}
+
+// A drink structure stores info in the "drinks" table.
+type drink struct {
+	Name    string
+	Alcohol bool
+}
+
+var (
+	users = []user{
+		{Username: "lancelot", Drinks: []string{"beer", "coffee"}},
+		{Username: "arthur", Drinks: []string{"coke", "beer", "coffee"}},
+		{Username: "robin", Drinks: []string{"pepsi"}},
+		{Username: "galahad"},
+	}
+	drinks = []drink{
+		{Name: "coke", Alcohol: false},
+		{Name: "pepsi", Alcohol: false},
+		{Name: "beer", Alcohol: true},
+		{Name: "coffee", Alcohol: false},
+	}
+)
+
+// createTestDB creates a K/V DB with 2 tables.
+func createTestDB(t *testing.T) (fname string, db *kvdb, usersTbl, drinksTbl *kvtable) {
+	fname = fmt.Sprintf("%s/sync_kvdb_test_%d_%d", os.TempDir(), os.Getpid(), time.Now().UnixNano())
+	db, tbls, err := kvdbOpen(fname, []string{"users", "drinks"})
+	if err != nil {
+		os.Remove(fname)
+		t.Fatalf("cannot create new K/V DB file %s: %v", fname, err)
+	}
+
+	usersTbl, drinksTbl = tbls[0], tbls[1]
+	return
+}
+
+// initTestTables initializes the K/V tables used by the tests.
+func initTestTables(t *testing.T, usersTbl, drinksTbl *kvtable, useCreate bool) {
+	userPut, drinkPut, funcName := usersTbl.set, drinksTbl.set, "set()"
+	if useCreate {
+		userPut, drinkPut, funcName = usersTbl.create, drinksTbl.create, "create()"
+	}
+
+	for _, uu := range users {
+		if err := userPut(uu.Username, &uu); err != nil {
+			t.Fatalf("%s failed for user %s", funcName, uu.Username)
+		}
+	}
+
+	for _, dd := range drinks {
+		if err := drinkPut(dd.Name, &dd); err != nil {
+			t.Fatalf("%s failed for drink %s", funcName, dd.Name)
+		}
+	}
+
+	return
+}
+
+// checkTestTables verifies the contents of the K/V tables.
+func checkTestTables(t *testing.T, usersTbl, drinksTbl *kvtable) {
+	for _, uu := range users {
+		var u2 user
+		if err := usersTbl.get(uu.Username, &u2); err != nil {
+			t.Fatalf("get() failed for user %s", uu.Username)
+		}
+		if !reflect.DeepEqual(u2, uu) {
+			t.Fatalf("got wrong data for user %s: %#v instead of %#v", uu.Username, u2, uu)
+		}
+		if !usersTbl.hasKey(uu.Username) {
+			t.Fatalf("hasKey() did not find user %s", uu.Username)
+		}
+	}
+	for _, dd := range drinks {
+		var d2 drink
+		if err := drinksTbl.get(dd.Name, &d2); err != nil {
+			t.Fatalf("get() failed for drink %s", dd.Name)
+		}
+		if !reflect.DeepEqual(d2, dd) {
+			t.Fatalf("got wrong data for drink %s: %#v instead of %#v", dd.Name, d2, dd)
+		}
+		if !drinksTbl.hasKey(dd.Name) {
+			t.Fatalf("hasKey() did not find drink %s", dd.Name)
+		}
+	}
+
+	if num := usersTbl.getNumKeys(); num != uint64(len(users)) {
+		t.Fatalf("getNumKeys(): wrong user count: got %v instead of %v", num, len(users))
+	}
+	if num := drinksTbl.getNumKeys(); num != uint64(len(drinks)) {
+		t.Fatalf("getNumKeys(): wrong drink count: got %v instead of %v", num, len(drinks))
+	}
+}
+
+func TestKVDBSet(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+
+	db.flush()
+}
+
+func TestKVDBCreate(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, true)
+
+	db.flush()
+}
+
+func TestKVDBBadGet(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	// The DB is empty, all gets must fail.
+	for _, uu := range users {
+		var u2 user
+		if err := usersTbl.get(uu.Username, &u2); err == nil {
+			t.Fatalf("get() found non-existent user %s in file %s: %v", uu.Username, kvdbfile, u2)
+		}
+	}
+	for _, dd := range drinks {
+		var d2 drink
+		if err := drinksTbl.get(dd.Name, &d2); err == nil {
+			t.Fatalf("get() found non-existent drink %s in file %s: %v", dd.Name, kvdbfile, d2)
+		}
+	}
+}
+
+func TestKVDBBadUpdate(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	// The DB is empty, all updates must fail.
+	for _, uu := range users {
+		u2 := user{Username: uu.Username}
+		if err := usersTbl.update(uu.Username, &u2); err == nil {
+			t.Fatalf("update() worked for a non-existent user %s in file %s", uu.Username, kvdbfile)
+		}
+	}
+	for _, dd := range drinks {
+		d2 := drink{Name: dd.Name}
+		if err := drinksTbl.update(dd.Name, &d2); err == nil {
+			t.Fatalf("update() worked for a non-existent drink %s in file %s", dd.Name, kvdbfile)
+		}
+	}
+}
+
+func TestKVDBBadHasKey(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	// The DB is empty, all key-checks must fail.
+	for _, uu := range users {
+		if usersTbl.hasKey(uu.Username) {
+			t.Fatalf("hasKey() found non-existent user %s in file %s", uu.Username, kvdbfile)
+		}
+	}
+	for _, dd := range drinks {
+		if drinksTbl.hasKey(dd.Name) {
+			t.Fatalf("hasKey() found non-existent drink %s in file %s", dd.Name, kvdbfile)
+		}
+	}
+}
+
+func TestKVDBGet(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+	checkTestTables(t, usersTbl, drinksTbl)
+
+	db.flush()
+	checkTestTables(t, usersTbl, drinksTbl)
+}
+
+func TestKVDBBadCreate(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+
+	// Must not be able to re-create the same entries.
+	for _, uu := range users {
+		u2 := user{Username: uu.Username}
+		if err := usersTbl.create(uu.Username, &u2); err == nil {
+			t.Fatalf("create() worked for an existing user %s in file %s", uu.Username, kvdbfile)
+		}
+	}
+	for _, dd := range drinks {
+		d2 := drink{Name: dd.Name}
+		if err := drinksTbl.create(dd.Name, &d2); err == nil {
+			t.Fatalf("create() worked for an existing drink %s in file %s", dd.Name, kvdbfile)
+		}
+	}
+
+	db.flush()
+}
+
+func TestKVDBReopen(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+
+	initTestTables(t, usersTbl, drinksTbl, true)
+
+	// Close the re-open the file.
+	db.flush()
+	db.close()
+
+	db, tbls, err := kvdbOpen(kvdbfile, []string{"users", "drinks"})
+	if err != nil {
+		t.Fatalf("Cannot re-open existing K/V DB file %s", kvdbfile)
+	}
+	defer db.close()
+
+	usersTbl, drinksTbl = tbls[0], tbls[1]
+	checkTestTables(t, usersTbl, drinksTbl)
+}
+
+func TestKVDBKeyIter(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+
+	// Get the list of all entry keys in each table.
+	keylist := ""
+	err := usersTbl.keyIter(func(key string) {
+		keylist += key + ","
+	})
+	if err != nil || keylist != "arthur,galahad,lancelot,robin," {
+		t.Fatalf("keyIter() failed in file %s: err %v, user names: %v", kvdbfile, err, keylist)
+	}
+	keylist = ""
+	err = drinksTbl.keyIter(func(key string) {
+		keylist += key + ","
+	})
+	if err != nil || keylist != "beer,coffee,coke,pepsi," {
+		t.Fatalf("keyIter() failed in file %s: err %v, drink names: %v", kvdbfile, err, keylist)
+	}
+
+	db.flush()
+}
+
+func TestKVDBUpdate(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+	db.flush()
+	db.close()
+
+	db, tbls, err := kvdbOpen(kvdbfile, []string{"users", "drinks"})
+	if err != nil {
+		t.Fatalf("Cannot re-open existing K/V DB file %s", kvdbfile)
+	}
+	defer db.close()
+
+	usersTbl, drinksTbl = tbls[0], tbls[1]
+
+	for _, uu := range users {
+		key := uu.Username
+		u2 := uu
+		u2.Username += "-New"
+
+		if err = usersTbl.update(key, &u2); err != nil {
+			t.Fatalf("update() failed for user %s in file %s", key, kvdbfile)
+		}
+
+		var u3 user
+		if err = usersTbl.get(key, &u3); err != nil {
+			t.Fatalf("get() failed for user %s in file %s", key, kvdbfile)
+		}
+		if !reflect.DeepEqual(u3, u2) {
+			t.Fatalf("got wrong new data for user %s in file %s: %#v instead of %#v", key, kvdbfile, u3, u2)
+		}
+	}
+
+	for _, dd := range drinks {
+		key := dd.Name
+		d2 := dd
+		d2.Alcohol = !d2.Alcohol
+
+		if err = drinksTbl.update(key, &d2); err != nil {
+			t.Fatalf("update() failed for drink %s in file %s", key, kvdbfile)
+		}
+
+		var d3 drink
+		if err = drinksTbl.get(key, &d3); err != nil {
+			t.Fatalf("get() failed for drink %s in file %s", key, kvdbfile)
+		}
+		if !reflect.DeepEqual(d3, d2) {
+			t.Fatalf("got wrong new data for drink %s in file %s: %#v instead of %#v", key, kvdbfile, d3, d2)
+		}
+	}
+
+	db.flush()
+}
+
+func TestKVDBSetAgain(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+
+	for _, uu := range users {
+		key := uu.Username
+		u2 := uu
+		u2.Username += "-New"
+
+		if err := usersTbl.set(key, &u2); err != nil {
+			t.Fatalf("set() again failed for user %s in file %s", key, kvdbfile)
+		}
+
+		var u3 user
+		if err := usersTbl.get(key, &u3); err != nil {
+			t.Fatalf("get() failed for user %s in file %s", key, kvdbfile)
+		}
+		if !reflect.DeepEqual(u3, u2) {
+			t.Fatalf("got wrong new data for user %s in file %s: %#v instead of %#v", key, kvdbfile, u3, u2)
+		}
+	}
+
+	for _, dd := range drinks {
+		key := dd.Name
+		d2 := dd
+		d2.Alcohol = !d2.Alcohol
+
+		if err := drinksTbl.update(key, &d2); err != nil {
+			t.Fatalf("set() again failed for drink %s in file %s", key, kvdbfile)
+		}
+
+		var d3 drink
+		if err := drinksTbl.get(key, &d3); err != nil {
+			t.Fatalf("get() failed for drink %s in file %s", key, kvdbfile)
+		}
+		if !reflect.DeepEqual(d3, d2) {
+			t.Fatalf("got wrong new data for drink %s in file %s: %#v instead of %#v", key, kvdbfile, d3, d2)
+		}
+	}
+
+	db.flush()
+}
+
+func TestKVDBDelete(t *testing.T) {
+	kvdbfile, db, usersTbl, drinksTbl := createTestDB(t)
+	defer os.Remove(kvdbfile)
+	defer db.close()
+
+	initTestTables(t, usersTbl, drinksTbl, false)
+
+	db.flush()
+
+	// Delete entries and verify that they no longer exist.
+
+	for _, uu := range users {
+		key := uu.Username
+		if err := usersTbl.del(key); err != nil {
+			t.Errorf("del() failed for user %s in file %s", key, kvdbfile)
+		}
+		if usersTbl.hasKey(key) {
+			t.Errorf("hasKey() still finds deleted user %s in file %s", key, kvdbfile)
+		}
+	}
+
+	for _, dd := range drinks {
+		key := dd.Name
+		if err := drinksTbl.del(key); err != nil {
+			t.Errorf("del() failed for drink %s in file %s", key, kvdbfile)
+		}
+		if drinksTbl.hasKey(key) {
+			t.Errorf("hasKey() still finds deleted drink %s in file %s", key, kvdbfile)
+		}
+	}
+
+	db.flush()
+}
diff --git a/services/syncbase/sync/replay_test.go b/services/syncbase/sync/replay_test.go
new file mode 100644
index 0000000..3c86a69
--- /dev/null
+++ b/services/syncbase/sync/replay_test.go
@@ -0,0 +1,226 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Used to ease the setup of Veyron Sync test scenarios.
+// Parses a sync command file and returns a vector of commands to execute.
+//
+// Used by different test replay engines:
+// - dagReplayCommands() executes the parsed commands at the DAG API level.
+// - logReplayCommands() executes the parsed commands at the Log API level.
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+const (
+	addLocal = iota
+	addRemote
+	setDevTable
+	linkLocal
+	linkRemote
+)
+
+type syncCommand struct {
+	cmd     int
+	objID   ObjId
+	version Version
+	parents []Version
+	logrec  string
+	devID   DeviceId
+	genVec  GenVector
+	txID    TxId
+	txCount uint32
+	deleted bool
+}
+
+func strToVersion(verStr string) (Version, error) {
+	ver, err := strconv.ParseUint(verStr, 10, 64)
+	if err != nil {
+		return 0, err
+	}
+	return Version(ver), nil
+}
+
+func parseSyncCommands(file string) ([]syncCommand, error) {
+	cmds := []syncCommand{}
+	sf, err := os.Open("testdata/" + file)
+	if err != nil {
+		return nil, err
+	}
+	defer sf.Close()
+
+	scanner := bufio.NewScanner(sf)
+	lineno := 0
+	for scanner.Scan() {
+		lineno++
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || line[0] == '#' {
+			continue
+		}
+
+		args := strings.Split(line, "|")
+		nargs := len(args)
+
+		switch args[0] {
+		case "addl", "addr":
+			expNargs := 9
+			if nargs != expNargs {
+				return nil, fmt.Errorf("%s:%d: need %d args instead of %d", file, lineno, expNargs, nargs)
+			}
+			version, err := strToVersion(args[2])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid version: %s", file, lineno, args[2])
+			}
+			var parents []Version
+			for i := 3; i <= 4; i++ {
+				if args[i] != "" {
+					pver, err := strToVersion(args[i])
+					if err != nil {
+						return nil, fmt.Errorf("%s:%d: invalid parent: %s", file, lineno, args[i])
+					}
+					parents = append(parents, pver)
+				}
+			}
+
+			txID, err := strToTxId(args[6])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid TxId: %s", file, lineno, args[6])
+			}
+			txCount, err := strconv.ParseUint(args[7], 10, 32)
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid tx count: %s", file, lineno, args[7])
+			}
+			del, err := strconv.ParseBool(args[8])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid deleted bit: %s", file, lineno, args[8])
+			}
+			cmd := syncCommand{
+				version: version,
+				parents: parents,
+				logrec:  args[5],
+				txID:    txID,
+				txCount: uint32(txCount),
+				deleted: del,
+			}
+			if args[0] == "addl" {
+				cmd.cmd = addLocal
+			} else {
+				cmd.cmd = addRemote
+			}
+			if cmd.objID, err = strToObjId(args[1]); err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid object ID: %s", file, lineno, args[1])
+			}
+			cmds = append(cmds, cmd)
+
+		case "setdev":
+			expNargs := 3
+			if nargs != expNargs {
+				return nil, fmt.Errorf("%s:%d: need %d args instead of %d", file, lineno, expNargs, nargs)
+			}
+
+			genVec := make(GenVector)
+			for _, elem := range strings.Split(args[2], ",") {
+				kv := strings.Split(elem, ":")
+				if len(kv) != 2 {
+					return nil, fmt.Errorf("%s:%d: invalid gen vector key/val: %s", file, lineno, elem)
+				}
+				genID, err := strToGenId(kv[1])
+				if err != nil {
+					return nil, fmt.Errorf("%s:%d: invalid gen ID: %s", file, lineno, kv[1])
+				}
+				genVec[DeviceId(kv[0])] = genID
+			}
+
+			cmd := syncCommand{cmd: setDevTable, devID: DeviceId(args[1]), genVec: genVec}
+			cmds = append(cmds, cmd)
+
+		case "linkl", "linkr":
+			expNargs := 6
+			if nargs != expNargs {
+				return nil, fmt.Errorf("%s:%d: need %d args instead of %d", file, lineno, expNargs, nargs)
+			}
+
+			version, err := strToVersion(args[2])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid version: %s", file, lineno, args[2])
+			}
+			if args[3] == "" {
+				return nil, fmt.Errorf("%s:%d: parent (to-node) version not specified", file, lineno)
+			}
+			if args[4] != "" {
+				return nil, fmt.Errorf("%s:%d: cannot specify a 2nd parent (to-node): %s", file, lineno, args[4])
+			}
+			parent, err := strToVersion(args[3])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid parent (to-node) version: %s", file, lineno, args[3])
+			}
+
+			cmd := syncCommand{version: version, parents: []Version{parent}, logrec: args[5]}
+			if args[0] == "linkl" {
+				cmd.cmd = linkLocal
+			} else {
+				cmd.cmd = linkRemote
+			}
+			if cmd.objID, err = strToObjId(args[1]); err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid object ID: %s", file, lineno, args[1])
+			}
+			cmds = append(cmds, cmd)
+
+		default:
+			return nil, fmt.Errorf("%s:%d: invalid operation: %s", file, lineno, args[0])
+		}
+	}
+
+	err = scanner.Err()
+	return cmds, err
+}
+
+func dagReplayCommands(dag *dag, syncfile string) error {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		return err
+	}
+
+	for _, cmd := range cmds {
+		switch cmd.cmd {
+		case addLocal:
+			err = dag.addNode(cmd.objID, cmd.version, false, cmd.deleted, cmd.parents, cmd.logrec, NoTxId)
+			if err != nil {
+				return fmt.Errorf("cannot add local node %d:%d to DAG: %v", cmd.objID, cmd.version, err)
+			}
+			if err := dag.moveHead(cmd.objID, cmd.version); err != nil {
+				return fmt.Errorf("cannot move head to %d:%d in DAG: %v", cmd.objID, cmd.version, err)
+			}
+			dag.flush()
+
+		case addRemote:
+			err = dag.addNode(cmd.objID, cmd.version, true, cmd.deleted, cmd.parents, cmd.logrec, NoTxId)
+			if err != nil {
+				return fmt.Errorf("cannot add remote node %d:%d to DAG: %v", cmd.objID, cmd.version, err)
+			}
+			dag.flush()
+
+		case linkLocal:
+			if err = dag.addParent(cmd.objID, cmd.version, cmd.parents[0], false); err != nil {
+				return fmt.Errorf("cannot add local parent %d to DAG node %d:%d: %v",
+					cmd.parents[0], cmd.objID, cmd.version, err)
+			}
+			dag.flush()
+
+		case linkRemote:
+			if err = dag.addParent(cmd.objID, cmd.version, cmd.parents[0], true); err != nil {
+				return fmt.Errorf("cannot add remote parent %d to DAG node %d:%d: %v",
+					cmd.parents[0], cmd.objID, cmd.version, err)
+			}
+			dag.flush()
+		}
+	}
+	return nil
+}
diff --git a/services/syncbase/sync/sgtable.go b/services/syncbase/sync/sgtable.go
new file mode 100644
index 0000000..41818c1
--- /dev/null
+++ b/services/syncbase/sync/sgtable.go
@@ -0,0 +1,500 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// The SyncGroup Table stores the group information in a K/V DB.  It also
+// maintains an index to provide access by SyncGroup ID or name.
+//
+// The SyncGroup info is fetched from the SyncGroup server by the create or
+// join operations, and is regularly updated after that.
+//
+// The DB contains two tables persisted to disk (data, names) and one
+// in-memory (ephemeral) map (members):
+//   * data:    one entry per SyncGroup ID containing the SyncGroup data
+//   * names:   one entry per SyncGroup name pointing to its SyncGroup ID
+//   * members: an inverted index of SyncGroup members to SyncGroup IDs
+//              built from the list of SyncGroup joiners
+
+import (
+	"errors"
+	"fmt"
+	"path"
+	"strconv"
+
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/stats"
+)
+
+var (
+	errBadSGTable = errors.New("invalid SyncGroup Table")
+)
+
+type syncGroupTable struct {
+	fname   string                 // file pathname
+	store   *kvdb                  // underlying K/V store
+	sgData  *kvtable               // pointer to "data" table in the kvdb
+	sgNames *kvtable               // pointer to "names" table in the kvdb
+	members map[string]*memberInfo // in-memory tracking of SyncGroup member info
+
+	// SyncGroup Table stats
+	numSGs     *stats.Integer // number of SyncGroups
+	numMembers *stats.Integer // number of Sync members
+}
+
+type syncGroupData struct {
+	SrvInfo   SyncGroupInfo // SyncGroup info from SyncGroupServer
+	LocalPath string        // local path of the SyncGroup in the Store
+}
+
+type memberInfo struct {
+	gids map[GroupId]*memberMetaData // map of SyncGroup IDs joined and their metadata
+}
+
+type memberMetaData struct {
+	metaData JoinerMetaData // joiner metadata at the SyncGroup server
+}
+
+type sgSet map[GroupId]struct{} // a set of SyncGroups
+
+// strToGroupId converts a SyncGroup ID in string format to an GroupId.
+func strToGroupId(str string) (GroupId, error) {
+	id, err := strconv.ParseUint(str, 10, 64)
+	if err != nil {
+		return NoGroupId, err
+	}
+	return GroupId(id), nil
+}
+
+// openSyncGroupTable opens or creates a syncGroupTable for the given filename.
+func openSyncGroupTable(filename string) (*syncGroupTable, error) {
+	// Open the file and create it if it does not exist.
+	// Also initialize the store and its tables.
+	db, tbls, err := kvdbOpen(filename, []string{"data", "names"})
+	if err != nil {
+		return nil, err
+	}
+
+	s := &syncGroupTable{
+		fname:      filename,
+		store:      db,
+		sgData:     tbls[0],
+		sgNames:    tbls[1],
+		members:    make(map[string]*memberInfo),
+		numSGs:     stats.NewInteger(statsNumSyncGroup),
+		numMembers: stats.NewInteger(statsNumMember),
+	}
+
+	// Reconstruct the in-memory tracking maps by iterating over the SyncGroups.
+	// This is needed when an existing SyncGroup Table file is re-opened.
+	s.sgData.keyIter(func(gidStr string) {
+		// Get the SyncGroup data given the group ID in string format (as the data table key).
+		gid, err := strToGroupId(gidStr)
+		if err != nil {
+			return
+		}
+
+		data, err := s.getSyncGroupByID(gid)
+		if err != nil {
+			return
+		}
+
+		s.numSGs.Incr(1)
+
+		// Add all SyncGroup members to the members inverted index.
+		s.addAllMembers(data)
+	})
+
+	return s, nil
+}
+
+// close closes the syncGroupTable and invalidates its structure.
+func (s *syncGroupTable) close() {
+	if s.store != nil {
+		s.store.close() // this also closes the tables
+		stats.Delete(statsNumSyncGroup)
+		stats.Delete(statsNumMember)
+	}
+	*s = syncGroupTable{} // zero out the structure
+}
+
+// flush flushes the syncGroupTable store to disk.
+func (s *syncGroupTable) flush() {
+	if s.store != nil {
+		s.store.flush()
+	}
+}
+
+// addSyncGroup adds a new SyncGroup given its information.
+func (s *syncGroupTable) addSyncGroup(sgData *syncGroupData) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	if sgData == nil {
+		return errors.New("group information not specified")
+	}
+	gid, name := sgData.SrvInfo.Id, path.Join(sgData.SrvInfo.ServerName, sgData.SrvInfo.GroupName)
+	if name == "" {
+		return errors.New("group name not specified")
+	}
+	if sgData.LocalPath == "" {
+		return errors.New("group local path not specified")
+	}
+	if len(sgData.SrvInfo.Joiners) == 0 {
+		return errors.New("group has no joiners")
+	}
+
+	if s.hasSGDataEntry(gid) {
+		return fmt.Errorf("group %d already exists", gid)
+	}
+	if s.hasSGNameEntry(name) {
+		return fmt.Errorf("group name %s already exists", name)
+	}
+
+	// Add the group name and data entries.
+	if err := s.setSGNameEntry(name, gid); err != nil {
+		return err
+	}
+
+	if err := s.setSGDataEntry(gid, sgData); err != nil {
+		s.delSGNameEntry(name)
+		return err
+	}
+
+	s.numSGs.Incr(1)
+	s.addAllMembers(sgData)
+	return nil
+}
+
+// getSyncGroupID retrieves the SyncGroup ID given its name.
+func (s *syncGroupTable) getSyncGroupID(name string) (GroupId, error) {
+	return s.getSGNameEntry(name)
+}
+
+// getSyncGroupName retrieves the SyncGroup name given its ID.
+func (s *syncGroupTable) getSyncGroupName(gid GroupId) (string, error) {
+	data, err := s.getSyncGroupByID(gid)
+	if err != nil {
+		return "", err
+	}
+
+	return path.Join(data.SrvInfo.ServerName, data.SrvInfo.GroupName), nil
+}
+
+// getSyncGroupByID retrieves the SyncGroup given its ID.
+func (s *syncGroupTable) getSyncGroupByID(gid GroupId) (*syncGroupData, error) {
+	return s.getSGDataEntry(gid)
+}
+
+// getSyncGroupByName retrieves the SyncGroup given its name.
+func (s *syncGroupTable) getSyncGroupByName(name string) (*syncGroupData, error) {
+	gid, err := s.getSyncGroupID(name)
+	if err != nil {
+		return nil, err
+	}
+	return s.getSyncGroupByID(gid)
+}
+
+// updateSyncGroup updates the SyncGroup data.
+func (s *syncGroupTable) updateSyncGroup(data *syncGroupData) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	if data == nil {
+		return errors.New("SyncGroup data not specified")
+	}
+	if data.SrvInfo.GroupName == "" {
+		return errors.New("group name not specified")
+	}
+	if len(data.SrvInfo.Joiners) == 0 {
+		return errors.New("group has no joiners")
+	}
+
+	fullGroupName := path.Join(data.SrvInfo.ServerName, data.SrvInfo.GroupName)
+	oldData, err := s.getSyncGroupByName(fullGroupName)
+	if err != nil {
+		return err
+	}
+
+	if data.SrvInfo.Id != oldData.SrvInfo.Id {
+		return fmt.Errorf("cannot change ID of SyncGroup name %s", fullGroupName)
+	}
+	if data.LocalPath == "" {
+		data.LocalPath = oldData.LocalPath
+	} else if data.LocalPath != oldData.LocalPath {
+		return fmt.Errorf("cannot change local path of SyncGroup name %s", fullGroupName)
+	}
+
+	// Get the old set of SyncGroup joiners and diff it with the new set.
+	// Add all the current members because this inserts the new members and
+	// updates the metadata of the existing ones (addMember() is like a "put").
+	// Delete the members that are no longer part of the SyncGroup.
+	gid := oldData.SrvInfo.Id
+	newJoiners, oldJoiners := data.SrvInfo.Joiners, oldData.SrvInfo.Joiners
+
+	for member, memberData := range newJoiners {
+		s.addMember(member, gid, memberData)
+	}
+
+	for member := range oldJoiners {
+		if _, ok := newJoiners[member]; !ok {
+			s.delMember(member, gid)
+		}
+	}
+
+	return s.setSGDataEntry(gid, data)
+}
+
+// delSyncGroupByID deletes the SyncGroup given its ID.
+func (s *syncGroupTable) delSyncGroupByID(gid GroupId) error {
+	data, err := s.getSyncGroupByID(gid)
+	if err != nil {
+		return err
+	}
+	if err = s.delSGNameEntry(path.Join(data.SrvInfo.ServerName, data.SrvInfo.GroupName)); err != nil {
+		return err
+	}
+
+	s.numSGs.Incr(-1)
+	s.delAllMembers(data)
+	return s.delSGDataEntry(gid)
+}
+
+// delSyncGroupByName deletes the SyncGroup given its name.
+func (s *syncGroupTable) delSyncGroupByName(name string) error {
+	gid, err := s.getSyncGroupID(name)
+	if err != nil {
+		return err
+	}
+
+	return s.delSyncGroupByID(gid)
+}
+
+// getAllSyncGroupNames returns the names of all SyncGroups.
+func (s *syncGroupTable) getAllSyncGroupNames() ([]string, error) {
+	if s.store == nil {
+		return nil, errBadSGTable
+	}
+
+	names := make([]string, 0)
+
+	err := s.sgNames.keyIter(func(name string) {
+		names = append(names, name)
+	})
+
+	if err != nil {
+		return nil, err
+	}
+	return names, nil
+}
+
+// getMembers returns all SyncGroup members and the count of SyncGroups each one joined.
+func (s *syncGroupTable) getMembers() (map[string]uint32, error) {
+	if s.store == nil {
+		return nil, errBadSGTable
+	}
+
+	members := make(map[string]uint32)
+	for member, info := range s.members {
+		members[member] = uint32(len(info.gids))
+	}
+
+	return members, nil
+}
+
+// getMemberInfo returns SyncGroup information for a given member.
+func (s *syncGroupTable) getMemberInfo(member string) (*memberInfo, error) {
+	if s.store == nil {
+		return nil, errBadSGTable
+	}
+
+	info, ok := s.members[member]
+	if !ok {
+		return nil, fmt.Errorf("unknown member: %s", member)
+	}
+
+	return info, nil
+}
+
+// addMember inserts or updates a (member, group ID) entry in the in-memory
+// structure that indexes SyncGroup memberships based on member names and stores
+// in it the member's joiner metadata.
+func (s *syncGroupTable) addMember(member string, gid GroupId, metadata JoinerMetaData) {
+	if s.store == nil {
+		return
+	}
+
+	info, ok := s.members[member]
+	if !ok {
+		info = &memberInfo{gids: make(map[GroupId]*memberMetaData)}
+		s.members[member] = info
+		s.numMembers.Incr(1)
+	}
+
+	info.gids[gid] = &memberMetaData{metaData: metadata}
+}
+
+// delMember removes a (member, group ID) entry from the in-memory structure
+// that indexes SyncGroup memberships based on member names.
+func (s *syncGroupTable) delMember(member string, gid GroupId) {
+	if s.store == nil {
+		return
+	}
+
+	info, ok := s.members[member]
+	if !ok {
+		return
+	}
+
+	delete(info.gids, gid)
+	if len(info.gids) == 0 {
+		delete(s.members, member)
+		s.numMembers.Incr(-1)
+	}
+}
+
+// addAllMembers inserts all members of a SyncGroup in the in-memory structure
+// that indexes SyncGroup memberships based on member names.
+func (s *syncGroupTable) addAllMembers(data *syncGroupData) {
+	if s.store == nil || data == nil {
+		return
+	}
+
+	gid := data.SrvInfo.Id
+	for member, memberData := range data.SrvInfo.Joiners {
+		s.addMember(member, gid, memberData)
+	}
+}
+
+// delAllMembers removes all members of a SyncGroup from the in-memory structure
+// that indexes SyncGroup memberships based on member names.
+func (s *syncGroupTable) delAllMembers(data *syncGroupData) {
+	if s.store == nil || data == nil {
+		return
+	}
+
+	gid := data.SrvInfo.Id
+	for member := range data.SrvInfo.Joiners {
+		s.delMember(member, gid)
+	}
+}
+
+// Low-level functions to access the tables in the K/V DB.
+// They directly access the table entries without tracking their relationships.
+
+// sgDataKey returns the key used to access the SyncGroup data in the DB.
+func sgDataKey(gid GroupId) string {
+	return fmt.Sprintf("%d", gid)
+}
+
+// hasSGDataEntry returns true if the SyncGroup data entry exists in the DB.
+func (s *syncGroupTable) hasSGDataEntry(gid GroupId) bool {
+	if s.store == nil {
+		return false
+	}
+	key := sgDataKey(gid)
+	return s.sgData.hasKey(key)
+}
+
+// setSGDataEntry stores the SyncGroup data in the DB.
+func (s *syncGroupTable) setSGDataEntry(gid GroupId, data *syncGroupData) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	key := sgDataKey(gid)
+	return s.sgData.set(key, data)
+}
+
+// getSGDataEntry retrieves from the DB the SyncGroup data for a given group ID.
+func (s *syncGroupTable) getSGDataEntry(gid GroupId) (*syncGroupData, error) {
+	if s.store == nil {
+		return nil, errBadSGTable
+	}
+	var data syncGroupData
+	key := sgDataKey(gid)
+	if err := s.sgData.get(key, &data); err != nil {
+		return nil, err
+	}
+	return &data, nil
+}
+
+// delSGDataEntry deletes the SyncGroup data from the DB.
+func (s *syncGroupTable) delSGDataEntry(gid GroupId) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	key := sgDataKey(gid)
+	return s.sgData.del(key)
+}
+
+// sgNameKey returns the key used to access the SyncGroup name in the DB.
+func sgNameKey(name string) string {
+	return name
+}
+
+// hasSGNameEntry returns true if the SyncGroup name entry exists in the DB.
+func (s *syncGroupTable) hasSGNameEntry(name string) bool {
+	if s.store == nil {
+		return false
+	}
+	key := sgNameKey(name)
+	return s.sgNames.hasKey(key)
+}
+
+// setSGNameEntry stores the SyncGroup name to ID mapping in the DB.
+func (s *syncGroupTable) setSGNameEntry(name string, gid GroupId) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	key := sgNameKey(name)
+	return s.sgNames.set(key, gid)
+}
+
+// getSGNameEntry retrieves the SyncGroup name to ID mapping from the DB.
+func (s *syncGroupTable) getSGNameEntry(name string) (GroupId, error) {
+	var gid GroupId
+	if s.store == nil {
+		return gid, errBadSGTable
+	}
+	key := sgNameKey(name)
+	err := s.sgNames.get(key, &gid)
+	return gid, err
+}
+
+// delSGNameEntry deletes the SyncGroup name to ID mapping from the DB.
+func (s *syncGroupTable) delSGNameEntry(name string) error {
+	if s.store == nil {
+		return errBadSGTable
+	}
+	key := sgNameKey(name)
+	return s.sgNames.del(key)
+}
+
+// dump writes to the log file information on all SyncGroups.
+func (s *syncGroupTable) dump() {
+	if s.store == nil {
+		return
+	}
+
+	s.sgData.keyIter(func(gidStr string) {
+		// Get the SyncGroup data given the group ID in string format (as the data table key).
+		gid, err := strToGroupId(gidStr)
+		if err != nil {
+			return
+		}
+
+		data, err := s.getSyncGroupByID(gid)
+		if err != nil {
+			return
+		}
+
+		members := make([]string, 0, len(data.SrvInfo.Joiners))
+		for joiner := range data.SrvInfo.Joiners {
+			members = append(members, joiner)
+		}
+		vlog.VI(1).Infof("DUMP: SyncGroup %s: id %v, path %s, members: %s",
+			path.Join(data.SrvInfo.ServerName, data.SrvInfo.GroupName),
+			gid, data.LocalPath, members)
+	})
+}
diff --git a/services/syncbase/sync/sgtable_test.go b/services/syncbase/sync/sgtable_test.go
new file mode 100644
index 0000000..c80a2a9
--- /dev/null
+++ b/services/syncbase/sync/sgtable_test.go
@@ -0,0 +1,769 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Veyron SyncGroup Table.
+
+import (
+	"os"
+	"reflect"
+	"testing"
+
+	"v.io/x/ref/lib/stats"
+)
+
+// TestSyncGroupTableOpen tests the creation of a SyncGroup Table, closing and re-opening it.
+// It also verifies that its backing file is created and that a 2nd close is safe.
+func TestSyncGroupTableOpen(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	fsize := getFileSize(sgfile)
+	if fsize < 0 {
+		//t.Fatalf("SyncGroup Table file %s not created", sgfile)
+	}
+
+	sg.flush()
+	oldfsize := fsize
+	fsize = getFileSize(sgfile)
+	if fsize <= oldfsize {
+		//t.Fatalf("SyncGroup Table file %s not flushed", sgfile)
+	}
+
+	sg.close()
+
+	sg, err = openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot re-open existing SyncGroup Table file %s", sgfile)
+	}
+
+	oldfsize = fsize
+	fsize = getFileSize(sgfile)
+	if fsize != oldfsize {
+		t.Fatalf("SyncGroup Table file %s size changed across re-open", sgfile)
+	}
+
+	sg.close()
+	sg.close() // multiple closes should be a safe NOP
+
+	fsize = getFileSize(sgfile)
+	if fsize != oldfsize {
+		t.Fatalf("SyncGroup Table file %s size changed across close", sgfile)
+	}
+
+	// Fail opening a SyncGroup Table in a non-existent directory.
+	_, err = openSyncGroupTable("/not/really/there/junk.sg")
+	if err == nil {
+		//t.Fatalf("openSyncGroupTable() did not fail when using a bad pathname")
+	}
+}
+
+// TestInvalidSyncGroupTable tests using methods on an invalid (closed) SyncGroup Table.
+func TestInvalidSyncGroupTable(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	sg.close()
+
+	sgid, err := strToGroupId("1234")
+	if err != nil {
+		t.Error(err)
+	}
+
+	validateError := func(t *testing.T, err error, funcName string) {
+		if err == nil || err.Error() != "invalid SyncGroup Table" {
+			t.Errorf("%s() did not fail on a closed SyncGroup Table: %v", funcName, err)
+		}
+	}
+
+	err = sg.addSyncGroup(&syncGroupData{})
+	validateError(t, err, "addSyncGroup")
+
+	_, err = sg.getSyncGroupID("foobar")
+	validateError(t, err, "getSyncGroupID")
+
+	_, err = sg.getSyncGroupName(sgid)
+	validateError(t, err, "getSyncGroupName")
+
+	_, err = sg.getSyncGroupByID(sgid)
+	validateError(t, err, "getSyncGroupByID")
+
+	_, err = sg.getSyncGroupByName("foobar")
+	validateError(t, err, "getSyncGroupByName")
+
+	err = sg.updateSyncGroup(&syncGroupData{})
+	validateError(t, err, "updateSyncGroup")
+
+	err = sg.delSyncGroupByID(sgid)
+	validateError(t, err, "delSyncGroupByID")
+
+	err = sg.delSyncGroupByName("foobar")
+	validateError(t, err, "delSyncGroupByName")
+
+	_, err = sg.getAllSyncGroupNames()
+	validateError(t, err, "getAllSyncGroupNames")
+
+	_, err = sg.getMembers()
+	validateError(t, err, "getMembers")
+
+	_, err = sg.getMemberInfo("foobar")
+	validateError(t, err, "getMemberInfo")
+
+	err = sg.setSGDataEntry(sgid, &syncGroupData{})
+	validateError(t, err, "setSGDataEntry")
+
+	_, err = sg.getSGDataEntry(sgid)
+	validateError(t, err, "getSGDataEntry")
+
+	err = sg.delSGDataEntry(sgid)
+	validateError(t, err, "delSGDataEntry")
+
+	err = sg.setSGNameEntry("foobar", sgid)
+	validateError(t, err, "setSGNameEntry")
+
+	_, err = sg.getSGNameEntry("foobar")
+	validateError(t, err, "getSGNameEntry")
+
+	err = sg.delSGNameEntry("foobar")
+	validateError(t, err, "delSGNameEntry")
+
+	// These calls should be harmless NOPs.
+	sg.dump()
+	sg.flush()
+	sg.close()
+	sg.addMember("foobar", sgid, JoinerMetaData{})
+	sg.delMember("foobar", sgid)
+	sg.addAllMembers(&syncGroupData{})
+	sg.delAllMembers(&syncGroupData{})
+
+	if sg.hasSGDataEntry(sgid) {
+		t.Errorf("hasSGDataEntry() found an entry on a closed SyncGroup Table")
+	}
+	if sg.hasSGNameEntry("foobar") {
+		t.Errorf("hasSGNameEntry() found an entry on a closed SyncGroup Table")
+	}
+}
+
+// checkSGStats verifies the SyncGroup Table stats counters.
+func checkSGStats(t *testing.T, which string, numSG, numMembers int64) {
+	if num, err := stats.Value(statsNumSyncGroup); err != nil || num != numSG {
+		t.Errorf("num-syncgroups (%s): got %v (err: %v) instead of %v", which, num, err, numSG)
+	}
+	if num, err := stats.Value(statsNumMember); err != nil || num != numMembers {
+		t.Errorf("num-members (%s): got %v  (err: %v) instead of %v", which, num, err, numMembers)
+	}
+}
+
+// TestAddSyncGroup tests adding SyncGroups.
+func TestAddSyncGroup(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	checkSGStats(t, "add-1", 0, 0)
+
+	sgname := "foobar"
+	sgid, err := strToGroupId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	sgData := &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid,
+			GroupName: sgname,
+			Joiners: map[string]JoinerMetaData{
+				"phone":  JoinerMetaData{SyncPriority: 10},
+				"tablet": JoinerMetaData{SyncPriority: 25},
+				"cloud":  JoinerMetaData{SyncPriority: 1},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	err = sg.addSyncGroup(sgData)
+	if err != nil {
+		t.Errorf("adding SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	// Verify SyncGroup ID, name, and data.
+	if id, err := sg.getSyncGroupID(sgname); err != nil || id != sgid {
+		t.Errorf("cannot get back ID of SyncGroup %s: got ID %d instead of %d; err: %v", sgname, id, sgid, err)
+	}
+	if name, err := sg.getSyncGroupName(sgid); err != nil || name != sgname {
+		t.Errorf("cannot get back name of SyncGroup ID %d: got %s instead of %s; err: %v", sgid, name, sgname, err)
+	}
+
+	data, err := sg.getSyncGroupByID(sgid)
+	if err != nil {
+		t.Errorf("cannot get SyncGroup by ID %d: %v", sgid, err)
+	}
+	if !reflect.DeepEqual(data, sgData) {
+		t.Errorf("invalid SyncGroup data for group ID %d: got %v instead of %v", sgid, data, sgData)
+	}
+
+	data, err = sg.getSyncGroupByName(sgname)
+	if err != nil {
+		t.Errorf("cannot get SyncGroup by Name %s: %v", sgname, err)
+	}
+	if !reflect.DeepEqual(data, sgData) {
+		t.Errorf("invalid SyncGroup data for group name %s: got %v instead of %v", sgname, data, sgData)
+	}
+
+	// Verify membership data.
+	members, err := sg.getMembers()
+	if err != nil {
+		t.Errorf("cannot get all SyncGroup members: %v", err)
+	}
+	expMembers := map[string]uint32{"phone": 1, "tablet": 1, "cloud": 1}
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	expMetaData := map[string]*memberMetaData{
+		"phone":  &memberMetaData{metaData: JoinerMetaData{SyncPriority: 10}},
+		"tablet": &memberMetaData{metaData: JoinerMetaData{SyncPriority: 25}},
+		"cloud":  &memberMetaData{metaData: JoinerMetaData{SyncPriority: 1}},
+	}
+	for mm := range members {
+		info, err := sg.getMemberInfo(mm)
+		if err != nil || info == nil {
+			t.Errorf("cannot get info for SyncGroup member %s: info: %v, err: %v", mm, info, err)
+		}
+		if len(info.gids) != 1 {
+			t.Errorf("invalid info for SyncGroup member %s: %v", mm, info)
+		}
+		expJoinerMetaData := expMetaData[mm]
+		joinerMetaData := info.gids[sgid]
+		if !reflect.DeepEqual(joinerMetaData, expJoinerMetaData) {
+			t.Errorf("invalid joiner Data for SyncGroup member %s under group ID %d: got %v instead of %v",
+				mm, sgid, joinerMetaData, expJoinerMetaData)
+		}
+	}
+
+	checkSGStats(t, "add-2", 1, 3)
+
+	// Use a non-existent member.
+	if info, err := sg.getMemberInfo("should-not-be-there"); err == nil {
+		t.Errorf("found info for invalid SyncGroup member: %v", info)
+	}
+
+	// Adding a SyncGroup for a pre-existing group ID or name should fail.
+	err = sg.addSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("re-adding SyncGroup %d did not fail", sgid)
+	}
+
+	sgData.SrvInfo.Id, err = strToGroupId("5555")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = sg.addSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("adding SyncGroup %s with a different ID did not fail", sgname)
+	}
+
+	checkSGStats(t, "add-3", 1, 3)
+
+	sg.dump()
+	sg.close()
+}
+
+// TestInvalidAddSyncGroup tests adding SyncGroups.
+func TestInvalidAddSyncGroup(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	sgname := "foobar"
+	sgid, err := strToGroupId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = sg.addSyncGroup(nil)
+	if err == nil {
+		t.Errorf("adding a nil SyncGroup did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData := &syncGroupData{}
+	sgData.SrvInfo.Id = sgid
+
+	err = sg.addSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("adding a SyncGroup with an empty name did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData.SrvInfo.GroupName = sgname
+
+	err = sg.addSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("adding a SyncGroup with no local path did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData.LocalPath = "/foo/bar"
+
+	err = sg.addSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("adding a SyncGroup with no joiners did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sg.dump()
+	sg.close()
+}
+
+// TestUpdateSyncGroup tests updating a SyncGroup.
+func TestUpdateSyncGroup(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	err = sg.updateSyncGroup(nil)
+	if err == nil {
+		t.Errorf("updating a nil SyncGroup did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData := &syncGroupData{}
+	err = sg.updateSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("updating a SyncGroup with an empty name did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData.SrvInfo.GroupName = "blabla"
+	err = sg.updateSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("updating a SyncGroup with no joiners did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData.SrvInfo.Joiners = map[string]JoinerMetaData{
+		"phone": JoinerMetaData{SyncPriority: 10},
+	}
+	err = sg.updateSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("updating a SyncGroup with a non-existing name did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	// Create the SyncGroup to update later.
+	sgname := "foobar"
+	sgid, err := strToGroupId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	sgData = &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid,
+			GroupName: sgname,
+			Joiners: map[string]JoinerMetaData{
+				"phone":  JoinerMetaData{SyncPriority: 10},
+				"tablet": JoinerMetaData{SyncPriority: 25},
+				"cloud":  JoinerMetaData{SyncPriority: 1},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	err = sg.addSyncGroup(sgData)
+	if err != nil {
+		t.Errorf("creating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	checkSGStats(t, "up-1", 1, 3)
+
+	// Update it using different group or root IDs, which is not allowed.
+	xid, err := strToGroupId("9999")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	sgData.SrvInfo.Id = xid
+
+	err = sg.updateSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("updating a SyncGroup with an ID mismatch did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	sgData.SrvInfo.Id = sgid
+	sgData.LocalPath = "hahahaha"
+	err = sg.updateSyncGroup(sgData)
+	if err == nil {
+		t.Errorf("updating a SyncGroup with a local path mismatch did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	checkSGStats(t, "up-2", 1, 3)
+
+	// Update it using a modified set of joiners.
+	// An empty string indicates no change to the local path.
+	sgData.LocalPath = ""
+	sgData.SrvInfo.Joiners["universe"] = JoinerMetaData{SyncPriority: 0}
+	delete(sgData.SrvInfo.Joiners, "cloud")
+
+	err = sg.updateSyncGroup(sgData)
+	if err != nil {
+		t.Errorf("updating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	// Do some NOP member deletions (bad member, bad group ID).
+	// SyncGroup verification (below) should see the expected info asserting these were NOPs.
+	sg.delMember("blablablablabla", sgid)
+	sg.delMember("phone", xid)
+
+	checkSGStats(t, "up-3", 1, 3)
+
+	// Verify updated SyncGroup.
+	if id, err := sg.getSyncGroupID(sgname); err != nil || id != sgid {
+		t.Errorf("cannot get back ID of updated SyncGroup %s: got ID %d instead of %d; err: %v", sgname, id, sgid, err)
+	}
+	if name, err := sg.getSyncGroupName(sgid); err != nil || name != sgname {
+		t.Errorf("cannot get back name of updated SyncGroup ID %d: got %s instead of %s; err: %v", sgid, name, sgname, err)
+	}
+
+	expData := &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid,
+			GroupName: sgname,
+			Joiners: map[string]JoinerMetaData{
+				"phone":    JoinerMetaData{SyncPriority: 10},
+				"tablet":   JoinerMetaData{SyncPriority: 25},
+				"universe": JoinerMetaData{SyncPriority: 0},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	data, err := sg.getSyncGroupByID(sgid)
+	if err != nil {
+		t.Errorf("cannot get updated SyncGroup by ID %d: %v", sgid, err)
+	}
+	if !reflect.DeepEqual(data, expData) {
+		t.Errorf("invalid SyncGroup data for updated group ID %d: got %v instead of %v", sgid, data, expData)
+	}
+
+	data, err = sg.getSyncGroupByName(sgname)
+	if err != nil {
+		t.Errorf("cannot get updated SyncGroup by Name %s: %v", sgname, err)
+	}
+	if !reflect.DeepEqual(data, expData) {
+		t.Errorf("invalid SyncGroup data for updated group name %s: got %v instead of %v", sgname, data, expData)
+	}
+
+	// Verify membership data.
+	members, err := sg.getMembers()
+	if err != nil {
+		t.Errorf("cannot get all SyncGroup members after update: %v", err)
+	}
+	expMembers := map[string]uint32{"phone": 1, "tablet": 1, "universe": 1}
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members after update: got %v instead of %v", members, expMembers)
+	}
+
+	expMetaData := map[string]*memberMetaData{
+		"phone":    &memberMetaData{metaData: JoinerMetaData{SyncPriority: 10}},
+		"tablet":   &memberMetaData{metaData: JoinerMetaData{SyncPriority: 25}},
+		"universe": &memberMetaData{metaData: JoinerMetaData{SyncPriority: 0}},
+	}
+	for mm := range members {
+		info, err := sg.getMemberInfo(mm)
+		if err != nil || info == nil {
+			t.Errorf("cannot get info for SyncGroup member %s: info: %v, err: %v", mm, info, err)
+		}
+		if len(info.gids) != 1 {
+			t.Errorf("invalid info for SyncGroup member %s: %v", mm, info)
+		}
+		expJoinerMetaData := expMetaData[mm]
+		joinerMetaData := info.gids[sgid]
+		if !reflect.DeepEqual(joinerMetaData, expJoinerMetaData) {
+			t.Errorf("invalid joiner Data for SyncGroup member %s under group ID %d: got %v instead of %v",
+				mm, sgid, joinerMetaData, expJoinerMetaData)
+		}
+	}
+
+	sg.dump()
+	sg.close()
+}
+
+// TestDeleteSyncGroup tests deleting a SyncGroup.
+func TestDeleteSyncGroup(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	sgname := "foobar"
+	sgid, err := strToGroupId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Delete non-existing SyncGroups.
+	err = sg.delSyncGroupByID(sgid)
+	if err == nil {
+		t.Errorf("deleting a non-existing SyncGroup ID did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	err = sg.delSyncGroupByName(sgname)
+	if err == nil {
+		t.Errorf("deleting a non-existing SyncGroup name did not fail in SyncGroup Table file %s", sgfile)
+	}
+
+	checkSGStats(t, "del-1", 0, 0)
+
+	// Create the SyncGroup to delete later.
+	sgData := &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid,
+			GroupName: sgname,
+			Joiners: map[string]JoinerMetaData{
+				"phone":  JoinerMetaData{SyncPriority: 10},
+				"tablet": JoinerMetaData{SyncPriority: 25},
+				"cloud":  JoinerMetaData{SyncPriority: 1},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	err = sg.addSyncGroup(sgData)
+	if err != nil {
+		t.Errorf("creating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	checkSGStats(t, "del-2", 1, 3)
+
+	// Delete it by ID.
+	err = sg.delSyncGroupByID(sgid)
+	if err != nil {
+		t.Errorf("deleting SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	checkSGStats(t, "del-3", 0, 0)
+
+	// Create it again then delete it by name.
+	err = sg.addSyncGroup(sgData)
+	if err != nil {
+		t.Errorf("creating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid, sgfile, err)
+	}
+
+	checkSGStats(t, "del-4", 1, 3)
+
+	err = sg.delSyncGroupByName(sgname)
+	if err != nil {
+		t.Errorf("deleting SyncGroup name %s failed in SyncGroup Table file %s: %v", sgname, sgfile, err)
+	}
+
+	checkSGStats(t, "del-5", 0, 0)
+
+	sg.dump()
+	sg.close()
+}
+
+// TestMultiSyncGroups tests creating multiple SyncGroups.
+func TestMultiSyncGroups(t *testing.T) {
+	sgfile := getFileName()
+	defer os.Remove(sgfile)
+
+	sg, err := openSyncGroupTable(sgfile)
+	if err != nil {
+		t.Fatalf("cannot open new SyncGroup Table file %s", sgfile)
+	}
+
+	sgname1, sgname2 := "foo", "bar"
+	sgid1, err := strToGroupId("1234")
+	if err != nil {
+		t.Fatal(err)
+	}
+	sgid2, err := strToGroupId("8888")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Add two SyncGroups.
+	sgData1 := &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid1,
+			GroupName: sgname1,
+			Joiners: map[string]JoinerMetaData{
+				"phone":  JoinerMetaData{SyncPriority: 10},
+				"tablet": JoinerMetaData{SyncPriority: 25},
+				"cloud":  JoinerMetaData{SyncPriority: 1},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	sgData2 := &syncGroupData{
+		SrvInfo: SyncGroupInfo{
+			Id:        sgid2,
+			GroupName: sgname2,
+			Joiners: map[string]JoinerMetaData{
+				"tablet": JoinerMetaData{SyncPriority: 111},
+				"door":   JoinerMetaData{SyncPriority: 33},
+				"lamp":   JoinerMetaData{SyncPriority: 9},
+			},
+		},
+		LocalPath: "/foo/bar",
+	}
+
+	err = sg.addSyncGroup(sgData1)
+	if err != nil {
+		t.Errorf("creating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid1, sgfile, err)
+	}
+
+	checkSGStats(t, "multi-1", 1, 3)
+
+	err = sg.addSyncGroup(sgData2)
+	if err != nil {
+		t.Errorf("creating SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid2, sgfile, err)
+	}
+
+	checkSGStats(t, "multi-2", 2, 5)
+
+	// Verify SyncGroup names.
+	sgNames, err := sg.getAllSyncGroupNames()
+	if err != nil {
+		t.Errorf("cannot get all SyncGroup names: %v", err)
+	}
+	if len(sgNames) != 2 {
+		t.Errorf("wrong number of SyncGroup names: %d instead of 2", len(sgNames))
+	}
+	expNames := map[string]struct{}{sgname1: struct{}{}, sgname2: struct{}{}}
+	for _, name := range sgNames {
+		if _, ok := expNames[name]; !ok {
+			t.Errorf("unknown SyncGroup name returned: %s", name)
+		} else {
+			delete(expNames, name)
+		}
+	}
+
+	if len(expNames) > 0 {
+		t.Errorf("SyncGroup names missing, not returned: %v", expNames)
+	}
+
+	// Verify SyncGroup membership data.
+	members, err := sg.getMembers()
+	if err != nil {
+		t.Errorf("cannot get all SyncGroup members: %v", err)
+	}
+
+	expMembers := map[string]uint32{"phone": 1, "tablet": 2, "cloud": 1, "door": 1, "lamp": 1}
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	expMemberInfo := map[string]*memberInfo{
+		"phone": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid1: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 10}},
+			},
+		},
+		"tablet": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid1: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 25}},
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 111}},
+			},
+		},
+		"cloud": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid1: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 1}},
+			},
+		},
+		"door": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 33}},
+			},
+		},
+		"lamp": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 9}},
+			},
+		},
+	}
+
+	for mm := range members {
+		info, err := sg.getMemberInfo(mm)
+		if err != nil || info == nil {
+			t.Errorf("cannot get info for SyncGroup member %s: info: %v, err: %v", mm, info, err)
+		}
+		expInfo := expMemberInfo[mm]
+		if !reflect.DeepEqual(info, expInfo) {
+			t.Errorf("invalid info for SyncGroup member %s: got %v instead of %v", mm, info, expInfo)
+		}
+	}
+
+	// Delete the 1st SyncGroup.
+	err = sg.delSyncGroupByID(sgid1)
+	if err != nil {
+		t.Errorf("deleting SyncGroup ID %d failed in SyncGroup Table file %s: %v", sgid1, sgfile, err)
+	}
+
+	checkSGStats(t, "multi-3", 1, 3)
+
+	// Verify SyncGroup membership data.
+	members, err = sg.getMembers()
+	if err != nil {
+		t.Errorf("cannot get all SyncGroup members: %v", err)
+	}
+
+	expMembers = map[string]uint32{"tablet": 1, "door": 1, "lamp": 1}
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	expMemberInfo = map[string]*memberInfo{
+		"tablet": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 111}},
+			},
+		},
+		"door": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 33}},
+			},
+		},
+		"lamp": &memberInfo{
+			gids: map[GroupId]*memberMetaData{
+				sgid2: &memberMetaData{metaData: JoinerMetaData{SyncPriority: 9}},
+			},
+		},
+	}
+
+	for mm := range members {
+		info, err := sg.getMemberInfo(mm)
+		if err != nil || info == nil {
+			t.Errorf("cannot get info for SyncGroup member %s: info: %v, err: %v", mm, info, err)
+		}
+		expInfo := expMemberInfo[mm]
+		if !reflect.DeepEqual(info, expInfo) {
+			t.Errorf("invalid info for SyncGroup member %s: got %v instead of %v", mm, info, expInfo)
+		}
+	}
+
+	sg.dump()
+	sg.close()
+}
diff --git a/services/syncbase/sync/testdata/local-init-00.log.sync b/services/syncbase/sync/testdata/local-init-00.log.sync
new file mode 100644
index 0000000..46b3502
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-00.log.sync
@@ -0,0 +1,6 @@
+# Create an object locally and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-00|0|1|false
+addl|1234|2|1||logrec-01|0|1|false
+addl|1234|3|2||logrec-02|0|1|false
diff --git a/services/syncbase/sync/testdata/local-init-00.sync b/services/syncbase/sync/testdata/local-init-00.sync
new file mode 100644
index 0000000..9ab99dc
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-00.sync
@@ -0,0 +1,6 @@
+# Create an object locally and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|0|||logrec-00|0|1|false
+addl|1234|1|0||logrec-01|0|1|false
+addl|1234|2|1||logrec-02|0|1|false
diff --git a/services/syncbase/sync/testdata/local-init-01.log.sync b/services/syncbase/sync/testdata/local-init-01.log.sync
new file mode 100644
index 0000000..2f5930c
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-01.log.sync
@@ -0,0 +1,9 @@
+# Create objects locally and update one and delete another.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|12|1|||logrec-00|0|1|false
+addl|12|2|1||logrec-01|0|1|false
+addl|12|3|2||logrec-02|0|1|false
+
+addl|45|1|||logrec-00|0|1|false
+addl|45|0|1||logrec-00|0|1|true
\ No newline at end of file
diff --git a/services/syncbase/sync/testdata/local-init-01.sync b/services/syncbase/sync/testdata/local-init-01.sync
new file mode 100644
index 0000000..1178c62
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-01.sync
@@ -0,0 +1,12 @@
+# Create an object DAG locally with branches and resolved conflicts.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|0|||logrec-00|0|1|false
+addl|1234|1|0||logrec-01|0|1|false
+addl|1234|2|1||logrec-02|0|1|false
+addl|1234|3|1||logrec-03|0|1|false
+addl|1234|4|2|3|logrec-04|0|1|false
+addl|1234|5|4||logrec-05|0|1|false
+addl|1234|6|1||logrec-06|0|1|false
+addl|1234|7|5|6|logrec-07|0|1|false
+addl|1234|8|7||logrec-08|0|1|false
diff --git a/services/syncbase/sync/testdata/local-init-02.log.sync b/services/syncbase/sync/testdata/local-init-02.log.sync
new file mode 100644
index 0000000..7f9f6a7
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-02.log.sync
@@ -0,0 +1,9 @@
+# Create objects locally and update one and delete another.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|12|1|||logrec-00|0|1|false
+addl|12|2|1||logrec-01|0|1|false
+addl|12|3|2||logrec-02|0|1|false
+
+addl|45|10|||logrec-00|0|1|false
+addl|45|20|10||logrec-00|0|1|false
\ No newline at end of file
diff --git a/services/syncbase/sync/testdata/local-init-02.sync b/services/syncbase/sync/testdata/local-init-02.sync
new file mode 100644
index 0000000..cb60a79
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-02.sync
@@ -0,0 +1,10 @@
+# Create DAGs for 3 objects locally.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-a-01|0|1|false
+addl|1234|2|1||logrec-a-02|0|1|false
+
+addl|6789|1|||logrec-b-01|0|1|false
+addl|6789|2|1||logrec-b-02|0|1|false
+
+addl|2222|1|||logrec-c-01|0|1|false
diff --git a/services/syncbase/sync/testdata/local-init-03.sync b/services/syncbase/sync/testdata/local-init-03.sync
new file mode 100644
index 0000000..202a752
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-03.sync
@@ -0,0 +1,10 @@
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-01|0|1|false
+addl|1234|2|1||logrec-02|0|1|false
+addl|1234|3|1||logrec-03|0|1|false
+addl|1234|4|2||logrec-04|0|1|false
+addl|1234|5|2||logrec-05|0|1|true
+addl|1234|6|4|5|logrec-06|0|1|false
+addl|1234|7|3|5|logrec-07|0|1|false
+addl|1234|8|6|7|logrec-08|0|1|false
diff --git a/services/syncbase/sync/testdata/local-init-watch.log.sync b/services/syncbase/sync/testdata/local-init-watch.log.sync
new file mode 100644
index 0000000..3e895c8
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-init-watch.log.sync
@@ -0,0 +1,3 @@
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|082bc42e15af4fcf611d7f19a8d7831f|4|||logrec-00|0|1|false
diff --git a/services/syncbase/sync/testdata/local-resolve-00.sync b/services/syncbase/sync/testdata/local-resolve-00.sync
new file mode 100644
index 0000000..7026060
--- /dev/null
+++ b/services/syncbase/sync/testdata/local-resolve-00.sync
@@ -0,0 +1,4 @@
+# Create an object locally and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|6|2|5|logrec-06|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-2obj-del.log.sync b/services/syncbase/sync/testdata/remote-2obj-del.log.sync
new file mode 100644
index 0000000..5274ecf
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-2obj-del.log.sync
@@ -0,0 +1,7 @@
+# Update one object and delete another object remotely.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|12|4|3||VeyronPhone:10:1:0|0|1|false
+addr|12|5|4||VeyronPhone:10:1:1|0|1|false
+addr|12|6|5||VeyronPhone:10:1:2|0|1|true
+addr|45|2|1||VeyronPhone:10:1:3|0|1|false
\ No newline at end of file
diff --git a/services/syncbase/sync/testdata/remote-conf-00.log.sync b/services/syncbase/sync/testdata/remote-conf-00.log.sync
new file mode 100644
index 0000000..1f9bb5b
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-00.log.sync
@@ -0,0 +1,8 @@
+# Update an object remotely three times triggering one conflict after
+# it was created locally up to v3 (i.e. assume the remote sync received
+# it from the local sync at v2, then updated separately).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|2||VeyronPhone:10:1:0|0|1|false
+addr|1234|5|4||VeyronPhone:10:1:1|0|1|false
+addr|1234|6|5||VeyronPhone:10:1:2|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-conf-00.sync b/services/syncbase/sync/testdata/remote-conf-00.sync
new file mode 100644
index 0000000..8fae794
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-00.sync
@@ -0,0 +1,8 @@
+# Update an object remotely three times triggering one conflict after
+# it was created locally up to v2 (i.e. assume the remote sync received
+# it from the local sync at v1, then updated separately).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|3|1||logrec-03|0|1|false
+addr|1234|4|3||logrec-04|0|1|false
+addr|1234|5|4||logrec-05|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-conf-01.log.sync b/services/syncbase/sync/testdata/remote-conf-01.log.sync
new file mode 100644
index 0000000..9581f69
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-01.log.sync
@@ -0,0 +1,10 @@
+# Update an object remotely three times triggering a conflict with
+# 2 graft points: v1 and v4.  This assumes that the remote sync got
+# v1, made its own conflicting v4 that it resolved into v5 (against v2)
+# then made a v6 change.  When the local sync gets back this info it
+# sees 2 graft points: v1-v4 and v2-v5.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|1||VeyronLaptop:10:1:0|0|1|false
+addr|1234|5|2|4|VeyronPhone:10:1:0|0|1|false
+addr|1234|6|5||VeyronPhone:10:1:1|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-conf-01.sync b/services/syncbase/sync/testdata/remote-conf-01.sync
new file mode 100644
index 0000000..7485aeb
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-01.sync
@@ -0,0 +1,10 @@
+# Update an object remotely three times triggering a conflict with
+# 2 graft points: v0 and v2.  This assumes that the remote sync got
+# v0, made its own conflicting v3 that it resolved into v4 (against v1)
+# then made a v5 change.  When the local sync gets back this info it
+# sees 2 graft points: v0-v3 and v1-v4.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|3|0||logrec-03|0|1|false
+addr|1234|4|1|3|logrec-04|0|1|false
+addr|1234|5|4||logrec-05|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-conf-02.log.sync b/services/syncbase/sync/testdata/remote-conf-02.log.sync
new file mode 100644
index 0000000..3c36277
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-02.log.sync
@@ -0,0 +1,15 @@
+# Update an object remotely three times triggering one conflict after
+# it was created locally up to v3 (i.e. assume the remote sync received
+# it from the local sync at v2, then updated separately).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|2||VeyronPhone:10:1:0|0|1|false
+addr|1234|5|4||VeyronPhone:10:1:1|0|1|false
+addr|1234|6|5||VeyronPhone:10:1:2|0|1|false
+
+addr|12|4|2||VeyronPhone:99:1:0|0|1|false
+addr|12|5|4||VeyronPhone:99:1:1|0|1|false
+addr|12|6|5||VeyronPhone:99:1:2|0|1|false
+
+addr|45|30|20||VeyronPhone:99:2:0|0|1|false
+addr|45|40|30||VeyronPhone:99:2:1|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-conf-link.log.sync b/services/syncbase/sync/testdata/remote-conf-link.log.sync
new file mode 100644
index 0000000..a324e4f
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-conf-link.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the local version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|1||VeyronPhone:10:1:0|0|1|false
+linkr|1234|4|2||VeyronPhone:10:1:1
diff --git a/services/syncbase/sync/testdata/remote-init-00.log.sync b/services/syncbase/sync/testdata/remote-init-00.log.sync
new file mode 100644
index 0000000..9795d53
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-init-00.log.sync
@@ -0,0 +1,6 @@
+# Create an object remotely and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|1|||VeyronPhone:10:1:0|0|1|false
+addr|1234|2|1||VeyronPhone:10:1:1|0|1|false
+addr|1234|3|2||VeyronPhone:10:1:2|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-init-00.sync b/services/syncbase/sync/testdata/remote-init-00.sync
new file mode 100644
index 0000000..18d288e
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-init-00.sync
@@ -0,0 +1,6 @@
+# Create an object remotely and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|0|||logrec-00|0|1|false
+addr|1234|1|0||logrec-01|0|1|false
+addr|1234|2|1||logrec-02|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-init-01.log.sync b/services/syncbase/sync/testdata/remote-init-01.log.sync
new file mode 100644
index 0000000..1db4c50
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-init-01.log.sync
@@ -0,0 +1,6 @@
+# Create an object remotely and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|1|||VeyronPhone:10:5:0|0|1|false
+addr|1234|2|1||VeyronPhone:10:5:1|0|1|false
+addr|1234|3|2||VeyronPhone:10:5:2|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-init-02.log.sync b/services/syncbase/sync/testdata/remote-init-02.log.sync
new file mode 100644
index 0000000..1274b7d
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-init-02.log.sync
@@ -0,0 +1,17 @@
+# Create objects and transactions remotely.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|12|1|||VeyronPhone:10:1:0|0|1|false
+
+addr|12|2|1||VeyronPhone:10:1:1|100|3|false
+addr|45|1|||VeyronPhone:10:1:2|100|3|false
+addr|78|1|||VeyronPhone:10:1:3|100|3|false
+
+addr|78|2|1||VeyronPhone:10:1:4|0|1|false
+
+addr|78|3|1||VeyronLaptop:10:1:0|0|1|false
+
+addr|78|4|2|3|VeyronPhone:10:2:0|0|1|false
+
+addr|12|3|2||VeyronPhone:10:2:1|101|2|false
+addr|45|2|1||VeyronPhone:10:2:2|101|2|false
diff --git a/services/syncbase/sync/testdata/remote-init-03.log.sync b/services/syncbase/sync/testdata/remote-init-03.log.sync
new file mode 100644
index 0000000..1ccac35
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-init-03.log.sync
@@ -0,0 +1,6 @@
+# Create an object remotely and delete it.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|1|||VeyronPhone:10:1:0|0|1|false
+addr|1234|2|1||VeyronPhone:10:1:1|0|1|false
+addr|1234|3|2||VeyronPhone:10:1:2|0|1|true
diff --git a/services/syncbase/sync/testdata/remote-noconf-00.log.sync b/services/syncbase/sync/testdata/remote-noconf-00.log.sync
new file mode 100644
index 0000000..e2e2afa
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-00.log.sync
@@ -0,0 +1,8 @@
+# Update an object remotely three times without triggering a conflict
+# after it was created locally up to v3 (i.e. assume the remote sync
+# received it from the local sync first, then updated it).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|3||VeyronPhone:10:1:0|0|1|false
+addr|1234|5|4||VeyronPhone:10:1:1|0|1|false
+addr|1234|6|5||VeyronPhone:10:1:2|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-noconf-00.sync b/services/syncbase/sync/testdata/remote-noconf-00.sync
new file mode 100644
index 0000000..d44b6ac
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-00.sync
@@ -0,0 +1,8 @@
+# Update an object remotely three times without triggering a conflict
+# after it was created locally up to v2 (i.e. assume the remote sync
+# received it from the local sync first, then updated it).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|3|2||logrec-03|0|1|false
+addr|1234|4|3||logrec-04|0|1|false
+addr|1234|5|4||logrec-05|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-noconf-link-00.log.sync b/services/syncbase/sync/testdata/remote-noconf-link-00.log.sync
new file mode 100644
index 0000000..6945bb2
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-link-00.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the remote version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|1||VeyronPhone:10:1:0|0|1|false
+linkr|1234|2|4||VeyronPhone:10:1:1
diff --git a/services/syncbase/sync/testdata/remote-noconf-link-01.log.sync b/services/syncbase/sync/testdata/remote-noconf-link-01.log.sync
new file mode 100644
index 0000000..0c6969e
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-link-01.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the local version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|1||VeyronPhone:10:1:0|0|1|false
+linkr|1234|4|3||VeyronPhone:10:1:1
diff --git a/services/syncbase/sync/testdata/remote-noconf-link-02.log.sync b/services/syncbase/sync/testdata/remote-noconf-link-02.log.sync
new file mode 100644
index 0000000..df9e128
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-link-02.log.sync
@@ -0,0 +1,6 @@
+# Update an object remotely, detect conflict, and bless the remote version, and continue updating.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|1234|4|1||VeyronPhone:10:1:0|0|1|false
+linkr|1234|3|4||VeyronPhone:10:1:1
+addr|1234|5|3||VeyronPhone:10:2:0|0|1|false
diff --git a/services/syncbase/sync/testdata/remote-noconf-link-repeat.log.sync b/services/syncbase/sync/testdata/remote-noconf-link-repeat.log.sync
new file mode 100644
index 0000000..82e11c6
--- /dev/null
+++ b/services/syncbase/sync/testdata/remote-noconf-link-repeat.log.sync
@@ -0,0 +1,4 @@
+# Resolve the same conflict on two different devices.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+linkr|1234|3|4||VeyronLaptop:10:1:0
diff --git a/services/syncbase/sync/testdata/test-1obj.gc.sync b/services/syncbase/sync/testdata/test-1obj.gc.sync
new file mode 100644
index 0000000..4d1a8d0
--- /dev/null
+++ b/services/syncbase/sync/testdata/test-1obj.gc.sync
@@ -0,0 +1,14 @@
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+# Local node is A. Remote nodes are B and C.
+### NOT UP-TO-DATE
+addr|12345|0|||C:1:0|false|false
+addr|12345|1|0||B:1:0|false|false
+addl|12345|2|0||A:1:0|false|false
+addl|12345|3|1|2|A:2:0|false|false
+addr|12345|4|3||C:2:0|false|false
+addr|12345|5|3||B:2:0|false|false
+addr|12345|6|4|5|B:3:0|false|false
+# Devtable state
+setdev|A|A:2,B:3,C:2
+setdev|B|A:2,B:3,C:2
+setdev|C|A:2,B:1,C:2
diff --git a/services/syncbase/sync/testdata/test-3obj.gc.sync b/services/syncbase/sync/testdata/test-3obj.gc.sync
new file mode 100644
index 0000000..ce0656c
--- /dev/null
+++ b/services/syncbase/sync/testdata/test-3obj.gc.sync
@@ -0,0 +1,45 @@
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+# Local node is A. Remote nodes are B and C.
+### NOT UP-TO-DATE
+addl|123|1|||A:1:0|false|false
+
+addr|456|1|||B:1:0|false|false
+
+addr|456|2|1||B:2:0|false|false
+addr|123|2|1||B:2:1|false|false
+
+addl|456|3|2||A:2:0|false|false
+addl|123|4|2||A:2:1|false|false
+
+addr|789|1|||C:1:0|false|false
+
+addr|789|2|1||C:2:0|false|false
+
+addr|123|3|1||C:3:0|false|false
+addr|789|3|2||C:3:1|false|false
+
+addr|123|5|3|2|C:4:0|false|false
+
+addl|123|6|4|5|A:3:0|false|false
+addl|456|4|3||A:3:1|false|false
+addl|789|4|3||A:3:2|false|false
+
+addr|456|5|2||B:3:0|false|false
+
+addl|456|7|4|5|A:4:0|false|false
+
+addr|456|6|2||C:5:0|false|false
+addr|123|7|5||C:5:1|false|false
+addr|123|8|7||C:5:2|false|false
+addr|789|5|3||C:5:3|false|false
+
+addl|123|9|6|8|A:5:0|false|false
+addl|456|8|6|7|A:5:1|false|false
+addl|789|6|4|5|A:5:2|false|false
+
+addl|123|10|9||A:6:0|false|false
+
+# Devtable state
+setdev|A|A:6,B:3,C:5
+setdev|B|A:4,B:3,C:4
+setdev|C|A:4,B:3,C:4
diff --git a/services/syncbase/sync/util_test.go b/services/syncbase/sync/util_test.go
new file mode 100644
index 0000000..4c4bb88
--- /dev/null
+++ b/services/syncbase/sync/util_test.go
@@ -0,0 +1,250 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Utilities for testing.
+import (
+	"container/list"
+	"fmt"
+	"os"
+	"time"
+)
+
+// getFileName generates a filename for a temporary (per unit test) kvdb file.
+func getFileName() string {
+	return fmt.Sprintf("%s/sync_test_%d_%d", os.TempDir(), os.Getpid(), time.Now().UnixNano())
+}
+
+// createTempDir creates a unique temporary directory to store kvdb files.
+func createTempDir() (string, error) {
+	dir := fmt.Sprintf("%s/sync_test_%d_%d/", os.TempDir(), os.Getpid(), time.Now().UnixNano())
+	if err := os.MkdirAll(dir, 0700); err != nil {
+		return "", err
+	}
+	return dir, nil
+}
+
+// getFileSize returns the size of a file.
+func getFileSize(fname string) int64 {
+	finfo, err := os.Stat(fname)
+	if err != nil {
+		return -1
+	}
+	return finfo.Size()
+}
+
+// dummyStream struct emulates stream of log records received from RPC.
+type dummyStream struct {
+	l     *list.List
+	value LogRec
+}
+
+func newStream() *dummyStream {
+	ds := &dummyStream{
+		l: list.New(),
+	}
+	return ds
+}
+
+func (ds *dummyStream) Advance() bool {
+	if ds.l.Len() > 0 {
+		ds.value = ds.l.Remove(ds.l.Front()).(LogRec)
+		return true
+	}
+	return false
+}
+
+func (ds *dummyStream) Value() LogRec {
+	return ds.value
+}
+
+func (ds *dummyStream) RecvStream() interface {
+	Advance() bool
+	Value() LogRec
+	Err() error
+} {
+	return ds
+}
+
+func (*dummyStream) Err() error { return nil }
+
+func (ds *dummyStream) Finish() (map[ObjId]GenVector, error) {
+	return nil, nil
+}
+
+func (ds *dummyStream) Cancel() {
+}
+
+func (ds *dummyStream) add(rec LogRec) {
+	ds.l.PushBack(rec)
+}
+
+// logReplayCommands replays local log records parsed from the input file.
+func logReplayCommands(log *iLog, syncfile string, srid ObjId) error {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		return err
+	}
+
+	for _, cmd := range cmds {
+		switch cmd.cmd {
+		case addLocal:
+			parent := NoVersion
+			if cmd.parents != nil {
+				parent = cmd.parents[0]
+			}
+
+			val := &LogValue{
+				//Mutation: raw.Mutation{Version: cmd.version},
+				Delete:  cmd.deleted,
+				TxId:    cmd.txID,
+				TxCount: cmd.txCount,
+			}
+			err = log.processWatchRecord(cmd.objID, cmd.version, parent, val, srid)
+			if err != nil {
+				return fmt.Errorf("cannot replay local log records %d:%v err %v",
+					cmd.objID, cmd.version, err)
+			}
+		default:
+			return fmt.Errorf("unknown cmd %v", cmd.cmd)
+		}
+	}
+
+	return nil
+}
+
+// createReplayStream creates a dummy stream of log records parsed from the input file.
+func createReplayStream(syncfile string) (*dummyStream, error) {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		return nil, err
+	}
+
+	stream := newStream()
+	for _, cmd := range cmds {
+		id, srid, gnum, lsn, err := splitLogRecKey(cmd.logrec)
+		if err != nil {
+			return nil, err
+		}
+		rec := LogRec{
+			DevId:      id,
+			SyncRootId: srid,
+			GenNum:     gnum,
+			SeqNum:     lsn,
+			ObjId:      cmd.objID,
+			CurVers:    cmd.version,
+			Parents:    cmd.parents,
+			Value: LogValue{
+				//Mutation: raw.Mutation{Version: cmd.version},
+				Delete:  cmd.deleted,
+				TxId:    cmd.txID,
+				TxCount: cmd.txCount,
+			},
+		}
+
+		switch cmd.cmd {
+		case addRemote:
+			rec.RecType = NodeRec
+		case linkRemote:
+			rec.RecType = LinkRec
+		default:
+			return nil, err
+		}
+		stream.add(rec)
+	}
+
+	return stream, nil
+}
+
+//
+// // populates the log and dag state as part of state initialization.
+// func populateLogAndDAG(s *syncd, rec *LogRec) error {
+// 	logKey, err := s.log.putLogRec(rec)
+// 	if err != nil {
+// 		return err
+// 	}
+//
+// 	if err := s.dag.addNode(rec.ObjId, rec.CurVers, false, rec.Value.Delete, rec.Parents, logKey, NoTxId); err != nil {
+// 		return err
+// 	}
+// 	if err := s.dag.moveHead(rec.ObjId, rec.CurVers); err != nil {
+// 		return err
+// 	}
+// 	return nil
+// }
+//
+//
+// // vsyncInitState initializes log, dag and devtable state obtained from an input trace-like file.
+// func vsyncInitState(s *syncd, syncfile string) error {
+// 	cmds, err := parseSyncCommands(syncfile)
+// 	if err != nil {
+// 		return err
+// 	}
+//
+// 	var curGen GenId
+// 	genMap := make(map[string]*genMetadata)
+//
+// 	for _, cmd := range cmds {
+// 		switch cmd.cmd {
+// 		case addLocal, addRemote:
+// 			id, sgid, gnum, lsn, err := splitLogRecKey(cmd.logrec)
+// 			if err != nil {
+// 				return err
+// 			}
+// 			rec := &LogRec{
+// 				DevId:   id,
+// 				SGrpID:  sgid,
+// 				GenNum:  gnum,
+// 				SeqNum:     lsn,
+// 				ObjId:   cmd.objID,
+// 				CurVers: cmd.version,
+// 				Parents: cmd.parents,
+// 				Value:   LogValue{Continued: cmd.continued, Delete: cmd.deleted},
+// 			}
+// 			if err := populateLogAndDAG(s, rec); err != nil {
+// 				return err
+// 			}
+// 			key := generationKey(id, sgid, gnum)
+// 			if m, ok := genMap[key]; !ok {
+// 				genMap[key] = &genMetadata{
+// 					Pos:    s.log.head.Curorder,
+// 					Count:  1,
+// 					MaxSeqNum: rec.SeqNum,
+// 				}
+// 				s.log.head.Curorder++
+// 			} else {
+// 				m.Count++
+// 				if rec.SeqNum > m.MaxSeqNum {
+// 					m.MaxSeqNum = rec.SeqNum
+// 				}
+// 			}
+// 			if cmd.cmd == addLocal {
+// 				curGen = gnum
+// 			}
+//
+// 		case setDevTable:
+// 			if err := s.devtab.putGenVec(cmd.devID, cmd.genVec); err != nil {
+// 				return err
+// 			}
+// 		}
+// 	}
+//
+// 	// Initializing genMetadata.
+// 	for key, gen := range genMap {
+// 		dev, gnum, err := splitGenerationKey(key)
+// 		if err != nil {
+// 			return err
+// 		}
+// 		if err := s.log.putGenMetadata(dev, gnum, gen); err != nil {
+// 			return err
+// 		}
+// 	}
+//
+// 	// Initializing generation in log header.
+// 	s.log.head.Curgen = curGen + 1
+//
+// 	return nil
+// }
+//
diff --git a/services/syncbase/sync/vsync.vdl b/services/syncbase/sync/vsync.vdl
new file mode 100644
index 0000000..8ad9d30
--- /dev/null
+++ b/services/syncbase/sync/vsync.vdl
@@ -0,0 +1,182 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+  "v.io/v23/security/access"
+)
+
+// temporary types
+type ObjId string
+type Version uint64
+type GroupId uint64
+
+// DeviceId is the globally unique Id of a device.
+type DeviceId string
+// GenId is the unique Id per generation per device.
+type GenId uint64
+// SeqNum is the log sequence number.
+type SeqNum uint64
+// GenVector is the generation vector.
+type GenVector map[DeviceId]GenId
+// TxId is the unique Id per transaction.
+type TxId uint64
+// GroupIdSet is the list of SyncGroup Ids.
+type GroupIdSet []GroupId
+
+const (
+	// NodeRec type log record adds a new node in the dag.
+	NodeRec = byte(0)
+	// LinkRec type log record adds a new link in the dag.
+	LinkRec = byte(1)
+
+	// Sync interface has Object name "global/vsync/<devid>/sync".
+	SyncSuffix = "sync"
+
+	// temporary nil values
+	NoObjId = ObjId("")
+	NoVersion = Version(0)
+	NoGroupId = GroupId(0)
+)
+
+// LogRec represents a single log record that is exchanged between two
+// peers.
+//
+// It contains log related metadata: DevId is the id of the device
+// that created the log record, SyncRootId is the id of a SyncRoot this log
+// record was created under, GNum is the Id of the generation that the
+// log record is part of, SeqNum is the log sequence number of the log
+// record in the generation GNum, and RecType is the type of log
+// record.
+//
+// It also contains information relevant to the updates to an object
+// in the store: ObjId is the id of the object that was
+// updated. CurVers is the current version number of the
+// object. Parents can contain 0, 1 or 2 parent versions that the
+// current version is derived from, and Value is the actual value of
+// the object mutation.
+type LogRec struct {
+	// Log related information.
+	DevId      DeviceId
+	SyncRootId ObjId
+	GenNum     GenId
+	SeqNum     SeqNum
+	RecType    byte
+
+	// Object related information.
+	ObjId     ObjId
+	CurVers   Version
+	Parents   []Version
+	Value     LogValue
+}
+
+// LogValue represents an object mutation within a transaction.
+type LogValue struct {
+	// Mutation is the store mutation representing the change in the object.
+	//Mutation raw.Mutation
+	// SyncTime is the timestamp of the mutation when it arrives at the Sync server.
+	SyncTime int64
+	// Delete indicates whether the mutation resulted in the object being
+	// deleted from the store.
+	Delete bool
+	// TxId is the unique Id of the transaction this mutation belongs to.
+	TxId TxId
+	// TxCount is the number of mutations in the transaction TxId.
+	TxCount uint32
+}
+
+// DeviceStats contains high-level information on a device participating in
+// peer-to-peer synchronization.
+type DeviceStats struct {
+	DevId         DeviceId                  // Device Id.
+	LastSync      int64                     // Timestamp of last sync from the device.
+	GenVectors    map[ObjId]GenVector         // Generation vectors per SyncRoot.
+	IsSelf        bool                      // True if the responder is on this device.
+}
+
+// SyncGroupStats contains high-level information on a SyncGroup.
+type SyncGroupStats struct {
+	Name       string         // Global name of the SyncGroup.
+	Id         GroupId        // Global Id of the SyncGroup.
+	Path       string         // Local store path for the root of the SyncGroup.
+	RootObjId  ObjId          // Id of the store object at the root path.
+	NumJoiners uint32         // Number of members currently in the SyncGroup.
+}
+
+// SyncGroupMember contains information on a SyncGroup member.
+type SyncGroupMember struct {
+	Name       string         // Name of SyncGroup member.
+	Id         GroupId        // Global Id of the SyncGroup.
+	Metadata   JoinerMetaData // Extra member metadata.
+}
+
+// A SyncGroupInfo is the conceptual state of a SyncGroup object.
+type SyncGroupInfo struct {
+	Id         GroupId         // Globally unique SyncGroup Id.
+	ServerName string          // Global Vanadium name of SyncGroupServer.
+	GroupName  string          // Relative name of group; global name is ServerName/GroupName.
+	Config     SyncGroupConfig // Configuration parameters of this SyncGroup.
+	ETag       string          // Version Id for concurrency control.
+
+	// A map from joiner names to the associated metaData for devices that
+	// have called Join() or Create() and not subsequently called Leave()
+	// or had Eject() called on them.  The map returned by the calls below
+	// may contain only a subset of joiners if the number is large.
+	Joiners map[string]JoinerMetaData
+
+	// Blessings for joiners of this SyncGroup will be self-signed by the
+	// SyncGroupServer, and will have names matching
+	// JoinerBlessingPrefix/Name/...
+	JoinerBlessingPrefix string
+}
+
+// A SyncGroupConfig contains some fields of SyncGroupInfo that
+// are passed at create time, but which can be changed later.
+type SyncGroupConfig struct {
+	Desc         string               // Human readable description.
+	Options      map[string]any       // Options for future evolution.
+	Permissions  access.Permissions   // The object's Permissions.
+
+	// Mount tables used to advertise for synchronization.
+	// Typically, we will have only one entry.  However, an array allows
+	// mount tables to be changed over time.
+	MountTables []string
+
+	BlessingsDurationNanos int64   // Duration of blessings, in nanoseconds. 0 => use server default.
+}
+
+// A JoinerMetaData contains the non-name information stored per joiner.
+type JoinerMetaData struct {
+	// SyncPriority is a hint to bias the choice of syncing partners.
+	// Members of the SyncGroup should choose to synchronize more often
+	// with partners with lower values.
+	SyncPriority int32
+}
+
+// Sync allows a device to GetDeltas from another device.
+type Sync interface {
+	// GetDeltas returns a device's current generation vector and all
+	// the missing log records when compared to the incoming generation vector.
+	GetDeltas(in map[ObjId]GenVector, sgs map[ObjId]GroupIdSet, clientId DeviceId) stream<_, LogRec> (map[ObjId]GenVector | error) {access.Write}
+
+	// GetObjectHistory returns the mutation history of a store object.
+	GetObjectHistory(oid ObjId) stream<_, LogRec> (Version | error)
+
+	// GetDeviceStats returns information on devices participating in
+	// peer-to-peer synchronization.
+	GetDeviceStats() stream<_, DeviceStats> error {access.Admin}
+
+	// GetSyncGroupMembers returns information on SyncGroup members.
+	// If SyncGroup names are specified, only members of these SyncGroups
+	// are returned.  Otherwise, if the slice of names is nil or empty,
+	// members of all SyncGroups are returned.
+	GetSyncGroupMembers(sgNames []string) stream<_, SyncGroupMember> error {access.Admin}
+
+	// GetSyncGroupStats returns high-level information on all SyncGroups.
+	GetSyncGroupStats() stream<_, SyncGroupStats> error {access.Admin}
+
+	// Dump writes to the Sync log internal information used for debugging.
+	Dump() error {access.Admin}
+}
diff --git a/services/syncbase/sync/vsync.vdl.go b/services/syncbase/sync/vsync.vdl.go
new file mode 100644
index 0000000..e202947
--- /dev/null
+++ b/services/syncbase/sync/vsync.vdl.go
@@ -0,0 +1,1095 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: vsync.vdl
+
+package vsync
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/security/access"
+)
+
+// temporary types
+type ObjId string
+
+func (ObjId) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.ObjId"
+}) {
+}
+
+type Version uint64
+
+func (Version) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.Version"
+}) {
+}
+
+type GroupId uint64
+
+func (GroupId) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.GroupId"
+}) {
+}
+
+// DeviceId is the globally unique Id of a device.
+type DeviceId string
+
+func (DeviceId) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.DeviceId"
+}) {
+}
+
+// GenId is the unique Id per generation per device.
+type GenId uint64
+
+func (GenId) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.GenId"
+}) {
+}
+
+// SeqNum is the log sequence number.
+type SeqNum uint64
+
+func (SeqNum) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.SeqNum"
+}) {
+}
+
+// GenVector is the generation vector.
+type GenVector map[DeviceId]GenId
+
+func (GenVector) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.GenVector"
+}) {
+}
+
+// TxId is the unique Id per transaction.
+type TxId uint64
+
+func (TxId) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.TxId"
+}) {
+}
+
+// GroupIdSet is the list of SyncGroup Ids.
+type GroupIdSet []GroupId
+
+func (GroupIdSet) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.GroupIdSet"
+}) {
+}
+
+// LogRec represents a single log record that is exchanged between two
+// peers.
+//
+// It contains log related metadata: DevId is the id of the device
+// that created the log record, SyncRootId is the id of a SyncRoot this log
+// record was created under, GNum is the Id of the generation that the
+// log record is part of, SeqNum is the log sequence number of the log
+// record in the generation GNum, and RecType is the type of log
+// record.
+//
+// It also contains information relevant to the updates to an object
+// in the store: ObjId is the id of the object that was
+// updated. CurVers is the current version number of the
+// object. Parents can contain 0, 1 or 2 parent versions that the
+// current version is derived from, and Value is the actual value of
+// the object mutation.
+type LogRec struct {
+	// Log related information.
+	DevId      DeviceId
+	SyncRootId ObjId
+	GenNum     GenId
+	SeqNum     SeqNum
+	RecType    byte
+	// Object related information.
+	ObjId   ObjId
+	CurVers Version
+	Parents []Version
+	Value   LogValue
+}
+
+func (LogRec) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.LogRec"
+}) {
+}
+
+// LogValue represents an object mutation within a transaction.
+type LogValue struct {
+	// Mutation is the store mutation representing the change in the object.
+	//Mutation raw.Mutation
+	// SyncTime is the timestamp of the mutation when it arrives at the Sync server.
+	SyncTime int64
+	// Delete indicates whether the mutation resulted in the object being
+	// deleted from the store.
+	Delete bool
+	// TxId is the unique Id of the transaction this mutation belongs to.
+	TxId TxId
+	// TxCount is the number of mutations in the transaction TxId.
+	TxCount uint32
+}
+
+func (LogValue) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.LogValue"
+}) {
+}
+
+// DeviceStats contains high-level information on a device participating in
+// peer-to-peer synchronization.
+type DeviceStats struct {
+	DevId      DeviceId            // Device Id.
+	LastSync   int64               // Timestamp of last sync from the device.
+	GenVectors map[ObjId]GenVector // Generation vectors per SyncRoot.
+	IsSelf     bool                // True if the responder is on this device.
+}
+
+func (DeviceStats) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.DeviceStats"
+}) {
+}
+
+// SyncGroupStats contains high-level information on a SyncGroup.
+type SyncGroupStats struct {
+	Name       string  // Global name of the SyncGroup.
+	Id         GroupId // Global Id of the SyncGroup.
+	Path       string  // Local store path for the root of the SyncGroup.
+	RootObjId  ObjId   // Id of the store object at the root path.
+	NumJoiners uint32  // Number of members currently in the SyncGroup.
+}
+
+func (SyncGroupStats) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.SyncGroupStats"
+}) {
+}
+
+// SyncGroupMember contains information on a SyncGroup member.
+type SyncGroupMember struct {
+	Name     string         // Name of SyncGroup member.
+	Id       GroupId        // Global Id of the SyncGroup.
+	Metadata JoinerMetaData // Extra member metadata.
+}
+
+func (SyncGroupMember) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.SyncGroupMember"
+}) {
+}
+
+// A SyncGroupInfo is the conceptual state of a SyncGroup object.
+type SyncGroupInfo struct {
+	Id         GroupId         // Globally unique SyncGroup Id.
+	ServerName string          // Global Vanadium name of SyncGroupServer.
+	GroupName  string          // Relative name of group; global name is ServerName/GroupName.
+	Config     SyncGroupConfig // Configuration parameters of this SyncGroup.
+	ETag       string          // Version Id for concurrency control.
+	// A map from joiner names to the associated metaData for devices that
+	// have called Join() or Create() and not subsequently called Leave()
+	// or had Eject() called on them.  The map returned by the calls below
+	// may contain only a subset of joiners if the number is large.
+	Joiners map[string]JoinerMetaData
+	// Blessings for joiners of this SyncGroup will be self-signed by the
+	// SyncGroupServer, and will have names matching
+	// JoinerBlessingPrefix/Name/...
+	JoinerBlessingPrefix string
+}
+
+func (SyncGroupInfo) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.SyncGroupInfo"
+}) {
+}
+
+// A SyncGroupConfig contains some fields of SyncGroupInfo that
+// are passed at create time, but which can be changed later.
+type SyncGroupConfig struct {
+	Desc        string                // Human readable description.
+	Options     map[string]*vdl.Value // Options for future evolution.
+	Permissions access.Permissions    // The object's Permissions.
+	// Mount tables used to advertise for synchronization.
+	// Typically, we will have only one entry.  However, an array allows
+	// mount tables to be changed over time.
+	MountTables            []string
+	BlessingsDurationNanos int64 // Duration of blessings, in nanoseconds. 0 => use server default.
+}
+
+func (SyncGroupConfig) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.SyncGroupConfig"
+}) {
+}
+
+// A JoinerMetaData contains the non-name information stored per joiner.
+type JoinerMetaData struct {
+	// SyncPriority is a hint to bias the choice of syncing partners.
+	// Members of the SyncGroup should choose to synchronize more often
+	// with partners with lower values.
+	SyncPriority int32
+}
+
+func (JoinerMetaData) __VDLReflect(struct {
+	Name string "v.io/syncbase/x/ref/services/syncbase/sync.JoinerMetaData"
+}) {
+}
+
+func init() {
+	vdl.Register((*ObjId)(nil))
+	vdl.Register((*Version)(nil))
+	vdl.Register((*GroupId)(nil))
+	vdl.Register((*DeviceId)(nil))
+	vdl.Register((*GenId)(nil))
+	vdl.Register((*SeqNum)(nil))
+	vdl.Register((*GenVector)(nil))
+	vdl.Register((*TxId)(nil))
+	vdl.Register((*GroupIdSet)(nil))
+	vdl.Register((*LogRec)(nil))
+	vdl.Register((*LogValue)(nil))
+	vdl.Register((*DeviceStats)(nil))
+	vdl.Register((*SyncGroupStats)(nil))
+	vdl.Register((*SyncGroupMember)(nil))
+	vdl.Register((*SyncGroupInfo)(nil))
+	vdl.Register((*SyncGroupConfig)(nil))
+	vdl.Register((*JoinerMetaData)(nil))
+}
+
+// NodeRec type log record adds a new node in the dag.
+const NodeRec = byte(0)
+
+// LinkRec type log record adds a new link in the dag.
+const LinkRec = byte(1)
+
+// Sync interface has Object name "global/vsync/<devid>/sync".
+const SyncSuffix = "sync"
+
+// temporary nil values
+const NoObjId = ObjId("")
+
+const NoVersion = Version(0)
+
+const NoGroupId = GroupId(0)
+
+// SyncClientMethods is the client interface
+// containing Sync methods.
+//
+// Sync allows a device to GetDeltas from another device.
+type SyncClientMethods interface {
+	// GetDeltas returns a device's current generation vector and all
+	// the missing log records when compared to the incoming generation vector.
+	GetDeltas(ctx *context.T, in map[ObjId]GenVector, sgs map[ObjId]GroupIdSet, clientId DeviceId, opts ...rpc.CallOpt) (SyncGetDeltasClientCall, error)
+	// GetObjectHistory returns the mutation history of a store object.
+	GetObjectHistory(ctx *context.T, oid ObjId, opts ...rpc.CallOpt) (SyncGetObjectHistoryClientCall, error)
+	// GetDeviceStats returns information on devices participating in
+	// peer-to-peer synchronization.
+	GetDeviceStats(*context.T, ...rpc.CallOpt) (SyncGetDeviceStatsClientCall, error)
+	// GetSyncGroupMembers returns information on SyncGroup members.
+	// If SyncGroup names are specified, only members of these SyncGroups
+	// are returned.  Otherwise, if the slice of names is nil or empty,
+	// members of all SyncGroups are returned.
+	GetSyncGroupMembers(ctx *context.T, sgNames []string, opts ...rpc.CallOpt) (SyncGetSyncGroupMembersClientCall, error)
+	// GetSyncGroupStats returns high-level information on all SyncGroups.
+	GetSyncGroupStats(*context.T, ...rpc.CallOpt) (SyncGetSyncGroupStatsClientCall, error)
+	// Dump writes to the Sync log internal information used for debugging.
+	Dump(*context.T, ...rpc.CallOpt) error
+}
+
+// SyncClientStub adds universal methods to SyncClientMethods.
+type SyncClientStub interface {
+	SyncClientMethods
+	rpc.UniversalServiceMethods
+}
+
+// SyncClient returns a client stub for Sync.
+func SyncClient(name string) SyncClientStub {
+	return implSyncClientStub{name}
+}
+
+type implSyncClientStub struct {
+	name string
+}
+
+func (c implSyncClientStub) GetDeltas(ctx *context.T, i0 map[ObjId]GenVector, i1 map[ObjId]GroupIdSet, i2 DeviceId, opts ...rpc.CallOpt) (ocall SyncGetDeltasClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetDeltas", []interface{}{i0, i1, i2}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetDeltasClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) GetObjectHistory(ctx *context.T, i0 ObjId, opts ...rpc.CallOpt) (ocall SyncGetObjectHistoryClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetObjectHistory", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetObjectHistoryClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) GetDeviceStats(ctx *context.T, opts ...rpc.CallOpt) (ocall SyncGetDeviceStatsClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetDeviceStats", nil, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetDeviceStatsClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) GetSyncGroupMembers(ctx *context.T, i0 []string, opts ...rpc.CallOpt) (ocall SyncGetSyncGroupMembersClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetSyncGroupMembers", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetSyncGroupMembersClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) GetSyncGroupStats(ctx *context.T, opts ...rpc.CallOpt) (ocall SyncGetSyncGroupStatsClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetSyncGroupStats", nil, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetSyncGroupStatsClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) Dump(ctx *context.T, opts ...rpc.CallOpt) (err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "Dump", nil, opts...); err != nil {
+		return
+	}
+	err = call.Finish()
+	return
+}
+
+// SyncGetDeltasClientStream is the client stream for Sync.GetDeltas.
+type SyncGetDeltasClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetDeltas client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() LogRec
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncGetDeltasClientCall represents the call returned from Sync.GetDeltas.
+type SyncGetDeltasClientCall interface {
+	SyncGetDeltasClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() (map[ObjId]GenVector, error)
+}
+
+type implSyncGetDeltasClientCall struct {
+	rpc.ClientCall
+	valRecv LogRec
+	errRecv error
+}
+
+func (c *implSyncGetDeltasClientCall) RecvStream() interface {
+	Advance() bool
+	Value() LogRec
+	Err() error
+} {
+	return implSyncGetDeltasClientCallRecv{c}
+}
+
+type implSyncGetDeltasClientCallRecv struct {
+	c *implSyncGetDeltasClientCall
+}
+
+func (c implSyncGetDeltasClientCallRecv) Advance() bool {
+	c.c.valRecv = LogRec{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetDeltasClientCallRecv) Value() LogRec {
+	return c.c.valRecv
+}
+func (c implSyncGetDeltasClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetDeltasClientCall) Finish() (o0 map[ObjId]GenVector, err error) {
+	err = c.ClientCall.Finish(&o0)
+	return
+}
+
+// SyncGetObjectHistoryClientStream is the client stream for Sync.GetObjectHistory.
+type SyncGetObjectHistoryClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetObjectHistory client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() LogRec
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncGetObjectHistoryClientCall represents the call returned from Sync.GetObjectHistory.
+type SyncGetObjectHistoryClientCall interface {
+	SyncGetObjectHistoryClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() (Version, error)
+}
+
+type implSyncGetObjectHistoryClientCall struct {
+	rpc.ClientCall
+	valRecv LogRec
+	errRecv error
+}
+
+func (c *implSyncGetObjectHistoryClientCall) RecvStream() interface {
+	Advance() bool
+	Value() LogRec
+	Err() error
+} {
+	return implSyncGetObjectHistoryClientCallRecv{c}
+}
+
+type implSyncGetObjectHistoryClientCallRecv struct {
+	c *implSyncGetObjectHistoryClientCall
+}
+
+func (c implSyncGetObjectHistoryClientCallRecv) Advance() bool {
+	c.c.valRecv = LogRec{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetObjectHistoryClientCallRecv) Value() LogRec {
+	return c.c.valRecv
+}
+func (c implSyncGetObjectHistoryClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetObjectHistoryClientCall) Finish() (o0 Version, err error) {
+	err = c.ClientCall.Finish(&o0)
+	return
+}
+
+// SyncGetDeviceStatsClientStream is the client stream for Sync.GetDeviceStats.
+type SyncGetDeviceStatsClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetDeviceStats client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() DeviceStats
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncGetDeviceStatsClientCall represents the call returned from Sync.GetDeviceStats.
+type SyncGetDeviceStatsClientCall interface {
+	SyncGetDeviceStatsClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncGetDeviceStatsClientCall struct {
+	rpc.ClientCall
+	valRecv DeviceStats
+	errRecv error
+}
+
+func (c *implSyncGetDeviceStatsClientCall) RecvStream() interface {
+	Advance() bool
+	Value() DeviceStats
+	Err() error
+} {
+	return implSyncGetDeviceStatsClientCallRecv{c}
+}
+
+type implSyncGetDeviceStatsClientCallRecv struct {
+	c *implSyncGetDeviceStatsClientCall
+}
+
+func (c implSyncGetDeviceStatsClientCallRecv) Advance() bool {
+	c.c.valRecv = DeviceStats{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetDeviceStatsClientCallRecv) Value() DeviceStats {
+	return c.c.valRecv
+}
+func (c implSyncGetDeviceStatsClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetDeviceStatsClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncGetSyncGroupMembersClientStream is the client stream for Sync.GetSyncGroupMembers.
+type SyncGetSyncGroupMembersClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetSyncGroupMembers client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() SyncGroupMember
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncGetSyncGroupMembersClientCall represents the call returned from Sync.GetSyncGroupMembers.
+type SyncGetSyncGroupMembersClientCall interface {
+	SyncGetSyncGroupMembersClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncGetSyncGroupMembersClientCall struct {
+	rpc.ClientCall
+	valRecv SyncGroupMember
+	errRecv error
+}
+
+func (c *implSyncGetSyncGroupMembersClientCall) RecvStream() interface {
+	Advance() bool
+	Value() SyncGroupMember
+	Err() error
+} {
+	return implSyncGetSyncGroupMembersClientCallRecv{c}
+}
+
+type implSyncGetSyncGroupMembersClientCallRecv struct {
+	c *implSyncGetSyncGroupMembersClientCall
+}
+
+func (c implSyncGetSyncGroupMembersClientCallRecv) Advance() bool {
+	c.c.valRecv = SyncGroupMember{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetSyncGroupMembersClientCallRecv) Value() SyncGroupMember {
+	return c.c.valRecv
+}
+func (c implSyncGetSyncGroupMembersClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetSyncGroupMembersClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncGetSyncGroupStatsClientStream is the client stream for Sync.GetSyncGroupStats.
+type SyncGetSyncGroupStatsClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetSyncGroupStats client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() SyncGroupStats
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncGetSyncGroupStatsClientCall represents the call returned from Sync.GetSyncGroupStats.
+type SyncGetSyncGroupStatsClientCall interface {
+	SyncGetSyncGroupStatsClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncGetSyncGroupStatsClientCall struct {
+	rpc.ClientCall
+	valRecv SyncGroupStats
+	errRecv error
+}
+
+func (c *implSyncGetSyncGroupStatsClientCall) RecvStream() interface {
+	Advance() bool
+	Value() SyncGroupStats
+	Err() error
+} {
+	return implSyncGetSyncGroupStatsClientCallRecv{c}
+}
+
+type implSyncGetSyncGroupStatsClientCallRecv struct {
+	c *implSyncGetSyncGroupStatsClientCall
+}
+
+func (c implSyncGetSyncGroupStatsClientCallRecv) Advance() bool {
+	c.c.valRecv = SyncGroupStats{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetSyncGroupStatsClientCallRecv) Value() SyncGroupStats {
+	return c.c.valRecv
+}
+func (c implSyncGetSyncGroupStatsClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetSyncGroupStatsClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncServerMethods is the interface a server writer
+// implements for Sync.
+//
+// Sync allows a device to GetDeltas from another device.
+type SyncServerMethods interface {
+	// GetDeltas returns a device's current generation vector and all
+	// the missing log records when compared to the incoming generation vector.
+	GetDeltas(call SyncGetDeltasServerCall, in map[ObjId]GenVector, sgs map[ObjId]GroupIdSet, clientId DeviceId) (map[ObjId]GenVector, error)
+	// GetObjectHistory returns the mutation history of a store object.
+	GetObjectHistory(call SyncGetObjectHistoryServerCall, oid ObjId) (Version, error)
+	// GetDeviceStats returns information on devices participating in
+	// peer-to-peer synchronization.
+	GetDeviceStats(SyncGetDeviceStatsServerCall) error
+	// GetSyncGroupMembers returns information on SyncGroup members.
+	// If SyncGroup names are specified, only members of these SyncGroups
+	// are returned.  Otherwise, if the slice of names is nil or empty,
+	// members of all SyncGroups are returned.
+	GetSyncGroupMembers(call SyncGetSyncGroupMembersServerCall, sgNames []string) error
+	// GetSyncGroupStats returns high-level information on all SyncGroups.
+	GetSyncGroupStats(SyncGetSyncGroupStatsServerCall) error
+	// Dump writes to the Sync log internal information used for debugging.
+	Dump(rpc.ServerCall) error
+}
+
+// SyncServerStubMethods is the server interface containing
+// Sync methods, as expected by rpc.Server.
+// The only difference between this interface and SyncServerMethods
+// is the streaming methods.
+type SyncServerStubMethods interface {
+	// GetDeltas returns a device's current generation vector and all
+	// the missing log records when compared to the incoming generation vector.
+	GetDeltas(call *SyncGetDeltasServerCallStub, in map[ObjId]GenVector, sgs map[ObjId]GroupIdSet, clientId DeviceId) (map[ObjId]GenVector, error)
+	// GetObjectHistory returns the mutation history of a store object.
+	GetObjectHistory(call *SyncGetObjectHistoryServerCallStub, oid ObjId) (Version, error)
+	// GetDeviceStats returns information on devices participating in
+	// peer-to-peer synchronization.
+	GetDeviceStats(*SyncGetDeviceStatsServerCallStub) error
+	// GetSyncGroupMembers returns information on SyncGroup members.
+	// If SyncGroup names are specified, only members of these SyncGroups
+	// are returned.  Otherwise, if the slice of names is nil or empty,
+	// members of all SyncGroups are returned.
+	GetSyncGroupMembers(call *SyncGetSyncGroupMembersServerCallStub, sgNames []string) error
+	// GetSyncGroupStats returns high-level information on all SyncGroups.
+	GetSyncGroupStats(*SyncGetSyncGroupStatsServerCallStub) error
+	// Dump writes to the Sync log internal information used for debugging.
+	Dump(rpc.ServerCall) error
+}
+
+// SyncServerStub adds universal methods to SyncServerStubMethods.
+type SyncServerStub interface {
+	SyncServerStubMethods
+	// Describe the Sync interfaces.
+	Describe__() []rpc.InterfaceDesc
+}
+
+// SyncServer returns a server stub for Sync.
+// It converts an implementation of SyncServerMethods into
+// an object that may be used by rpc.Server.
+func SyncServer(impl SyncServerMethods) SyncServerStub {
+	stub := implSyncServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := rpc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := rpc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implSyncServerStub struct {
+	impl SyncServerMethods
+	gs   *rpc.GlobState
+}
+
+func (s implSyncServerStub) GetDeltas(call *SyncGetDeltasServerCallStub, i0 map[ObjId]GenVector, i1 map[ObjId]GroupIdSet, i2 DeviceId) (map[ObjId]GenVector, error) {
+	return s.impl.GetDeltas(call, i0, i1, i2)
+}
+
+func (s implSyncServerStub) GetObjectHistory(call *SyncGetObjectHistoryServerCallStub, i0 ObjId) (Version, error) {
+	return s.impl.GetObjectHistory(call, i0)
+}
+
+func (s implSyncServerStub) GetDeviceStats(call *SyncGetDeviceStatsServerCallStub) error {
+	return s.impl.GetDeviceStats(call)
+}
+
+func (s implSyncServerStub) GetSyncGroupMembers(call *SyncGetSyncGroupMembersServerCallStub, i0 []string) error {
+	return s.impl.GetSyncGroupMembers(call, i0)
+}
+
+func (s implSyncServerStub) GetSyncGroupStats(call *SyncGetSyncGroupStatsServerCallStub) error {
+	return s.impl.GetSyncGroupStats(call)
+}
+
+func (s implSyncServerStub) Dump(call rpc.ServerCall) error {
+	return s.impl.Dump(call)
+}
+
+func (s implSyncServerStub) Globber() *rpc.GlobState {
+	return s.gs
+}
+
+func (s implSyncServerStub) Describe__() []rpc.InterfaceDesc {
+	return []rpc.InterfaceDesc{SyncDesc}
+}
+
+// SyncDesc describes the Sync interface.
+var SyncDesc rpc.InterfaceDesc = descSync
+
+// descSync hides the desc to keep godoc clean.
+var descSync = rpc.InterfaceDesc{
+	Name:    "Sync",
+	PkgPath: "v.io/syncbase/x/ref/services/syncbase/sync",
+	Doc:     "// Sync allows a device to GetDeltas from another device.",
+	Methods: []rpc.MethodDesc{
+		{
+			Name: "GetDeltas",
+			Doc:  "// GetDeltas returns a device's current generation vector and all\n// the missing log records when compared to the incoming generation vector.",
+			InArgs: []rpc.ArgDesc{
+				{"in", ``},       // map[ObjId]GenVector
+				{"sgs", ``},      // map[ObjId]GroupIdSet
+				{"clientId", ``}, // DeviceId
+			},
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // map[ObjId]GenVector
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+		{
+			Name: "GetObjectHistory",
+			Doc:  "// GetObjectHistory returns the mutation history of a store object.",
+			InArgs: []rpc.ArgDesc{
+				{"oid", ``}, // ObjId
+			},
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // Version
+			},
+		},
+		{
+			Name: "GetDeviceStats",
+			Doc:  "// GetDeviceStats returns information on devices participating in\n// peer-to-peer synchronization.",
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+		{
+			Name: "GetSyncGroupMembers",
+			Doc:  "// GetSyncGroupMembers returns information on SyncGroup members.\n// If SyncGroup names are specified, only members of these SyncGroups\n// are returned.  Otherwise, if the slice of names is nil or empty,\n// members of all SyncGroups are returned.",
+			InArgs: []rpc.ArgDesc{
+				{"sgNames", ``}, // []string
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+		{
+			Name: "GetSyncGroupStats",
+			Doc:  "// GetSyncGroupStats returns high-level information on all SyncGroups.",
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+		{
+			Name: "Dump",
+			Doc:  "// Dump writes to the Sync log internal information used for debugging.",
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+	},
+}
+
+// SyncGetDeltasServerStream is the server stream for Sync.GetDeltas.
+type SyncGetDeltasServerStream interface {
+	// SendStream returns the send side of the Sync.GetDeltas server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item LogRec) error
+	}
+}
+
+// SyncGetDeltasServerCall represents the context passed to Sync.GetDeltas.
+type SyncGetDeltasServerCall interface {
+	rpc.ServerCall
+	SyncGetDeltasServerStream
+}
+
+// SyncGetDeltasServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetDeltasServerCall.
+type SyncGetDeltasServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncGetDeltasServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetDeltasServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.GetDeltas server stream.
+func (s *SyncGetDeltasServerCallStub) SendStream() interface {
+	Send(item LogRec) error
+} {
+	return implSyncGetDeltasServerCallSend{s}
+}
+
+type implSyncGetDeltasServerCallSend struct {
+	s *SyncGetDeltasServerCallStub
+}
+
+func (s implSyncGetDeltasServerCallSend) Send(item LogRec) error {
+	return s.s.Send(item)
+}
+
+// SyncGetObjectHistoryServerStream is the server stream for Sync.GetObjectHistory.
+type SyncGetObjectHistoryServerStream interface {
+	// SendStream returns the send side of the Sync.GetObjectHistory server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item LogRec) error
+	}
+}
+
+// SyncGetObjectHistoryServerCall represents the context passed to Sync.GetObjectHistory.
+type SyncGetObjectHistoryServerCall interface {
+	rpc.ServerCall
+	SyncGetObjectHistoryServerStream
+}
+
+// SyncGetObjectHistoryServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetObjectHistoryServerCall.
+type SyncGetObjectHistoryServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncGetObjectHistoryServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetObjectHistoryServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.GetObjectHistory server stream.
+func (s *SyncGetObjectHistoryServerCallStub) SendStream() interface {
+	Send(item LogRec) error
+} {
+	return implSyncGetObjectHistoryServerCallSend{s}
+}
+
+type implSyncGetObjectHistoryServerCallSend struct {
+	s *SyncGetObjectHistoryServerCallStub
+}
+
+func (s implSyncGetObjectHistoryServerCallSend) Send(item LogRec) error {
+	return s.s.Send(item)
+}
+
+// SyncGetDeviceStatsServerStream is the server stream for Sync.GetDeviceStats.
+type SyncGetDeviceStatsServerStream interface {
+	// SendStream returns the send side of the Sync.GetDeviceStats server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item DeviceStats) error
+	}
+}
+
+// SyncGetDeviceStatsServerCall represents the context passed to Sync.GetDeviceStats.
+type SyncGetDeviceStatsServerCall interface {
+	rpc.ServerCall
+	SyncGetDeviceStatsServerStream
+}
+
+// SyncGetDeviceStatsServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetDeviceStatsServerCall.
+type SyncGetDeviceStatsServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncGetDeviceStatsServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetDeviceStatsServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.GetDeviceStats server stream.
+func (s *SyncGetDeviceStatsServerCallStub) SendStream() interface {
+	Send(item DeviceStats) error
+} {
+	return implSyncGetDeviceStatsServerCallSend{s}
+}
+
+type implSyncGetDeviceStatsServerCallSend struct {
+	s *SyncGetDeviceStatsServerCallStub
+}
+
+func (s implSyncGetDeviceStatsServerCallSend) Send(item DeviceStats) error {
+	return s.s.Send(item)
+}
+
+// SyncGetSyncGroupMembersServerStream is the server stream for Sync.GetSyncGroupMembers.
+type SyncGetSyncGroupMembersServerStream interface {
+	// SendStream returns the send side of the Sync.GetSyncGroupMembers server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item SyncGroupMember) error
+	}
+}
+
+// SyncGetSyncGroupMembersServerCall represents the context passed to Sync.GetSyncGroupMembers.
+type SyncGetSyncGroupMembersServerCall interface {
+	rpc.ServerCall
+	SyncGetSyncGroupMembersServerStream
+}
+
+// SyncGetSyncGroupMembersServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetSyncGroupMembersServerCall.
+type SyncGetSyncGroupMembersServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncGetSyncGroupMembersServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetSyncGroupMembersServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.GetSyncGroupMembers server stream.
+func (s *SyncGetSyncGroupMembersServerCallStub) SendStream() interface {
+	Send(item SyncGroupMember) error
+} {
+	return implSyncGetSyncGroupMembersServerCallSend{s}
+}
+
+type implSyncGetSyncGroupMembersServerCallSend struct {
+	s *SyncGetSyncGroupMembersServerCallStub
+}
+
+func (s implSyncGetSyncGroupMembersServerCallSend) Send(item SyncGroupMember) error {
+	return s.s.Send(item)
+}
+
+// SyncGetSyncGroupStatsServerStream is the server stream for Sync.GetSyncGroupStats.
+type SyncGetSyncGroupStatsServerStream interface {
+	// SendStream returns the send side of the Sync.GetSyncGroupStats server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item SyncGroupStats) error
+	}
+}
+
+// SyncGetSyncGroupStatsServerCall represents the context passed to Sync.GetSyncGroupStats.
+type SyncGetSyncGroupStatsServerCall interface {
+	rpc.ServerCall
+	SyncGetSyncGroupStatsServerStream
+}
+
+// SyncGetSyncGroupStatsServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetSyncGroupStatsServerCall.
+type SyncGetSyncGroupStatsServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncGetSyncGroupStatsServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetSyncGroupStatsServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.GetSyncGroupStats server stream.
+func (s *SyncGetSyncGroupStatsServerCallStub) SendStream() interface {
+	Send(item SyncGroupStats) error
+} {
+	return implSyncGetSyncGroupStatsServerCallSend{s}
+}
+
+type implSyncGetSyncGroupStatsServerCallSend struct {
+	s *SyncGetSyncGroupStatsServerCallStub
+}
+
+func (s implSyncGetSyncGroupStatsServerCallSend) Send(item SyncGroupStats) error {
+	return s.s.Send(item)
+}
diff --git a/services/syncbase/sync/vsyncd.go b/services/syncbase/sync/vsyncd.go
new file mode 100644
index 0000000..ac25c29
--- /dev/null
+++ b/services/syncbase/sync/vsyncd.go
@@ -0,0 +1,647 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Package vsync provides veyron sync daemon utility functions. Sync
+// daemon serves incoming GetDeltas requests and contacts other peers
+// to get deltas from them. When it receives a GetDeltas request, the
+// incoming generation vector is diffed with the local generation
+// vector, and missing generations are sent back. When it receives
+// log records in response to a GetDeltas request, it replays those
+// log records to get in sync with the sender.
+import (
+	"fmt"
+	"math/rand"
+	"strings"
+	"sync"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/stats"
+)
+
+var (
+	rng *rand.Rand
+)
+
+// Names of Sync stats entries.
+const (
+	statsKvdbPath       = "vsync/kvdb-path"
+	statsDevId          = "vsync/dev-id"
+	statsNumDagObj      = "vsync/num-dag-objects"
+	statsNumDagNode     = "vsync/num-dag-nodes"
+	statsNumDagTx       = "vsync/num-dag-tx"
+	statsNumDagPrivNode = "vsync/num-dag-privnodes"
+	statsNumSyncGroup   = "vsync/num-syncgroups"
+	statsNumMember      = "vsync/num-members"
+)
+
+// syncd contains the metadata for the sync daemon.
+type syncd struct {
+	// Pointers to metadata structures.
+	log    *iLog
+	devtab *devTable
+	dag    *dag
+	sgtab  *syncGroupTable
+
+	// Filesystem path to store metadata.
+	kvdbPath string
+
+	// Local device id.
+	id DeviceId
+	// Handle to the server to publish names to mount tables during syncgroup create/join.
+	server rpc.Server
+	// Map to track the set of names already published for this sync service.
+	names map[string]struct{}
+	// Relative name of the sync service to be advertised to the syncgroup server.
+	// This is built off the local device id.
+	relName string
+
+	// Authorizer for sync service.
+	auth security.Authorizer
+
+	// RWlock to concurrently access log and device table data structures.
+	lock sync.RWMutex
+
+	// Mutex to serialize syncgroup operations and an going
+	// initiation round. If both "lock" and "sgOp" need to be
+	// acquired, sgOp must be acquired first.
+	sgOp sync.Mutex
+
+	// Channel used by Sync responders to notify the SyncGroup refresher
+	// to fetch from SyncGroupServer updated info for some SyncGroups.
+	// sgRefresh chan *refreshRequest
+
+	// State to coordinate shutting down all spawned goroutines.
+	pending sync.WaitGroup
+	closed  chan struct{}
+
+	// Handlers for goroutine procedures.
+	// hdlGC        *syncGC
+	hdlWatcher   *syncWatcher
+	hdlInitiator *syncInitiator
+
+	// The context used for background activities.
+	ctx *context.T
+}
+
+func (o ObjId) String() string {
+	return string(o)
+}
+
+func (o ObjId) IsValid() bool {
+	return o != NoObjId
+}
+
+func init() {
+	rng = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+}
+
+func NewVersion() Version {
+	for {
+		if v := Version(rng.Int63()); v != NoVersion {
+			return v
+		}
+	}
+}
+
+// NewSyncd creates a new syncd instance.
+//
+// Syncd concurrency: syncd initializes three goroutines at
+// startup. The "watcher" thread is responsible for watching the store
+// for changes to its objects. The "initiator" thread is responsible
+// for periodically checking the neighborhood and contacting a peer to
+// obtain changes from that peer. The "gc" thread is responsible for
+// periodically checking if any log records and dag state can be
+// pruned. All these 3 threads perform write operations to the data
+// structures, and synchronize by acquiring a write lock on s.lock. In
+// addition, when syncd receives an incoming RPC, it responds to the
+// request by acquiring a read lock on s.lock. Thus, at any instant in
+// time, either one of the watcher, initiator or gc threads is active,
+// or any number of responders can be active, serving incoming
+// requests. Fairness between these threads follows from
+// sync.RWMutex. The spec says that the writers cannot be starved by
+// the readers but it does not guarantee FIFO. We may have to revisit
+// this in the future.
+func NewSyncd(devid string, syncTick time.Duration, server rpc.Server, ctx *context.T) *syncd {
+	return newSyncdCore(devid, syncTick, server, ctx)
+}
+
+// newSyncdCore is the internal function that creates the Syncd
+// structure and initilizes its thread (goroutines).  It takes a
+// Veyron Store parameter to separate the core of Syncd setup from the
+// external dependency on Veyron Store.
+func newSyncdCore(devid string, syncTick time.Duration,
+	server rpc.Server, ctx *context.T) *syncd {
+	// TODO(kash): Get this to compile
+	return nil
+	// s := &syncd{ctx: ctx}
+
+	// storePath := "/tmp"
+
+	// // If no authorizer is specified, allow access to all Principals.
+	// if s.auth = vflag.NewAuthorizerOrDie(); s.auth == nil {
+	// 	s.auth = allowEveryone{}
+	// }
+
+	// // Bootstrap my own DeviceId.
+	// s.id = DeviceId(devid)
+
+	// // Cache the server to publish names if required.
+	// s.server = server
+	// s.relName = devIDToName(s.id)
+	// s.names = make(map[string]struct{})
+
+	// var err error
+	// // Log init.
+	// if s.log, err = openILog(storePath+"/vsync_ilog.db", s); err != nil {
+	// 	vlog.Fatalf("newSyncd: openILog failed: err %v", err)
+	// }
+
+	// // DevTable init.
+	// if s.devtab, err = openDevTable(storePath+"/vsync_dtab.db", s); err != nil {
+	// 	vlog.Fatalf("newSyncd: openDevTable failed: err %v", err)
+	// }
+
+	// // Dag init.
+	// if s.dag, err = openDAG(storePath + "/vsync_dag.db"); err != nil {
+	// 	vlog.Fatalf("newSyncd: OpenDag failed: err %v", err)
+	// }
+
+	// // SyncGroupTable init.
+	// if s.sgtab, err = openSyncGroupTable(storePath + "/vsync_sgtab.db"); err != nil {
+	// 	vlog.Fatalf("newSyncd: openSyncGroupTable failed: err %v", err)
+	// }
+
+	// // Channel for responders to notify the SyncGroup refresher.
+	// // Give the channel some buffering to ease a burst of responders
+	// // while the refresher is still finishing a previous request.
+	// // s.sgRefresh = make(chan *refreshRequest, 4096)
+
+	// // Channel to propagate close event to all threads.
+	// s.closed = make(chan struct{})
+
+	// s.pending.Add(2)
+
+	// // Get deltas every peerSyncInterval.
+	// s.hdlInitiator = newInitiator(s, syncTick)
+	// go s.hdlInitiator.contactPeers()
+
+	// // // Garbage collect every garbageCollectInterval.
+	// // s.hdlGC = newGC(s)
+	// // go s.hdlGC.garbageCollect()
+
+	// // Start a watcher thread that will get updates from local store.
+	// s.hdlWatcher = newWatcher(s)
+	// go s.hdlWatcher.watchStore()
+
+	// // Start a thread to refresh SyncGroup info from SyncGroupServer.
+	// // go s.syncGroupRefresher()
+
+	// // Server stats.
+	// stats.NewString(statsKvdbPath).Set(s.kvdbPath)
+	// stats.NewString(statsDevId).Set(string(s.id))
+
+	// return s
+}
+
+// devIDToName converts a device ID to its relative name.
+func devIDToName(id DeviceId) string {
+	return naming.Join([]string{"global", "vsync", string(id)}...)
+}
+
+// nameToDevId extracts the DeviceId from a name.
+func (s *syncd) nameToDevId(name string) DeviceId {
+	parts := strings.Split(name, "/")
+	return DeviceId(parts[len(parts)-1])
+}
+
+// Close cleans up syncd state.
+func (s *syncd) Close() {
+	close(s.closed)
+	s.pending.Wait()
+
+	stats.Delete(statsKvdbPath)
+	stats.Delete(statsDevId)
+
+	// TODO(hpucha): close without flushing.
+}
+
+// isSyncClosing returns true if Close() was called i.e. the "closed" channel is closed.
+func (s *syncd) isSyncClosing() bool {
+	select {
+	case <-s.closed:
+		return true
+	default:
+		return false
+	}
+}
+
+// GetDeltas responds to the incoming request from a client by sending missing generations to the client.
+func (s *syncd) GetDeltas(call SyncGetDeltasServerCall, in map[ObjId]GenVector,
+	syncGroups map[ObjId]GroupIdSet, clientID DeviceId) (map[ObjId]GenVector, error) {
+
+	vlog.VI(1).Infof("GetDeltas:: Received vector %v from client %s", in, clientID)
+
+	// Handle misconfiguration: the client cannot have the same ID as this node.
+	if clientID == s.id {
+		vlog.VI(1).Infof("GetDeltas:: impostor alert: client ID %s is the same as mine %s", clientID, s.id)
+		return nil, fmt.Errorf("impostor: cannot be %s, for this node is %s", clientID, s.id)
+	}
+
+	out := make(map[ObjId]GenVector)
+	for sr, gvIn := range in {
+		gvOut, gens, gensInfo, err := s.prepareGensToReply(call, sr, syncGroups[sr], gvIn)
+		if err != nil {
+			vlog.Errorf("GetDeltas:: prepareGensToReply for sr %s failed with err %v",
+				sr.String(), err)
+			continue
+		}
+
+		for pos, v := range gens {
+			gen := gensInfo[pos]
+			var count uint64
+			for i := SeqNum(0); i <= gen.MaxSeqNum; i++ {
+				count++
+				rec, err := s.getLogRec(v.devID, v.srID, v.genID, i)
+				if err != nil {
+					vlog.Fatalf("GetDeltas:: Couldn't get log record %s %s %d %d, err %v",
+						v.devID, v.srID.String(), v.genID, i, err)
+				}
+				vlog.VI(1).Infof("Sending log record %v", rec)
+				if err := call.SendStream().Send(*rec); err != nil {
+					vlog.Errorf("GetDeltas:: Couldn't send stream err: %v", err)
+					return nil, err
+				}
+			}
+			if count != gen.Count {
+				vlog.Fatalf("GetDeltas:: GenMetadata has incorrect log records for generation %s %s %d %v",
+					v.devID, v.srID.String(), v.genID, gen)
+			}
+		}
+		out[sr] = gvOut
+	}
+
+	// Notify the SyncGroup Refresher about the SyncGroup IDs associated
+	// with the SyncRoots in the incoming request from the client.
+	// Note: the initial slice capacity is only a lower-bound.
+	// Note: by using the SyncRoots from the incoming "in" map and all
+	// all SyncGroup IDs for each SyncRoot, the refresh is eager because
+	// it does not filter out SyncRoots and SyncGroup IDs by the Permissions.
+	// sgIDs := make([]syncgroup.Id, 0, len(in))
+	// for sr := range in {
+	//	for _, id := range syncGroups[sr] {
+	//		sgIDs = append(sgIDs, id)
+	//	}
+	// }
+	// s.notifySyncGroupRefresher(devIDToName(clientID), sgIDs)
+
+	return out, nil
+}
+
+// // updateDeviceInfo updates the remote device's information based on
+// // the incoming GetDeltas request.
+// func (s *syncd) updateDeviceInfo(ClientID DeviceId, In GenVector) error {
+//	s.lock.Lock()
+//	defer s.lock.Unlock()
+
+// // Note that the incoming client generation vector cannot be
+// // used for garbage collection. We can only garbage collect
+// // based on the generations we receive from other
+// // devices. Receiving a set of generations assures that all
+// // updates branching from those generations are also received
+// // and hence generations present on all devices can be
+// // GC'ed. This function sends generations to other devices and
+// // hence does not use the generation vector for GC.
+// //
+// // TODO(hpucha): Cache the client's incoming generation vector
+// // to assist in tracking missing generations and hence next
+// // peer to contact.
+//	if !s.devtab.hasDevInfo(ClientID) {
+//		if err := s.devtab.addDevice(ClientID); err != nil {
+//			return err
+//		}
+//	}
+//	return nil
+// }
+
+// prepareGensToReply verifies if the requestor has access to the
+// requested syncroot. If allowed, it processes the incoming
+// generation vector and returns the metadata of all the missing
+// generations between the incoming and the local generation vector
+// for that syncroot.
+func (s *syncd) prepareGensToReply(call rpc.ServerCall, srid ObjId, sgVec GroupIdSet, In GenVector) (GenVector, []*genOrder, []*genMetadata, error) {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+
+	// Respond to the caller if any of the syncgroups
+	// corresponding to the syncroot allow access.
+	var allowedErr error
+	// for _, sg := range sgVec {
+	// 	// Check permissions for the syncgroup.
+	// 	sgData, err := s.sgtab.getSyncGroupByID(sg)
+	// 	if err != nil {
+	// 		return GenVector{}, nil, nil, err
+	// 	}
+	// 	if vlog.V(2) {
+	// 		b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	// 		vlog.Infof("prepareGensToReply:: matching acl %v, remote names %v", sgData.SrvInfo.Config.Permissions, b)
+	// 	}
+	// 	// TODO(ashankar): Consider changing TaggedPermissionsAuthorizer so that it doesn't return an error -
+	// 	// instead of an error, it just locks down access completely. The error condition is really
+	// 	// really rare and doing so will make for shorter code?
+	// 	auth, err := access.TaggedPermissionsAuthorizer(sgData.SrvInfo.Config.Permissions, access.TypicalTagType())
+	// 	if err != nil {
+	// 		vlog.Errorf("unable to create authorizer: %v", err)
+	// 		allowedErr = fmt.Errorf("server error: authorization policy misconfigured")
+	// 	} else if allowedErr = auth.Authorize(ctx); allowedErr == nil {
+	// 		break
+	// 	}
+	// }
+	if allowedErr != nil {
+		return GenVector{}, nil, nil, allowedErr
+	}
+
+	// // TODO(hpucha): Need only for GC.
+	// if err := s.updateDeviceInfo(ClientID, gvIn); err != nil {
+	//	vlog.Fatalf("GetDeltas:: updateDeviceInfo failed with err %v", err)
+	// }
+
+	// Get local generation vector.
+	out, err := s.devtab.getGenVec(s.id, srid)
+	if err != nil {
+		return GenVector{}, nil, nil, err
+	}
+
+	// Diff the two generation vectors.
+	gens, err := s.devtab.diffGenVectors(out, In, srid)
+	if err != nil {
+		return GenVector{}, nil, nil, err
+	}
+
+	// Get the metadata for all the generations in the reply.
+	gensInfo := make([]*genMetadata, len(gens))
+	for pos, v := range gens {
+		gen, err := s.log.getGenMetadata(v.devID, v.srID, v.genID)
+		if err != nil || gen.Count <= 0 {
+			return GenVector{}, nil, nil, err
+		}
+		gensInfo[pos] = gen
+	}
+
+	return out, gens, gensInfo, nil
+}
+
+// getLogRec gets the log record for a given generation and lsn.
+func (s *syncd) getLogRec(dev DeviceId, srid ObjId, gen GenId, lsn SeqNum) (*LogRec, error) {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	return s.log.getLogRec(dev, srid, gen, lsn)
+}
+
+// // GetSyncGroupStats gives the client high-level information on all SyncGroups.
+// // The streamed information is returned sorted by SyncGroup name.
+// func (s *syncd) GetSyncGroupStats(ctx SyncGetSyncGroupStatsContext) error {
+// 	// Get a snapshot of the SyncGroup names.
+// 	// The names are a copy, OK to unlock here.
+// 	s.lock.RLock()
+// 	names, err := s.sgtab.getAllSyncGroupNames()
+// 	s.lock.RUnlock()
+
+// 	if err != nil {
+// 		vlog.Errorf("GetSyncGroupStats:: cannot get SyncGroup names: %v", err)
+// 		return err
+// 	}
+// 	if len(names) == 0 {
+// 		return nil
+// 	}
+
+// 	// Send the SyncGroup stats in the stream in sorted order.
+// 	// Skip SyncGroups that got deleted in the meanwhile.
+// 	sort.Strings(names)
+// 	for _, name := range names {
+// 		// The SyncGroup object is a copy, OK to unlock here.
+// 		s.lock.RLock()
+// 		sg, err := s.sgtab.getSyncGroupByName(name)
+// 		s.lock.RUnlock()
+
+// 		if err != nil {
+// 			continue // skip deleted SyncGroup
+// 		}
+
+// 		stats := SyncGroupStats{
+// 			Name:       name,
+// 			SGObjId:      sg.SrvInfo.SGObjId,
+// 			Path:       sg.LocalPath,
+// 			RootObjId:    ObjId(sg.SrvInfo.RootObjId),
+// 			NumJoiners: uint32(len(sg.SrvInfo.Joiners)),
+// 		}
+
+// 		if err = ctx.SendStream().Send(stats); err != nil {
+// 			vlog.Errorf("GetSyncGroupStats:: cannot send stream data: %v", err)
+// 			return err
+// 		}
+// 	}
+
+// 	return nil
+// }
+
+// // GetSyncGroupMembers gives the client information on SyncGroup members.
+// // If SyncGroup names are specified, only their member information is sent.
+// // Otherwise information on members from all SyncGroups is sent.
+// func (s *syncd) GetSyncGroupMembers(ctx SyncGetSyncGroupMembersContext, sgNames []string) error {
+
+// 	// If SyncGroup names are given, fetch their IDs.
+// 	// Also get a snapshot of the SyncGroup members.
+// 	wantedGroupIds := make(map[syncgroup.Id]bool)
+
+// 	s.lock.RLock()
+// 	for _, name := range sgNames {
+// 		sgid, err := s.sgtab.getSyncGroupID(name)
+// 		if err != nil {
+// 			s.lock.RUnlock()
+// 			vlog.Errorf("GetSyncGroupMembers:: invalid SyncGroup %s: %v", name, err)
+// 			return err
+// 		}
+// 		wantedGroupIds[sgid] = true
+// 	}
+
+// 	memberMap, err := s.sgtab.getMembers() // The map is a copy, OK to unlock here.
+// 	s.lock.RUnlock()
+
+// 	if err != nil {
+// 		vlog.Errorf("GetSyncGroupMembers:: cannot get members: %v", err)
+// 		return err
+// 	}
+// 	if len(memberMap) == 0 {
+// 		return nil
+// 	}
+
+// 	// Send the member information in the stream in sorted order.
+// 	// Skip members that left in the meanwhile.
+// 	// If SyncGroup names are given, only return members in these groups.
+// 	members := make([]string, 0, len(memberMap))
+// 	for member := range memberMap {
+// 		members = append(members, member)
+// 	}
+// 	sort.Strings(members)
+
+// 	for _, member := range members {
+// 		// The member info points to in-memory data; cannot unlock
+// 		// while accessing that info.
+// 		s.lock.RLock()
+// 		info, err := s.sgtab.getMemberInfo(member)
+// 		if err != nil {
+// 			s.lock.RUnlock()
+// 			continue // skip member no longer there
+// 		}
+
+// 		replyData := make([]SyncGroupMember, 0, len(info.gids))
+// 		for sgid, meta := range info.gids {
+// 			if len(wantedGroupIds) > 0 && !wantedGroupIds[sgid] {
+// 				continue // skip unwanted SyncGroups
+// 			}
+
+// 			data := SyncGroupMember{
+// 				Name:     member,
+// 				SGObjId:    sgid,
+// 				Metadata: meta.metaData,
+// 			}
+// 			replyData = append(replyData, data)
+// 		}
+
+// 		s.lock.RUnlock()
+
+// 		for _, data := range replyData {
+// 			if err = ctx.SendStream().Send(data); err != nil {
+// 				vlog.Errorf("GetSyncGroupMembers:: cannot send stream data: %v", err)
+// 				return err
+// 			}
+// 		}
+// 	}
+
+// 	return nil
+// }
+
+// Dump writes to the Sync log internal information used for debugging.
+func (s *syncd) Dump(call rpc.ServerCall) error {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+
+	vlog.VI(1).Infof("Dump: ==== BEGIN ====")
+	s.devtab.dump()
+	// s.sgtab.dump()
+	s.log.dump()
+	s.dag.dump()
+	vlog.VI(1).Infof("Dump: ==== END ====")
+	return nil
+}
+
+// GetDeviceStats gives the client information on devices that have synchronized
+// with the server.
+func (s *syncd) GetDeviceStats(call SyncGetDeviceStatsServerCall) error {
+	// Get a snapshot of the device IDs.
+	// The device IDs are copies, OK to unlock here.
+	s.lock.RLock()
+	myID := s.id
+	devIDs, err := s.devtab.getDeviceIds()
+	s.lock.RUnlock()
+
+	if err != nil {
+		vlog.Errorf("GetDeviceStats:: cannot get device IDs: %v", err)
+		return err
+	}
+
+	// Send the device stats in the stream.
+	for _, id := range devIDs {
+		// The device info is a copy, OK to unlock here.
+		s.lock.RLock()
+		info, err := s.devtab.getDevInfo(id)
+		s.lock.RUnlock()
+
+		if err != nil {
+			continue // skip unknown devices
+		}
+
+		stats := DeviceStats{
+			DevId:      id,
+			LastSync:   info.Ts.Unix(),
+			GenVectors: info.Vectors,
+			IsSelf:     id == myID,
+		}
+
+		if err = call.SendStream().Send(stats); err != nil {
+			vlog.Errorf("GetDeviceStats:: cannot send stream data: %v", err)
+			return err
+		}
+	}
+
+	return nil
+}
+
+// GetObjectHistory gives the client the mutation history of a store object.
+// It also returns the object version that is at the "head" of the graph (DAG).
+func (s *syncd) GetObjectHistory(call SyncGetObjectHistoryServerCall, oid ObjId) (Version, error) {
+	// Get the object version numbers in reverse chronological order,
+	// starting from the "head" version back through its ancestors.
+	// Also get the DAG parents of each object version.
+	var versions []Version
+	parents := make(map[Version][]Version)
+
+	s.lock.RLock()
+	head, err := s.dag.getHead(oid)
+	if err != nil {
+		// For now ignore objects that do not yet have a "head" version.
+		// They are either not part of any SyncGroup, or this Sync server
+		// has not finished catching up (Sync or Watch operations).
+		vlog.Errorf("GetObjectHistory:: unknown object %v: %v", oid, err)
+		s.lock.RUnlock()
+		return NoVersion, err
+	}
+
+	s.dag.ancestorIter(oid, []Version{head},
+		func(oid ObjId, v Version, node *dagNode) error {
+			versions = append(versions, v)
+			parents[v] = node.Parents
+			return nil
+		})
+	s.lock.RUnlock()
+
+	// Stream the object log records in the given order.
+	for _, v := range versions {
+		// The log record is a copy, OK to unlock here.
+		s.lock.RLock()
+		rec, err := s.hdlInitiator.getLogRec(oid, v)
+		s.lock.RUnlock()
+
+		if err != nil {
+			vlog.Errorf("GetObjectHistory:: cannot get log record for %v:%v: %v", oid, v, err)
+			return NoVersion, err
+		}
+
+		// To give the client a full DAG, send the parents as seen in
+		// the DAG node instead of the ones in the log record.
+		// Note: DAG nodes contain the full sets of parents whereas
+		// log records may have subsets due to the different types of
+		// records: NodeRec-Parents + LinkRec-Parent == DAG-Parents.
+		rec.Parents = parents[v]
+
+		if err = call.SendStream().Send(*rec); err != nil {
+			vlog.Errorf("GetObjectHistory:: cannot send stream data: %v", err)
+			return NoVersion, err
+		}
+	}
+
+	return head, nil
+}
+
+// allowEveryone implements the authorization policy of ... allowing every principal.
+type allowEveryone struct{}
+
+func (allowEveryone) Authorize(security.Call) error { return nil }
diff --git a/services/syncbase/sync/watcher.go b/services/syncbase/sync/watcher.go
new file mode 100644
index 0000000..f9799ae
--- /dev/null
+++ b/services/syncbase/sync/watcher.go
@@ -0,0 +1,257 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Veyron Sync hook to the Store Watch API.  When applications update objects
+// in the local Veyron Store, Sync learns about these via the Watch API stream
+// of object mutations.  In turn, this Sync watcher thread updates the DAG and
+// ILog records to track the object change histories.
+
+import (
+	"time"
+
+	"v.io/v23/context"
+	"v.io/x/lib/vlog"
+)
+
+var (
+	// watchRetryDelay is how long the watcher waits before calling the Watch() RPC again
+	// after the previous call fails.
+	watchRetryDelay = 2 * time.Second
+	// streamRetryDelay is how long the watcher waits before creating a new Watch stream
+	// after the previous stream ends.
+	streamRetryDelay = 1 * time.Second
+)
+
+// syncWatcher contains the metadata for the Sync Watcher thread.
+type syncWatcher struct {
+	// syncd is a pointer to the Syncd instance owning this Watcher.
+	syncd *syncd
+}
+
+// newWatcher creates a new Sync Watcher instance attached to the given Syncd instance.
+func newWatcher(syncd *syncd) *syncWatcher {
+	return &syncWatcher{syncd: syncd}
+}
+
+func (w *syncWatcher) watchStreamCanceler(cancel context.CancelFunc, done <-chan struct{}) {
+	select {
+	case <-w.syncd.closed:
+		vlog.VI(1).Info("watchStreamCanceler: Syncd channel closed, cancel stream and exit")
+		cancel()
+	case <-done:
+		vlog.VI(1).Info("watchStreamCanceler: done, exit")
+	}
+}
+
+// watchStore consumes change records obtained by watching the store
+// for updates and applies them to the sync DBs.
+func (w *syncWatcher) watchStore() {
+	defer w.syncd.pending.Done()
+
+	// 	// If no Veyron store is configured, there is nothing to watch.
+	// 	if w.syncd.store == nil {
+	// 		vlog.VI(1).Info("watchStore: Veyron store not configured; skipping the watcher")
+	// 		return
+	// 	}
+
+	// 	// Get a Watch stream, process it, repeat till end-of-life.
+	// 	for {
+	// 		ctx, _ := vtrace.SetNewTrace(w.syncd.ctx)
+	// 		ctx, cancel := context.WithCancel(ctx)
+	// 		stream := w.getWatchStream(ctx)
+	// 		if stream == nil {
+	// 			return // Syncd is exiting.
+	// 		}
+
+	// 		// Spawn a goroutine to detect the Syncd "closed" channel and cancel the RPC stream
+	// 		// to unblock the watcher.  The "done" channel lets the watcher terminate the goroutine.
+	// 		go w.watchStreamCanceler(cancel, ctx.Done())
+
+	// 		// Process the stream of Watch updates until it closes (similar to "tail -f").
+	// 		w.processWatchStream(stream)
+
+	// 		if w.syncd.isSyncClosing() {
+	// 			return // Syncd is exiting.
+	// 		}
+
+	// 		stream.Finish()
+	// 		cancel()
+
+	// 		// TODO(rdaoud): we need a rate-limiter here in case the stream closes too quickly.
+	// 		// If the stream stays up long enough, no need to sleep before getting a new one.
+	// 		time.Sleep(streamRetryDelay)
+	// 	}
+}
+
+// // getWatchStream() returns a Watch API stream and handles retries if the Watch() call fails.
+// // If the stream is nil, it means Syncd is exiting cleanly and the caller should terminate.
+// func (w *syncWatcher) getWatchStream(ctx *context.T) watch.GlobWatcherWatchGlobCall {
+// 	for {
+// 		w.syncd.lock.RLock()
+// 		resmark := w.syncd.devtab.head.Resmark
+// 		w.syncd.lock.RUnlock()
+
+// 		req := raw.Request{}
+// 		if resmark != nil {
+// 			req.ResumeMarker = resmark
+// 		}
+
+// 		stream, err := w.syncd.store.Watch(ctx, req)
+// 		if err == nil {
+// 			return stream
+// 		}
+
+// 		if w.syncd.isSyncClosing() {
+// 			vlog.VI(1).Info("getWatchStream: exiting, Syncd closed its channel: ", err)
+// 			return nil
+// 		}
+
+// 		vlog.VI(1).Info("getWatchStream: call to Watch() failed, retrying a bit later: ", err)
+// 		time.Sleep(watchRetryDelay)
+// 	}
+// }
+
+// // processWatchStream reads the stream of Watch updates and applies the object mutations.
+// // Ideally this call does not return as the stream should be un-ending (like "tail -f").
+// // If the stream is closed, distinguish between the cases of end-of-stream vs Syncd canceling
+// // the stream to trigger a clean exit.
+// func (w *syncWatcher) processWatchStream(call watch.GlobWatcherWatchGlobCall) {
+// 	var txChanges []*types.Change = nil
+// 	var syncTime int64
+
+// 	stream := call.RecvStream()
+// 	for stream.Advance() {
+// 		change := stream.Value()
+
+// 		// Timestamp of the start of a batch of changes arriving at the Sync server.
+// 		if txChanges == nil {
+// 			syncTime = time.Now().UnixNano()
+// 		}
+// 		txChanges = append(txChanges, &change)
+
+// 		// Process the transaction when its last change record is received.
+// 		if !change.Continued {
+// 			if err := w.processTransaction(txChanges, syncTime); err != nil {
+// 				// TODO(rdaoud): don't crash, instead add retry policies to attempt some degree of
+// 				// self-healing from a data corruption where feasible, otherwise quarantine this device
+// 				// from the cluster and stop Syncd to avoid propagating data corruptions.
+// 				vlog.Fatal("processWatchStream:", err)
+// 			}
+// 			txChanges = nil
+// 		}
+// 	}
+
+// 	err := stream.Err()
+// 	if err == nil {
+// 		err = io.EOF
+// 	}
+
+// 	if w.syncd.isSyncClosing() {
+// 		vlog.VI(1).Info("processWatchStream: exiting, Syncd closed its channel: ", err)
+// 	} else {
+// 		vlog.VI(1).Info("processWatchStream: RPC stream error, re-issue Watch(): ", err)
+// 	}
+// }
+
+// // matchSyncRoot returns the SyncRoot (SyncGroup root ObjId) that matches the given object path.
+// // It traverses the object path looking if any of the IDs along the way are SyncRoots.
+// // If no match is found, it returns the invalid ID (zero).
+// // Note: the path IDs given by Watch are ordered: object, parent, grand-parent, etc.., root.
+// // As a result, this function returns the narrowest matching SyncGroup root ObjId.
+// func matchSyncRoot(objPathIDs []ObjId, sgObjIds map[ObjId]struct{}) ObjId {
+// 	for _, oid := range objPathIDs {
+// 		if _, ok := sgObjIds[oid]; ok {
+// 			return oid
+// 		}
+// 	}
+// 	return ObjId{}
+// }
+
+// // processTransaction applies a batch of changes (object mutations) that form a single transaction
+// // received from the Watch API.  The function grabs the write-lock to access the Log and DAG DBs.
+// func (w *syncWatcher) processTransaction(txBatch []*types.Change, syncTime int64) error {
+// 	w.syncd.lock.Lock()
+// 	defer w.syncd.lock.Unlock()
+
+// 	count := uint32(len(txBatch))
+// 	if count == 0 {
+// 		return nil
+// 	}
+
+// 	// Get the current set of SyncGroup Root ObjIds and use it to determine for each object
+// 	// the SyncGroup it belongs to, if any.
+// 	sgRootObjIds, err := w.syncd.sgtab.getAllSyncRoots()
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	// If the batch has more than one mutation, start a DAG transaction for it.
+// 	txID := NoTxId
+// 	if count > 1 {
+// 		txID = w.syncd.dag.addNodeTxStart(txID)
+// 		if txID == NoTxId {
+// 			return fmt.Errorf("failed to generate transaction ID")
+// 		}
+// 	}
+
+// 	vlog.VI(1).Infof("processTransaction: ready to process a batch of %d changes for transaction %v", count, txID)
+
+// 	for _, ch := range txBatch {
+// 		rc, ok := ch.Value.(*raw.RawChange)
+// 		if !ok {
+// 			return fmt.Errorf("invalid change value, not a raw change: %#v", ch)
+// 		}
+
+// 		mu := &rc.Mutation
+// 		isDeleted := ch.State == types.DoesNotExist
+
+// 		// Determine the SyncGroup root ObjId that matches this object, if any.
+// 		rootObjId := matchSyncRoot(rc.PathIDs, sgRootObjIds)
+
+// 		if rootObjId.IsValid() {
+// 			// The object matches a SyncGroup: process its watch record to track it in the Sync logs.
+// 			// All LogValues belonging to the same transaction get the same timestamp.
+// 			val := &LogValue{
+// 				Mutation: *mu,
+// 				SyncTime: syncTime,
+// 				Delete:   isDeleted,
+// 				TxId:     txID,
+// 				TxCount:  count,
+// 			}
+// 			vlog.VI(2).Infof("processTransaction: processing record %v, Tx %v", val, txID)
+
+// 			err = w.syncd.log.processWatchRecord(mu.Id, mu.Version, mu.PriorVersion, val, rootObjId)
+// 		} else {
+// 			// The object does not match any SyncGroup: stash it in the "private" table to save its info
+// 			// in case it becomes shared in the future, at which time it would be added to the Sync logs.
+// 			if isDeleted {
+// 				err = w.syncd.dag.delPrivNode(mu.Id)
+// 			} else {
+// 				priv := &privNode{Mutation: mu, PathIDs: rc.PathIDs, SyncTime: syncTime, TxId: txID, TxCount: count}
+// 				err = w.syncd.dag.setPrivNode(mu.Id, priv)
+// 			}
+// 		}
+
+// 		if err != nil {
+// 			return fmt.Errorf("cannot process mutation: %#v, mu %#v: %s", ch, mu, err)
+// 		}
+// 	}
+
+// 	// End the DAG transaction if any.
+// 	if txID != NoTxId {
+// 		// Note: if there are no shared objects in the transaction, the addNodeTxEnd() call
+// 		// does not persist the Tx state, it only cleans up the in-memory scaffolding.
+// 		// This is the desired behavior, Sync only needs to track the state of transactions
+// 		// that have at least one shared object.
+// 		if err := w.syncd.dag.addNodeTxEnd(txID, count); err != nil {
+// 			return err
+// 		}
+// 	}
+
+// 	// Update the device table with the new resume marker of the last record.
+// 	w.syncd.devtab.head.Resmark = txBatch[count-1].ResumeMarker
+// 	return nil
+// }
diff --git a/services/syncbase/syncbased/main.go b/services/syncbase/syncbased/main.go
new file mode 100644
index 0000000..e4f9c18
--- /dev/null
+++ b/services/syncbase/syncbased/main.go
@@ -0,0 +1,56 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// syncbased is a syncbase daemon.
+package main
+
+// Example invocation:
+// syncbased --veyron.tcp.address="127.0.0.1:0" --name=syncbased
+
+import (
+	"flag"
+
+	"v.io/v23"
+	"v.io/v23/security/access"
+	"v.io/x/lib/vlog"
+
+	"v.io/syncbase/x/ref/services/syncbase/server"
+	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
+	"v.io/x/ref/lib/signals"
+	_ "v.io/x/ref/profiles/roaming"
+)
+
+// TODO(sadovsky): Perhaps this should be one of the standard Veyron flags.
+var (
+	name = flag.String("name", "", "Name to mount at.")
+)
+
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatal("v23.NewServer() failed: ", err)
+	}
+	if _, err := s.Listen(v23.GetListenSpec(ctx)); err != nil {
+		vlog.Fatal("s.Listen() failed: ", err)
+	}
+
+	// TODO(sadovsky): Use a real Permissions.
+	service := server.NewService(memstore.New())
+	if err := service.Create(access.Permissions{}); err != nil {
+		vlog.Fatal("service.Create() failed: ", err)
+	}
+	d := server.NewDispatcher(service)
+
+	// Publish the service in the mount table.
+	if err := s.ServeDispatcher(*name, d); err != nil {
+		vlog.Fatal("s.ServeDispatcher() failed: ", err)
+	}
+	vlog.Info("Mounted at: ", *name)
+
+	// Wait forever.
+	<-signals.ShutdownOnSignals(ctx)
+}