diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..68469d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: ci +on: + push: + release: + types: [published] + +jobs: + test: + name: Code quality and unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup go-task + uses: arduino/setup-task@v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + token: ${{ secrets.GITHUB_TOKEN }} + cache-dependency-path: | + datatrails-common-api/go.sum + + - name: Install Go quality tools + run: | + go install golang.org/x/tools/cmd/goimports@latest + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${PWD}/bin" v1.55.2 + echo "${PWD}/bin" >> $GITHUB_PATH + + - name: Code Quality + run: | + # Note: it is by design that we don't use the builder + task codeqa + + - name: Test + run: | + # Note: it is by design that we don't use the builder + task test \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index 03bf037..5de32cf 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,7 +23,7 @@ includes: tasks: - qa-basic: + codeqa: desc: | format the source and run all the quality checks (Does not run unit tests) @@ -39,4 +39,4 @@ tasks: test: desc: run the tests cmds: - - task: codeqa:unit-tests + - task: codeqa:test diff --git a/go.mod b/go.mod index fd339ab..5dcbe9d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/datatrails/go-datatrails-simplehash go 1.21 require ( - github.com/datatrails/go-datatrails-common-api-gen v0.2.0 + github.com/datatrails/go-datatrails-common-api-gen v0.3.7 github.com/golang/protobuf v1.5.3 github.com/zeebo/bencode v1.0.0 google.golang.org/protobuf v1.31.0 @@ -15,7 +15,7 @@ require ( github.com/cilium/ebpf v0.12.3 // indirect github.com/containerd/cgroups/v3 v3.0.2 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/datatrails/go-datatrails-common v0.9.0 // indirect + github.com/datatrails/go-datatrails-common v0.10.2 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -30,10 +30,10 @@ require ( go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/grpc v1.59.0 // indirect ) diff --git a/go.sum b/go.sum index e6fb766..37822c9 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,10 @@ github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKk github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/datatrails/go-datatrails-common v0.9.0 h1:kdDTPfi+GOP6Q0C2XRET33MfNxPNYw3QXcIstFnxqeE= -github.com/datatrails/go-datatrails-common v0.9.0/go.mod h1:LsPfbYoTEEdPnANm0+seLRX2OO7c5yF7tZw8499prAM= -github.com/datatrails/go-datatrails-common-api-gen v0.2.0 h1:2pVpQSpStPsaUCPM3vNqEtNJu4VZeXYrZTvZUlPeDwk= -github.com/datatrails/go-datatrails-common-api-gen v0.2.0/go.mod h1:SiE9nDEhmD+B7tAbniH0bTl0vI4cTfnFPa5aUPq3/kE= +github.com/datatrails/go-datatrails-common v0.10.2 h1:OnvayMGpeia3wHMrj26njuQlpsOvGjYOsHChvj6ErtQ= +github.com/datatrails/go-datatrails-common v0.10.2/go.mod h1:LsPfbYoTEEdPnANm0+seLRX2OO7c5yF7tZw8499prAM= +github.com/datatrails/go-datatrails-common-api-gen v0.3.7 h1:TEbf6HwjXsiKUZrYgStKroJz+O9QA1wIRZPIKPd1Crc= +github.com/datatrails/go-datatrails-common-api-gen v0.3.7/go.mod h1:qGRrvhR3DCw90EYOQLtWvvNtUZbcmAKYAkgr2/yfOKI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -68,17 +68,17 @@ golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUU golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c h1:3kC/TjQ+xzIblQv39bCOyRk8fbEeJcDHwbyxPUU2BpA= -golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k= +google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 h1:ZcOkrmX74HbKFYnpPY8Qsw93fC29TbJXspYKaBkSXDQ= +google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/simplehash/hasher.go b/simplehash/hasher.go new file mode 100644 index 0000000..4cadef0 --- /dev/null +++ b/simplehash/hasher.go @@ -0,0 +1,69 @@ +package simplehash + +import ( + "crypto/sha256" + "hash" + + v2assets "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/datatrails/go-datatrails-common-api-gen/marshalers/simpleoneof" +) + +type Hasher struct { + hasher hash.Hash + marshaler *simpleoneof.Marshaler +} + +func NewHasher() Hasher { + h := Hasher{ + hasher: sha256.New(), + marshaler: NewEventMarshaler(), + } + return h +} + +func (h *Hasher) Sum(b []byte) []byte { return h.hasher.Sum(b) } + +// Reset resets the hasher state +// This is only useful in combination with WithAccumulate +func (h *Hasher) Reset() { h.hasher.Reset() } + +// NewEventMarshaler creates a flat marshaler to transform events to api format. +// +// otherwise attributes look like this: {"foo":{"str_val": "bar"}} instead of {"foo": "bar"} +// this mimics the public list events api response, so minimises changes to the +// public api response, to reproduce the anchor +func NewEventMarshaler() *simpleoneof.Marshaler { + return v2assets.NewFlatMarshalerForEvents() +} + +func (h *Hasher) applyEventOptions(o HashOptions, event *v2assets.EventResponse) { + if o.publicFromPermissioned { + PublicFromPermissionedEvent(event) + } + + // force the commited time in the hash. only useful to the service that is + // actually doing the committing. public consumers only ever see confirmed + // events with the timestamp already in place. + if o.committed != nil { + event.TimestampCommitted = o.committed + } +} + +func (h *Hasher) applyHashingOptions(o HashOptions) { + + // By default, one hash at at time with a reset. + if !o.accumulateHash { + h.hasher.Reset() + } + + // If the prefix is provided it must be first. + if len(o.prefix) != 0 { + h.hasher.Write(o.prefix) + } + + // If the idcommitted is provided, add it to the hash immediately before the + // event data. + if o.idcommitted != nil { + h.hasher.Write(o.idcommitted) + } +} diff --git a/simplehash/options.go b/simplehash/options.go index 18d3deb..84b93cd 100644 --- a/simplehash/options.go +++ b/simplehash/options.go @@ -2,6 +2,7 @@ package simplehash import ( "encoding/binary" + "errors" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -12,7 +13,6 @@ import ( type HashOptions struct { accumulateHash bool publicFromPermissioned bool - asConfirmed bool prefix []byte committed *timestamppb.Timestamp idcommitted []byte @@ -20,6 +20,10 @@ type HashOptions struct { type HashOption func(*HashOptions) +var ( + ErrInvalidOption = errors.New("option not supported by this method") +) + // WithIDCommitted includes the snowflakeid unique commitment timestamp in the hash // idcommitted is never (legitimately) zero func WithIDCommitted(idcommitted uint64) HashOption { @@ -56,9 +60,3 @@ func WithPublicFromPermissioned() HashOption { o.publicFromPermissioned = true } } - -func WithAsConfirmed() HashOption { - return func(o *HashOptions) { - o.asConfirmed = true - } -} diff --git a/simplehash/publicevent.go b/simplehash/publicevent.go new file mode 100644 index 0000000..0cbfb7e --- /dev/null +++ b/simplehash/publicevent.go @@ -0,0 +1,10 @@ +package simplehash + +import v2assets "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + +// PublicFromPermissionedEvent translates the permissioned event and asset identities to +// their public counter parts. +func PublicFromPermissionedEvent(event *v2assets.EventResponse) { + event.Identity = v2assets.PublicIdentityFromPermissioned(event.Identity) + event.AssetIdentity = v2assets.PublicIdentityFromPermissioned(event.AssetIdentity) +} diff --git a/simplehash/schemav2.go b/simplehash/schemav2.go index b6a9eec..c023cb8 100644 --- a/simplehash/schemav2.go +++ b/simplehash/schemav2.go @@ -3,9 +3,7 @@ package simplehash // Public go lang implementation of the simplehash DataTrails event encoding scheme import ( - "crypto/sha256" "encoding/json" - "errors" "fmt" "hash" @@ -14,27 +12,36 @@ import ( "github.com/zeebo/bencode" ) -var ( - ErrInvalidOption = errors.New("option not supported by this method") -) +// V2Event is a struct that contains ONLY the event fields we want to hash for schema v2 +type V2Event struct { + Identity string `json:"identity"` + AssetIdentity string `json:"asset_identity"` + EventAttributes map[string]any `json:"event_attributes"` + AssetAttributes map[string]any `json:"asset_attributes"` + Operation string `json:"operation"` + Behaviour string `json:"behaviour"` + TimestampDeclared string `json:"timestamp_declared"` + TimestampAccepted string `json:"timestamp_accepted"` + TimestampCommitted string `json:"timestamp_committed"` + PrincipalAccepted map[string]any `json:"principal_accepted"` + PrincipalDeclared map[string]any `json:"principal_declared"` + ConfirmationStatus string `json:"confirmation_status"` + From string `json:"from"` + TenantIdentity string `json:"tenant_identity"` +} type HasherV2 struct { - hasher hash.Hash - marshaler *simpleoneof.Marshaler + Hasher } func NewHasherV2() HasherV2 { + h := HasherV2{ - hasher: sha256.New(), - marshaler: NewEventMarshaler(), + Hasher: NewHasher(), } return h } -// Reset resets the hasher state -// This is only useful in combination with WithAccumulate -func (h *HasherV2) Reset() { h.hasher.Reset() } - // HashEvent hashes a single event according to the canonical simple hash event // format available to api consumers. The source event is in the grpc proto buf // format. GRPC endpoints are not presently exposed by the platform. @@ -50,10 +57,6 @@ func (h *HasherV2) Reset() { h.hasher.Reset() } // boundaries. // - WithPublicFromPermissioned should be set if the event is the // permissioned (owner) counter part of a public attestation. -// - WithAsConfirmed should be set if the caller is implementing CONFIRMATION -// as part of an evidence subsystem implementation. The expectation is that -// the caller has a PENDING record to hand, and is in the process of -// creating the CONFIRMED record. It is the CONFIRMED record that needs to // be publicly verifiable. func (h *HasherV2) HashEvent(event *v2assets.EventResponse, opts ...HashOption) error { o := HashOptions{} @@ -61,34 +64,7 @@ func (h *HasherV2) HashEvent(event *v2assets.EventResponse, opts ...HashOption) opt(&o) } - var err error - - // By default, one hash at at time with a reset. - if !o.accumulateHash { - h.hasher.Reset() - } - - if len(o.prefix) != 0 { - h.hasher.Write(o.prefix) - } - - if o.publicFromPermissioned { - PublicFromPermissionedEvent(event) - } - - // If the caller is responsible for evidence confirmation they will have a - // pending event in their hand. But ultimately it is the confirmed record - // that is evidential and subject to public verification. - if o.asConfirmed { - event.ConfirmationStatus = v2assets.ConfirmationStatus_CONFIRMED - } - - // force the commited time in the hash. only useful to the service that is - // actually doing the committing. public consumers only ever see confirmed - // events with the timestamp already in place. - if o.committed != nil { - event.TimestampCommitted = o.committed - } + h.Hasher.applyEventOptions(o, event) // Note that we _don't_ take any notice of confirmation status. @@ -97,10 +73,8 @@ func (h *HasherV2) HashEvent(event *v2assets.EventResponse, opts ...HashOption) return err } - // If the idcommitted is provided, add it to the hash first - if o.idcommitted != nil { - h.hasher.Write(o.idcommitted) - } + // Hash data accumulation starts here + h.Hasher.applyHashingOptions(o) return V2HashEvent(h.hasher, v2Event) } @@ -120,14 +94,6 @@ func (h *HasherV2) HashEventJSON(event []byte, opts ...HashOption) error { for _, opt := range opts { opt(&o) } - - var err error - - // By default, one hash at at time with a reset. - if !o.accumulateHash { - h.hasher.Reset() - } - if o.publicFromPermissioned { // It is api response data, so the details of protected vs public should already have been dealt with. return ErrInvalidOption @@ -138,20 +104,7 @@ func (h *HasherV2) HashEventJSON(event []byte, opts ...HashOption) error { return err } - // If the caller is responsible for evidence confirmation they will have a - // pending event in their hand. But ultimately it is the confirmed record - // that is evidential and subject to public verification. - if o.asConfirmed { - // TODO: This probably is also not legit for an api consumer, but it - // does let the customer *anticipate* the hash and check we produce the - // correct one. - v2Event.ConfirmationStatus = v2assets.ConfirmationStatus_name[int32(v2assets.ConfirmationStatus_CONFIRMED)] - } - - // If the idcommitted is provided, add it to the hash first - if o.idcommitted != nil { - h.hasher.Write(o.idcommitted) - } + h.Hasher.applyHashingOptions(o) return V2HashEvent(h.hasher, v2Event) } @@ -160,33 +113,6 @@ func (h *HasherV2) Sum() []byte { return h.hasher.Sum(nil) } -// V2Event is a struct that contains ONLY the event fields we want to hash for schema v2 -type V2Event struct { - Identity string `json:"identity"` - AssetIdentity string `json:"asset_identity"` - EventAttributes map[string]any `json:"event_attributes"` - AssetAttributes map[string]any `json:"asset_attributes"` - Operation string `json:"operation"` - Behaviour string `json:"behaviour"` - TimestampDeclared string `json:"timestamp_declared"` - TimestampAccepted string `json:"timestamp_accepted"` - TimestampCommitted string `json:"timestamp_committed"` - PrincipalAccepted map[string]any `json:"principal_accepted"` - PrincipalDeclared map[string]any `json:"principal_declared"` - ConfirmationStatus string `json:"confirmation_status"` - From string `json:"from"` - TenantIdentity string `json:"tenant_identity"` -} - -// NewEventMarshaler creates a flat marshaler to transform events to api format. -// -// otherwise attributes look like this: {"foo":{"str_val": "bar"}} instead of {"foo": "bar"} -// this mimics the public list events api response, so minimises changes to the -// public api response, to reproduce the anchor -func NewEventMarshaler() *simpleoneof.Marshaler { - return v2assets.NewFlatMarshalerForEvents() -} - // V2FromEventJSON unmarshals rest api formated json into the event struct func V2FromEventJSON(eventJson []byte) (V2Event, error) { var err error @@ -209,13 +135,6 @@ func V2FromEventResponse(marshaler *simpleoneof.Marshaler, event *v2assets.Event return V2FromEventJSON(eventJson) } -// PublicFromPermissionedEvent translates the permissioned event and asset identities to -// their public counter parts. -func PublicFromPermissionedEvent(event *v2assets.EventResponse) { - event.Identity = v2assets.PublicIdentityFromPermissioned(event.Identity) - event.AssetIdentity = v2assets.PublicIdentityFromPermissioned(event.AssetIdentity) -} - // EventSimpleHashV2 hashes a single event according to the canonical simple hash event format // available to api consumers. // diff --git a/simplehash/schemav2_test.go b/simplehash/schemav2_test.go index 4ba857b..14af016 100644 --- a/simplehash/schemav2_test.go +++ b/simplehash/schemav2_test.go @@ -16,15 +16,14 @@ import ( var ( // Note these events correspond to the VALID_EVENTS in // https://github.com/datatrails/datatrails-simplehash-python/blob/main/unittests/constants.py - // @39ec71e744cf0cff44d2e60142308e0669687901 - expectedHashAll = "61211c916cd113a1cf424ac729924de46aa6259919825dbdf8ec78c5c14665e2" - expectedHashes = []string{ + expectedHashAllV2 = "61211c916cd113a1cf424ac729924de46aa6259919825dbdf8ec78c5c14665e2" + expectedHashesV2 = []string{ "681458c64f5ca35717e69df83c392c5f671a71c18f7830ccae676edfdb7179f1", "19111226f169ee67b41265aa27dc3792bf10ca463bc873361cae27d7e1bd6786", } - validEvents = []*v2assets.EventResponse{ + validEventsV2 = []*v2assets.EventResponse{ // SimpleHashV2: "681458c64f5ca35717e69df83c392c5f671a71c18f7830ccae676edfdb7179f1" { Identity: "assets/03c60f22-588c-4f12-b3c2-e98c7f2e98a0/events/409ae05a-183d-4e55-8aa6-889159edefd3", @@ -52,17 +51,26 @@ var ( Issuer: "https://rkvt.com", Subject: "117303158125148247777", DisplayName: "William Defoe", - Email: "WilliamDefoe@datatrails.ai", + Email: "WilliamDefoe@rkvst.com", }, PrincipalAccepted: &v2assets.Principal{ Issuer: "https://rkvt.com", Subject: "117303158125148247777", DisplayName: "William Defoe", - Email: "WilliamDefoe@datatrails.ai", + Email: "WilliamDefoe@rkvst.com", }, ConfirmationStatus: v2assets.ConfirmationStatus_CONFIRMED, From: "0xf8dfc073650503aeD429E414bE7e972f8F095e70", TenantIdentity: "tenant/0684984b-654d-4301-ad10-a508126e187d", + MerklelogEntry: &v2assets.MerkleLogEntry{ + LogVersion: 1, + LogEpoch: 2, + Commit: &v2assets.MerkleLogCommitMongoDB{ + LeafIndex: 1, + Index: 2, + Idtimestamp: "0xff00ff00ff", + }, + }, }, { Identity: "assets/a987b910-f567-4cca-9869-bbbeb12aec20/events/936ba508-ee65-426d-8903-52c59cb4655b", @@ -90,13 +98,13 @@ var ( Issuer: "https://rkvt.com", Subject: "227303158125148248888", DisplayName: "John Cena", - Email: "JohnCena@datatrails.ai", + Email: "JohnCena@rkvst.com", }, PrincipalAccepted: &v2assets.Principal{ Issuer: "https://rkvt.com", Subject: "227303158125148248888", DisplayName: "John Cena", - Email: "JohnCena@datatrails.ai", + Email: "JohnCena@rkvst.com", }, ConfirmationStatus: v2assets.ConfirmationStatus_CONFIRMED, From: "0xa453a973650503aeD429E414bE7e972f8F095f81", @@ -126,20 +134,20 @@ func TestEventSimpleHashV2(t *testing.T) { args{ sha256.New(), NewEventMarshaler(), - validEvents[0], + validEventsV2[0], }, false, - expectedHashes[0], + expectedHashesV2[0], }, { "VALID_EVENTS[1]", args{ sha256.New(), NewEventMarshaler(), - validEvents[1], + validEventsV2[1], }, false, - expectedHashes[1], + expectedHashesV2[1], }, } for _, tt := range tests { @@ -178,18 +186,20 @@ func TestHasherV2_HashEvent(t *testing.T) { NewEventMarshaler(), }, args{ - validEvents, + validEventsV2, []HashOption{WithAccumulate()}, }, false, - expectedHashAll, + expectedHashAllV2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := &HasherV2{ - hasher: tt.fields.hasher, - marshaler: tt.fields.marshaler, + Hasher: Hasher{ + hasher: tt.fields.hasher, + marshaler: tt.fields.marshaler, + }, } for _, event := range tt.args.events { if err := h.HashEvent(event, tt.args.opts...); (err != nil) != tt.wantErr { @@ -226,11 +236,11 @@ func TestHasherV2_HashEventJSON(t *testing.T) { NewEventMarshaler(), }, args{ - validEvents, + validEventsV2, []HashOption{WithAccumulate()}, }, false, - expectedHashAll, + expectedHashAllV2, }, } for _, tt := range tests { @@ -238,8 +248,10 @@ func TestHasherV2_HashEventJSON(t *testing.T) { var err error h := &HasherV2{ - hasher: tt.fields.hasher, - marshaler: tt.fields.marshaler, + Hasher: Hasher{ + hasher: tt.fields.hasher, + marshaler: tt.fields.marshaler, + }, } for _, event := range tt.args.events { var eventJson []byte diff --git a/simplehash/schemav3.go b/simplehash/schemav3.go new file mode 100644 index 0000000..0f71f21 --- /dev/null +++ b/simplehash/schemav3.go @@ -0,0 +1,123 @@ +package simplehash + +import ( + "encoding/json" + "fmt" + "hash" + + v2assets "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "github.com/datatrails/go-datatrails-common-api-gen/marshalers/simpleoneof" + "github.com/zeebo/bencode" +) + +// V3Event is a struct that contains ONLY the event fields we want to hash for schema v3 +type V3Event struct { + Identity string `json:"identity"` + EventAttributes map[string]any `json:"event_attributes"` + AssetAttributes map[string]any `json:"asset_attributes"` + Operation string `json:"operation"` + Behaviour string `json:"behaviour"` + TimestampDeclared string `json:"timestamp_declared"` + TimestampAccepted string `json:"timestamp_accepted"` + TimestampCommitted string `json:"timestamp_committed"` + PrincipalAccepted map[string]any `json:"principal_accepted"` + PrincipalDeclared map[string]any `json:"principal_declared"` + TenantIdentity string `json:"tenant_identity"` +} + +func V3HashEvent(hasher hash.Hash, v3Event V3Event) error { + + var err error + + // Note that we _don't_ take any notice of confirmation status. + + // TODO: we ought to be able to avoid this double encode decode, but it is fiddly + eventJson, err := json.Marshal(v3Event) + if err != nil { + return fmt.Errorf("EventSimpleHashV3: failed to marshal event : %v", err) + } + + var jsonAny any + + if err = json.Unmarshal(eventJson, &jsonAny); err != nil { + return fmt.Errorf("EventSimpleHashV3: failed to unmarshal events: %v", err) + } + + bencodeEvent, err := bencode.EncodeBytes(jsonAny) + if err != nil { + return fmt.Errorf("EventSimpleHashV3: failed to bencode events: %v", err) + } + + hasher.Write(bencodeEvent) + + return nil +} + +type HasherV3 struct { + Hasher +} + +func NewHasherV3() HasherV3 { + + h := HasherV3{ + Hasher: NewHasher(), + } + return h +} + +// V3FromEventJSON unmarshals rest api formated json into the event struct +func V3FromEventJSON(eventJson []byte) (V3Event, error) { + var err error + + eventShashV3 := V3Event{} + err = json.Unmarshal(eventJson, &eventShashV3) + if err != nil { + return V3Event{}, err + } + return eventShashV3, nil +} + +// V2FromEventResponse transforms a single event in grpc proto format (message bus +// compatible) to the canonical, publicly verifiable, api format. +func V3FromEventResponse(marshaler *simpleoneof.Marshaler, event *v2assets.EventResponse) (V3Event, error) { + eventJson, err := marshaler.Marshal(event) + if err != nil { + return V3Event{}, err + } + return V3FromEventJSON(eventJson) +} + +// HashEvent hashes a single event according to the canonical simple hash event +// format available to api consumers. The source event is in the grpc proto buf +// format. GRPC endpoints are not presently exposed by the platform. +// +// Options: +// - WithIDCommitted prefix the data to hash with the bigendian encoding of +// idtimestamp before hashing. +// - WithPrefix is used to provide domain separation, the provided bytes are +// pre-pended to the data to be hashed. Eg H(prefix || data) +// This option can be used multiple times, the prefix bytes are appended to +// any previously supplied. +// - WithAccumulate callers wishing to implement batched hashing of multiple +// events in series should set this. They should call Reset() at their batch +// boundaries. +// - WithPublicFromPermissioned should be set if the event is the +// permissioned (owner) counter part of a public attestation. +func (h *HasherV3) HashEvent(event *v2assets.EventResponse, opts ...HashOption) error { + + o := HashOptions{} + for _, opt := range opts { + opt(&o) + } + + h.applyEventOptions(o, event) + + v3Event, err := V3FromEventResponse(h.marshaler, event) + if err != nil { + return err + } + + h.applyHashingOptions(o) + + return V3HashEvent(h.hasher, v3Event) +} diff --git a/simplehash/schemav3_test.go b/simplehash/schemav3_test.go new file mode 100644 index 0000000..387573e --- /dev/null +++ b/simplehash/schemav3_test.go @@ -0,0 +1,64 @@ +package simplehash + +import ( + "crypto/sha256" + "encoding/hex" + "testing" + + v2assets "github.com/datatrails/go-datatrails-common-api-gen/assets/v2/assets" + "gotest.tools/v3/assert" +) + +var ( + expectedHashAllV3 = "c52caf06bf525ae7e2fde8e08e2d2cac30ceb8b9f761503d7f671213b07fc576" +) + +func TestHasherV3_HashEvent(t *testing.T) { + type fields struct { + Hasher Hasher + } + type args struct { + events []*v2assets.EventResponse + opts []HashOption + } + tests := []struct { + name string + fields fields + args args + wantErr bool + expectedHash string + }{ + { + "valid events [:1] (both together)", + fields{ + Hasher: Hasher{ + sha256.New(), + NewEventMarshaler(), + }, + }, + args{ + validEventsV2, + []HashOption{WithAccumulate()}, + }, + false, + expectedHashAllV3, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &HasherV3{ + Hasher: tt.fields.Hasher, + } + for _, event := range tt.args.events { + if err := h.HashEvent(event, tt.args.opts...); (err != nil) != tt.wantErr { + t.Errorf("HasherV3.HashEvent() error = %v, wantErr %v", err, tt.wantErr) + } + } + if tt.expectedHash == "" { + return + } + actualHash := hex.EncodeToString(h.Hasher.Sum(nil)) + assert.Equal(t, tt.expectedHash, actualHash) + }) + } +} diff --git a/taskfiles/Taskfile_codeqa.yml b/taskfiles/Taskfile_codeqa.yml index a2a698d..9ac038a 100644 --- a/taskfiles/Taskfile_codeqa.yml +++ b/taskfiles/Taskfile_codeqa.yml @@ -38,7 +38,7 @@ tasks: goimports {{.VERBOSE}} -w . golangci-lint {{.VERBOSE}} run --timeout 10m ./... - unit-tests: + test: desc: "run unit tests" cmds: - go test {{.GO_TEST_TAGS}} ./...