From 3996ca2d95c5d4e6e76aba2cb941c7b9ce5afe2c Mon Sep 17 00:00:00 2001 From: jgough Date: Fri, 10 Jan 2025 11:14:08 +0000 Subject: [PATCH 01/18] Input massif reader into verify inclusion instead of azblob reader re: AB#10216 --- logverification/verifiableentry.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/logverification/verifiableentry.go b/logverification/verifiableentry.go index bb4b7c0..800a006 100644 --- a/logverification/verifiableentry.go +++ b/logverification/verifiableentry.go @@ -6,8 +6,6 @@ import ( "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" ) @@ -155,7 +153,7 @@ func (ve *VerifiableLogEntry) MMRSalt() ([]byte, error) { } // massif gets the massif context for the VerifiableLogEntry. -func (vle *VerifiableLogEntry) massif(reader azblob.Reader, options ...MassifOption) (*massifs.MassifContext, error) { +func (vle *VerifiableLogEntry) massif(reader massifs.MassifReader, options ...MassifOption) (*massifs.MassifContext, error) { massifOptions := ParseMassifOptions(options...) massifHeight := massifOptions.massifHeight @@ -169,8 +167,7 @@ func (vle *VerifiableLogEntry) massif(reader azblob.Reader, options ...MassifOpt // 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) + return Massif(vle.MerkleLogCommit.Index, reader, logIdentity, massifHeight) } @@ -178,7 +175,7 @@ func (vle *VerifiableLogEntry) massif(reader azblob.Reader, options ...MassifOpt // 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) { +func (vle *VerifiableLogEntry) VerifyInclusion(reader massifs.MassifReader, options ...MassifOption) (bool, error) { massif, err := vle.massif(reader, options...) From 343b16ea47ee60c16d6feab048a01a65e7f7ff4a Mon Sep 17 00:00:00 2001 From: jgough Date: Fri, 10 Jan 2025 11:19:05 +0000 Subject: [PATCH 02/18] fixup --- logverification/verifiableentry.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logverification/verifiableentry.go b/logverification/verifiableentry.go index 800a006..a63b5f7 100644 --- a/logverification/verifiableentry.go +++ b/logverification/verifiableentry.go @@ -153,7 +153,7 @@ func (ve *VerifiableLogEntry) MMRSalt() ([]byte, error) { } // massif gets the massif context for the VerifiableLogEntry. -func (vle *VerifiableLogEntry) massif(reader massifs.MassifReader, options ...MassifOption) (*massifs.MassifContext, error) { +func (vle *VerifiableLogEntry) massif(reader *massifs.MassifReader, options ...MassifOption) (*massifs.MassifContext, error) { massifOptions := ParseMassifOptions(options...) massifHeight := massifOptions.massifHeight @@ -167,7 +167,7 @@ func (vle *VerifiableLogEntry) massif(reader massifs.MassifReader, options ...Ma // log identity is currently `tenant/logid` logIdentity := fmt.Sprintf("tenant/%s", logUuid.String()) - return Massif(vle.MerkleLogCommit.Index, reader, logIdentity, massifHeight) + return Massif(vle.MerkleLogCommit.Index, *reader, logIdentity, massifHeight) } @@ -175,7 +175,7 @@ func (vle *VerifiableLogEntry) massif(reader massifs.MassifReader, options ...Ma // 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 massifs.MassifReader, options ...MassifOption) (bool, error) { +func (vle *VerifiableLogEntry) VerifyInclusion(reader *massifs.MassifReader, options ...MassifOption) (bool, error) { massif, err := vle.massif(reader, options...) From 7ed467eff561fc492750dd8ab8811cb5afbf1244 Mon Sep 17 00:00:00 2001 From: jgough Date: Fri, 10 Jan 2025 14:01:24 +0000 Subject: [PATCH 03/18] fixup --- logverification/massif.go | 6 +++--- logverification/verifiableentry.go | 13 ++++++++++--- logverification/verifyconsistency.go | 2 +- logverification/verifylist.go | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/logverification/massif.go b/logverification/massif.go index 1ba7ffd..e3326c1 100644 --- a/logverification/massif.go +++ b/logverification/massif.go @@ -21,7 +21,7 @@ var ( // 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) @@ -54,7 +54,7 @@ func MassifFromEvent(verifiableEvent *VerifiableAssetsV2Event, reader azblob.Rea } massifReader := massifs.NewMassifReader(logger.Sugar, reader) - return Massif(verifiableEvent.MerkleLogCommit.Index, massifReader, tenantId, massifHeight) + return Massif(verifiableEvent.MerkleLogCommit.Index, &massifReader, tenantId, massifHeight) } // ChooseHashingSchema chooses the hashing schema based on the log version in the massif blob start record. @@ -75,7 +75,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/verifiableentry.go b/logverification/verifiableentry.go index a63b5f7..59e7f42 100644 --- a/logverification/verifiableentry.go +++ b/logverification/verifiableentry.go @@ -1,6 +1,7 @@ package logverification import ( + "context" "crypto/sha256" "encoding/binary" "fmt" @@ -42,6 +43,12 @@ const ( IDTimestapSizeBytes = 8 ) +type MassifGetter interface { + GetMassif( + ctx context.Context, tenantIdentity string, massifIndex uint64, opts ...massifs.ReaderOption, + ) (massifs.MassifContext, error) +} + // MMREntryFields are the fields that when hashed result in the MMR Entry type MMREntryFields struct { @@ -153,7 +160,7 @@ func (ve *VerifiableLogEntry) MMRSalt() ([]byte, error) { } // massif gets the massif context for the VerifiableLogEntry. -func (vle *VerifiableLogEntry) massif(reader *massifs.MassifReader, options ...MassifOption) (*massifs.MassifContext, error) { +func (vle *VerifiableLogEntry) massif(reader MassifGetter, options ...MassifOption) (*massifs.MassifContext, error) { massifOptions := ParseMassifOptions(options...) massifHeight := massifOptions.massifHeight @@ -167,7 +174,7 @@ func (vle *VerifiableLogEntry) massif(reader *massifs.MassifReader, options ...M // log identity is currently `tenant/logid` logIdentity := fmt.Sprintf("tenant/%s", logUuid.String()) - return Massif(vle.MerkleLogCommit.Index, *reader, logIdentity, massifHeight) + return Massif(vle.MerkleLogCommit.Index, reader, logIdentity, massifHeight) } @@ -175,7 +182,7 @@ func (vle *VerifiableLogEntry) massif(reader *massifs.MassifReader, options ...M // 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 *massifs.MassifReader, options ...MassifOption) (bool, error) { +func (vle *VerifiableLogEntry) VerifyInclusion(reader MassifGetter, options ...MassifOption) (bool, error) { massif, err := vle.massif(reader, options...) 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/verifylist.go b/logverification/verifylist.go index b627064..f9b415f 100644 --- a/logverification/verifylist.go +++ b/logverification/verifylist.go @@ -332,7 +332,7 @@ func VerifyEventInList( // We now do an inclusion proof on the event, to prove that the event 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 } From 0c4733650ff683d408669220b3768584a180edb2 Mon Sep 17 00:00:00 2001 From: jgough Date: Fri, 10 Jan 2025 15:30:02 +0000 Subject: [PATCH 04/18] fixup --- logverification/assetsv2.go | 5 +++++ logverification/eventsv1.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/logverification/assetsv2.go b/logverification/assetsv2.go index 8b064ed..73e3778 100644 --- a/logverification/assetsv2.go +++ b/logverification/assetsv2.go @@ -136,3 +136,8 @@ func (ve *VerifiableAssetsV2Event) LogTenant() (string, error) { return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil } + +// GetVerifiableLogEntry gets the verifiable log entry +func (ve *VerifiableAssetsV2Event) GetVerifiableLogEntry() *VerifiableLogEntry { + return &ve.VerifiableLogEntry +} diff --git a/logverification/eventsv1.go b/logverification/eventsv1.go index 8d72b45..1ee4638 100644 --- a/logverification/eventsv1.go +++ b/logverification/eventsv1.go @@ -169,3 +169,8 @@ func (ve *VerifiableEventsV1Event) LogTenant() (string, error) { return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil } + +// GetVerifiableLogEntry gets the verifiable log entry +func (ve *VerifiableEventsV1Event) GetVerifiableLogEntry() *VerifiableLogEntry { + return &ve.VerifiableLogEntry +} From 361aae26570703a9a67ff9765a0b0f999b5e5044 Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 11:12:20 +0000 Subject: [PATCH 05/18] fixup refactor into app entries --- logverification/app/appentry.go | 290 ++++++++++++++++++ logverification/{ => app}/assetsv2.go | 89 ++++-- .../assetsv2_test.go} | 17 +- logverification/app/consts.go | 8 + logverification/app/consts_test.go | 51 +++ logverification/{ => app}/eventsv1.go | 49 +-- logverification/{ => app}/logversion0.go | 6 +- logverification/{ => app}/logversion0_test.go | 2 +- logverification/app/massif.go | 41 +++ logverification/app/massifgetteroptions.go | 66 ++++ logverification/app/massifoptions.go | 56 ++++ logverification/eventsv1_test.go | 172 ----------- logverification/leafrange.go | 7 +- logverification/massif.go | 15 +- logverification/massifoptions.go | 24 +- logverification/validation.go | 16 +- logverification/validation_test.go | 9 +- logverification/verifiableentry.go | 199 ------------ logverification/verifyevent.go | 30 -- logverification/verifylist.go | 5 +- logverification/verifylist_test.go | 5 +- 21 files changed, 653 insertions(+), 504 deletions(-) create mode 100644 logverification/app/appentry.go rename logverification/{ => app}/assetsv2.go (53%) rename logverification/{verifyevent_test.go => app/assetsv2_test.go} (73%) create mode 100644 logverification/app/consts.go create mode 100644 logverification/app/consts_test.go rename logverification/{ => app}/eventsv1.go (71%) rename logverification/{ => app}/logversion0.go (89%) rename logverification/{ => app}/logversion0_test.go (97%) create mode 100644 logverification/app/massif.go create mode 100644 logverification/app/massifgetteroptions.go create mode 100644 logverification/app/massifoptions.go delete mode 100644 logverification/eventsv1_test.go delete mode 100644 logverification/verifiableentry.go delete mode 100644 logverification/verifyevent.go diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go new file mode 100644 index 0000000..a6605b4 --- /dev/null +++ b/logverification/app/appentry.go @@ -0,0 +1,290 @@ +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" +) + +/** + * 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 +} + +// 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 + + // MerkleLogConfirm used to define information about the log seal + MerkleLogConfirm *assets.MerkleLogConfirm +} + +// NewAppEntry creates a new app entry entry +func NewAppEntry( + appId string, + logId []byte, + extraBytes []byte, + mmrEntryFields *MMREntryFields, + merklelogCommit *assets.MerkleLogCommit, +) *AppEntry { + + verifiableLogEntry := &AppEntry{ + AppId: appId, + LogId: logId, + ExtraBytes: extraBytes, + MMREntryFields: mmrEntryFields, + MerkleLogCommit: merklelogCommit, + } + + return verifiableLogEntry +} + +// 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 { + return ae.MerkleLogCommit.Index +} + +// 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 (ve *AppEntry) 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 +} + +// 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 + +} + +// 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, +// logverification.WithMassifTenantId("tenant/foo"), +// logverification.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.massifReader != nil { + massifReader = massifOptions.massifReader + } 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/assetsv2.go b/logverification/app/assetsv2.go similarity index 53% rename from logverification/assetsv2.go rename to logverification/app/assetsv2.go index 73e3778..637412c 100644 --- a/logverification/assetsv2.go +++ b/logverification/app/assetsv2.go @@ -1,12 +1,13 @@ -package logverification +package app import ( + "crypto/sha256" "encoding/json" - "fmt" "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" ) @@ -15,14 +16,14 @@ import ( * 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 +// AssetsV2AppEntry is the assetsv2 app provided data for a corresponding log entry. +type AssetsV2AppEntry struct { + *AppEntry } -// 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) { +// 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) ([]AssetsV2AppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -33,9 +34,9 @@ func NewVerifiableAssetsV2Events(eventsJson []byte) ([]VerifiableAssetsV2Event, return nil, err } - events := []VerifiableAssetsV2Event{} + events := []AssetsV2AppEntry{} for _, eventJson := range eventListJson.Events { - verifiableEvent, err := NewVerifiableAssetsV2Event(eventJson) + verifiableEvent, err := NewAssetsV2AppEntry(eventJson) if err != nil { return nil, err } @@ -51,9 +52,9 @@ func NewVerifiableAssetsV2Events(eventsJson []byte) ([]VerifiableAssetsV2Event, 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) { +// 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 @@ -86,8 +87,8 @@ func NewVerifiableAssetsV2Event(eventJson []byte) (*VerifiableAssetsV2Event, err return nil, err } - return &VerifiableAssetsV2Event{ - VerifiableLogEntry: VerifiableLogEntry{ + return &AssetsV2AppEntry{ + AppEntry: &AppEntry{ AppId: entry.Identity, LogId: logId[:], MMREntryFields: &MMREntryFields{ @@ -100,13 +101,14 @@ func NewVerifiableAssetsV2Event(eventJson []byte) (*VerifiableAssetsV2Event, err }, nil } -// MMREntry gets the MMR Entry from the VerifiableAssetsV2Event +// 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 (ve *VerifiableAssetsV2Event) MMREntry() ([]byte, error) { +func (ve *AssetsV2AppEntry) MMREntry() ([]byte, error) { hasher := LogVersion0Hasher{} eventHash, err := hasher.HashEvent(ve.MMREntryFields.SerializedBytes) if err != nil { @@ -116,28 +118,59 @@ func (ve *VerifiableAssetsV2Event) MMREntry() ([]byte, error) { return eventHash, nil } -// MMRSalt gets the MMR Salt, which is the datatrails provided fields included on the MMR Entry. +// 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 (ve *VerifiableAssetsV2Event) MMRSalt() ([]byte, error) { +func (ve *AssetsV2AppEntry) 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 +// 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 // -// as a tenant identity. -func (ve *VerifiableAssetsV2Event) LogTenant() (string, error) { +// 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...) - logTenantUuid, err := uuid.FromBytes(ve.LogId) if err != nil { - return "", err + return false, err } - return fmt.Sprintf("tenant/%s", logTenantUuid.String()), nil + proof, err := ae.Proof(WithMassifContext(massif)) + if err != nil { + return false, err + } + return ae.VerifyProof(proof, WithMassifContext(massif)) } -// GetVerifiableLogEntry gets the verifiable log entry -func (ve *VerifiableAssetsV2Event) GetVerifiableLogEntry() *VerifiableLogEntry { - return &ve.VerifiableLogEntry +// GetAppEntry gets the generic app entry. +func (ve *AssetsV2AppEntry) GetAppEntry() *AppEntry { + return ve.AppEntry } 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 71% rename from logverification/eventsv1.go rename to logverification/app/eventsv1.go index 1ee4638..fed0332 100644 --- a/logverification/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -1,8 +1,7 @@ -package logverification +package app import ( "encoding/json" - "fmt" "sort" "strings" @@ -25,14 +24,14 @@ const ( 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) ([]EventsV1AppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -43,9 +42,9 @@ func NewVerifiableEventsV1Events(eventsJson []byte, logTenant string) ([]Verifia return nil, err } - events := []VerifiableEventsV1Event{} + events := []EventsV1AppEntry{} for _, eventJson := range eventListJson.Events { - verifiableEvent, err := NewVerifiableEventsV1Event(eventJson, logTenant) + verifiableEvent, err := NewEventsV1AppEntry(eventJson, logTenant) if err != nil { return nil, err } @@ -63,7 +62,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 +117,16 @@ func NewVerifiableEventsV1Event(eventJson []byte, logTenant string, opts ...Veri return nil, err } - verifableLogEntryOptions := ParseVerifableLogEntryOptions() - - return &VerifiableEventsV1Event{ - VerifiableLogEntry: VerifiableLogEntry{ + 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 } @@ -157,20 +153,7 @@ 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 -} - // GetVerifiableLogEntry gets the verifiable log entry -func (ve *VerifiableEventsV1Event) GetVerifiableLogEntry() *VerifiableLogEntry { - return &ve.VerifiableLogEntry +func (ve *EventsV1AppEntry) GetVerifiableLogEntry() *AppEntry { + return ve.AppEntry } 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..a2bf5ff 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.MerkleLogCommit.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..eece2d6 --- /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, massifReader MassifGetter, tenantId string, massifHeight uint8) (*massifs.MassifContext, error) { + + massifIndex := massifs.MassifIndexFromMMRIndex(massifHeight, mmrIndex) + + ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) + defer cancel() + + massif, err := massifReader.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..9904850 --- /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 + + massifReader 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.massifReader = 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/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..d2e9325 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,7 +15,7 @@ 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.AssetsV2AppEntry) (uint64, uint64) { lowerBoundMMRIndex := sortedEvents[0].MerkleLogCommit.Index lowerBoundLeafIndex := mmr.LeafCount(lowerBoundMMRIndex+1) - 1 // Note: LeafCount takes an mmrIndex here not a size diff --git a/logverification/massif.go b/logverification/massif.go index e3326c1..b01814f 100644 --- a/logverification/massif.go +++ b/logverification/massif.go @@ -7,6 +7,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" ) @@ -18,6 +19,12 @@ 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. @@ -38,12 +45,12 @@ func Massif(mmrIndex uint64, massifReader MassifGetter, tenantId string, massifH // 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) { +func MassifFromEvent(verifiableEvent *app.AssetsV2AppEntry, reader azblob.Reader, options ...MassifOption) (*massifs.MassifContext, error) { massifOptions := ParseMassifOptions(options...) - massifHeight := massifOptions.massifHeight + massifHeight := massifOptions.MassifHeight // if tenant ID is not supplied, find it based on the given eventJson - tenantId := massifOptions.tenantId + tenantId := massifOptions.TenantId if tenantId == "" { var err error @@ -63,7 +70,7 @@ 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") } 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..449f7e3 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 @@ -17,20 +21,20 @@ var ( // Validate performs basic validation on the VerifiableEvent, ensuring that critical fields // are present. -func (e *VerifiableAssetsV2Event) Validate() error { - if e.AppId == "" { +func Validate(appEntry *app.AssetsV2AppEntry) error { + if appEntry.AppId == "" { return ErrNonEmptyEventIDRequired } - if len(e.LogId) == 0 { + if len(appEntry.LogId) == 0 { return ErrNonEmptyTenantIDRequired } - if e.MerkleLogCommit == nil { + if appEntry.MerkleLogCommit == nil { return ErrCommitEntryRequired } - if e.MerkleLogCommit.Idtimestamp == "" { + if appEntry.MerkleLogCommit.Idtimestamp == "" { return ErrIdTimestampRequired } diff --git a/logverification/validation_test.go b/logverification/validation_test.go index 4153f5d..aa92b94 100644 --- a/logverification/validation_test.go +++ b/logverification/validation_test.go @@ -4,6 +4,7 @@ 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" ) @@ -12,7 +13,7 @@ func TestVerifiableEvent_Validate(t *testing.T) { type fields struct { AppId string LogId []byte - MMREntryFields *MMREntryFields + MMREntryFields *app.MMREntryFields MerkleLogCommit *assets.MerkleLogCommit } @@ -86,8 +87,8 @@ func TestVerifiableEvent_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := &VerifiableAssetsV2Event{ - VerifiableLogEntry: VerifiableLogEntry{ + e := &app.AssetsV2AppEntry{ + AppEntry: &app.AppEntry{ AppId: tt.fields.AppId, LogId: tt.fields.LogId, MMREntryFields: tt.fields.MMREntryFields, @@ -95,7 +96,7 @@ func TestVerifiableEvent_Validate(t *testing.T) { }, } - 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 59e7f42..0000000 --- a/logverification/verifiableentry.go +++ /dev/null @@ -1,199 +0,0 @@ -package logverification - -import ( - "context" - "crypto/sha256" - "encoding/binary" - "fmt" - - "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" - "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 -) - -type MassifGetter interface { - GetMassif( - ctx context.Context, tenantIdentity string, massifIndex uint64, opts ...massifs.ReaderOption, - ) (massifs.MassifContext, error) -} - -// 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 MassifGetter, 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()) - - return Massif(vle.MerkleLogCommit.Index, reader, 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 MassifGetter, 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/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 f9b415f..d418763 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" ) @@ -169,7 +170,7 @@ var ( * 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, eventList []app.AssetsV2AppEntry, options ...VerifyOption) ([]uint64, error) { verifyOptions := ParseOptions(options...) @@ -237,7 +238,7 @@ func VerifyList(reader azblob.Reader, eventList []VerifiableAssetsV2Event, optio func VerifyEventInList( hasher hash.Hash, leafIndex uint64, - event VerifiableAssetsV2Event, + event app.AssetsV2AppEntry, reader massifs.MassifReader, massifContext *massifs.MassifContext, tenantID string, diff --git a/logverification/verifylist_test.go b/logverification/verifylist_test.go index ed80d50..789c1e0 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.AssetsV2AppEntry { eventJsonList := serializeTestEvents(t, events) - result, err := NewVerifiableAssetsV2Events(eventJsonList) + result, err := app.NewAssetsV2AppEntries(eventJsonList) require.NoError(t, err) return result From b0c1eadadf7939c0291369dbfe8a13b5c9395bac Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 11:42:09 +0000 Subject: [PATCH 06/18] fixup2 --- logverification/app/eventsv1.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logverification/app/eventsv1.go b/logverification/app/eventsv1.go index fed0332..32b0410 100644 --- a/logverification/app/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -153,7 +153,7 @@ func NewEventsV1ExtraBytes(originTenant string) ([]byte, error) { return extraBytes, nil } -// GetVerifiableLogEntry gets the verifiable log entry -func (ve *EventsV1AppEntry) GetVerifiableLogEntry() *AppEntry { +// GetAppEntry gets the verifiable app entry +func (ve *EventsV1AppEntry) GetAppEntry() *AppEntry { return ve.AppEntry } From 38bd9c05a5acc16a563fe8d1859caee3398f53ce Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 11:44:21 +0000 Subject: [PATCH 07/18] fixup --- logverification/app/assetsv2.go | 6 +++--- logverification/app/eventsv1.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/logverification/app/assetsv2.go b/logverification/app/assetsv2.go index 637412c..76cb919 100644 --- a/logverification/app/assetsv2.go +++ b/logverification/app/assetsv2.go @@ -23,7 +23,7 @@ type AssetsV2AppEntry struct { // 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) ([]AssetsV2AppEntry, error) { +func NewAssetsV2AppEntries(eventsJson []byte) ([]*AssetsV2AppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -34,14 +34,14 @@ func NewAssetsV2AppEntries(eventsJson []byte) ([]AssetsV2AppEntry, error) { return nil, err } - events := []AssetsV2AppEntry{} + events := []*AssetsV2AppEntry{} for _, eventJson := range eventListJson.Events { verifiableEvent, err := NewAssetsV2AppEntry(eventJson) 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. diff --git a/logverification/app/eventsv1.go b/logverification/app/eventsv1.go index 32b0410..9847554 100644 --- a/logverification/app/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -31,7 +31,7 @@ type EventsV1AppEntry struct { // 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) ([]EventsV1AppEntry, error) { +func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]*EventsV1AppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -42,14 +42,14 @@ func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]EventsV1AppEn return nil, err } - events := []EventsV1AppEntry{} + events := []*EventsV1AppEntry{} for _, eventJson := range eventListJson.Events { 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. From 91911bf00a66f3400800678c353996463804d26b Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 12:02:36 +0000 Subject: [PATCH 08/18] fixup --- logverification/app/appentry.go | 3 ++- logverification/massif.go | 23 ----------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index a6605b4..e246939 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -14,7 +14,8 @@ import ( ) /** - * Verifiable :Log Entry is a Log Entry that can be verified. + * 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: * diff --git a/logverification/massif.go b/logverification/massif.go index b01814f..4115e2c 100644 --- a/logverification/massif.go +++ b/logverification/massif.go @@ -5,8 +5,6 @@ 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" ) @@ -43,27 +41,6 @@ func Massif(mmrIndex uint64, massifReader MassifGetter, tenantId string, massifH 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 *app.AssetsV2AppEntry, 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) { From 1b4a324c45dea7d676ef17091019cb54f4cffe73 Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 14:08:18 +0000 Subject: [PATCH 09/18] fixup integration tests --- logverification/leafrange.go | 2 +- logverification/verifylist.go | 4 ++-- logverification/verifylist_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/logverification/leafrange.go b/logverification/leafrange.go index d2e9325..460ac41 100644 --- a/logverification/leafrange.go +++ b/logverification/leafrange.go @@ -15,7 +15,7 @@ import ( // 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 []app.AssetsV2AppEntry) (uint64, uint64) { +func LeafRange(sortedEvents []*app.AssetsV2AppEntry) (uint64, uint64) { lowerBoundMMRIndex := sortedEvents[0].MerkleLogCommit.Index lowerBoundLeafIndex := mmr.LeafCount(lowerBoundMMRIndex+1) - 1 // Note: LeafCount takes an mmrIndex here not a size diff --git a/logverification/verifylist.go b/logverification/verifylist.go index d418763..27dfaf8 100644 --- a/logverification/verifylist.go +++ b/logverification/verifylist.go @@ -170,7 +170,7 @@ var ( * to be included on. E.g. the public tenant * for public events. */ -func VerifyList(reader azblob.Reader, eventList []app.AssetsV2AppEntry, options ...VerifyOption) ([]uint64, error) { +func VerifyList(reader azblob.Reader, eventList []*app.AssetsV2AppEntry, options ...VerifyOption) ([]uint64, error) { verifyOptions := ParseOptions(options...) @@ -207,7 +207,7 @@ func VerifyList(reader azblob.Reader, eventList []app.AssetsV2AppEntry, options } - eventType, err := VerifyEventInList(hasher, leafIndex, event, massifReader, &massifContext, tenantId) + eventType, err := VerifyEventInList(hasher, leafIndex, *event, massifReader, &massifContext, tenantId) if err != nil { // NOTE: for now fail at the first sign of an EXCLUDED event. diff --git a/logverification/verifylist_test.go b/logverification/verifylist_test.go index 789c1e0..7755736 100644 --- a/logverification/verifylist_test.go +++ b/logverification/verifylist_test.go @@ -31,7 +31,7 @@ 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) []app.AssetsV2AppEntry { +func protoEventsToVerifiableEvents(t *testing.T, events []*assets.EventResponse) []*app.AssetsV2AppEntry { eventJsonList := serializeTestEvents(t, events) result, err := app.NewAssetsV2AppEntries(eventJsonList) require.NoError(t, err) From b76b56eddd90275849f43109ae6dfae61a59aff0 Mon Sep 17 00:00:00 2001 From: jgough Date: Mon, 13 Jan 2025 14:15:59 +0000 Subject: [PATCH 10/18] fixup --- logverification/app/appentry.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index e246939..9181a1b 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -170,20 +170,21 @@ func (ae *AppEntry) LogTenant() (string, error) { } -// 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, -// logverification.WithMassifTenantId("tenant/foo"), -// logverification.WithMassifHeight(14), -// ) +/** 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...) From 4fe80dee162b316dfd5f4b13d5f531a7a05e729d Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 10:16:44 +0000 Subject: [PATCH 11/18] fixup review comments --- logverification/app/appentry.go | 159 +++++++++++++++++++++-------- logverification/app/assetsv2.go | 21 ++-- logverification/app/eventsv1.go | 20 ++-- logverification/app/logversion0.go | 2 +- logverification/leafrange.go | 6 +- logverification/validation.go | 12 +-- logverification/validation_test.go | 23 ++--- logverification/verifylist.go | 142 +++++++++++++------------- logverification/verifylist_test.go | 4 +- 9 files changed, 225 insertions(+), 164 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index 9181a1b..aa6845d 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -46,14 +46,52 @@ const ( 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 + // 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 + // 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. @@ -63,23 +101,20 @@ type MMREntryFields struct { // 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 + // 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 + // 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 + // extraBytes are extrabytes provided by datatrails for the specific app + extraBytes []byte // MMREntryFields used to determine the MMR Entry - MMREntryFields *MMREntryFields + 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 + merkleLogCommit *assets.MerkleLogCommit } // NewAppEntry creates a new app entry entry @@ -91,15 +126,15 @@ func NewAppEntry( merklelogCommit *assets.MerkleLogCommit, ) *AppEntry { - verifiableLogEntry := &AppEntry{ - AppId: appId, - LogId: logId, - ExtraBytes: extraBytes, - MMREntryFields: mmrEntryFields, - MerkleLogCommit: merklelogCommit, + appEntry := &AppEntry{ + appID: appId, + logID: logId, + extraBytes: extraBytes, + mmrEntryFields: mmrEntryFields, + merkleLogCommit: merklelogCommit, } - return verifiableLogEntry + return appEntry } // MMREntry derives the mmr entry of the corresponding log entry from the app data. @@ -111,7 +146,7 @@ func (ae *AppEntry) MMREntry() ([]byte, error) { hasher := sha256.New() // domain - hasher.Write([]byte{ae.MMREntryFields.Domain}) + hasher.Write([]byte{ae.mmrEntryFields.domain}) // mmr salt mmrSalt, err := ae.MMRSalt() @@ -122,7 +157,7 @@ func (ae *AppEntry) MMREntry() ([]byte, error) { hasher.Write(mmrSalt) // serialized bytes - hasher.Write(ae.MMREntryFields.SerializedBytes) + hasher.Write(ae.mmrEntryFields.serializedBytes) return hasher.Sum(nil), nil @@ -130,7 +165,60 @@ func (ae *AppEntry) MMREntry() ([]byte, error) { // MMRIndex gets the mmr index of the corresponding log entry. func (ae *AppEntry) MMRIndex() uint64 { - return ae.MerkleLogCommit.Index + + 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. @@ -141,10 +229,10 @@ func (ve *AppEntry) MMRSalt() ([]byte, error) { mmrSalt := make([]byte, MMRSaltSize) - copy(mmrSalt[:24], ve.ExtraBytes) + copy(mmrSalt[:24], ve.extraBytes) // get the byte representation of idtimestamp - idTimestamp, _, err := massifs.SplitIDTimestampHex(ve.MerkleLogCommit.Idtimestamp) + idTimestamp, _, err := massifs.SplitIDTimestampHex(ve.merkleLogCommit.Idtimestamp) if err != nil { return nil, err } @@ -157,19 +245,6 @@ func (ve *AppEntry) MMRSalt() ([]byte, error) { return mmrSalt, nil } -// 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 - -} - /** 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: @@ -214,7 +289,7 @@ func (ae *AppEntry) Massif(options ...MassifGetterOption) (*massifs.MassifContex // 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) + logUuid, err := uuid.FromBytes(ae.logID) if err != nil { return nil, err } @@ -223,7 +298,7 @@ func (ae *AppEntry) Massif(options ...MassifGetterOption) (*massifs.MassifContex logIdentity = fmt.Sprintf("tenant/%s", logUuid.String()) } - return Massif(ae.MerkleLogCommit.Index, massifReader, logIdentity, massifHeight) + return Massif(ae.merkleLogCommit.Index, massifReader, logIdentity, massifHeight) } diff --git a/logverification/app/assetsv2.go b/logverification/app/assetsv2.go index 76cb919..8fb5038 100644 --- a/logverification/app/assetsv2.go +++ b/logverification/app/assetsv2.go @@ -23,7 +23,7 @@ type AssetsV2AppEntry struct { // 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) ([]*AssetsV2AppEntry, error) { +func NewAssetsV2AppEntries(eventsJson []byte) ([]VerifiableAppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -34,7 +34,7 @@ func NewAssetsV2AppEntries(eventsJson []byte) ([]*AssetsV2AppEntry, error) { return nil, err } - events := []*AssetsV2AppEntry{} + events := []VerifiableAppEntry{} for _, eventJson := range eventListJson.Events { verifiableEvent, err := NewAssetsV2AppEntry(eventJson) if err != nil { @@ -46,7 +46,7 @@ func NewAssetsV2AppEntries(eventsJson []byte) ([]*AssetsV2AppEntry, error) { // 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 @@ -89,14 +89,13 @@ func NewAssetsV2AppEntry(eventJson []byte) (*AssetsV2AppEntry, error) { 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 + 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, + merkleLogCommit: merkleLog.Commit, }, }, nil } @@ -110,7 +109,7 @@ func NewAssetsV2AppEntry(eventJson []byte) (*AssetsV2AppEntry, error) { // the serialization. func (ve *AssetsV2AppEntry) MMREntry() ([]byte, error) { hasher := LogVersion0Hasher{} - eventHash, err := hasher.HashEvent(ve.MMREntryFields.SerializedBytes) + eventHash, err := hasher.HashEvent(ve.mmrEntryFields.serializedBytes) if err != nil { return nil, err } diff --git a/logverification/app/eventsv1.go b/logverification/app/eventsv1.go index 9847554..997ba96 100644 --- a/logverification/app/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -31,7 +31,7 @@ type EventsV1AppEntry struct { // 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) ([]*EventsV1AppEntry, error) { +func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]VerifiableAppEntry, error) { // get the event list out of events eventListJson := struct { Events []json.RawMessage `json:"events"` @@ -42,7 +42,7 @@ func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]*EventsV1AppE return nil, err } - events := []*EventsV1AppEntry{} + events := []VerifiableAppEntry{} for _, eventJson := range eventListJson.Events { verifiableEvent, err := NewEventsV1AppEntry(eventJson, logTenant) if err != nil { @@ -54,7 +54,7 @@ func NewEventsV1AppEntries(eventsJson []byte, logTenant string) ([]*EventsV1AppE // 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 @@ -119,14 +119,14 @@ func NewEventsV1AppEntry(eventJson []byte, logTenant string) (*EventsV1AppEntry, return &EventsV1AppEntry{ AppEntry: &AppEntry{ - AppId: entry.Identity, - LogId: logId[:], - MMREntryFields: &MMREntryFields{ - Domain: byte(0), - SerializedBytes: serializedBytes, + appID: entry.Identity, + logID: logId[:], + mmrEntryFields: &MMREntryFields{ + domain: byte(0), + serializedBytes: serializedBytes, }, - ExtraBytes: extraBytes, - MerkleLogCommit: merkleLogCommit, + extraBytes: extraBytes, + merkleLogCommit: merkleLogCommit, }, }, nil } diff --git a/logverification/app/logversion0.go b/logverification/app/logversion0.go index a2bf5ff..9acaacf 100644 --- a/logverification/app/logversion0.go +++ b/logverification/app/logversion0.go @@ -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(assetsAppEntry.MerkleLogCommit.Idtimestamp) + idCommitted, _, err := massifs.SplitIDTimestampHex(assetsAppEntry.IDTimestamp()) if err != nil { return nil, err } diff --git a/logverification/leafrange.go b/logverification/leafrange.go index 460ac41..bdeddfb 100644 --- a/logverification/leafrange.go +++ b/logverification/leafrange.go @@ -15,12 +15,12 @@ import ( // 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 []*app.AssetsV2AppEntry) (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/validation.go b/logverification/validation.go index 449f7e3..1a8ed9f 100644 --- a/logverification/validation.go +++ b/logverification/validation.go @@ -21,20 +21,16 @@ var ( // Validate performs basic validation on the VerifiableEvent, ensuring that critical fields // are present. -func Validate(appEntry *app.AssetsV2AppEntry) error { - if appEntry.AppId == "" { +func Validate(appEntry app.AppEntryGetter) error { + if appEntry.AppID() == "" { return ErrNonEmptyEventIDRequired } - if len(appEntry.LogId) == 0 { + if len(appEntry.LogID()) == 0 { return ErrNonEmptyTenantIDRequired } - if appEntry.MerkleLogCommit == nil { - return ErrCommitEntryRequired - } - - if appEntry.MerkleLogCommit.Idtimestamp == "" { + if appEntry.IDTimestamp() == "" { return ErrIdTimestampRequired } diff --git a/logverification/validation_test.go b/logverification/validation_test.go index aa92b94..d48cb71 100644 --- a/logverification/validation_test.go +++ b/logverification/validation_test.go @@ -62,16 +62,7 @@ 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", @@ -88,12 +79,12 @@ func TestVerifiableEvent_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := &app.AssetsV2AppEntry{ - AppEntry: &app.AppEntry{ - AppId: tt.fields.AppId, - LogId: tt.fields.LogId, - MMREntryFields: tt.fields.MMREntryFields, - MerkleLogCommit: tt.fields.MerkleLogCommit, - }, + AppEntry: app.NewAppEntry( + tt.fields.AppId, + tt.fields.LogId, + []byte{}, + tt.fields.MMREntryFields, + tt.fields.MerkleLogCommit), } err := Validate(e) diff --git a/logverification/verifylist.go b/logverification/verifylist.go index 27dfaf8..c85b017 100644 --- a/logverification/verifylist.go +++ b/logverification/verifylist.go @@ -83,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: * @@ -122,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 []*app.AssetsV2AppEntry, options ...VerifyOption) ([]uint64, error) { +func VerifyList(reader azblob.Reader, appEntries []app.VerifiableAppEntry, options ...VerifyOption) ([]uint64, error) { verifyOptions := ParseOptions(options...) @@ -179,19 +179,19 @@ func VerifyList(reader azblob.Reader, eventList []*app.AssetsV2AppEntry, options 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 @@ -200,14 +200,14 @@ func VerifyList(reader azblob.Reader, eventList []*app.AssetsV2AppEntry, options // 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. @@ -217,7 +217,7 @@ func VerifyList(reader azblob.Reader, eventList []*app.AssetsV2AppEntry, options } // 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 @@ -225,55 +225,55 @@ func VerifyList(reader azblob.Reader, eventList []*app.AssetsV2AppEntry, options 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 app.AssetsV2AppEntry, + 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 @@ -290,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 @@ -321,16 +321,16 @@ 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) @@ -344,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 7755736..67ec32b 100644 --- a/logverification/verifylist_test.go +++ b/logverification/verifylist_test.go @@ -31,7 +31,7 @@ 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) []*app.AssetsV2AppEntry { +func protoEventsToVerifiableEvents(t *testing.T, events []*assets.EventResponse) []app.VerifiableAppEntry { eventJsonList := serializeTestEvents(t, events) result, err := app.NewAssetsV2AppEntries(eventJsonList) require.NoError(t, err) @@ -133,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 From 1036377575800ff01ef550222cd62129b9ad6868 Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 11:03:27 +0000 Subject: [PATCH 12/18] fixup --- logverification/app/appentry.go | 6 ++++-- logverification/app/eventsv1.go | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index aa6845d..aa2bcf4 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -43,6 +43,8 @@ import ( const ( MMRSaltSize = 32 + ExtraBytesSize = 24 + IDTimestapSizeBytes = 8 ) @@ -229,7 +231,7 @@ func (ve *AppEntry) MMRSalt() ([]byte, error) { mmrSalt := make([]byte, MMRSaltSize) - copy(mmrSalt[:24], ve.extraBytes) + copy(mmrSalt[:ExtraBytesSize], ve.extraBytes) // get the byte representation of idtimestamp idTimestamp, _, err := massifs.SplitIDTimestampHex(ve.merkleLogCommit.Idtimestamp) @@ -240,7 +242,7 @@ func (ve *AppEntry) MMRSalt() ([]byte, error) { idTimestampBytes := make([]byte, IDTimestapSizeBytes) binary.BigEndian.PutUint64(idTimestampBytes, idTimestamp) - copy(mmrSalt[24:], idTimestampBytes) + copy(mmrSalt[ExtraBytesSize:], idTimestampBytes) return mmrSalt, nil } diff --git a/logverification/app/eventsv1.go b/logverification/app/eventsv1.go index 997ba96..393331a 100644 --- a/logverification/app/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -20,8 +20,6 @@ const ( // EventsV1AppDomain is the events v1 app domain EventsV1AppDomain = byte(1) - - ExtraBytesSize = 24 ) // EventsV1AppEntry is the assetsv2 app provided data for a corresponding log entry. From 75bccc7cef7e55c6ab7083464d090393b9151f83 Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 11:06:07 +0000 Subject: [PATCH 13/18] fixup --- logverification/app/appentry.go | 4 ++-- logverification/app/massif.go | 4 ++-- logverification/app/massifgetteroptions.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index aa2bcf4..9022677 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -273,8 +273,8 @@ func (ae *AppEntry) Massif(options ...MassifGetterOption) (*massifs.MassifContex var massifReader MassifGetter // now check if we have a massif reader - if massifOptions.massifReader != nil { - massifReader = massifOptions.massifReader + if massifOptions.massifGetter != nil { + massifReader = massifOptions.massifGetter } else { // otherwise use azblob reader to get it if massifOptions.azblobReader == nil { diff --git a/logverification/app/massif.go b/logverification/app/massif.go index eece2d6..863d1b2 100644 --- a/logverification/app/massif.go +++ b/logverification/app/massif.go @@ -25,14 +25,14 @@ type MassifGetter interface { // Massif gets the massif (blob) that contains the given mmrIndex, from azure blob storage // // defined by the azblob configuration. -func Massif(mmrIndex uint64, massifReader MassifGetter, tenantId string, massifHeight uint8) (*massifs.MassifContext, error) { +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 := massifReader.GetMassif(ctx, tenantId, massifIndex) + massif, err := massifGetter.GetMassif(ctx, tenantId, massifIndex) if err != nil { return nil, err } diff --git a/logverification/app/massifgetteroptions.go b/logverification/app/massifgetteroptions.go index 9904850..8feac43 100644 --- a/logverification/app/massifgetteroptions.go +++ b/logverification/app/massifgetteroptions.go @@ -15,7 +15,7 @@ type MassifGetterOptions struct { azblobReader azblob.Reader - massifReader MassifGetter + massifGetter MassifGetter massifContext *massifs.MassifContext } @@ -33,7 +33,7 @@ func WithMassifContext(massifContext *massifs.MassifContext) MassifGetterOption // to obtain the massif for the app entry. func WithMassifReader(massifReader MassifGetter, massifOpts ...MassifOption) MassifGetterOption { return func(mo *MassifGetterOptions) { - mo.massifReader = massifReader + mo.massifGetter = massifReader opts := ParseMassifOptions(massifOpts...) mo.MassifOptions = &opts } From a02400882e826da4959200acd6859c0f19cb546f Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 11:06:58 +0000 Subject: [PATCH 14/18] fixup --- logverification/validation_test.go | 48 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/logverification/validation_test.go b/logverification/validation_test.go index d48cb71..2423970 100644 --- a/logverification/validation_test.go +++ b/logverification/validation_test.go @@ -11,10 +11,10 @@ import ( func TestVerifiableEvent_Validate(t *testing.T) { type fields struct { - AppId string - LogId []byte - MMREntryFields *app.MMREntryFields - MerkleLogCommit *assets.MerkleLogCommit + appID string + logID []byte + mmrEntryFields *app.MMREntryFields + merkleLogCommit *assets.MerkleLogCommit } tests := []struct { @@ -25,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", }, @@ -38,10 +38,10 @@ 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", }, @@ -51,10 +51,10 @@ func TestVerifiableEvent_Validate(t *testing.T) { { 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", }, @@ -65,10 +65,10 @@ func TestVerifiableEvent_Validate(t *testing.T) { // 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: "", }, @@ -80,11 +80,11 @@ func TestVerifiableEvent_Validate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { e := &app.AssetsV2AppEntry{ AppEntry: app.NewAppEntry( - tt.fields.AppId, - tt.fields.LogId, + tt.fields.appID, + tt.fields.logID, []byte{}, - tt.fields.MMREntryFields, - tt.fields.MerkleLogCommit), + tt.fields.mmrEntryFields, + tt.fields.merkleLogCommit), } err := Validate(e) From 8a6168a6df37113925b517b8b2585a93f1a74bcd Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 11:08:25 +0000 Subject: [PATCH 15/18] fixup --- logverification/app/appentry.go | 6 +++--- logverification/app/assetsv2.go | 11 +++-------- logverification/app/eventsv1.go | 5 ----- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/logverification/app/appentry.go b/logverification/app/appentry.go index 9022677..7b8c419 100644 --- a/logverification/app/appentry.go +++ b/logverification/app/appentry.go @@ -227,14 +227,14 @@ func (ae *AppEntry) Domain() byte { // 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 (ve *AppEntry) MMRSalt() ([]byte, error) { +func (ae *AppEntry) MMRSalt() ([]byte, error) { mmrSalt := make([]byte, MMRSaltSize) - copy(mmrSalt[:ExtraBytesSize], ve.extraBytes) + copy(mmrSalt[:ExtraBytesSize], ae.extraBytes) // get the byte representation of idtimestamp - idTimestamp, _, err := massifs.SplitIDTimestampHex(ve.merkleLogCommit.Idtimestamp) + idTimestamp, _, err := massifs.SplitIDTimestampHex(ae.merkleLogCommit.Idtimestamp) if err != nil { return nil, err } diff --git a/logverification/app/assetsv2.go b/logverification/app/assetsv2.go index 8fb5038..f482a15 100644 --- a/logverification/app/assetsv2.go +++ b/logverification/app/assetsv2.go @@ -107,9 +107,9 @@ func NewAssetsV2AppEntry(eventJson []byte) (*AssetsV2AppEntry, error) { // // NOTE: the original event json isn't really serializedbytes, but the LogVersion0 hasher includes // the serialization. -func (ve *AssetsV2AppEntry) MMREntry() ([]byte, error) { +func (ae *AssetsV2AppEntry) MMREntry() ([]byte, error) { hasher := LogVersion0Hasher{} - eventHash, err := hasher.HashEvent(ve.mmrEntryFields.serializedBytes) + eventHash, err := hasher.HashEvent(ae.mmrEntryFields.serializedBytes) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func (ve *AssetsV2AppEntry) MMREntry() ([]byte, error) { // MMRSalt is the datatrails provided fields included on the MMR Entry. // // For assetsv2 events this is empty. -func (ve *AssetsV2AppEntry) MMRSalt() ([]byte, error) { +func (ae *AssetsV2AppEntry) MMRSalt() ([]byte, error) { return []byte{}, nil // MMRSalt is always empty for assetsv2 events } @@ -168,8 +168,3 @@ func (ae *AssetsV2AppEntry) VerifyInclusion(options ...MassifGetterOption) (bool return ae.VerifyProof(proof, WithMassifContext(massif)) } - -// GetAppEntry gets the generic app entry. -func (ve *AssetsV2AppEntry) GetAppEntry() *AppEntry { - return ve.AppEntry -} diff --git a/logverification/app/eventsv1.go b/logverification/app/eventsv1.go index 393331a..4bad19a 100644 --- a/logverification/app/eventsv1.go +++ b/logverification/app/eventsv1.go @@ -150,8 +150,3 @@ func NewEventsV1ExtraBytes(originTenant string) ([]byte, error) { return extraBytes, nil } - -// GetAppEntry gets the verifiable app entry -func (ve *EventsV1AppEntry) GetAppEntry() *AppEntry { - return ve.AppEntry -} From d68b61d7e06603ca401090e7af9e50464a373344 Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 11:10:17 +0000 Subject: [PATCH 16/18] fixup --- logverification/validation.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logverification/validation.go b/logverification/validation.go index 1a8ed9f..a045aec 100644 --- a/logverification/validation.go +++ b/logverification/validation.go @@ -13,17 +13,18 @@ import ( // 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 Validate(appEntry app.AppEntryGetter) error { if appEntry.AppID() == "" { - return ErrNonEmptyEventIDRequired + return ErrNonEmptyAppIDRequired } if len(appEntry.LogID()) == 0 { From a8f7bb40151811fa8e9484c97b6dcbb5bedce499 Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 12:07:05 +0000 Subject: [PATCH 17/18] fixup add more unit tests --- logverification/app/appentry_test.go | 326 +++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 logverification/app/appentry_test.go 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) + }) + } +} From 8d703a6e3b9f12c4c1a4ad5d84aca223bc82e3e5 Mon Sep 17 00:00:00 2001 From: jgough Date: Tue, 14 Jan 2025 12:07:45 +0000 Subject: [PATCH 18/18] fixup --- logverification/validation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logverification/validation_test.go b/logverification/validation_test.go index 2423970..f49626c 100644 --- a/logverification/validation_test.go +++ b/logverification/validation_test.go @@ -46,7 +46,7 @@ func TestVerifiableEvent_Validate(t *testing.T) { Idtimestamp: "018fa97ef269039b00", }, }, - expectedErr: ErrNonEmptyEventIDRequired, + expectedErr: ErrNonEmptyAppIDRequired, }, { name: "missing tenant identity returns specific error",