diff --git a/go.mod b/go.mod index 81d8ae0..62b1b60 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( filippo.io/edwards25519 v1.1.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.7.1-0.20260401115005-c2265cbd04b0 + github.com/code-payments/ocp-protobuf-api v1.8.1 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.2.1 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index 6ce0c22..e0dc268 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.7.1-0.20260401115005-c2265cbd04b0 h1:zQdbkGJKttqckZ8GH66fx0g6D6UB/WHT7z5MNiBHMIE= -github.com/code-payments/ocp-protobuf-api v1.7.1-0.20260401115005-c2265cbd04b0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.8.1 h1:IaCVADbbTUtZwf0Rk8Pf8PygsancuOXc+A3CcTG/74w= +github.com/code-payments/ocp-protobuf-api v1.8.1/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/ocp/integration/moderation.go b/ocp/integration/moderation.go new file mode 100644 index 0000000..ff2d956 --- /dev/null +++ b/ocp/integration/moderation.go @@ -0,0 +1,25 @@ +package integration + +import ( + "context" + + "github.com/code-payments/ocp-server/ocp/common" +) + +// Moderation is an OCP integration enabling custom moderation rules +type Moderation interface { + // ValidateAttestation validates a moderation attestation for a piece of moderated content + ValidateAttestation(ctx context.Context, owner *common.Account, rawAttestation []byte, content any) (bool, error) +} + +type allowEverythingModerationIntegration struct { +} + +// NewAllowEverythingModerationIntegration returns a default Moderation integration that allows everything +func NewAllowEverythingModerationIntegration() Moderation { + return &allowEverythingModerationIntegration{} +} + +func (i *allowEverythingModerationIntegration) ValidateAttestation(_ context.Context, _ *common.Account, _ []byte, _ any) (bool, error) { + return true, nil +} diff --git a/ocp/rpc/currency/icon.go b/ocp/rpc/currency/icon.go index adaf251..6163e19 100644 --- a/ocp/rpc/currency/icon.go +++ b/ocp/rpc/currency/icon.go @@ -72,6 +72,14 @@ func (s *currencyServer) UpdateIcon(ctx context.Context, req *currencypb.UpdateI return ¤cypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_DENIED}, nil } + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.ModerationAttestation.RawValue, req.Icon) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_DENIED}, nil + } + processed, ext, contentType, err := processIcon(req.Icon) if err == errInvalidIcon { return ¤cypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_INVALID_ICON}, nil diff --git a/ocp/rpc/currency/launch.go b/ocp/rpc/currency/launch.go index f3b8b38..8a8020a 100644 --- a/ocp/rpc/currency/launch.go +++ b/ocp/rpc/currency/launch.go @@ -63,6 +63,14 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil } + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.NameModerationAttestation.RawValue, req.Name) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate name moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + symbol := strings.TrimSpace(req.Symbol) if len(symbol) == 0 { symbol = strings.ToUpper(strings.Map( @@ -77,15 +85,53 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque if len(symbol) > currencycreator.MaxCurrencyConfigAccountSymbolLength { symbol = symbol[0:currencycreator.MaxCurrencyConfigAccountSymbolLength] } - } else if symbol != req.Symbol { - return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } else { + if symbol != req.Symbol { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + if req.SymbolModerationAttestation == nil { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.SymbolModerationAttestation.RawValue, req.Symbol) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate symbol moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } } description := strings.TrimSpace(req.Description) + if description != req.Description { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + if len(req.Description) > 0 { + if req.DescriptionModerationAttestation == nil { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.DescriptionModerationAttestation.RawValue, req.Description) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate description moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + } var processedIcon []byte var iconExt, iconContentType string if len(req.Icon) > 0 { + if req.IconModerationAttestation == nil { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.IconModerationAttestation.RawValue, req.Icon) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate icon moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_DENIED}, nil + } + processedIcon, iconExt, iconContentType, err = processIcon(req.Icon) if err == errInvalidIcon { return ¤cypb.LaunchResponse{Result: currencypb.LaunchResponse_INVALID_ICON}, nil diff --git a/ocp/rpc/currency/metadata.go b/ocp/rpc/currency/metadata.go index 6586b82..56acf89 100644 --- a/ocp/rpc/currency/metadata.go +++ b/ocp/rpc/currency/metadata.go @@ -85,6 +85,14 @@ func (s *currencyServer) UpdateMetadata(ctx context.Context, req *currencypb.Upd } if req.NewDescription != nil { + isModerated, err := s.moderation.ValidateAttestation(ctx, ownerAccount, req.NewDescription.ModerationAttestation.RawValue, req.NewDescription.Value) + if err != nil { + log.With(zap.Error(err)).Warn("failed to validate description moderation attestation") + return nil, status.Error(codes.Internal, "") + } else if !isModerated { + return ¤cypb.UpdateMetadataResponse{Result: currencypb.UpdateMetadataResponse_DENIED}, nil + } + metadataRecord.Description = req.NewDescription.Value } diff --git a/ocp/rpc/currency/server.go b/ocp/rpc/currency/server.go index 5b29cf0..7ebdeff 100644 --- a/ocp/rpc/currency/server.go +++ b/ocp/rpc/currency/server.go @@ -12,6 +12,7 @@ import ( auth_util "github.com/code-payments/ocp-server/ocp/auth" currency_util "github.com/code-payments/ocp-server/ocp/currency" ocp_data "github.com/code-payments/ocp-server/ocp/data" + "github.com/code-payments/ocp-server/ocp/integration" ) type currencyServer struct { @@ -24,6 +25,7 @@ type currencyServer struct { auth *auth_util.RPCSignatureVerifier antispamGuard *antispam.Guard + moderation integration.Moderation s3Client *s3.Client @@ -40,6 +42,7 @@ func NewCurrencyServer( data ocp_data.Provider, mintDataProvider *currency_util.MintDataProvider, antispamGuard *antispam.Guard, + moderation integration.Moderation, s3Client *s3.Client, configProvider ConfigProvider, ) currencypb.CurrencyServer { @@ -56,6 +59,7 @@ func NewCurrencyServer( auth: auth_util.NewRPCSignatureVerifier(log, data), antispamGuard: antispamGuard, + moderation: moderation, s3Client: s3Client,