diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go new file mode 100644 index 0000000..7b8c419 --- /dev/null +++ b/logverification/app/appentry.go @@ -0,0 +1,369 @@ +package app + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + + "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/datatrails/go-datatrails-common/logger" + "github.com/datatrails/go-datatrails-merklelog/massifs" + "github.com/datatrails/go-datatrails-merklelog/mmr" + "github.com/google/uuid" +) + +/** + * AppEntry is the app provided data for a corresponding log entry. + * An AppEntry will derive fields used for log entry inclusion verification. + * + * The format for an MMR entry is the following: + * + * H( Domain | MMR Salt | Serialized Bytes) + * + * Where: + * * Domain - the hashing schema for the MMR Entry + * * MMR Salt - datatrails provided fields included in the MMR Entry (can be found in the corresponding Trie Value on the log) + * * Serialized Bytes - app (customer) provided fields in the MMR Entry, serialized in a consistent way. + * + * + * The format for a Trie Entry is the following: + * + * ( Trie Key | Trie Value ) + * + * Where the Trie Key is: + * + * H( Domain | LogId | AppId ) + * + * And Trie Value is: + * + * ( Extra Bytes | IdTimestamp ) + */ + +const ( + MMRSaltSize = 32 + + ExtraBytesSize = 24 + + IDTimestapSizeBytes = 8 +) + +// AppEntryGetter gets fields from the app entry or derives +// +// fields from the app entry. +type AppEntryGetter interface { + AppID() string + LogID() []byte + LogTenant() (string, error) + ExtraBytes() []byte + SerializedBytes() []byte + Domain() byte + + MMRIndex() uint64 + IDTimestamp() string + MMRSalt() ([]byte, error) + MMREntry() ([]byte, error) +} + +// AppEntryMassifGetter gets the massif for a specific app entry. +type AppEntryMassifGetter interface { + Massif(options ...MassifGetterOption) (*massifs.MassifContext, error) +} + +// AppEntryVerifier can be used to verify the inclusion of an app entry +// +// against its corresponding log entry. +type AppEntryVerifier interface { + Proof(options ...MassifGetterOption) ([][]byte, error) + VerifyProof(proof [][]byte, options ...MassifGetterOption) (bool, error) + VerifyInclusion(options ...MassifGetterOption) (bool, error) +} + +// VerifiableAppEntry includes all methods that could be needed for a verifiable app entry. +type VerifiableAppEntry interface { + AppEntryGetter + AppEntryMassifGetter + AppEntryVerifier +} + +// MMREntryFields are the fields that when hashed result in the MMR Entry +type MMREntryFields struct { + + // domain defines the hashing schema for the MMR Entry + domain byte + + // serializedBytes are app (customer) provided fields in the MMR Entry, serialized in a consistent way. + serializedBytes []byte +} + +// AppEntry is the app provided data for a corresponding log entry. +// +// It contains key information for verifying inclusion of the corresponding log entry. +// +// NOTE: all fields are sourced from the app data, or derived from it. +// NONE of the fields in an AppEntry are sourced from the log. +type AppEntry struct { + // appID is an identifier of the app committing the merkle log entry + appID string + + // logID is a uuid in byte form of the specific log identifier + logID []byte + + // extraBytes are extrabytes provided by datatrails for the specific app + extraBytes []byte + + // MMREntryFields used to determine the MMR Entry + mmrEntryFields *MMREntryFields + + // MerkleLogCommit used to define information about the log entry + merkleLogCommit *assets.MerkleLogCommit +} + +// NewAppEntry creates a new app entry entry +func NewAppEntry( + appId string, + logId []byte, + extraBytes []byte, + mmrEntryFields *MMREntryFields, + merklelogCommit *assets.MerkleLogCommit, +) *AppEntry { + + appEntry := &AppEntry{ + appID: appId, + logID: logId, + extraBytes: extraBytes, + mmrEntryFields: mmrEntryFields, + merkleLogCommit: merklelogCommit, + } + + return appEntry +} + +// MMREntry derives the mmr entry of the corresponding log entry from the app data. +// +// MMREntry is: +// - H( Domain | MMR Salt | Serialized Bytes) +func (ae *AppEntry) MMREntry() ([]byte, error) { + + hasher := sha256.New() + + // domain + hasher.Write([]byte{ae.mmrEntryFields.domain}) + + // mmr salt + mmrSalt, err := ae.MMRSalt() + if err != nil { + return nil, err + } + + hasher.Write(mmrSalt) + + // serialized bytes + hasher.Write(ae.mmrEntryFields.serializedBytes) + + return hasher.Sum(nil), nil + +} + +// MMRIndex gets the mmr index of the corresponding log entry. +func (ae *AppEntry) MMRIndex() uint64 { + + if ae.merkleLogCommit == nil { + return 0 + } + + return ae.merkleLogCommit.Index +} + +// IDTimestamp gets the idtimestamp of the corresponding log entry. +func (ae *AppEntry) IDTimestamp() string { + + if ae.merkleLogCommit == nil { + return "" + } + + return ae.merkleLogCommit.Idtimestamp +} + +// AppID gets the app id of the corresponding log entry. +func (ae *AppEntry) AppID() string { + return ae.appID +} + +// LogID gets the log id of the corresponding log entry. +func (ae *AppEntry) LogID() []byte { + return ae.logID +} + +// LogTenant returns the Log tenant that committed this app entry to the log +// as a tenant identity. +func (ae *AppEntry) LogTenant() (string, error) { + + logTenantUuid, err := uuid.FromBytes(ae.logID) + if err != nil { + return "", err + } + + return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil + +} + +// ExtraBytes gets the extrabytes of the corresponding log entry. +func (ae *AppEntry) ExtraBytes() []byte { + return ae.extraBytes +} + +// SerializedBytes gets the serialized bytes used to derive the mmr entry. +func (ae *AppEntry) SerializedBytes() []byte { + return ae.mmrEntryFields.serializedBytes +} + +// Domain gets the domain byte used to derive the mmr entry. +func (ae *AppEntry) Domain() byte { + return ae.mmrEntryFields.domain +} + +// MMRSalt derives the MMR Salt of the corresponding log entry from the app data. +// MMRSalt is the datatrails provided fields included on the MMR Entry. +// +// this is (extrabytes | idtimestamp) for any apps that adhere to log entry version 1. +func (ae *AppEntry) MMRSalt() ([]byte, error) { + + mmrSalt := make([]byte, MMRSaltSize) + + copy(mmrSalt[:ExtraBytesSize], ae.extraBytes) + + // get the byte representation of idtimestamp + idTimestamp, _, err := massifs.SplitIDTimestampHex(ae.merkleLogCommit.Idtimestamp) + if err != nil { + return nil, err + } + + idTimestampBytes := make([]byte, IDTimestapSizeBytes) + binary.BigEndian.PutUint64(idTimestampBytes, idTimestamp) + + copy(mmrSalt[ExtraBytesSize:], idTimestampBytes) + + return mmrSalt, nil +} + +/** Massif gets the massif context, for the massif of the corresponding log entry from the app data. + * + * The following massif options can be used, in priority order: + * - WithMassifContext + * - WithMassifReader + * - WithAzblobReader + * + * Example WithMassifReader: + * + * WithMassifReader( + * reader, + * WithMassifTenantId("tenant/foo"), + * WithMassifHeight(14), + * ) + */ +func (ae *AppEntry) Massif(options ...MassifGetterOption) (*massifs.MassifContext, error) { + + massifOptions := ParseMassifGetterOptions(options...) + + // first check if the options give a massif context to use, and use that + if massifOptions.massifContext != nil { + return massifOptions.massifContext, nil + } + + var massifReader MassifGetter + // now check if we have a massif reader + if massifOptions.massifGetter != nil { + massifReader = massifOptions.massifGetter + } else { + // otherwise use azblob reader to get it + if massifOptions.azblobReader == nil { + return nil, errors.New("no way of determining massif of app entry, please provide either a massif context, massif reader or azblob reader") + } + + newMassifReader := massifs.NewMassifReader(logger.Sugar, massifOptions.azblobReader) + massifReader = &newMassifReader + } + + massifHeight := massifOptions.MassifHeight + + logIdentity := massifOptions.TenantId + // if the log identity is not given, attempt to find it from the logId + if massifOptions.TenantId == "" { + // find the tenant log from the logID + logUuid, err := uuid.FromBytes(ae.logID) + if err != nil { + return nil, err + } + + // log identity is currently `tenant/logid` + logIdentity = fmt.Sprintf("tenant/%s", logUuid.String()) + } + + return Massif(ae.merkleLogCommit.Index, massifReader, logIdentity, massifHeight) + +} + +// Proof gets the inclusion proof of the corresponding log entry for the app data. +func (ae *AppEntry) Proof(options ...MassifGetterOption) ([][]byte, error) { + + massif, err := ae.Massif(options...) + + if err != nil { + return nil, err + } + + // Get the size of the complete tenant MMR + mmrSize := massif.RangeCount() + + proof, err := mmr.InclusionProof(massif, mmrSize-1, ae.MMRIndex()) + if err != nil { + return nil, err + } + + return proof, nil +} + +// VerifyProof verifies the given inclusion proof of the corresponding log entry for the app data. +func (ae *AppEntry) VerifyProof(proof [][]byte, options ...MassifGetterOption) (bool, error) { + + massif, err := ae.Massif(options...) + + if err != nil { + return false, err + } + + // Get the size of the complete tenant MMR + mmrSize := massif.RangeCount() + + hasher := sha256.New() + + mmrEntry, err := ae.MMREntry() + if err != nil { + return false, err + } + + return mmr.VerifyInclusion(massif, hasher, mmrSize, mmrEntry, + ae.MMRIndex(), proof) + +} + +// VerifyInclusion verifies the inclusion of the app entry +// against the corresponding log entry in immutable merkle log +// +// Returns true if the app entry is included on the log, otherwise false. +func (ae *AppEntry) VerifyInclusion(options ...MassifGetterOption) (bool, error) { + + massif, err := ae.Massif(options...) + + if err != nil { + return false, err + } + + proof, err := ae.Proof(WithMassifContext(massif)) + if err != nil { + return false, err + } + + return ae.VerifyProof(proof, WithMassifContext(massif)) +} diff --git a/logverification/app/appentry_test.go b/logverification/app/appentry_test.go new file mode 100644 index 0000000..908c5a3 --- /dev/null +++ b/logverification/app/appentry_test.go @@ -0,0 +1,326 @@ +package app + +import ( + "testing" + + "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/stretchr/testify/assert" +) + +// TestNewAppEntry tests: +// +// 1. we can get all non derived fields for the app entry getter +func TestNewAppEntry(t *testing.T) { + type args struct { + appId string + logId []byte + extraBytes []byte + mmrEntryFields *MMREntryFields + merklelogCommit *assets.MerkleLogCommit + } + tests := []struct { + name string + args args + expected *AppEntry + }{ + { + name: "positive", + args: args{ + appId: "events/1234", + logId: []byte("1234"), + extraBytes: []byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + }, // 24 bytes long + mmrEntryFields: &MMREntryFields{ + domain: 0, + serializedBytes: []byte("its a me, an app entry"), + }, + merklelogCommit: &assets.MerkleLogCommit{ + Index: 16, + Idtimestamp: "0x1234", + }, + }, + expected: &AppEntry{ + appID: "events/1234", + logID: []byte("1234"), + extraBytes: []byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + }, // 24 bytes long + mmrEntryFields: &MMREntryFields{ + domain: 0, + serializedBytes: []byte("its a me, an app entry"), + }, + merkleLogCommit: &assets.MerkleLogCommit{ + Index: 16, + Idtimestamp: "0x1234", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := NewAppEntry( + test.args.appId, + test.args.logId, + test.args.extraBytes, + test.args.mmrEntryFields, + test.args.merklelogCommit, + ) + + appEntryGetter := AppEntryGetter(actual) + + assert.Equal(t, test.expected.appID, appEntryGetter.AppID()) + assert.Equal(t, test.expected.logID, appEntryGetter.LogID()) + assert.Equal(t, test.expected.extraBytes, appEntryGetter.ExtraBytes()) + + // mmr entry fields + assert.Equal(t, test.expected.mmrEntryFields.domain, appEntryGetter.Domain()) + assert.Equal(t, test.expected.mmrEntryFields.serializedBytes, appEntryGetter.SerializedBytes()) + + // merklelog commit + assert.Equal(t, test.expected.merkleLogCommit.Index, appEntryGetter.MMRIndex()) + assert.Equal(t, test.expected.merkleLogCommit.Idtimestamp, appEntryGetter.IDTimestamp()) + + }) + } +} + +// TestAppEntry_MMREntry tests: +// +// 1. Known Answer Test (KAT) for mmr entry for log version 1 +func TestAppEntry_MMREntry(t *testing.T) { + type fields struct { + extraBytes []byte + mmrEntryFields *MMREntryFields + merkleLogCommit *assets.MerkleLogCommit + } + tests := []struct { + name string + fields fields + expected []byte + err error + }{ + // TODO: Add test cases. + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + ae := &AppEntry{ + extraBytes: test.fields.extraBytes, + mmrEntryFields: test.fields.mmrEntryFields, + merkleLogCommit: test.fields.merkleLogCommit, + } + actual, err := ae.MMREntry() + + assert.Equal(t, test.err, err) + assert.Equal(t, test.expected, actual) + }) + } +} + +// TestAppEntry_MMRIndex tests: +// +// 1. an index > 0 returns that index. +// 2. an index == 0 returns 0. +// 3. a nil merklelog commit returns 0. +func TestAppEntry_MMRIndex(t *testing.T) { + type fields struct { + merkleLogCommit *assets.MerkleLogCommit + } + tests := []struct { + name string + fields fields + expected uint64 + }{ + { + name: "non 0 index", + fields: fields{ + merkleLogCommit: &assets.MerkleLogCommit{ + Index: 176, + }, + }, + expected: 176, + }, + { + name: "0 index", + fields: fields{ + merkleLogCommit: &assets.MerkleLogCommit{ + Index: 0, + }, + }, + expected: 0, + }, + { + name: "nil merklelog commit", + fields: fields{ + merkleLogCommit: nil, + }, + expected: 0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + ae := &AppEntry{ + merkleLogCommit: test.fields.merkleLogCommit, + } + + actual := ae.MMRIndex() + + assert.Equal(t, test.expected, actual) + }) + } +} + +// TestAppEntry_IDTimestamp tests: +// +// 1. a non empty idtimestamp returns that idtimestamp. +// 2. an empty idtimestamp returns "". +// 3. a nil merklelog commit returns "". +func TestAppEntry_IDTimestamp(t *testing.T) { + type fields struct { + merkleLogCommit *assets.MerkleLogCommit + } + tests := []struct { + name string + fields fields + expected string + }{ + { + name: "non empty idtimestamp", + fields: fields{ + merkleLogCommit: &assets.MerkleLogCommit{ + Idtimestamp: "0x1234", + }, + }, + expected: "0x1234", + }, + { + name: "empty idtimestamp", + fields: fields{ + merkleLogCommit: &assets.MerkleLogCommit{ + Idtimestamp: "", + }, + }, + expected: "", + }, + { + name: "nil merklelog commit", + fields: fields{ + merkleLogCommit: nil, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + ae := &AppEntry{ + merkleLogCommit: test.fields.merkleLogCommit, + } + + actual := ae.IDTimestamp() + + assert.Equal(t, test.expected, actual) + }) + } +} + +// TestAppEntry_MMRSalt tests: +// +// 1. Known Answer Test for MMRSalt for log version 0. +// 2. Boundary overflow test for mmr salt values higher than 24 bytes +// 3. Boundary underflow test for mmr salt values lower than 24 bytes +func TestAppEntry_MMRSalt(t *testing.T) { + type fields struct { + extraBytes []byte + merkleLogCommit *assets.MerkleLogCommit + } + tests := []struct { + name string + fields fields + expected []byte + err error + }{ + { + name: "positive kat", + fields: fields{ + extraBytes: []byte{ + 1, // app domain + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, // 23 remaining bytes + }, + merkleLogCommit: &assets.MerkleLogCommit{ + Idtimestamp: "0x01931acb7b14043b00", + }, + }, + expected: []byte{ + 0x1, // app domain + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, // remaining bytes + 0x93, 0x1a, 0xcb, 0x7b, 0x14, 0x4, 0x3b, 0x0, // idtimestamp + }, + }, + { + name: "extrabyte overflow boundary", + fields: fields{ + extraBytes: []byte{ + 1, // app domain + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, // 24 remaining bytes (overflow by 1 byte) + }, + merkleLogCommit: &assets.MerkleLogCommit{ + Idtimestamp: "0x01931acb7b14043b00", + }, + }, + expected: []byte{ + 0x1, // app domain + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, // remaining bytes + 0x93, 0x1a, 0xcb, 0x7b, 0x14, 0x4, 0x3b, 0x0, // idtimestamp + }, + }, + { + name: "extrabyte underflow boundary", + fields: fields{ + extraBytes: []byte{ + 1, // app domain + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, // 22 remaining bytes (undeflow by 1 byte) + }, + merkleLogCommit: &assets.MerkleLogCommit{ + Idtimestamp: "0x01931acb7b14043b00", + }, + }, + expected: []byte{ + 0x1, // app domain + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x0, // remaining bytes (expect last byte to be padded) + 0x93, 0x1a, 0xcb, 0x7b, 0x14, 0x4, 0x3b, 0x0, // idtimestamp + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ae := &AppEntry{ + extraBytes: test.fields.extraBytes, + merkleLogCommit: test.fields.merkleLogCommit, + } + + actual, err := ae.MMRSalt() + + assert.Equal(t, test.err, err) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/logverification/app/assetsv2.go b/logverification/app/assetsv2.go new file mode 100644 index 0000000..f482a15 --- /dev/null +++ b/logverification/app/assetsv2.go @@ -0,0 +1,170 @@ +package app + +import ( + "crypto/sha256" + "encoding/json" + "sort" + "strings" + + "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/datatrails/go-datatrails-merklelog/mmr" + "github.com/google/uuid" + "google.golang.org/protobuf/encoding/protojson" +) + +/** + * assetsv2 contains all log entry specific functions for the assetsv2 app (app domain 0). + */ + +// AssetsV2AppEntry is the assetsv2 app provided data for a corresponding log entry. +type AssetsV2AppEntry struct { + *AppEntry +} + +// NewAssetsV2AppEntries takes a list of events JSON (e.g. from the assetsv2 events list API), converts them +// into AssetsV2AppEntries and then returns them sorted by ascending MMR index. +func NewAssetsV2AppEntries(eventsJson []byte) ([]VerifiableAppEntry, error) { + // get the event list out of events + eventListJson := struct { + Events []json.RawMessage `json:"events"` + }{} + + err := json.Unmarshal(eventsJson, &eventListJson) + if err != nil { + return nil, err + } + + events := []VerifiableAppEntry{} + for _, eventJson := range eventListJson.Events { + verifiableEvent, err := NewAssetsV2AppEntry(eventJson) + if err != nil { + return nil, err + } + + events = append(events, verifiableEvent) + } + + // Sorting the events by MMR index guarantees that they're sorted in log append order. + sort.Slice(events, func(i, j int) bool { + return events[i].MMRIndex() < events[j].MMRIndex() + }) + + return events, nil +} + +// NewAssetsV2AppEntry takes a single assetsv2 event JSON and returns an AssetsV2AppEntry, +// providing just enough information to verify the incluson of and identify the event. +func NewAssetsV2AppEntry(eventJson []byte) (*AssetsV2AppEntry, error) { + + // special care is needed here to deal with uint64 types. json marshal / + // un marshal treats them as strings because they don't fit in a + // javascript Number + + // Unmarshal into a generic type to get just the bits we need. Use + // defered decoding to get the raw merklelog entry as it must be + // unmarshaled using protojson and the specific generated target type. + entry := struct { + Identity string `json:"identity,omitempty"` + TenantIdentity string `json:"tenant_identity,omitempty"` + // Note: the proof_details top level field can be ignored here because it is a 'oneof' + MerklelogEntry json.RawMessage `json:"merklelog_entry,omitempty"` + }{} + err := json.Unmarshal(eventJson, &entry) + if err != nil { + return nil, err + } + + merkleLog := &assets.MerkleLogEntry{} + err = protojson.Unmarshal(entry.MerklelogEntry, merkleLog) + if err != nil { + return nil, err + } + + // get the logID from the event log tenant + logUuid := strings.TrimPrefix(entry.TenantIdentity, "tenant/") + logId, err := uuid.Parse(logUuid) + if err != nil { + return nil, err + } + + return &AssetsV2AppEntry{ + AppEntry: &AppEntry{ + appID: entry.Identity, + logID: logId[:], + mmrEntryFields: &MMREntryFields{ + domain: byte(0), + serializedBytes: eventJson, // we cheat a bit here, because the eventJson isn't really serialized + }, + merkleLogCommit: merkleLog.Commit, + }, + }, nil +} + +// MMREntry derives the mmr entry of the corresponding log entry from the assetsv2 app data. +// +// for assetsv2 this is simplehashv3 hash and the 'serializedBytes' is the original +// event json. +// +// NOTE: the original event json isn't really serializedbytes, but the LogVersion0 hasher includes +// the serialization. +func (ae *AssetsV2AppEntry) MMREntry() ([]byte, error) { + hasher := LogVersion0Hasher{} + eventHash, err := hasher.HashEvent(ae.mmrEntryFields.serializedBytes) + if err != nil { + return nil, err + } + + return eventHash, nil +} + +// MMRSalt derives the MMR Salt of the corresponding log entry from the app data. +// MMRSalt is the datatrails provided fields included on the MMR Entry. +// +// For assetsv2 events this is empty. +func (ae *AssetsV2AppEntry) MMRSalt() ([]byte, error) { + return []byte{}, nil // MMRSalt is always empty for assetsv2 events +} + +// VerifyProof verifies the given inclusion proof of the corresponding log entry for the app data. +func (ae *AssetsV2AppEntry) VerifyProof(proof [][]byte, options ...MassifGetterOption) (bool, error) { + + massif, err := ae.Massif(options...) + + if err != nil { + return false, err + } + + // Get the size of the complete tenant MMR + mmrSize := massif.RangeCount() + + hasher := sha256.New() + + mmrEntry, err := ae.MMREntry() + if err != nil { + return false, err + } + + return mmr.VerifyInclusion(massif, hasher, mmrSize, mmrEntry, + ae.MMRIndex(), proof) + +} + +// VerifyInclusion verifies the inclusion of the app entry +// against the corresponding log entry in immutable merkle log +// +// Returns true if the app entry is included on the log, otherwise false. +func (ae *AssetsV2AppEntry) VerifyInclusion(options ...MassifGetterOption) (bool, error) { + + massif, err := ae.Massif(options...) + + if err != nil { + return false, err + } + + proof, err := ae.Proof(WithMassifContext(massif)) + if err != nil { + return false, err + } + + return ae.VerifyProof(proof, WithMassifContext(massif)) +} diff --git a/logverification/verifyevent_test.go b/logverification/app/assetsv2_test.go similarity index 73% rename from logverification/verifyevent_test.go rename to logverification/app/assetsv2_test.go index e130317..a40a957 100644 --- a/logverification/verifyevent_test.go +++ b/logverification/app/assetsv2_test.go @@ -1,6 +1,6 @@ //go:build integration && azurite -package logverification +package app import ( "testing" @@ -12,18 +12,18 @@ import ( "github.com/stretchr/testify/require" ) -// TestVerifyEvent tests: +// TestVerifyAssetsV2Event tests: // // An end to end run through of proof generation to proof verification // // of an event stored on an emulated azure blob storage. -func TestVerifyEvent(t *testing.T) { +func TestVerifyAssetsV2Event(t *testing.T) { tc, g, _ := integrationsupport.NewAzuriteTestContext(t, "TestVerify") // use the same tenant ID for all events tenantID := mmrtesting.DefaultGeneratorTenantIdentity - events := integrationsupport.GenerateTenantLog(&tc, g, 10, tenantID, true, integrationsupport.TestMassifHeight) + events := integrationsupport.GenerateTenantLog(&tc, g, 1, tenantID, true, integrationsupport.TestMassifHeight) event := events[len(events)-1] // convert the last event into json @@ -31,14 +31,19 @@ func TestVerifyEvent(t *testing.T) { eventJSON, err := marshaler.Marshal(event) require.NoError(t, err) - verifiableEvent, err := NewVerifiableAssetsV2Event(eventJSON) + appEntry, err := NewAssetsV2AppEntry(eventJSON) require.NoError(t, err) // NOTE: we would usually use azblob.NewReaderNoAuth() // instead of tc.Storer. But the azurite emulator // doesn't allow for public reads, unlike actual // blob storage that does. - verified, err := VerifyEvent(tc.Storer, *verifiableEvent, WithMassifHeight(integrationsupport.TestMassifHeight)) + verified, err := appEntry.VerifyInclusion( + WithAzblobReader( + tc.Storer, + WithMassifHeight(integrationsupport.TestMassifHeight), + ), + ) require.NoError(t, err) assert.Equal(t, true, verified) } diff --git a/logverification/app/consts.go b/logverification/app/consts.go new file mode 100644 index 0000000..13146af --- /dev/null +++ b/logverification/app/consts.go @@ -0,0 +1,8 @@ +package app + +const ( + DefaultMassifHeight = 14 + + // LeafTypePlain is the domain separator for events + LeafTypePlain = uint8(0) +) diff --git a/logverification/app/consts_test.go b/logverification/app/consts_test.go new file mode 100644 index 0000000..43b0992 --- /dev/null +++ b/logverification/app/consts_test.go @@ -0,0 +1,51 @@ +package app + +/** + * Defines constants only used in testing. + */ + +const ( + testEventJson = ` + { + "identity": "assets/9ccdc19b-44a1-434c-afab-14f8eac3405c/events/82c9f5c2-fe77-4885-86aa-417f654d3b2f", + "asset_identity": "assets/9ccdc19b-44a1-434c-afab-14f8eac3405c", + "event_attributes": { + "1": "pour flour and milk into bowl", + "2": "mix together until gloopy", + "3": "slowly add in the sugar while still mixing", + "4": "finally add in the eggs", + "5": "put in the over until golden brown" + }, + "asset_attributes": {}, + "operation": "Record", + "behaviour": "RecordEvidence", + "timestamp_declared": "2024-01-24T11:42:16Z", + "timestamp_accepted": "2024-01-24T11:42:16Z", + "timestamp_committed": "2024-01-24T11:42:17.121Z", + "principal_declared": { + "issuer": "cupcake-world", + "subject": "chris the cupcake connoisseur", + "display_name": "chris", + "email": "chris@example.com" + }, + "principal_accepted": { + "issuer": "https://app.dev-user-0.dev.datatrails.ai/appidpv1", + "subject": "924c9054-c342-47a3-a7b8-8c0bfedd37a3", + "display_name": "API", + "email": "" + }, + "confirmation_status": "COMMITTED", + "transaction_id": "", + "block_number": 0, + "transaction_index": 0, + "from": "0xc98130dc7b292FB485F842785f6F63A520a404A5", + "tenant_identity": "tenant/15c551cf-40ed-4cdb-a94b-142d6e3c620a", + "merklelog_entry": { + "commit": { + "index": 53, + "idtimestamp": "0x018d3b472e22146400" + } + } + } + ` +) diff --git a/logverification/eventsv1.go b/logverification/app/eventsv1.go similarity index 66% rename from logverification/eventsv1.go rename to logverification/app/eventsv1.go index 8d72b45..4bad19a 100644 --- a/logverification/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -1,8 +1,7 @@ -package logverification +package app import ( "encoding/json" - "fmt" "sort" "strings" @@ -21,18 +20,16 @@ const ( // EventsV1AppDomain is the events v1 app domain EventsV1AppDomain = byte(1) - - ExtraBytesSize = 24 ) -// VerifiableEventsV1Event contains key information for verifying inclusion of merkle log events -type VerifiableEventsV1Event struct { - VerifiableLogEntry +// EventsV1AppEntry is the assetsv2 app provided data for a corresponding log entry. +type EventsV1AppEntry struct { + *AppEntry } -// NewVerifiableEventsV1Events takes a list of events JSON (e.g. from the events list API), converts them -// into VerifiableEventsV1Event and then returns them sorted by ascending MMR index. -func NewVerifiableEventsV1Events(eventsJson []byte, logTenant string) ([]VerifiableEventsV1Event, error) { +// NewEventsV1AppEntries takes a list of events JSON (e.g. from the events list API), converts them +// into EventsV1AppEntries and then returns them sorted by ascending MMR index. +func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]VerifiableAppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -43,19 +40,19 @@ func NewVerifiableEventsV1Events(eventsJson []byte, logTenant string) ([]Verifia return nil, err } - events := []VerifiableEventsV1Event{} + events := []VerifiableAppEntry{} for _, eventJson := range eventListJson.Events { - verifiableEvent, err := NewVerifiableEventsV1Event(eventJson, logTenant) + verifiableEvent, err := NewEventsV1AppEntry(eventJson, logTenant) if err != nil { return nil, err } - events = append(events, *verifiableEvent) + events = append(events, verifiableEvent) } // Sorting the events by MMR index guarantees that they're sorted in log append order. sort.Slice(events, func(i, j int) bool { - return events[i].MerkleLogCommit.Index < events[j].MerkleLogCommit.Index + return events[i].MMRIndex() < events[j].MMRIndex() }) return events, nil @@ -63,7 +60,7 @@ func NewVerifiableEventsV1Events(eventsJson []byte, logTenant string) ([]Verifia // NewVerifiableEventsV1Events takes a single eventsv1 event JSON and returns a VerifiableEventsV1Event, // providing just enough information to verify and identify the event. -func NewVerifiableEventsV1Event(eventJson []byte, logTenant string, opts ...VerifiableLogEntryOption) (*VerifiableEventsV1Event, error) { +func NewEventsV1AppEntry(eventJson []byte, logTenant string) (*EventsV1AppEntry, error) { // special care is needed here to deal with uint64 types. json marshal / // un marshal treats them as strings because they don't fit in a @@ -118,19 +115,16 @@ func NewVerifiableEventsV1Event(eventJson []byte, logTenant string, opts ...Veri return nil, err } - verifableLogEntryOptions := ParseVerifableLogEntryOptions() - - return &VerifiableEventsV1Event{ - VerifiableLogEntry: VerifiableLogEntry{ - AppId: entry.Identity, - LogId: logId[:], - MMREntryFields: &MMREntryFields{ - Domain: byte(0), - SerializedBytes: serializedBytes, + return &EventsV1AppEntry{ + AppEntry: &AppEntry{ + appID: entry.Identity, + logID: logId[:], + mmrEntryFields: &MMREntryFields{ + domain: byte(0), + serializedBytes: serializedBytes, }, - ExtraBytes: extraBytes, - MerkleLogCommit: merkleLogCommit, - MerkleLogConfirm: verifableLogEntryOptions.merkleLogConfirm, + extraBytes: extraBytes, + merkleLogCommit: merkleLogCommit, }, }, nil } @@ -156,16 +150,3 @@ func NewEventsV1ExtraBytes(originTenant string) ([]byte, error) { return extraBytes, nil } - -// LogTenant returns the Log tenant that committed this assetsv2 event to the log -// -// as a tenant identity. -func (ve *VerifiableEventsV1Event) LogTenant() (string, error) { - - logTenantUuid, err := uuid.FromBytes(ve.LogId) - if err != nil { - return "", err - } - - return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil -} diff --git a/logverification/logversion0.go b/logverification/app/logversion0.go similarity index 89% rename from logverification/logversion0.go rename to logverification/app/logversion0.go index fd34eff..9acaacf 100644 --- a/logverification/logversion0.go +++ b/logverification/app/logversion0.go @@ -1,4 +1,4 @@ -package logverification +package app import ( "github.com/datatrails/go-datatrails-merklelog/massifs" @@ -29,7 +29,7 @@ func NewLogVersion0Hasher() *LogVersion0Hasher { // - id timestamp is the timestamp id found on the event merklelog entry // - simplehashv3 is the datatrails simplehash v3 schema for hashing datatrails events func (h *LogVersion0Hasher) HashEvent(eventJson []byte) ([]byte, error) { - merkleLogEntry, err := MerklelogEntry(eventJson) + assetsAppEntry, err := NewAssetsV2AppEntry(eventJson) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func (h *LogVersion0Hasher) HashEvent(eventJson []byte) ([]byte, error) { } // the idCommitted is in hex from the event, we need to convert it to uint64 - idCommitted, _, err := massifs.SplitIDTimestampHex(merkleLogEntry.Commit.Idtimestamp) + idCommitted, _, err := massifs.SplitIDTimestampHex(assetsAppEntry.IDTimestamp()) if err != nil { return nil, err } diff --git a/logverification/logversion0_test.go b/logverification/app/logversion0_test.go similarity index 97% rename from logverification/logversion0_test.go rename to logverification/app/logversion0_test.go index d2af207..9e07176 100644 --- a/logverification/logversion0_test.go +++ b/logverification/app/logversion0_test.go @@ -1,4 +1,4 @@ -package logverification +package app import ( "encoding/hex" diff --git a/logverification/app/massif.go b/logverification/app/massif.go new file mode 100644 index 0000000..863d1b2 --- /dev/null +++ b/logverification/app/massif.go @@ -0,0 +1,41 @@ +package app + +import ( + "context" + "errors" + "time" + + "github.com/datatrails/go-datatrails-merklelog/massifs" +) + +const ( + contextTimeout = 30 * time.Second +) + +var ( + ErrNilMassifContext = errors.New("nil massif context") +) + +type MassifGetter interface { + GetMassif( + ctx context.Context, tenantIdentity string, massifIndex uint64, opts ...massifs.ReaderOption, + ) (massifs.MassifContext, error) +} + +// Massif gets the massif (blob) that contains the given mmrIndex, from azure blob storage +// +// defined by the azblob configuration. +func Massif(mmrIndex uint64, massifGetter MassifGetter, tenantId string, massifHeight uint8) (*massifs.MassifContext, error) { + + massifIndex := massifs.MassifIndexFromMMRIndex(massifHeight, mmrIndex) + + ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) + defer cancel() + + massif, err := massifGetter.GetMassif(ctx, tenantId, massifIndex) + if err != nil { + return nil, err + } + + return &massif, nil +} diff --git a/logverification/app/massifgetteroptions.go b/logverification/app/massifgetteroptions.go new file mode 100644 index 0000000..8feac43 --- /dev/null +++ b/logverification/app/massifgetteroptions.go @@ -0,0 +1,66 @@ +package app + +import ( + "github.com/datatrails/go-datatrails-common/azblob" + "github.com/datatrails/go-datatrails-merklelog/massifs" +) + +/** + * Massif Options for the App are how the App Entries retrieve the correct massif to get data from their corresponding log entry. + */ + +// MassifGetterOptions how an app entry retrieves its massif +type MassifGetterOptions struct { + *MassifOptions + + azblobReader azblob.Reader + + massifGetter MassifGetter + + massifContext *massifs.MassifContext +} + +type MassifGetterOption func(*MassifGetterOptions) + +// WithMassifContext is an option that ensures the app entry uses the given +// +// massif context +func WithMassifContext(massifContext *massifs.MassifContext) MassifGetterOption { + return func(mo *MassifGetterOptions) { mo.massifContext = massifContext } +} + +// WithMassifReader is an option that ensures the given massif reader is used +// to obtain the massif for the app entry. +func WithMassifReader(massifReader MassifGetter, massifOpts ...MassifOption) MassifGetterOption { + return func(mo *MassifGetterOptions) { + mo.massifGetter = massifReader + opts := ParseMassifOptions(massifOpts...) + mo.MassifOptions = &opts + } +} + +// WithAzBlobReader is an option that ensures the given azblob reader is used +// to obtain the massif for the app entry. +func WithAzblobReader(azblobReader azblob.Reader, massifOpts ...MassifOption) MassifGetterOption { + return func(mo *MassifGetterOptions) { + mo.azblobReader = azblobReader + opts := ParseMassifOptions(massifOpts...) + mo.MassifOptions = &opts + } +} + +// ParseMassifGetterOptions parses the given options into a MassifGetterOptions struct +func ParseMassifGetterOptions(options ...MassifGetterOption) MassifGetterOptions { + massifOptions := MassifGetterOptions{ + MassifOptions: &MassifOptions{ + NonLeafNode: false, // default to erroring on non leaf nodes + MassifHeight: DefaultMassifHeight, // set the default massif height first + }, + } + + for _, option := range options { + option(&massifOptions) + } + + return massifOptions +} diff --git a/logverification/app/massifoptions.go b/logverification/app/massifoptions.go new file mode 100644 index 0000000..90e266a --- /dev/null +++ b/logverification/app/massifoptions.go @@ -0,0 +1,56 @@ +package app + +type MassifOptions struct { + + // NonLeafNode is an optional suppression + // + // of errors that occur due to attempting to get + // a massif based on a non leaf node mmrIndex. + NonLeafNode bool + + // TenantId is an optional tenant ID to use instead + // of the TenantId found on the eventJson. + TenantId string + + // MassifHeight is an optional massif height for the massif + // instead of the default. + MassifHeight uint8 +} + +type MassifOption func(*MassifOptions) + +// WithNonLeafNode is an optional suppression +// +// of errors that occur due to attempting to get +// a massif based on a non leaf node mmrIndex. +func WithNonLeafNode(nonLeafNode bool) MassifOption { + return func(mo *MassifOptions) { mo.NonLeafNode = nonLeafNode } +} + +// WithMassifTenantId is an optional tenant ID to use instead +// +// of the tenantId found on the eventJson. +func WithMassifTenantId(tenantId string) MassifOption { + return func(mo *MassifOptions) { mo.TenantId = tenantId } +} + +// WithMassifHeight is an optional massif height for the massif +// +// instead of the default. +func WithMassifHeight(massifHeight uint8) MassifOption { + return func(mo *MassifOptions) { mo.MassifHeight = massifHeight } +} + +// ParseMassifOptions parses the given options into a MassifOptions struct +func ParseMassifOptions(options ...MassifOption) MassifOptions { + massifOptions := MassifOptions{ + NonLeafNode: false, // default to erroring on non leaf nodes + MassifHeight: DefaultMassifHeight, // set the default massif height first + } + + for _, option := range options { + option(&massifOptions) + } + + return massifOptions +} diff --git a/logverification/assetsv2.go b/logverification/assetsv2.go deleted file mode 100644 index 8b064ed..0000000 --- a/logverification/assetsv2.go +++ /dev/null @@ -1,138 +0,0 @@ -package logverification - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" - "github.com/google/uuid" - "google.golang.org/protobuf/encoding/protojson" -) - -/** - * assetsv2 contains all log entry specific functions for the assetsv2 app (app domain 0). - */ - -// VerifiableAssetsV2Event contains key information for verifying inclusion of merkle log events -type VerifiableAssetsV2Event struct { - VerifiableLogEntry -} - -// NewVerifiableAssetsV2Events takes a list of events JSON (e.g. from the events list API), converts them -// into VerifiableAssetsV2Events and then returns them sorted by ascending MMR index. -func NewVerifiableAssetsV2Events(eventsJson []byte) ([]VerifiableAssetsV2Event, error) { - // get the event list out of events - eventListJson := struct { - Events []json.RawMessage `json:"events"` - }{} - - err := json.Unmarshal(eventsJson, &eventListJson) - if err != nil { - return nil, err - } - - events := []VerifiableAssetsV2Event{} - for _, eventJson := range eventListJson.Events { - verifiableEvent, err := NewVerifiableAssetsV2Event(eventJson) - if err != nil { - return nil, err - } - - events = append(events, *verifiableEvent) - } - - // Sorting the events by MMR index guarantees that they're sorted in log append order. - sort.Slice(events, func(i, j int) bool { - return events[i].MerkleLogCommit.Index < events[j].MerkleLogCommit.Index - }) - - return events, nil -} - -// NewVerifiableAssetsV2Events takes a single assetsv2 event JSON and returns a VerifiableAssetsV2Event, -// providing just enough information to verify and identify the event. -func NewVerifiableAssetsV2Event(eventJson []byte) (*VerifiableAssetsV2Event, error) { - - // special care is needed here to deal with uint64 types. json marshal / - // un marshal treats them as strings because they don't fit in a - // javascript Number - - // Unmarshal into a generic type to get just the bits we need. Use - // defered decoding to get the raw merklelog entry as it must be - // unmarshaled using protojson and the specific generated target type. - entry := struct { - Identity string `json:"identity,omitempty"` - TenantIdentity string `json:"tenant_identity,omitempty"` - // Note: the proof_details top level field can be ignored here because it is a 'oneof' - MerklelogEntry json.RawMessage `json:"merklelog_entry,omitempty"` - }{} - err := json.Unmarshal(eventJson, &entry) - if err != nil { - return nil, err - } - - merkleLog := &assets.MerkleLogEntry{} - err = protojson.Unmarshal(entry.MerklelogEntry, merkleLog) - if err != nil { - return nil, err - } - - // get the logID from the event log tenant - logUuid := strings.TrimPrefix(entry.TenantIdentity, "tenant/") - logId, err := uuid.Parse(logUuid) - if err != nil { - return nil, err - } - - return &VerifiableAssetsV2Event{ - VerifiableLogEntry: VerifiableLogEntry{ - AppId: entry.Identity, - LogId: logId[:], - MMREntryFields: &MMREntryFields{ - Domain: byte(0), - SerializedBytes: eventJson, // we cheat a bit here, because the eventJson isn't really serialized - }, - MerkleLogCommit: merkleLog.Commit, - MerkleLogConfirm: merkleLog.Confirm, - }, - }, nil -} - -// MMREntry gets the MMR Entry from the VerifiableAssetsV2Event -// for assetsv2 this is simplehashv3 hash and the 'serializedBytes' is the original -// event json. -// -// NOTE: the original event json isn't really serializedbytes, but the LogVersion0 hasher includes -// the serialization. -func (ve *VerifiableAssetsV2Event) MMREntry() ([]byte, error) { - hasher := LogVersion0Hasher{} - eventHash, err := hasher.HashEvent(ve.MMREntryFields.SerializedBytes) - if err != nil { - return nil, err - } - - return eventHash, nil -} - -// MMRSalt gets the MMR Salt, which is the datatrails provided fields included on the MMR Entry. -// -// For assetsv2 events this is empty. -func (ve *VerifiableAssetsV2Event) MMRSalt() ([]byte, error) { - return []byte{}, nil // MMRSalt is always empty for assetsv2 events -} - -// LogTenant returns the Log tenant that committed this assetsv2 event to the log -// -// as a tenant identity. -func (ve *VerifiableAssetsV2Event) LogTenant() (string, error) { - - logTenantUuid, err := uuid.FromBytes(ve.LogId) - if err != nil { - return "", err - } - - return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil - -} diff --git a/logverification/eventsv1_test.go b/logverification/eventsv1_test.go deleted file mode 100644 index 5cf9e24..0000000 --- a/logverification/eventsv1_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package logverification - -import ( - "testing" - - "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" - "github.com/stretchr/testify/assert" -) - -// TestNewEventsV1ExtraBytes tests: -// -// 1. we get a valid extraBytes for an eventsv1 event -func TestNewEventsV1ExtraBytes(t *testing.T) { - type args struct { - originTenant string - } - tests := []struct { - name string - args args - expected []byte - expectedLen int - err error - }{ - { - name: "positive", - args: args{ - originTenant: "tenant/006e21d7-63d7-47bb-9a7e-0db55621317f", - }, - expected: []byte{ - 1, // app domain - 0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127, // 16 bytes for origin tenant uuid - 0, 0, 0, 0, 0, 0, 0, // 7 padded zeros - }, - expectedLen: 24, - err: nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, err := NewEventsV1ExtraBytes(test.args.originTenant) - - assert.Equal(t, test.err, err) - assert.Equal(t, test.expected, actual) - - assert.Equal(t, test.expectedLen, len(actual)) - }) - } -} - -// TestVerifiableEventsV1Event_LogTenant tests: -// -// 1. we get back a valid log tenant from the LogID -func TestVerifiableEventsV1Event_LogTenant(t *testing.T) { - type fields struct { - VerifiableLogEntry VerifiableLogEntry - } - tests := []struct { - name string - fields fields - expected string - err error - }{ - { - name: "positive", - fields: fields{ - VerifiableLogEntry: VerifiableLogEntry{ - LogId: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // 006e21d7-63d7-47bb-9a7e-0db55621317f uuid - }, - }, - expected: "tenant/006e21d7-63d7-47bb-9a7e-0db55621317f", - err: nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ve := &VerifiableEventsV1Event{ - VerifiableLogEntry: test.fields.VerifiableLogEntry, - } - actual, err := ve.LogTenant() - - assert.Equal(t, test.err, err) - assert.Equal(t, test.expected, actual) - - }) - } -} - -// TestNewVerifiableEventsV1Event tests: -// -// 1. KAT test from test deployment for a committed eventsv1 event (inclusion verification previously tested to work) -func TestNewVerifiableEventsV1Event(t *testing.T) { - - eventJson := []byte(` -{ - "identity": "events/0193bb7f-e975-7007-95ad-4691e2b9c1f6", - "attributes": { - "5": "put in the over until golden brown", - "1": "pour flour and milk into bowl", - "2": "mix together until gloopy", - "3": "slowly add in the sugar while still mixing", - "4": "finally add in the eggs" - }, - "trails": [ - "cake" - ], - "origin_tenant": "tenant/7e4a511f-d4ae-425c-b915-9c4ac09ca929", - "created_by": "c152c19b-0bbe-4fdc-94bb-cd808d600a43", - "created_at": 1734017542, - "confirmation_status": "COMMITTED", - "merklelog_commit": { - "index": "16", - "idtimestamp": "0193bb7feb86032500" - } -} - `) - - type args struct { - eventJson []byte - logTenant string - opts []VerifiableLogEntryOption - } - tests := []struct { - name string - args args - expected *VerifiableEventsV1Event - err error - }{ - { - name: "positive", - args: args{ - eventJson: eventJson, - logTenant: "tenant/7e4a511f-d4ae-425c-b915-9c4ac09ca929", - }, - expected: &VerifiableEventsV1Event{ - VerifiableLogEntry: VerifiableLogEntry{ - AppId: "events/0193bb7f-e975-7007-95ad-4691e2b9c1f6", - LogId: []byte{126, 74, 81, 31, 212, 174, 66, 92, 185, 21, 156, 74, 192, 156, 169, 41}, // 7e4a511f-d4ae-425c-b915-9c4ac09ca929 uuid - ExtraBytes: []byte{ - 1, // app domain - 126, 74, 81, 31, 212, 174, 66, 92, 185, 21, 156, 74, 192, 156, 169, 41, // 16 bytes for origin tenant uuid - 0, 0, 0, 0, 0, 0, 0, // 7 padded zeros - }, - MMREntryFields: &MMREntryFields{ - Domain: byte(0), - SerializedBytes: []byte("222:{\"attributes\":{\"1\":\"pour flour and milk into bowl\",\"2\":\"mix together until gloopy\",\"3\":\"slowly add in the sugar while still mixing\",\"4\":\"finally add in the eggs\",\"5\":\"put in the over until golden brown\"},\"trails\":[\"cake\"]}"), - }, - MerkleLogCommit: &assets.MerkleLogCommit{ - Index: 16, - Idtimestamp: "0193bb7feb86032500", - }, - }, - }, - err: nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, err := NewVerifiableEventsV1Event(test.args.eventJson, test.args.logTenant, test.args.opts...) - - assert.Equal(t, test.err, err) - - // tests all the fields - assert.Equal(t, test.expected.AppId, actual.AppId) - assert.Equal(t, test.expected.LogId, actual.LogId) - assert.Equal(t, test.expected.ExtraBytes, actual.ExtraBytes) - assert.Equal(t, test.expected.MMREntryFields, actual.MMREntryFields) - - assert.Equal(t, test.expected.MerkleLogCommit.Idtimestamp, actual.MerkleLogCommit.Idtimestamp) - assert.Equal(t, test.expected.MerkleLogCommit.Index, actual.MerkleLogCommit.Index) - }) - } -} diff --git a/logverification/leafrange.go b/logverification/leafrange.go index daf42c9..bdeddfb 100644 --- a/logverification/leafrange.go +++ b/logverification/leafrange.go @@ -1,6 +1,9 @@ package logverification -import "github.com/datatrails/go-datatrails-merklelog/mmr" +import ( + "github.com/datatrails/go-datatrails-logverification/logverification/app" + "github.com/datatrails/go-datatrails-merklelog/mmr" +) /** * Leaf Range holds utilities for finding the range of leaves in the merkle log to @@ -12,12 +15,12 @@ import "github.com/datatrails/go-datatrails-merklelog/mmr" // events, that have been sorted from lowest mmr index to highest mmr index. // // Returns the lower and upper bound of the leaf indexes for the leaf range. -func LeafRange(sortedEvents []VerifiableAssetsV2Event) (uint64, uint64) { +func LeafRange(sortedEvents []app.VerifiableAppEntry) (uint64, uint64) { - lowerBoundMMRIndex := sortedEvents[0].MerkleLogCommit.Index + lowerBoundMMRIndex := sortedEvents[0].MMRIndex() lowerBoundLeafIndex := mmr.LeafCount(lowerBoundMMRIndex+1) - 1 // Note: LeafCount takes an mmrIndex here not a size - upperBoundMMRIndex := sortedEvents[len(sortedEvents)-1].MerkleLogCommit.Index + upperBoundMMRIndex := sortedEvents[len(sortedEvents)-1].MMRIndex() upperBoundLeafIndex := mmr.LeafCount(upperBoundMMRIndex+1) - 1 // Note: LeafCount takes an mmrIndex here not a size return lowerBoundLeafIndex, upperBoundLeafIndex diff --git a/logverification/massif.go b/logverification/massif.go index 1ba7ffd..4115e2c 100644 --- a/logverification/massif.go +++ b/logverification/massif.go @@ -5,8 +5,7 @@ import ( "errors" "time" - "github.com/datatrails/go-datatrails-common/azblob" - "github.com/datatrails/go-datatrails-common/logger" + "github.com/datatrails/go-datatrails-logverification/logverification/app" "github.com/datatrails/go-datatrails-merklelog/massifs" ) @@ -18,10 +17,16 @@ var ( ErrNilMassifContext = errors.New("nil massif context") ) +type MassifGetter interface { + GetMassif( + ctx context.Context, tenantIdentity string, massifIndex uint64, opts ...massifs.ReaderOption, + ) (massifs.MassifContext, error) +} + // Massif gets the massif (blob) that contains the given mmrIndex, from azure blob storage // // defined by the azblob configuration. -func Massif(mmrIndex uint64, massifReader massifs.MassifReader, tenantId string, massifHeight uint8) (*massifs.MassifContext, error) { +func Massif(mmrIndex uint64, massifReader MassifGetter, tenantId string, massifHeight uint8) (*massifs.MassifContext, error) { massifIndex := massifs.MassifIndexFromMMRIndex(massifHeight, mmrIndex) @@ -36,34 +41,13 @@ func Massif(mmrIndex uint64, massifReader massifs.MassifReader, tenantId string, return &massif, nil } -// MassifFromEvent gets the massif (blob) that contains the given event, from azure blob storage -// defined by the azblob configuration. -func MassifFromEvent(verifiableEvent *VerifiableAssetsV2Event, reader azblob.Reader, options ...MassifOption) (*massifs.MassifContext, error) { - massifOptions := ParseMassifOptions(options...) - massifHeight := massifOptions.massifHeight - - // if tenant ID is not supplied, find it based on the given eventJson - tenantId := massifOptions.tenantId - if tenantId == "" { - - var err error - tenantId, err = verifiableEvent.LogTenant() - if err != nil { - return nil, err - } - } - - massifReader := massifs.NewMassifReader(logger.Sugar, reader) - return Massif(verifiableEvent.MerkleLogCommit.Index, massifReader, tenantId, massifHeight) -} - // ChooseHashingSchema chooses the hashing schema based on the log version in the massif blob start record. // See [Massif Basic File Format](https://github.com/datatrails/epic-8120-scalable-proof-mechanisms/blob/main/mmr/forestrie-massifs.md#massif-basic-file-format) func ChooseHashingSchema(massifStart massifs.MassifStart) (EventHasher, error) { switch massifStart.Version { case 0: - return NewLogVersion0Hasher(), nil + return app.NewLogVersion0Hasher(), nil default: return nil, errors.New("no hashing scheme for log version") } @@ -75,7 +59,7 @@ func ChooseHashingSchema(massifStart massifs.MassifStart) (EventHasher, error) { // // A Massif is a blob that contains a portion of the merkle log. // A MassifContext is the context used to get specific massifs. -func UpdateMassifContext(massifReader massifs.MassifReader, massifContext *massifs.MassifContext, mmrIndex uint64, tenantID string, massifHeight uint8) error { +func UpdateMassifContext(massifReader MassifGetter, massifContext *massifs.MassifContext, mmrIndex uint64, tenantID string, massifHeight uint8) error { // there is a chance here that massifContext is nil, in this case we can't do anything // as we set the massifContext as a side effect, and there is no pointer value. diff --git a/logverification/massifoptions.go b/logverification/massifoptions.go index e1941b1..1fd62cf 100644 --- a/logverification/massifoptions.go +++ b/logverification/massifoptions.go @@ -2,19 +2,19 @@ package logverification type MassifOptions struct { - // nonLeafNode is an optional suppression + // NonLeafNode is an optional suppression // // of errors that occur due to attempting to get // a massif based on a non leaf node mmrIndex. - nonLeafNode bool + NonLeafNode bool - // tenantId is an optional tenant ID to use instead - // of the tenantId found on the eventJson. - tenantId string + // TenantId is an optional tenant ID to use instead + // of the TenantId found on the eventJson. + TenantId string - // massifHeight is an optional massif height for the massif + // MassifHeight is an optional massif height for the massif // instead of the default. - massifHeight uint8 + MassifHeight uint8 } type MassifOption func(*MassifOptions) @@ -24,28 +24,28 @@ type MassifOption func(*MassifOptions) // of errors that occur due to attempting to get // a massif based on a non leaf node mmrIndex. func WithNonLeafNode(nonLeafNode bool) MassifOption { - return func(mo *MassifOptions) { mo.nonLeafNode = nonLeafNode } + return func(mo *MassifOptions) { mo.NonLeafNode = nonLeafNode } } // WithMassifTenantId is an optional tenant ID to use instead // // of the tenantId found on the eventJson. func WithMassifTenantId(tenantId string) MassifOption { - return func(mo *MassifOptions) { mo.tenantId = tenantId } + return func(mo *MassifOptions) { mo.TenantId = tenantId } } // WithMassifHeight is an optional massif height for the massif // // instead of the default. func WithMassifHeight(massifHeight uint8) MassifOption { - return func(mo *MassifOptions) { mo.massifHeight = massifHeight } + return func(mo *MassifOptions) { mo.MassifHeight = massifHeight } } // ParseMassifOptions parses the given options into a MassifOptions struct func ParseMassifOptions(options ...MassifOption) MassifOptions { massifOptions := MassifOptions{ - nonLeafNode: false, // default to erroring on non leaf nodes - massifHeight: DefaultMassifHeight, // set the default massif height first + NonLeafNode: false, // default to erroring on non leaf nodes + MassifHeight: DefaultMassifHeight, // set the default massif height first } for _, option := range options { diff --git a/logverification/validation.go b/logverification/validation.go index 9214c98..a045aec 100644 --- a/logverification/validation.go +++ b/logverification/validation.go @@ -1,6 +1,10 @@ package logverification -import "errors" +import ( + "errors" + + "github.com/datatrails/go-datatrails-logverification/logverification/app" +) // Note: We need this logic to detect incomplete JSON unmarshalled into these types. This should // eventually be replaced by JSON Schema validation. We believe its a problem to solve for the @@ -9,28 +13,25 @@ import "errors" // before. var ( + ErrNonEmptyAppIDRequired = errors.New("app id field is required and must be non-empty") ErrNonEmptyEventIDRequired = errors.New("event identity field is required and must be non-empty") ErrNonEmptyTenantIDRequired = errors.New("tenant identity field is required and must be non-empty") ErrCommitEntryRequired = errors.New("merkle log commit field is required") ErrIdTimestampRequired = errors.New("idtimestamp field is required and must be non-empty") ) -// Validate performs basic validation on the VerifiableEvent, ensuring that critical fields +// Validate performs basic validation on the AppEntryGetter, ensuring that critical fields // are present. -func (e *VerifiableAssetsV2Event) Validate() error { - if e.AppId == "" { - return ErrNonEmptyEventIDRequired +func Validate(appEntry app.AppEntryGetter) error { + if appEntry.AppID() == "" { + return ErrNonEmptyAppIDRequired } - if len(e.LogId) == 0 { + if len(appEntry.LogID()) == 0 { return ErrNonEmptyTenantIDRequired } - if e.MerkleLogCommit == nil { - return ErrCommitEntryRequired - } - - if e.MerkleLogCommit.Idtimestamp == "" { + if appEntry.IDTimestamp() == "" { return ErrIdTimestampRequired } diff --git a/logverification/validation_test.go b/logverification/validation_test.go index 4153f5d..f49626c 100644 --- a/logverification/validation_test.go +++ b/logverification/validation_test.go @@ -4,16 +4,17 @@ import ( "testing" "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/datatrails/go-datatrails-logverification/logverification/app" "github.com/datatrails/go-datatrails-simplehash/simplehash" "github.com/stretchr/testify/assert" ) func TestVerifiableEvent_Validate(t *testing.T) { type fields struct { - AppId string - LogId []byte - MMREntryFields *MMREntryFields - MerkleLogCommit *assets.MerkleLogCommit + appID string + logID []byte + mmrEntryFields *app.MMREntryFields + merkleLogCommit *assets.MerkleLogCommit } tests := []struct { @@ -24,10 +25,10 @@ func TestVerifiableEvent_Validate(t *testing.T) { { name: "valid input returns no error", fields: fields{ - AppId: "event/7189fa3d-9af1-40b1-975c-70f792142a82", - LogId: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f - MMREntryFields: nil, - MerkleLogCommit: &assets.MerkleLogCommit{ + appID: "event/7189fa3d-9af1-40b1-975c-70f792142a82", + logID: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f + mmrEntryFields: nil, + merkleLogCommit: &assets.MerkleLogCommit{ Index: uint64(0), Idtimestamp: "018fa97ef269039b00", }, @@ -37,23 +38,23 @@ func TestVerifiableEvent_Validate(t *testing.T) { { name: "missing event identity returns specific error", fields: fields{ - AppId: "", - LogId: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f - MMREntryFields: nil, - MerkleLogCommit: &assets.MerkleLogCommit{ + appID: "", + logID: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f + mmrEntryFields: nil, + merkleLogCommit: &assets.MerkleLogCommit{ Index: uint64(0), Idtimestamp: "018fa97ef269039b00", }, }, - expectedErr: ErrNonEmptyEventIDRequired, + expectedErr: ErrNonEmptyAppIDRequired, }, { name: "missing tenant identity returns specific error", fields: fields{ - AppId: "event/7189fa3d-9af1-40b1-975c-70f792142a82", - LogId: []byte{}, - MMREntryFields: nil, - MerkleLogCommit: &assets.MerkleLogCommit{ + appID: "event/7189fa3d-9af1-40b1-975c-70f792142a82", + logID: []byte{}, + mmrEntryFields: nil, + merkleLogCommit: &assets.MerkleLogCommit{ Index: uint64(0), Idtimestamp: "018fa97ef269039b00", }, @@ -61,22 +62,13 @@ func TestVerifiableEvent_Validate(t *testing.T) { expectedErr: ErrNonEmptyTenantIDRequired, }, { - name: "missing commit entry returns specific error", - fields: fields{ - AppId: "event/7189fa3d-9af1-40b1-975c-70f792142a82", - LogId: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f - MMREntryFields: nil, - MerkleLogCommit: nil, - }, - expectedErr: ErrCommitEntryRequired, - }, - { + // NOTE: this can happen if the commit is empty, so covers that case as well name: "missing idtimestamp returns specific error", fields: fields{ - AppId: "event/7189fa3d-9af1-40b1-975c-70f792142a82", - LogId: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f - MMREntryFields: nil, - MerkleLogCommit: &assets.MerkleLogCommit{ + appID: "event/7189fa3d-9af1-40b1-975c-70f792142a82", + logID: []byte{0, 110, 33, 215, 99, 215, 71, 187, 154, 126, 13, 181, 86, 33, 49, 127}, // tenant/006e21d7-63d7-47bb-9a7e-0db55621317f + mmrEntryFields: nil, + merkleLogCommit: &assets.MerkleLogCommit{ Index: uint64(0), Idtimestamp: "", }, @@ -86,16 +78,16 @@ func TestVerifiableEvent_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := &VerifiableAssetsV2Event{ - VerifiableLogEntry: VerifiableLogEntry{ - AppId: tt.fields.AppId, - LogId: tt.fields.LogId, - MMREntryFields: tt.fields.MMREntryFields, - MerkleLogCommit: tt.fields.MerkleLogCommit, - }, + e := &app.AssetsV2AppEntry{ + AppEntry: app.NewAppEntry( + tt.fields.appID, + tt.fields.logID, + []byte{}, + tt.fields.mmrEntryFields, + tt.fields.merkleLogCommit), } - err := e.Validate() + err := Validate(e) assert.ErrorIs(t, err, tt.expectedErr) }) } diff --git a/logverification/verifiableentry.go b/logverification/verifiableentry.go deleted file mode 100644 index bb4b7c0..0000000 --- a/logverification/verifiableentry.go +++ /dev/null @@ -1,195 +0,0 @@ -package logverification - -import ( - "crypto/sha256" - "encoding/binary" - "fmt" - - "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" - "github.com/datatrails/go-datatrails-common/azblob" - "github.com/datatrails/go-datatrails-common/logger" - "github.com/datatrails/go-datatrails-merklelog/massifs" - "github.com/google/uuid" -) - -/** - * Verifiable :Log Entry is a Log Entry that can be verified. - * - * The format for an MMR entry is the following: - * - * H( Domain | MMR Salt | Serialized Bytes) - * - * Where: - * * Domain - the hashing schema for the MMR Entry - * * MMR Salt - datatrails provided fields included in the MMR Entry (can be found in the corresponding Trie Value on the log) - * * Serialized Bytes - app (customer) provided fields in the MMR Entry, serialized in a consistent way. - * - * - * The format for a Trie Entry is the following: - * - * ( Trie Key | Trie Value ) - * - * Where the Trie Key is: - * - * H( Domain | LogId | AppId ) - * - * And Trie Value is: - * - * ( Extra Bytes | IdTimestamp ) - */ - -const ( - MMRSaltSize = 32 - - IDTimestapSizeBytes = 8 -) - -// MMREntryFields are the fields that when hashed result in the MMR Entry -type MMREntryFields struct { - - // Domain defines the hashing schema for the MMR Entry - Domain byte - - // SerializedBytes are app (customer) provided fields in the MMR Entry, serialized in a consistent way. - SerializedBytes []byte -} - -// VerifiableLogEntry contains key information for verifying inclusion and consistency of merkle log entries -type VerifiableLogEntry struct { - // AppId is an identifier of the app committing the merkle log entry - AppId string - - // AppId is a uuid in byte form of the specific log identifier - LogId []byte - - // ExtraBytes are extrabytes provided by datatrails for the specific app - ExtraBytes []byte - - // MMREntryFields used to determine the MMR Entry - MMREntryFields *MMREntryFields - - // MerkleLogCommit used to define information about the log entry - MerkleLogCommit *assets.MerkleLogCommit - - // MerkleLogConfirm used to define information about the log seal - MerkleLogConfirm *assets.MerkleLogConfirm -} - -// NewVerifiableLogEntry creates a new verifiable log entry -func NewVerifiableLogEntry( - appId string, - logId []byte, - extraBytes []byte, - mmrEntryFields *MMREntryFields, - merklelogCommit *assets.MerkleLogCommit, - opts ...VerifiableLogEntryOption, -) *VerifiableLogEntry { - - verifiableLogEntry := &VerifiableLogEntry{ - AppId: appId, - LogId: logId, - ExtraBytes: extraBytes, - MMREntryFields: mmrEntryFields, - MerkleLogCommit: merklelogCommit, - } - - // get all the options - verifiableLogEntryOptions := ParseVerifableLogEntryOptions(opts...) - verifiableLogEntry.MerkleLogConfirm = verifiableLogEntryOptions.merkleLogConfirm - - return verifiableLogEntry -} - -// MMREntry gets the mmr entry of a verifiable log entry -// -// MMREntry is: -// - H( Domain | MMR Salt | Serialized Bytes) -func (vle *VerifiableLogEntry) MMREntry() ([]byte, error) { - - hasher := sha256.New() - - // domain - hasher.Write([]byte{vle.MMREntryFields.Domain}) - - // mmr salt - mmrSalt, err := vle.MMRSalt() - if err != nil { - return nil, err - } - - hasher.Write(mmrSalt) - - // serialized bytes - hasher.Write(vle.MMREntryFields.SerializedBytes) - - return hasher.Sum(nil), nil - -} - -// MMRIndex gets the mmr index of the verifiable log entry -func (vle *VerifiableLogEntry) MMRIndex() uint64 { - return vle.MerkleLogCommit.Index -} - -// MMRSalt gets the MMR Salt, which is the datatrails provided fields included on the MMR Entry. -// -// this is (extrabytes | idtimestamp) for any apps that adhere to log entry version 1. -func (ve *VerifiableLogEntry) MMRSalt() ([]byte, error) { - - mmrSalt := make([]byte, MMRSaltSize) - - copy(mmrSalt[:24], ve.ExtraBytes) - - // get the byte representation of idtimestamp - idTimestamp, _, err := massifs.SplitIDTimestampHex(ve.MerkleLogCommit.Idtimestamp) - if err != nil { - return nil, err - } - - idTimestampBytes := make([]byte, IDTimestapSizeBytes) - binary.BigEndian.PutUint64(idTimestampBytes, idTimestamp) - - copy(mmrSalt[24:], idTimestampBytes) - - return mmrSalt, nil -} - -// massif gets the massif context for the VerifiableLogEntry. -func (vle *VerifiableLogEntry) massif(reader azblob.Reader, options ...MassifOption) (*massifs.MassifContext, error) { - - massifOptions := ParseMassifOptions(options...) - massifHeight := massifOptions.massifHeight - - // find the tenant log from the logID - logUuid, err := uuid.FromBytes(vle.LogId) - if err != nil { - return nil, err - } - - // log identity is currently `tenant/logid` - logIdentity := fmt.Sprintf("tenant/%s", logUuid.String()) - - massifReader := massifs.NewMassifReader(logger.Sugar, reader) - return Massif(vle.MerkleLogCommit.Index, massifReader, logIdentity, massifHeight) - -} - -// VerifyInclusion verifies the inclusion of the verifiable log entry -// against the immutable merkle log, acquired using the given reader. -// -// Returns true if the event is included on the log, otherwise false. -func (vle *VerifiableLogEntry) VerifyInclusion(reader azblob.Reader, options ...MassifOption) (bool, error) { - - massif, err := vle.massif(reader, options...) - - if err != nil { - return false, err - } - - proof, err := EventProof(vle, massif) - if err != nil { - return false, err - } - - return VerifyProof(vle, proof, massif) -} diff --git a/logverification/verifyconsistency.go b/logverification/verifyconsistency.go index bf9f4ed..2fb447f 100644 --- a/logverification/verifyconsistency.go +++ b/logverification/verifyconsistency.go @@ -40,7 +40,7 @@ func VerifyConsistency( massifReader := massifs.NewMassifReader(logger.Sugar, reader) // last massif in the merkle log for log state B - massifContextB, err := Massif(logStateB.MMRSize-1, massifReader, tenantID, DefaultMassifHeight) + massifContextB, err := Massif(logStateB.MMRSize-1, &massifReader, tenantID, DefaultMassifHeight) if err != nil { return false, fmt.Errorf("VerifyConsistency failed: unable to get the last massif for log state B: %w", err) } diff --git a/logverification/verifyevent.go b/logverification/verifyevent.go deleted file mode 100644 index 596ac29..0000000 --- a/logverification/verifyevent.go +++ /dev/null @@ -1,30 +0,0 @@ -package logverification - -import ( - "github.com/datatrails/go-datatrails-common/azblob" -) - -/** - * Verifies a single datatrails event is present on the immutable merkle log. - */ - -// VerifyEvent verifies the integrity of the given event json -// -// against the immutable merkle log, aquired using the given reader. -// -// Returns true if the event is found to be on the log, otherwise false. -func VerifyEvent(reader azblob.Reader, verifiableEvent VerifiableAssetsV2Event, options ...MassifOption) (bool, error) { - - massif, err := MassifFromEvent(&verifiableEvent, reader, options...) - - if err != nil { - return false, err - } - - proof, err := EventProof(&verifiableEvent, massif) - if err != nil { - return false, err - } - - return VerifyProof(&verifiableEvent, proof, massif) -} diff --git a/logverification/verifylist.go b/logverification/verifylist.go index b627064..c85b017 100644 --- a/logverification/verifylist.go +++ b/logverification/verifylist.go @@ -8,6 +8,7 @@ import ( "github.com/datatrails/go-datatrails-common/azblob" "github.com/datatrails/go-datatrails-common/logger" + "github.com/datatrails/go-datatrails-logverification/logverification/app" "github.com/datatrails/go-datatrails-merklelog/massifs" "github.com/datatrails/go-datatrails-merklelog/mmr" ) @@ -82,36 +83,36 @@ import ( * |-----------------------------| */ -type EventType int +type AppEntryType int const ( - // Unknown event is a given event that is unknown - Unknown EventType = iota + // Unknown app entry is a given app entry that is unknown + Unknown AppEntryType = iota - // Included event is a given event that is included on the immutable log + // Included app entry is a given app entry that is included on the immutable log Included - // Excluded event is a given event that is NOT included on the immutable log + // Excluded app entry is a given app entry that is NOT included on the immutable log Excluded - // Omitted is an event on the immutable log, that has not been given within an expected list of events. + // Omitted is an app entry on the immutable log, that has not been given within an expected list of app entries. Omitted ) var ( - ErrIntermediateNode = errors.New("event references an intermediate node on the merkle log") - ErrDuplicateEventMMRIndex = errors.New("event mmrIndex is the same as the previous event") - ErrEventNotOnLeaf = errors.New("event does not correspond to the event found on the leaf node") - ErrInclusionProofVerify = errors.New("event failed to verify the inclusion proof on the merkle log") - ErrNotEnoughEventsInList = errors.New("the number of events in the list is less than the number of leafs on the log") + ErrIntermediateNode = errors.New("app entry references an intermediate node on the merkle log") + ErrDuplicateAppEntryMMRIndex = errors.New("app entry mmrIndex is the same as the previous event") + ErrAppEntryNotOnLeaf = errors.New("app entry does not correspond to the event found on the leaf node") + ErrInclusionProofVerify = errors.New("app entry failed to verify the inclusion proof on the merkle log") + ErrNotEnoughAppEntriesInList = errors.New("the number of app entries in the list is less than the number of leafs on the log") ) -/** VerifyList verifies a given list of events against a range of leaves in the immutable merkle log. +/** VerifyList verifies a given list of app entries against a range of leaves in the immutable merkle log. * - * The list of events given is the json response from a datatrails list API call. + * The list of app entries for assetsv2 or eventsv1 is the json response from a datatrails list events API call. * - * The boundaries of the range of leaves are determined by the lowest and largest mmrIndex on the given list of events. + * The boundaries of the range of leaves are determined by the lowest and largest mmrIndex on the given list of app entries. * In the below example the event with the lowest mmrIndex matches leaf2 of the mmr, * and the event with the largest mmrIndex matches leaf4 of the mmr: * @@ -121,55 +122,55 @@ var ( * |-------------------------------| * * Once a range of leaves in the mmr has been established, we iterate over each leaf in the range - * and each event in the list (sorted lowest to highest by mmrIndex). + * and each app entry in the list (sorted lowest to highest by mmrIndex). * * We check that each event is INCLUDED in the mmr at the leaf index it is in tandem with * in the iteration: * * |----------------------| - * | event1 event2 event3 | event list (lowest mmrIndex to highest) + * | entry1 entry2 entry3 | app entry list (lowest mmrIndex to highest) * |----------------------| * ↓ ↓ ↓ * |----------------------| * | leaf1 leaf2 leaf3 | leaf range from merklelog * |----------------------| * - * If every event within the list is included at its expected leaf index, we say the list is COMPLETE. + * If every app entry within the list is included at its expected leaf index, we say the list is COMPLETE. * - * If an event within the list of events is not present on the immutable merklelog + * If an app entry within the list of app entries is not present on the immutable merklelog * at the expected leaf index it is in tandem with, we call that an EXCLUDED event. - * In the below example, event2 is an EXCLUDED event. (Note: proof of exclusion using the trie index is not shown in this demo) + * In the below example, entry2 is an EXCLUDED app entry. (Note: proof of exclusion using the trie index is not shown in this demo) * * |-----------------------------| - * | event1 event2 event3 event4 | event list (lowest mmrIndex to highest) + * | entry1 entry2 entry3 entry4 | app entry list (lowest mmrIndex to highest) * |-----------------------------| * ↓ ↓ ↓ * |-----------------------------| * | leaf1 leaf2 leaf3 | leaf range from merklelog * |-----------------------------| * - * If there is a leaf within the range of leaves that does not have an event, within the list of events included, - * we call that an OMITTED event. + * If there is a leaf within the range of leaves that does not have an app entry, within the list of app entries included, + * we call that an OMITTED app entry. * - * In the below example the event included at leaf2 is an example of an ommitted event. + * In the below example the app entry included at leaf2 is an example of an ommitted event. * * |-----------------------------| - * | event1 event3 event4 | event list (lowest mmrIndex to highest) + * | entry1 entry3 entry4 | app entry list (lowest mmrIndex to highest) * |-----------------------------| * ↓ ↓ ↓ * |-----------------------------| * | leaf1 leaf2 leaf3 leaf4 | leaf range from merklelog * |-----------------------------| * - * Returns the omitted event mmrIndexes. + * Returns the omitted app entry mmrIndexes. * * The options argument can be the following: * - * WithTenantId - the tenantId of the merklelog, the event is expected + * WithTenantId - the tenantId of the merklelog, the app entry is expected * to be included on. E.g. the public tenant * for public events. */ -func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, options ...VerifyOption) ([]uint64, error) { +func VerifyList(reader azblob.Reader, appEntries []app.VerifiableAppEntry, options ...VerifyOption) ([]uint64, error) { verifyOptions := ParseOptions(options...) @@ -178,19 +179,19 @@ func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, optio massifContext := massifs.MassifContext{} omittedMMRIndices := []uint64{} - lowestLeafIndex, highestLeafIndex := LeafRange(eventList) + lowestLeafIndex, highestLeafIndex := LeafRange(appEntries) massifReader := massifs.NewMassifReader(logger.Sugar, reader) - eventIndex := 0 + appEntryIndex := 0 for leafIndex := lowestLeafIndex; leafIndex <= highestLeafIndex; leafIndex += 1 { - if eventIndex >= len(eventList) { - return nil, ErrNotEnoughEventsInList + if appEntryIndex >= len(appEntries) { + return nil, ErrNotEnoughAppEntriesInList } - event := eventList[eventIndex] + appEntry := appEntries[appEntryIndex] // ensure we set the tenantId if // if it passed in as an optional argument @@ -199,14 +200,14 @@ func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, optio // otherwise set it to the event tenantID var err error - tenantId, err = event.LogTenant() + tenantId, err = appEntry.LogTenant() if err != nil { return nil, err } } - eventType, err := VerifyEventInList(hasher, leafIndex, event, massifReader, &massifContext, tenantId) + appEntryType, err := VerifyAppEntryInList(hasher, leafIndex, appEntry, massifReader, &massifContext, tenantId) if err != nil { // NOTE: for now fail at the first sign of an EXCLUDED event. @@ -216,7 +217,7 @@ func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, optio } // if the event is OMITTED add the leaf to the omitted list - if eventType == Omitted { + if appEntryType == Omitted { omittedMMRIndices = append(omittedMMRIndices, mmr.MMRIndex(leafIndex)) // as the event is still the lowest mmrIndex we check this event @@ -224,55 +225,55 @@ func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, optio continue } - eventIndex += 1 + appEntryIndex += 1 } return omittedMMRIndices, nil } -// VerifyEventInList takes the next leaf in the list of leaves and the next event in the list of events +// VerifyAppEntryInList takes the next leaf in the list of leaves and the next app entry in the list of app entries // -// and verifies that the event is in that leaf position. -func VerifyEventInList( +// and verifies that the app entry is in that leaf position. +func VerifyAppEntryInList( hasher hash.Hash, leafIndex uint64, - event VerifiableAssetsV2Event, + appEntry app.VerifiableAppEntry, reader massifs.MassifReader, massifContext *massifs.MassifContext, tenantID string, -) (EventType, error) { +) (AppEntryType, error) { hasher.Reset() leafMMRIndex := mmr.MMRIndex(leafIndex) - eventMMRIndex := event.MerkleLogCommit.Index + appEntryMMRIndex := appEntry.MMRIndex() - // First we check if the event mmrIndex corresponds to a leaf node. + // First we check if the app entry mmrIndex corresponds to a leaf node. // // ONLY leaf nodes correspond to events. // // Therefore if the event mmrIndex corresponds to an intermediate node, - // the event is not in the merkle log, it is EXCLUDED. - indexHeight := mmr.IndexHeight(eventMMRIndex) + // the app entry is not in the merkle log, it is EXCLUDED. + indexHeight := mmr.IndexHeight(appEntryMMRIndex) // all leaf nodes are at height 0 if indexHeight != 0 { return Excluded, ErrIntermediateNode } - // When the next event in the list of events has an mmrindex LESS THAN the next leaf in the range of leaves. + // When the next app entry in the list of app entries has an mmrindex LESS THAN the next leaf in the range of leaves. // - // This means the mmr index of the event matches the previous leaf node. + // This means the mmr index of the app entry matches the previous leaf node. // // This can occur because one of the following: - // 1. The event is a duplicate of the previous event in the list. + // 1. The event is a duplicate of the previous app entry in the list. // 2. The event is not included on the previous leaf, but says it is. // - // In both cases we say the event is not on the merkle log, it is EXCLUDED. + // In both cases we say the app entry is not on the merkle log, it is EXCLUDED. // // Example: - // Event mmrIndex: 10 + // Entry mmrIndex: 10 // Leaf mmrIndex: 11 // // 14 @@ -289,23 +290,23 @@ func VerifyEventInList( // NOTE: in the future we may mark a duplicated event as DUPLICATED instead of EXCLUDED. // // NOTE: we can make the above assumptions because: - // 1. the event mmrIndex is the next in the list of events, - // so the previous event was included on the previous leaf node. - // 2. we have already checked that the event mmrIndex is not an intermediate node. - if eventMMRIndex < leafMMRIndex { - return Excluded, ErrDuplicateEventMMRIndex + // 1. the event mmrIndex is the next in the list of app entries, + // so the previous app entry was included on the previous leaf node. + // 2. we have already checked that the app entry mmrIndex is not an intermediate node. + if appEntryMMRIndex < leafMMRIndex { + return Excluded, ErrDuplicateAppEntryMMRIndex } - // When the next event in the list of events has an mmrindex GREATER THAN the next leaf in the range of leaves. + // When the next app entry in the list of app entries has an mmrindex GREATER THAN the next leaf in the range of leaves. // - // This means the mmr index of the event matches a future leaf node. + // This means the mmr index of the app entry matches a future leaf node. // - // This can occur because there are events on the merklelog that are not included in the list of events. - // The event at the leaf mmr index is an OMITTED event. + // This can occur because there are app entries on the merklelog that are not included in the list of app entries. + // The app entry at the leaf mmr index is an OMITTED app entry. // // // Example: - // Event mmrIndex: 10 + // Entry mmrIndex: 10 // Leaf mmrIndex: 4 // // 14 @@ -320,19 +321,19 @@ func VerifyEventInList( // 0 1 3 4 7 8 10 11 15 16 <- Leaf Nodes // // NOTE: we can make the above assumptions because: - // 1. the event mmrIndex is the next in the list of events, - // so the previous event was included on the previous leaf node. - // 2. we have already checked that the event mmrIndex is not an intermediate node. - if eventMMRIndex > leafMMRIndex { + // 1. the app entry mmrIndex is the next in the list of app entries, + // so the previous app entry was included on the previous leaf node. + // 2. we have already checked that the app entry mmrIndex is not an intermediate node. + if appEntryMMRIndex > leafMMRIndex { return Omitted, nil } - // If we reach this point, the next event in the list of events has an mmrindex EQUAL TO the next leaf in the range of leaves. + // If we reach this point, the next app entry in the list of app entries has an mmrindex EQUAL TO the next leaf in the range of leaves. // - // We now do an inclusion proof on the event, to prove that the event is included at the leaf node. + // We now do an inclusion proof on the app entry, to prove that the app entry is included at the leaf node. // Ensure we're using the correct massif for the current leaf - err := UpdateMassifContext(reader, massifContext, leafMMRIndex, tenantID, DefaultMassifHeight) + err := UpdateMassifContext(&reader, massifContext, leafMMRIndex, tenantID, DefaultMassifHeight) if err != nil { return Unknown, err } @@ -343,19 +344,19 @@ func VerifyEventInList( return Unknown, err } - mmrEntry, err := event.MMREntry() + mmrEntry, err := appEntry.MMREntry() if err != nil { return Unknown, err } - // Check that the leaf node mmrEntry is the same as the event hash + // Check that the leaf node mmrEntry is the same as the app entry mmrEntry // - // If its not, we know that the given event is not the same as the event on the leaf node. + // If its not, we know that the given app entry is not the same as the app entry on the leaf node. if !bytes.Equal(leafMMREntry, mmrEntry) { - return Excluded, ErrEventNotOnLeaf + return Excluded, ErrAppEntryNotOnLeaf } - // Now we know that the event is the event stored on the leaf node, + // Now we know that the app entry is the app entry stored on the leaf node, // we can do an inclusion proof of the leaf node on the merkle log. mmrSize := massifContext.RangeCount() diff --git a/logverification/verifylist_test.go b/logverification/verifylist_test.go index ed80d50..67ec32b 100644 --- a/logverification/verifylist_test.go +++ b/logverification/verifylist_test.go @@ -10,6 +10,7 @@ import ( "github.com/datatrails/go-datatrails-common-api-gen/attribute/v2/attribute" "github.com/datatrails/go-datatrails-common/logger" "github.com/datatrails/go-datatrails-logverification/integrationsupport" + "github.com/datatrails/go-datatrails-logverification/logverification/app" "github.com/datatrails/go-datatrails-merklelog/mmrtesting" "github.com/stretchr/testify/require" // TestVerifyListIntegration demonstrates how to verify the completeness of a list of events against a @@ -30,9 +31,9 @@ func serializeTestEvents(t *testing.T, events []*assets.EventResponse) []byte { // protoEventsToVerifiableEvents converts from he internally used proto EventResponse type // that our event generator returns, to the VerifiableEvent expected by logverification. -func protoEventsToVerifiableEvents(t *testing.T, events []*assets.EventResponse) []VerifiableAssetsV2Event { +func protoEventsToVerifiableEvents(t *testing.T, events []*assets.EventResponse) []app.VerifiableAppEntry { eventJsonList := serializeTestEvents(t, events) - result, err := NewVerifiableAssetsV2Events(eventJsonList) + result, err := app.NewAssetsV2AppEntries(eventJsonList) require.NoError(t, err) return result @@ -132,7 +133,7 @@ func TestVerifyList_TamperedEventContent_ShouldError(t *testing.T) { events := protoEventsToVerifiableEvents(t, generatedEvents) _, err := VerifyList(testContext.Storer, events) - require.ErrorIs(t, err, ErrEventNotOnLeaf) + require.ErrorIs(t, err, ErrAppEntryNotOnLeaf) } // TestVerifyList_IntermediateNode_ShouldError shows that an extra event at an intermediate node position