github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/oplog/log.go (about)

     1  // Package oplog is an operation-based replicated data type of append-only logs
     2  // oplog has three main structures: logbook, log, and op
     3  // A log is a sequence of operations attributed to a single author, designated
     4  // by a private key.
     5  // an operation is a record of some action an author took. Applications iterate
     6  // the sequence of operations to produce the current state.
     7  // Logs can be arranged into hierarchies to form logical groupings.
     8  // A book contains an author's logs, both logs they've written as well as logs
     9  // replicated from other authors. Books are encrypted at rest using the author
    10  // private key.
    11  package oplog
    12  
    13  import (
    14  	"context"
    15  	"crypto/aes"
    16  	"crypto/cipher"
    17  	"crypto/md5"
    18  	"crypto/rand"
    19  	"encoding/base32"
    20  	"encoding/hex"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  
    25  	flatbuffers "github.com/google/flatbuffers/go"
    26  	crypto "github.com/libp2p/go-libp2p-core/crypto"
    27  	"github.com/qri-io/qri/logbook/oplog/logfb"
    28  	"golang.org/x/crypto/blake2b"
    29  )
    30  
    31  var (
    32  	// ErrNotFound is a sentinel error for data not found in a logbook
    33  	ErrNotFound = fmt.Errorf("log: not found")
    34  )
    35  
    36  // Logstore persists a set of operations organized into hierarchical append-only
    37  // logs
    38  type Logstore interface {
    39  	// MergeLog adds a Log to the store, controlling for conflicts
    40  	// * logs that are already known to the store are merged with a
    41  	//   longest-log-wins strategy, adding all descendants
    42  	// * new top level logs are appended to the store, including all descendants
    43  	// * attempting to add a log with a parent not already in the store MUST fail
    44  	//
    45  	// TODO (b5) - currently a Log pointer doesn't provide a clear method for
    46  	// getting the ID of it's parent, which negates the potential for attempting
    47  	// to merge child log, so we don't need to control for the third point quite
    48  	// yet
    49  	MergeLog(ctx context.Context, l *Log) error
    50  
    51  	// Remove a log from the store, all descendant logs must be removed as well
    52  	RemoveLog(ctx context.Context, names ...string) error
    53  
    54  	// Logs lists top level logs in the store, that is, the set logs that have no
    55  	// parent. passing -1 as a limit returns all top level logs after the offset
    56  	//
    57  	// The order of logs returned is up to the store, but the stored order must
    58  	// be deterministic
    59  	Logs(ctx context.Context, offset, limit int) (topLevel []*Log, err error)
    60  
    61  	// get a log according to a hierarchy of log.Name() references
    62  	// for example, fetching HeadRef(ctx, "foo", "bar", "baz") is a request
    63  	// for the log at the hierarchy foo/bar/baz:
    64  	//   foo
    65  	//     bar
    66  	//       baz
    67  	//
    68  	// HeadRef must return ErrNotFound if any name in the heirarchy is  missing
    69  	// from the store
    70  	// Head references are mutated by adding operations to a log that modifies
    71  	// the name of the initialization model, which means names are not a
    72  	// persistent identifier
    73  	//
    74  	// HeadRef MAY return children of a log. If the returned log.Log value is
    75  	// populated, it MUST contain all children of the log.
    76  	// use Logstore.ChildrenĀ or Logstore.Descendants to populate missing children
    77  	HeadRef(ctx context.Context, names ...string) (*Log, error)
    78  
    79  	// get a log according to it's ID string
    80  	// Log must return ErrNotFound if the ID does not exist
    81  	//
    82  	// Log MAY return children of a log. If the returned log.Log value is
    83  	// populated, it MUST contain all children of the log.
    84  	// use Logstore.ChildrenĀ or Logstore.Descendants to populate missing children
    85  	Get(ctx context.Context, id string) (*Log, error)
    86  
    87  	// GetAuthorID fetches the first log that matches the given model and
    88  	// authorID. Note that AutorID is the initial operation AuthorID field,
    89  	// not the init identifier
    90  	GetAuthorID(ctx context.Context, model uint32, authorID string) (*Log, error)
    91  
    92  	// get the immediate descendants of a log, using the given log as an outparam.
    93  	// Children must only mutate Logs field of the passed-in log pointer
    94  	// added Children MAY include decendant logs
    95  	Children(ctx context.Context, l *Log) error
    96  	// get all generations of a log, using the given log as an outparam
    97  	// Descendants MUST only mutate Logs field of the passed-in log pointer
    98  	Descendants(ctx context.Context, l *Log) error
    99  
   100  	// ReplaceAll replaces the contents of the entire log
   101  	ReplaceAll(ctx context.Context, l *Log) error
   102  }
   103  
   104  // SparseAncestorsAllDescendantsLogstore is a an extension interface to
   105  // Logstore with an optimized method for getting a log with sparse parents and
   106  // all descendants
   107  type SparseAncestorsAllDescendantsLogstore interface {
   108  	Logstore
   109  	// GetSparseAncestorsAllDescendants is a fast-path method for getting a
   110  	// log that includes sparse parents & complete descendants. "sparse parents"
   111  	// have the only children given in parent specified
   112  	// AllDescendants include
   113  	// the returned log will match the ID of the request, with parents
   114  	GetSparseAncestorsAllDescendants(ctx context.Context, id string) (*Log, error)
   115  }
   116  
   117  // GetWithSparseAncestorsAllDescendants is a fast-path method for getting a
   118  // log that includes sparse parents & complete descendants. "sparse parents"
   119  // means the only children given in parent
   120  // the returned log will match the ID of the request, with parents
   121  func GetWithSparseAncestorsAllDescendants(ctx context.Context, store Logstore, id string) (*Log, error) {
   122  	if sparseLS, ok := store.(SparseAncestorsAllDescendantsLogstore); ok {
   123  		return sparseLS.GetSparseAncestorsAllDescendants(ctx, id)
   124  	}
   125  
   126  	l, err := store.Get(ctx, id)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	// TODO (b5) - this is error prone. logs may exist but not be fetched. This
   132  	// check is here now because not all implementations support the Descendants
   133  	// method properly.
   134  	// in the future remove this check once all implementations we know of have
   135  	// a working `Descendants` implementation
   136  	if len(l.Logs) == 0 {
   137  		if err = store.Descendants(ctx, l); err != nil {
   138  			return nil, err
   139  		}
   140  	}
   141  
   142  	cursor := l
   143  	for cursor.ParentID != "" {
   144  		parent := cursor.parent
   145  		if parent == nil {
   146  			if parent, err = store.Get(ctx, cursor.ParentID); err != nil {
   147  				return nil, err
   148  			}
   149  		}
   150  
   151  		// TODO (b5) - hack to carry signatures possibly stored on the child
   152  		// this is incorrect long term, but has the effect of producing correct
   153  		// signed logs. Need to switch to log-specific signatures ASAP
   154  		if parent.Signature == nil {
   155  			parent.Signature = cursor.Signature
   156  		}
   157  
   158  		cursor.parent = &Log{
   159  			ParentID:  parent.ParentID,
   160  			Ops:       parent.Ops,
   161  			Logs:      []*Log{cursor},
   162  			Signature: parent.Signature,
   163  		}
   164  		cursor = cursor.parent
   165  	}
   166  
   167  	return l, nil
   168  }
   169  
   170  // AuthorLogstore describes a store owned by a single author, it adds encryption
   171  // methods for safe local persistence as well as owner ID accessors
   172  type AuthorLogstore interface {
   173  	// All AuthorLogstores are Logstores
   174  	Logstore
   175  	// marshals all logs to a slice of bytes encrypted with the given private key
   176  	FlatbufferCipher(pk crypto.PrivKey) ([]byte, error)
   177  	// decrypt flatbuffer bytes, re-hydrating the store
   178  	UnmarshalFlatbufferCipher(ctx context.Context, pk crypto.PrivKey, ciphertext []byte) error
   179  }
   180  
   181  // Journal is a store of logs known to a single author, representing their
   182  // view of an abstract dataset graph. journals live in memory by default, and
   183  // can be encrypted for storage
   184  type Journal struct {
   185  	logs []*Log
   186  }
   187  
   188  // assert at compile time that a Journal pointer is an AuthorLogstore
   189  var _ AuthorLogstore = (*Journal)(nil)
   190  
   191  // MergeLog adds a log to the journal
   192  func (j *Journal) MergeLog(ctx context.Context, incoming *Log) error {
   193  	if incoming.ID() == "" {
   194  		return fmt.Errorf("oplog: log ID cannot be empty")
   195  	}
   196  
   197  	// Find a log with a matching id
   198  	found, err := j.Get(ctx, incoming.ID())
   199  	if err == nil {
   200  		// If found, merge it
   201  		found.Merge(incoming)
   202  		return nil
   203  	} else if !errors.Is(err, ErrNotFound) {
   204  		// Okay if log is not found by id, but any other error should be returned
   205  		return err
   206  	}
   207  
   208  	// Find a user log with a matching profileID
   209  	for _, lg := range j.logs {
   210  		if lg.FirstOpAuthorID() == incoming.FirstOpAuthorID() {
   211  			found = lg
   212  			found.Merge(incoming)
   213  			return nil
   214  		}
   215  	}
   216  
   217  	// Append to the end
   218  	j.logs = append(j.logs, incoming)
   219  	return nil
   220  }
   221  
   222  // RemoveLog removes a log from the journal
   223  // TODO (b5) - this currently won't work when trying to remove the root log
   224  func (j *Journal) RemoveLog(ctx context.Context, names ...string) error {
   225  	if len(names) == 0 {
   226  		return fmt.Errorf("name is required")
   227  	}
   228  
   229  	remove := names[len(names)-1]
   230  	parentPath := names[:len(names)-1]
   231  
   232  	if len(parentPath) == 0 {
   233  		for i, l := range j.logs {
   234  			if l.Name() == remove {
   235  				j.logs = append(j.logs[:i], j.logs[i+1:]...)
   236  				return nil
   237  			}
   238  		}
   239  		return ErrNotFound
   240  	}
   241  
   242  	parent, err := j.HeadRef(ctx, parentPath...)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	// iterate list looking for log to remove
   248  	for i, l := range parent.Logs {
   249  		if l.Name() == remove {
   250  			parent.Logs = append(parent.Logs[:i], parent.Logs[i+1:]...)
   251  			return nil
   252  		}
   253  	}
   254  
   255  	return ErrNotFound
   256  }
   257  
   258  // Get fetches a log for a given ID
   259  func (j *Journal) Get(_ context.Context, id string) (*Log, error) {
   260  	for _, lg := range j.logs {
   261  		if l, err := lg.Log(id); err == nil {
   262  			return l, nil
   263  		}
   264  	}
   265  	return nil, ErrNotFound
   266  }
   267  
   268  // GetAuthorID fetches the first log that matches the given model and authorID
   269  func (j *Journal) GetAuthorID(_ context.Context, model uint32, authorID string) (*Log, error) {
   270  	for _, lg := range j.logs {
   271  		// NOTE: old logbook entries erroneously used logbook identifiers in the AuthorID
   272  		// space when they should have been using external author Identifiers. In the short
   273  		// term we're relying on the fact that the 0th operation always uses an external
   274  		// identifier
   275  		if lg.Model() == model && lg.FirstOpAuthorID() == authorID {
   276  			return lg, nil
   277  		}
   278  	}
   279  	return nil, fmt.Errorf("getting log by author ID %q %w", authorID, ErrNotFound)
   280  }
   281  
   282  // HeadRef traverses the log graph & pulls out a log based on named head
   283  // references
   284  // HeadRef will not return logs that have been marked as removed. To fetch
   285  // removed logs either traverse the entire journal or reference a log by ID
   286  func (j *Journal) HeadRef(_ context.Context, names ...string) (*Log, error) {
   287  	if len(names) == 0 {
   288  		return nil, fmt.Errorf("name is required")
   289  	}
   290  
   291  	for _, log := range j.logs {
   292  		if log.Name() == names[0] && !log.Removed() {
   293  			return log.HeadRef(names[1:]...)
   294  		}
   295  	}
   296  	return nil, ErrNotFound
   297  }
   298  
   299  // Logs returns the full map of logs keyed by model type
   300  func (j *Journal) Logs(ctx context.Context, offset, limit int) (topLevel []*Log, err error) {
   301  	// fast-path for no pagination
   302  	if offset == 0 && limit == -1 {
   303  		return j.logs[:], nil
   304  	}
   305  
   306  	return nil, fmt.Errorf("log subsets not finished")
   307  }
   308  
   309  // UnmarshalFlatbufferCipher decrypts and loads a flatbuffer ciphertext
   310  func (j *Journal) UnmarshalFlatbufferCipher(ctx context.Context, pk crypto.PrivKey, ciphertext []byte) error {
   311  	plaintext, err := j.decrypt(pk, ciphertext)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	return j.unmarshalFlatbuffer(logfb.GetRootAsBook(plaintext, 0))
   317  }
   318  
   319  // Children gets all descentants of a log, because logbook stores all
   320  // descendants in memory, children is a proxy for descenants
   321  func (j *Journal) Children(ctx context.Context, l *Log) error {
   322  	return j.Descendants(ctx, l)
   323  }
   324  
   325  // Descendants gets all descentants of a log & assigns the results to the given
   326  // Log parameter, setting only the Logs field
   327  func (j *Journal) Descendants(ctx context.Context, l *Log) error {
   328  	got, err := j.Get(ctx, l.ID())
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	l.Logs = got.Logs
   334  	return nil
   335  }
   336  
   337  // ReplaceAll replaces the entirety of the logs
   338  func (j *Journal) ReplaceAll(ctx context.Context, l *Log) error {
   339  	j.logs = []*Log{l}
   340  	return nil
   341  }
   342  
   343  // FlatbufferCipher marshals journal to a flatbuffer and encrypts the book using
   344  // a given private key. This same private key must be retained elsewhere to read
   345  // the flatbuffer later on
   346  func (j Journal) FlatbufferCipher(pk crypto.PrivKey) ([]byte, error) {
   347  	return j.encrypt(pk, j.flatbufferBytes())
   348  }
   349  
   350  func (j Journal) cipher(pk crypto.PrivKey) (cipher.AEAD, error) {
   351  	pkBytes, err := pk.Raw()
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	hasher := md5.New()
   356  	hasher.Write(pkBytes)
   357  	hash := hex.EncodeToString(hasher.Sum(nil))
   358  
   359  	block, err := aes.NewCipher([]byte(hash))
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	return cipher.NewGCM(block)
   364  }
   365  
   366  func (j Journal) encrypt(pk crypto.PrivKey, data []byte) ([]byte, error) {
   367  	gcm, err := j.cipher(pk)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	nonce := make([]byte, gcm.NonceSize())
   373  	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	ciphertext := gcm.Seal(nonce, nonce, data, nil)
   378  	return ciphertext, nil
   379  }
   380  
   381  func (j Journal) decrypt(pk crypto.PrivKey, data []byte) ([]byte, error) {
   382  	gcm, err := j.cipher(pk)
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  
   387  	nonceSize := gcm.NonceSize()
   388  	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
   389  	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	return plaintext, nil
   394  }
   395  
   396  // flatbufferBytes formats book as a flatbuffer byte slice
   397  func (j Journal) flatbufferBytes() []byte {
   398  	builder := flatbuffers.NewBuilder(0)
   399  	off := j.marshalFlatbuffer(builder)
   400  	builder.Finish(off)
   401  	return builder.FinishedBytes()
   402  }
   403  
   404  // note: currently doesn't marshal book.author, we're considering deprecating
   405  // the author field
   406  func (j Journal) marshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
   407  
   408  	setsl := j.logs
   409  	count := len(setsl)
   410  	offsets := make([]flatbuffers.UOffsetT, count)
   411  	for i, lset := range setsl {
   412  		offsets[i] = lset.MarshalFlatbuffer(builder)
   413  	}
   414  	logfb.BookStartLogsVector(builder, count)
   415  	for i := count - 1; i >= 0; i-- {
   416  		builder.PrependUOffsetT(offsets[i])
   417  	}
   418  	sets := builder.EndVector(count)
   419  
   420  	logfb.BookStart(builder)
   421  	logfb.BookAddLogs(builder, sets)
   422  	return logfb.BookEnd(builder)
   423  }
   424  
   425  func (j *Journal) unmarshalFlatbuffer(b *logfb.Book) error {
   426  	newBook := Journal{}
   427  
   428  	count := b.LogsLength()
   429  	lfb := &logfb.Log{}
   430  	for i := 0; i < count; i++ {
   431  		if b.Logs(lfb, i) {
   432  			l := &Log{}
   433  			if err := l.UnmarshalFlatbuffer(lfb, nil); err != nil {
   434  				return err
   435  			}
   436  			newBook.logs = append(newBook.logs, l)
   437  		}
   438  	}
   439  
   440  	*j = newBook
   441  	return nil
   442  }
   443  
   444  // Log is a causally-ordered set of operations performed by a single author.
   445  // log attribution is verified by an author's signature
   446  type Log struct {
   447  	name     string // name value cache. not persisted
   448  	authorID string // authorID value cache. not persisted
   449  	parent   *Log   // parent link
   450  
   451  	ParentID  string // init id of parent Log
   452  	Signature []byte
   453  	Ops       []Op
   454  	Logs      []*Log
   455  }
   456  
   457  // InitLog creates a Log from an initialization operation
   458  func InitLog(initop Op) *Log {
   459  	return &Log{
   460  		Ops: []Op{initop},
   461  	}
   462  }
   463  
   464  // FromFlatbufferBytes initializes a log from flatbuffer data
   465  func FromFlatbufferBytes(data []byte) (*Log, error) {
   466  	rootfb := logfb.GetRootAsLog(data, 0)
   467  	lg := &Log{}
   468  	return lg, lg.UnmarshalFlatbuffer(rootfb, nil)
   469  }
   470  
   471  // Append adds an operation to the log
   472  func (lg *Log) Append(op Op) {
   473  	if op.Model == lg.Model() {
   474  		if op.Name != "" {
   475  			lg.name = op.Name
   476  		}
   477  		if op.AuthorID != "" {
   478  			lg.authorID = op.AuthorID
   479  		}
   480  	}
   481  	lg.Ops = append(lg.Ops, op)
   482  }
   483  
   484  // ID returns the hash of the initialization operation
   485  // if the log is empty, returns the empty string
   486  func (lg Log) ID() string {
   487  	if len(lg.Ops) == 0 {
   488  		return ""
   489  	}
   490  	return lg.Ops[0].Hash()
   491  }
   492  
   493  // Head gets the latest operation in the log
   494  func (lg Log) Head() Op {
   495  	if len(lg.Ops) == 0 {
   496  		return Op{}
   497  	}
   498  	return lg.Ops[len(lg.Ops)-1]
   499  }
   500  
   501  // Model gives the operation type for a log, based on the first operation
   502  // written to the log. Logs can contain multiple models of operations, but the
   503  // first operation written to a log determines the kind of log for
   504  // catagorization purposes
   505  func (lg Log) Model() uint32 {
   506  	return lg.Ops[0].Model
   507  }
   508  
   509  // Author returns one of two different things: either the user's ProfileID,
   510  // or the has of the first Op for the UserLog, depending on if they have
   511  // ever changed their username.
   512  func (lg Log) Author() (identifier string) {
   513  	if lg.authorID == "" {
   514  		m := lg.Model()
   515  		for _, o := range lg.Ops {
   516  			if o.Model == m && o.AuthorID != "" {
   517  				lg.authorID = o.AuthorID
   518  			}
   519  		}
   520  	}
   521  	return lg.authorID
   522  }
   523  
   524  // FirstOpAuthorID returns the authorID of the first Op. For UserLog, this is ProfileID
   525  func (lg Log) FirstOpAuthorID() string {
   526  	m := lg.Model()
   527  	for _, o := range lg.Ops {
   528  		if o.Model == m && o.AuthorID != "" {
   529  			return o.AuthorID
   530  		}
   531  	}
   532  	return ""
   533  }
   534  
   535  // Parent returns this log's parent if one exists
   536  func (lg *Log) Parent() *Log {
   537  	return lg.parent
   538  }
   539  
   540  // Name returns the human-readable name for this log, determined by the
   541  // initialization event
   542  func (lg Log) Name() string {
   543  	if lg.name == "" {
   544  		m := lg.Model()
   545  		for _, o := range lg.Ops {
   546  			if o.Model == m && o.Name != "" {
   547  				lg.name = o.Name
   548  			}
   549  		}
   550  	}
   551  	return lg.name
   552  }
   553  
   554  // Removed returns true if the log contains a remove operation for the log model
   555  func (lg Log) Removed() bool {
   556  	m := lg.Model()
   557  	for _, op := range lg.Ops {
   558  		if op.Model == m && op.Type == OpTypeRemove {
   559  			return true
   560  		}
   561  	}
   562  	return false
   563  }
   564  
   565  // DeepCopy produces a fresh duplicate of this log
   566  func (lg *Log) DeepCopy() *Log {
   567  	lg.FlatbufferBytes()
   568  	cp := &Log{}
   569  	if err := cp.UnmarshalFlatbufferBytes(lg.FlatbufferBytes()); err != nil {
   570  		panic(err)
   571  	}
   572  	cp.ParentID = lg.ParentID
   573  	return cp
   574  }
   575  
   576  // Log fetches a log by ID, checking the current log and all descendants for an
   577  // exact match
   578  func (lg *Log) Log(id string) (*Log, error) {
   579  	if lg.ID() == id {
   580  		return lg, nil
   581  	}
   582  	if len(lg.Logs) > 0 {
   583  		for _, l := range lg.Logs {
   584  			if got, err := l.Log(id); err == nil {
   585  				return got, nil
   586  			}
   587  		}
   588  	}
   589  	return nil, ErrNotFound
   590  }
   591  
   592  // HeadRef returns a descendant log, traversing the log tree by name
   593  // HeadRef will not return logs that have been marked as removed. To fetch
   594  // removed logs either traverse the entire book or reference a log by ID
   595  func (lg *Log) HeadRef(names ...string) (*Log, error) {
   596  	if len(names) == 0 {
   597  		return lg, nil
   598  	}
   599  
   600  	for _, log := range lg.Logs {
   601  		if log.Name() == names[0] && !log.Removed() {
   602  			return log.HeadRef(names[1:]...)
   603  		}
   604  	}
   605  	return nil, ErrNotFound
   606  }
   607  
   608  // AddChild appends a log as a direct descendant of this log, controlling
   609  // for duplicates
   610  func (lg *Log) AddChild(l *Log) {
   611  	l.ParentID = lg.ID()
   612  	l.parent = lg
   613  	for i, ch := range lg.Logs {
   614  		if ch.ID() == l.ID() {
   615  			if len(l.Ops) > len(ch.Ops) {
   616  				lg.Logs[i] = l
   617  			}
   618  			return
   619  		}
   620  	}
   621  	lg.Logs = append(lg.Logs, l)
   622  }
   623  
   624  // Merge combines two logs that are assumed to be a shared root, combining
   625  // children from both branches, matching branches prefer longer Opsets
   626  // Merging relies on comparison of initialization operations, which
   627  // must be present to constitute a match
   628  func (lg *Log) Merge(l *Log) {
   629  	// if the incoming log has more operations, use it & clear the cache
   630  	if len(l.Ops) > len(lg.Ops) {
   631  		lg.Ops = l.Ops
   632  		lg.name = ""
   633  		lg.authorID = ""
   634  		lg.Signature = nil
   635  	}
   636  
   637  LOOP:
   638  	for _, x := range l.Logs {
   639  		for j, y := range lg.Logs {
   640  			// if logs match. merge 'em
   641  			if x.Ops[0].Equal(y.Ops[0]) {
   642  				lg.Logs[j].Merge(x)
   643  				continue LOOP
   644  			}
   645  		}
   646  		// no match, append!
   647  		lg.AddChild(x)
   648  	}
   649  }
   650  
   651  // Verify confirms that the signature for a log matches
   652  func (lg Log) Verify(pub crypto.PubKey) error {
   653  	ok, err := pub.Verify(lg.SigningBytes(), lg.Signature)
   654  	if err != nil {
   655  		return err
   656  	}
   657  	if !ok {
   658  		return fmt.Errorf("invalid signature")
   659  	}
   660  	return nil
   661  }
   662  
   663  // Sign assigns the log signature by signing the logging checksum with a given
   664  // private key
   665  // TODO (b5) - this is assuming the log is authored by this private key. as soon
   666  // as we add collaborators, this won't be true
   667  func (lg *Log) Sign(pk crypto.PrivKey) (err error) {
   668  	lg.Signature, err = pk.Sign(lg.SigningBytes())
   669  	if err != nil {
   670  		return err
   671  	}
   672  
   673  	return nil
   674  }
   675  
   676  // SigningBytes perpares a byte slice for signing from a log's operations
   677  func (lg Log) SigningBytes() []byte {
   678  	hasher := md5.New()
   679  	for _, op := range lg.Ops {
   680  		hasher.Write([]byte(op.Ref))
   681  	}
   682  	return hasher.Sum(nil)
   683  }
   684  
   685  // FlatbufferBytes marshals a log to flabuffer-formatted bytes
   686  func (lg Log) FlatbufferBytes() []byte {
   687  	builder := flatbuffers.NewBuilder(0)
   688  	log := lg.MarshalFlatbuffer(builder)
   689  	builder.Finish(log)
   690  	return builder.FinishedBytes()
   691  }
   692  
   693  // MarshalFlatbuffer writes log to a flatbuffer, returning the ending byte
   694  // offset
   695  func (lg Log) MarshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
   696  	// build logs bottom up, collecting offsets
   697  	logcount := len(lg.Logs)
   698  	logoffsets := make([]flatbuffers.UOffsetT, logcount)
   699  	for i, o := range lg.Logs {
   700  		logoffsets[i] = o.MarshalFlatbuffer(builder)
   701  	}
   702  
   703  	logfb.LogStartLogsVector(builder, logcount)
   704  	for i := logcount - 1; i >= 0; i-- {
   705  		builder.PrependUOffsetT(logoffsets[i])
   706  	}
   707  	logs := builder.EndVector(logcount)
   708  
   709  	name := builder.CreateString(lg.Name())
   710  	id := builder.CreateString(lg.Author())
   711  	signature := builder.CreateByteString(lg.Signature)
   712  
   713  	count := len(lg.Ops)
   714  	offsets := make([]flatbuffers.UOffsetT, count)
   715  	for i, o := range lg.Ops {
   716  		offsets[i] = o.MarshalFlatbuffer(builder)
   717  	}
   718  
   719  	logfb.LogStartOpsetVector(builder, count)
   720  	for i := count - 1; i >= 0; i-- {
   721  		builder.PrependUOffsetT(offsets[i])
   722  	}
   723  	ops := builder.EndVector(count)
   724  
   725  	logfb.LogStart(builder)
   726  	logfb.LogAddName(builder, name)
   727  	logfb.LogAddIdentifier(builder, id)
   728  	logfb.LogAddSignature(builder, signature)
   729  	logfb.LogAddOpset(builder, ops)
   730  	logfb.LogAddLogs(builder, logs)
   731  	return logfb.LogEnd(builder)
   732  }
   733  
   734  // UnmarshalFlatbufferBytes is a convenince wrapper to deserialze a flatbuffer
   735  // slice into a log
   736  func (lg *Log) UnmarshalFlatbufferBytes(data []byte) error {
   737  	return lg.UnmarshalFlatbuffer(logfb.GetRootAsLog(data, 0), nil)
   738  }
   739  
   740  // UnmarshalFlatbuffer populates a logfb.Log from a Log pointer
   741  func (lg *Log) UnmarshalFlatbuffer(lfb *logfb.Log, parent *Log) (err error) {
   742  	newLg := Log{parent: parent}
   743  	if parent != nil {
   744  		newLg.ParentID = parent.ID()
   745  	}
   746  
   747  	if len(lfb.Signature()) != 0 {
   748  		newLg.Signature = lfb.Signature()
   749  	}
   750  
   751  	newLg.Ops = make([]Op, lfb.OpsetLength())
   752  	opfb := &logfb.Operation{}
   753  	for i := 0; i < lfb.OpsetLength(); i++ {
   754  		if lfb.Opset(opfb, i) {
   755  			newLg.Ops[i] = UnmarshalOpFlatbuffer(opfb)
   756  		}
   757  	}
   758  
   759  	if lfb.LogsLength() > 0 {
   760  		newLg.Logs = make([]*Log, lfb.LogsLength())
   761  		childfb := &logfb.Log{}
   762  		for i := 0; i < lfb.LogsLength(); i++ {
   763  			if lfb.Logs(childfb, i) {
   764  				newLg.Logs[i] = &Log{}
   765  				newLg.Logs[i].UnmarshalFlatbuffer(childfb, lg)
   766  				newLg.Logs[i].ParentID = newLg.ID()
   767  			}
   768  		}
   769  	}
   770  
   771  	*lg = newLg
   772  	return nil
   773  }
   774  
   775  // OpType is the set of all kinds of operations, they are two bytes in length
   776  // OpType splits the provided byte in half, using the higher 4 bits for the
   777  // "category" of operation, and the lower 4 bits for the type of operation
   778  // within the category
   779  // the second byte is reserved for future use
   780  type OpType byte
   781  
   782  const (
   783  	// OpTypeInit is the creation of a model
   784  	OpTypeInit OpType = 0x01
   785  	// OpTypeAmend represents amending a model
   786  	OpTypeAmend OpType = 0x02
   787  	// OpTypeRemove represents deleting a model
   788  	OpTypeRemove OpType = 0x03
   789  )
   790  
   791  // Op is an operation, a single atomic unit in a log that describes a state
   792  // change
   793  type Op struct {
   794  	Type      OpType   // type of operation
   795  	Model     uint32   // data model to operate on
   796  	Ref       string   // identifier of data this operation is documenting
   797  	Prev      string   // previous reference in a causal history
   798  	Relations []string // references this operation relates to. usage is operation type-dependant
   799  	Name      string   // human-readable name for the reference
   800  	AuthorID  string   // identifier for author
   801  
   802  	Timestamp int64  // operation timestamp, for annotation purposes only
   803  	Size      int64  // size of the referenced value in bytes
   804  	Note      string // operation annotation for users. eg: commit title
   805  }
   806  
   807  // Equal tests equality between two operations
   808  func (o Op) Equal(b Op) bool {
   809  	return o.Type == b.Type &&
   810  		o.Model == b.Model &&
   811  		o.Ref == b.Ref &&
   812  		o.Prev == b.Prev &&
   813  		len(o.Relations) == len(b.Relations) &&
   814  		o.Name == b.Name &&
   815  		o.AuthorID == b.AuthorID &&
   816  		o.Timestamp == b.Timestamp &&
   817  		o.Size == b.Size &&
   818  		o.Note == b.Note
   819  }
   820  
   821  // Hash uses lower-case base32 encoding for id bytes for a few reasons:
   822  // * base64 uses the "/" character, which messes with paths
   823  // * can be used as URLS
   824  // * doesn't rely on case, which means it works in case-insensitive contexts
   825  // * lowercase is easier on the eyes
   826  //
   827  // we're intentionally *not* using multiformat CIDs here. ID's are not
   828  // identifiers of content stored wholly in an immutable filesystem, they're a
   829  // reference to the intialization operation in a history
   830  var base32Enc = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
   831  
   832  // Hash returns the base32-lowercase-encoded blake2b-256 hash of the Op flatbuffer
   833  func (o Op) Hash() string {
   834  	builder := flatbuffers.NewBuilder(0)
   835  	end := o.MarshalFlatbuffer(builder)
   836  	builder.Finish(end)
   837  	data := builder.FinishedBytes()
   838  	sum := blake2b.Sum256(data)
   839  	return base32Enc.EncodeToString(sum[:])
   840  }
   841  
   842  // MarshalFlatbuffer writes this operation to a flatbuffer, returning the
   843  // ending byte offset
   844  func (o Op) MarshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
   845  	ref := builder.CreateString(o.Ref)
   846  	prev := builder.CreateString(o.Prev)
   847  	name := builder.CreateString(o.Name)
   848  	authorID := builder.CreateString(o.AuthorID)
   849  	note := builder.CreateString(o.Note)
   850  
   851  	count := len(o.Relations)
   852  	offsets := make([]flatbuffers.UOffsetT, count)
   853  	for i, r := range o.Relations {
   854  		offsets[i] = builder.CreateString(r)
   855  	}
   856  
   857  	logfb.OperationStartRelationsVector(builder, count)
   858  	for i := count - 1; i >= 0; i-- {
   859  		builder.PrependUOffsetT(offsets[i])
   860  	}
   861  	rels := builder.EndVector(count)
   862  
   863  	logfb.OperationStart(builder)
   864  	logfb.OperationAddType(builder, logfb.OpType(o.Type))
   865  	logfb.OperationAddModel(builder, o.Model)
   866  	logfb.OperationAddRef(builder, ref)
   867  	logfb.OperationAddRelations(builder, rels)
   868  	logfb.OperationAddPrev(builder, prev)
   869  	logfb.OperationAddName(builder, name)
   870  	logfb.OperationAddAuthorID(builder, authorID)
   871  	logfb.OperationAddTimestamp(builder, o.Timestamp)
   872  	logfb.OperationAddSize(builder, o.Size)
   873  	logfb.OperationAddNote(builder, note)
   874  	return logfb.OperationEnd(builder)
   875  }
   876  
   877  // UnmarshalOpFlatbuffer creates an op from a flatbuffer operation pointer
   878  func UnmarshalOpFlatbuffer(o *logfb.Operation) Op {
   879  	op := Op{
   880  		Type:      OpType(byte(o.Type())),
   881  		Model:     o.Model(),
   882  		Timestamp: o.Timestamp(),
   883  		Ref:       string(o.Ref()),
   884  		Prev:      string(o.Prev()),
   885  		Name:      string(o.Name()),
   886  		AuthorID:  string(o.AuthorID()),
   887  		Size:      o.Size(),
   888  		Note:      string(o.Note()),
   889  	}
   890  
   891  	if o.RelationsLength() > 0 {
   892  		op.Relations = make([]string, o.RelationsLength())
   893  		for i := 0; i < o.RelationsLength(); i++ {
   894  			op.Relations[i] = string(o.Relations(i))
   895  		}
   896  	}
   897  
   898  	return op
   899  }