diff --git a/.golangci.yml b/.golangci.yml index eb161f413..92391711a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -72,4 +72,10 @@ issues: - unused - deadcode - varcheck + # Ignore errors intentionally not returned in property-based tests. + # Needed here because the inline ignore directive is broken: + # https://github.com/gostaticanalysis/nilerr/issues/8 + - path: rpcserver_test.go + linters: + - nilerr new-from-rev: c723abd3c9db8a6a2f3f1eaa85ce5aefb52c8170 diff --git a/go.mod b/go.mod index e9ed5aa33..32db90375 100644 --- a/go.mod +++ b/go.mod @@ -201,9 +201,12 @@ require ( modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect nhooyr.io/websocket v1.8.7 // indirect + pgregory.net/rapid v1.1.0 sigs.k8s.io/yaml v1.2.0 // indirect ) // We want to format raw bytes as hex instead of base64. The forked version // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display + +replace pgregory.net/rapid v1.1.0 => github.com/chrisseto/rapid v0.0.0-20240815210052-cdeef406c65c // indirect diff --git a/go.sum b/go.sum index d9159df35..2eeba6fe7 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chrisseto/rapid v0.0.0-20240815210052-cdeef406c65c h1:GZtcJAFTBCr16eM7ytFwWMg9oLaMsRfSsVyi3lTo+mw= +github.com/chrisseto/rapid v0.0.0-20240815210052-cdeef406c65c/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= diff --git a/rpcserver.go b/rpcserver.go index 79fae06e4..9f077e1fc 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -4668,51 +4668,57 @@ func UnmarshalUniID(rpcID *unirpc.ID) (universe.Identifier, error) { return universe.Identifier{}, fmt.Errorf("unable to unmarshal "+ "proof type: %w", err) } + + var ( + assetIDBytes []byte + groupKeyBytes []byte + ) + switch { case rpcID.GetAssetId() != nil: - var assetID asset.ID - copy(assetID[:], rpcID.GetAssetId()) - - return universe.Identifier{ - AssetID: assetID, - ProofType: proofType, - }, nil + assetIDBytes = rpcID.GetAssetId() + if len(assetIDBytes) != sha256.Size { + return universe.Identifier{}, fmt.Errorf("asset ID " + + "must be 32 bytes") + } case rpcID.GetAssetIdStr() != "": - assetIDBytes, err := hex.DecodeString(rpcID.GetAssetIdStr()) - if err != nil { - return universe.Identifier{}, err + rpcAssetIDStr := rpcID.GetAssetIdStr() + if len(rpcAssetIDStr) != sha256.Size*2 { + return universe.Identifier{}, fmt.Errorf("asset ID " + + "string must be 64 chars") } - // TODO(roasbeef): reuse with above - - var assetID asset.ID - copy(assetID[:], assetIDBytes) - - return universe.Identifier{ - AssetID: assetID, - ProofType: proofType, - }, nil - - case rpcID.GetGroupKey() != nil: - groupKey, err := parseUserKey(rpcID.GetGroupKey()) + assetIDBytes, err = hex.DecodeString(rpcAssetIDStr) if err != nil { return universe.Identifier{}, err } - return universe.Identifier{ - GroupKey: groupKey, - ProofType: proofType, - }, nil + case rpcID.GetGroupKey() != nil: + groupKeyBytes = rpcID.GetGroupKey() case rpcID.GetGroupKeyStr() != "": - groupKeyBytes, err := hex.DecodeString(rpcID.GetGroupKeyStr()) + rpcGroupKeyStr := rpcID.GetGroupKeyStr() + groupKeyBytes, err = hex.DecodeString(rpcGroupKeyStr) if err != nil { return universe.Identifier{}, err } - // TODO(roasbeef): reuse with above + default: + return universe.Identifier{}, fmt.Errorf("no id set") + } + + switch { + case len(assetIDBytes) != 0: + var assetID asset.ID + copy(assetID[:], assetIDBytes) + + return universe.Identifier{ + AssetID: assetID, + ProofType: proofType, + }, nil + case len(groupKeyBytes) != 0: groupKey, err := parseUserKey(groupKeyBytes) if err != nil { return universe.Identifier{}, err @@ -4724,7 +4730,7 @@ func UnmarshalUniID(rpcID *unirpc.ID) (universe.Identifier, error) { }, nil default: - return universe.Identifier{}, fmt.Errorf("no id set") + return universe.Identifier{}, fmt.Errorf("malformed id") } } @@ -5329,6 +5335,10 @@ func unmarshalUniverseKey(key *unirpc.UniverseKey) (universe.Identifier, // unmarshalAssetLeaf unmarshals an asset leaf from the RPC form. func unmarshalAssetLeaf(leaf *unirpc.AssetLeaf) (*universe.Leaf, error) { + if leaf == nil { + return nil, fmt.Errorf("missing asset leaf") + } + // We'll just pull the asset details from the serialized issuance proof // itself. var proofAsset asset.Asset @@ -5359,6 +5369,10 @@ func unmarshalAssetLeaf(leaf *unirpc.AssetLeaf) (*universe.Leaf, error) { func (r *rpcServer) InsertProof(ctx context.Context, req *unirpc.AssetProof) (*unirpc.AssetProofResponse, error) { + if req == nil { + return nil, fmt.Errorf("missing proof and universe key") + } + universeID, leafKey, err := unmarshalUniverseKey(req.Key) if err != nil { return nil, err @@ -6184,44 +6198,40 @@ func unmarshalAssetSpecifier(req *rfqrpc.AssetSpecifier) (*asset.ID, // give precedence to the asset ID due to its higher level of // specificity. var ( - assetID *asset.ID - + assetIDBytes []byte + assetID *asset.ID groupKeyBytes []byte groupKey *btcec.PublicKey - - err error + err error ) switch { // Parse the asset ID if it's set. case len(req.GetAssetId()) > 0: - var assetIdBytes [32]byte - copy(assetIdBytes[:], req.GetAssetId()) - id := asset.ID(assetIdBytes) - assetID = &id + assetIDBytes = req.GetAssetId() + if len(assetIDBytes) != sha256.Size { + return nil, nil, fmt.Errorf("asset ID must be 32 bytes") + } case len(req.GetAssetIdStr()) > 0: - assetIDBytes, err := hex.DecodeString(req.GetAssetIdStr()) + reqAssetIDStr := req.GetAssetIdStr() + if len(reqAssetIDStr) != sha256.Size*2 { + return nil, nil, fmt.Errorf("asset ID string must be " + + "64 chars") + } + + assetIDBytes, err = hex.DecodeString(reqAssetIDStr) if err != nil { return nil, nil, fmt.Errorf("error decoding asset "+ "ID: %w", err) } - var id asset.ID - copy(id[:], assetIDBytes) - assetID = &id - // Parse the group key if it's set. case len(req.GetGroupKey()) > 0: groupKeyBytes = req.GetGroupKey() - groupKey, err = btcec.ParsePubKey(groupKeyBytes) - if err != nil { - return nil, nil, fmt.Errorf("error parsing group "+ - "key: %w", err) - } case len(req.GetGroupKeyStr()) > 0: - groupKeyBytes, err := hex.DecodeString( + groupKeyBytes, err = hex.DecodeString( req.GetGroupKeyStr(), ) if err != nil { @@ -6229,12 +6239,6 @@ func unmarshalAssetSpecifier(req *rfqrpc.AssetSpecifier) (*asset.ID, "key: %w", err) } - groupKey, err = btcec.ParsePubKey(groupKeyBytes) - if err != nil { - return nil, nil, fmt.Errorf("error parsing group "+ - "key: %w", err) - } - default: // At this point, we know that neither the asset ID nor the // group key are specified. Return an error. @@ -6242,6 +6246,23 @@ func unmarshalAssetSpecifier(req *rfqrpc.AssetSpecifier) (*asset.ID, "key must be specified") } + switch { + case len(assetIDBytes) != 0: + var id asset.ID + copy(id[:], assetIDBytes) + assetID = &id + + case len(groupKeyBytes) != 0: + groupKey, err = parseUserKey(groupKeyBytes) + if err != nil { + return nil, nil, fmt.Errorf("error parsing group "+ + "key: group key: %w", err) + } + + default: + return nil, nil, fmt.Errorf("malformed asset specifier") + } + return assetID, groupKey, nil } diff --git a/rpcserver_test.go b/rpcserver_test.go new file mode 100644 index 000000000..86b6193e7 --- /dev/null +++ b/rpcserver_test.go @@ -0,0 +1,566 @@ +package taprootassets + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "maps" + "reflect" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/universe" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + "pgregory.net/rapid" +) + +type rapidFieldGen = map[string]*rapid.Generator[any] +type rapidFieldMap = map[reflect.Type]rapidFieldGen +type rapidTypeMap = map[reflect.Type]*rapid.Generator[any] + +// Custom generators. +var ( + ByteSliceGen = rapid.SliceOf(rapid.Byte()) + + // Ignore private gRPC fields of messages, which we don't read when + // unmarshalling and cause issues with rapid.Make(). + ignorePrivateRPCFields = rapidFieldGen{ + "state": rapid.Just(protoimpl.MessageState{}).AsAny(), + "sizeCache": rapid.Just(protoimpl.SizeCache(0)).AsAny(), + "unknownFields": rapid.Just(protoimpl.UnknownFields{}).AsAny(), + } + + // Create a generator config for a gRPC message, which may include + // custom generators or type generator overrides. + genMakeConfig = func(rpcType any, customGens rapidFieldGen, + genOverrides rapidTypeMap) rapid.MakeConfig { + + cfg := rapid.MakeConfig{ + Types: make(rapidTypeMap), + Fields: make(rapidFieldMap), + } + + // Add custom generators for fields, by field name, to override + // default rapid.Make() behavior. + ignoredFields := maps.Clone(ignorePrivateRPCFields) + for k, v := range customGens { + ignoredFields[k] = v + } + + cfg.Fields[reflect.TypeOf(rpcType)] = ignoredFields + + // Add custom generators that will override the generators + // rapid.Make() would create for struct member types. + for k, v := range genOverrides { + cfg.Types[k] = v + } + + return cfg + } + + GenesisInfoGen = rapid.Ptr(rapid.MakeCustom[taprpc.GenesisInfo]( + genMakeConfig(taprpc.GenesisInfo{}, nil, nil), + ), true) + AssetGroupGen = rapid.Ptr(rapid.MakeCustom[taprpc.AssetGroup]( + genMakeConfig(taprpc.AssetGroup{}, nil, nil), + ), true) + AnchorInfoGen = rapid.Ptr(rapid.MakeCustom[taprpc.AnchorInfo]( + genMakeConfig(taprpc.AnchorInfo{}, nil, nil), + ), true) + PrevInputAssetGen = rapid.MakeCustom[taprpc.PrevInputAsset]( + genMakeConfig(taprpc.PrevInputAsset{}, nil, nil), + ) + + // Leave the split commitment for prev witnesses as nil. + emptySplitCommitmentGen = rapid.Just(taprpc.SplitCommitment{}) + splitCommitPtrGen = rapid.Ptr(emptySplitCommitmentGen, true) + nilSplitCommitment = rapidTypeMap{ + //nolint:lll + reflect.TypeOf(&taprpc.SplitCommitment{}): splitCommitPtrGen.AsAny(), + } + PrevWitnessGen = rapid.MakeCustom[taprpc.PrevWitness]( + genMakeConfig(taprpc.PrevWitness{}, nil, nilSplitCommitment), + ) + PrevWitnessesGen = rapid.Custom(func(t *rapid.T) []*taprpc.PrevWitness { + witnessGen := rapid.Ptr(PrevWitnessGen, false) + return rapid.SliceOf(witnessGen).Draw(t, "prev_witnesses") + }) + DecDisplayGen = rapid.Ptr(rapid.MakeCustom[taprpc.DecimalDisplay]( + genMakeConfig(taprpc.DecimalDisplay{}, nil, nil), + ), true) + + // Set generator overrides for members of taprpc.Asset that are gRPC + // messages. + assetMemberGens = rapidTypeMap{ + reflect.TypeOf(&taprpc.GenesisInfo{}): GenesisInfoGen.AsAny(), + reflect.TypeOf(&taprpc.AssetGroup{}): AssetGroupGen.AsAny(), + reflect.TypeOf(&taprpc.AnchorInfo{}): AnchorInfoGen.AsAny(), + //nolint:lll + reflect.TypeOf([]*taprpc.PrevWitness{}): PrevWitnessesGen.AsAny(), + reflect.TypeOf(&taprpc.DecimalDisplay{}): DecDisplayGen.AsAny(), + } + AssetGen = rapid.MakeCustom[taprpc.Asset]( + genMakeConfig(taprpc.Asset{}, nil, assetMemberGens), + ) + AssetPtrGen = rapid.Ptr(AssetGen, true) + + // Use the custom taprpc.Asset generator for *universerpc.AssetLeaf. + leafMemberGens = rapidTypeMap{ + reflect.TypeOf(&taprpc.Asset{}): AssetPtrGen.AsAny(), + } + AssetLeafGen = rapid.MakeCustom[universerpc.AssetLeaf]( + genMakeConfig(universerpc.AssetLeaf{}, nil, leafMemberGens), + ) + AssetLeafPtrGen = rapid.Ptr(AssetLeafGen, true) +) + +// Result is used to store the output of a fallible function call. +type Result[T any] struct { + res T + err error +} + +// genUniIDField is an interface that is used to compare generated input data +// with unmarshalled data. +type genUniIDField[T any, U universe.Identifier] interface { + // IsValid checks if the generated data should be rejected during + // unmarshal. + IsValid() error + + // IsEqual checks if the generated data is equal to the unmarshalled + // data. + IsEqual(Result[U]) error + + // Inner returns the generated data. + Inner() T + + // ValidInputErrorMsg returns an error message for valid input that + // unmarshal failed on. + ValidInputErrorMsg(error) error + + // InvalidInputErrorMsg returns an error message for an invalid input + // that unmarshal succeeded on. + InvalidInputErrorMsg(error) error +} + +// Compare compares generated input data to unmarshalled data, checking for +// the expected behavior of unmarshalling and data equality. +func Compare[T any, U universe.Identifier](gen genUniIDField[T, U], + res Result[U]) error { + + validGen := gen.IsValid() + + // Unmarshal was expected to fail. + if res.err != nil && validGen != nil { + return nil + } + + // Unmarshal failed on valid input. + if res.err != nil && validGen == nil { + return gen.ValidInputErrorMsg(res.err) + } + + // Unmarshal succeeded on invalid input. + if res.err == nil && validGen != nil { + return gen.InvalidInputErrorMsg(res.err) + } + + // Unmarhsal succeeded on valid input; check equality. + if res.err == nil && validGen == nil { + return gen.IsEqual(res) + } + + // This should be unreachable. + return nil +} + +// genAssetId is generated data used to populate universerpc.ID_AssetId. +type genAssetId struct { + Bytes []byte +} + +func (id genAssetId) Inner() []byte { + return id.Bytes +} + +// NewAssetId creates a new genAssetId instance. +func NewAssetId(t *rapid.T) genAssetId { + var id genAssetId + id.Bytes = ByteSliceGen.Draw(t, "ID") + + return id +} + +func (id genAssetId) IsValid() error { + // The only valid size for an asset ID is 32 bytes. + idSize := len(id.Bytes) + if idSize != sha256.Size { + return fmt.Errorf("generated asset ID invalid size: %d", idSize) + } + + return nil +} + +func (id genAssetId) IsEqual(other Result[universe.Identifier]) error { + otherBytes := other.res.AssetID[:] + if len(otherBytes) == 0 { + return fmt.Errorf("asset ID bytes not unmarshalled: %v", + id.Inner()) + } + + if !bytes.Equal(id.Bytes, otherBytes) { + return fmt.Errorf("asset ID mismatch: generated %x, "+ + "unmarshalled %x", id.Inner(), otherBytes) + } + + return nil +} + +func (id genAssetId) ValidInputErrorMsg(err error) error { + return fmt.Errorf("unmarshal asset ID bytes failed: %v, %w", + id.Inner(), err) +} + +func (id genAssetId) InvalidInputErrorMsg(err error) error { + return fmt.Errorf("invalid asset ID bytes not rejected: %v, %w", + id.Inner(), id.IsValid()) +} + +var _ genUniIDField[[]byte, universe.Identifier] = (*genAssetId)(nil) + +// genAssetIdStr is generated data used to populate universerpc.ID_AssetIdStr. +type genAssetIdStr struct { + Str string +} + +func (id genAssetIdStr) Inner() string { + return id.Str +} + +// NewAssetIDStr creates a new genAssetIdStr instance. +func NewAssetIDStr(t *rapid.T) genAssetIdStr { + var id genAssetIdStr + id.Str = rapid.String().Draw(t, "ID string") + + return id +} + +func (id genAssetIdStr) IsValid() error { + idSize := len(id.Inner()) + if idSize == 0 { + return fmt.Errorf("asset ID string empty") + } + + // Invalid hex should be rejected. + _, hexErr := hex.DecodeString(id.Inner()) + if hexErr != nil { + return fmt.Errorf("non-hex asset ID string: %w", hexErr) + } + + // The only valid size for a hex-encoded asset ID is 64 bytes. + if idSize != sha256.Size*2 { + return fmt.Errorf("asset ID string invalid size: %d", idSize) + } + + return nil +} + +func (id genAssetIdStr) IsEqual(other Result[universe.Identifier]) error { + otherStr := other.res.AssetID.String() + if len(otherStr) == 0 { + return fmt.Errorf("asset ID string not unmarshalled: "+ + "generated %v", id.Inner()) + } + + if id.Str != otherStr { + return fmt.Errorf("asset ID string mismatch: generated %s, "+ + "unmarshalled %s", id.Inner(), otherStr) + } + + return nil +} + +func (id genAssetIdStr) ValidInputErrorMsg(err error) error { + return fmt.Errorf("unmarshal asset ID string failed: %v, %w", + id.Inner(), err) +} + +func (id genAssetIdStr) InvalidInputErrorMsg(err error) error { + return fmt.Errorf("invalid asset ID string not rejected: %v, %w", + id.Inner(), id.IsValid()) +} + +var _ genUniIDField[string, universe.Identifier] = (*genAssetIdStr)(nil) + +// genGroupKey is generated data used to populate universerpc.ID_GroupKey. +type genGroupKey struct { + Bytes []byte +} + +func (id genGroupKey) Inner() []byte { + return id.Bytes +} + +// NewGroupKey creates a new genGroupKey instance. +func NewGroupKey(t *rapid.T) genGroupKey { + var id genGroupKey + id.Bytes = ByteSliceGen.Draw(t, "Group key") + + return id +} + +func (id genGroupKey) IsValid() error { + // The only valid size for a group key is 32 or 33 bytes. + idSize := len(id.Bytes) + if idSize != schnorr.PubKeyBytesLen && + idSize != btcec.PubKeyBytesLenCompressed { + + return fmt.Errorf("generated group key invalid size: %d", + idSize) + } + + // The generated key must be valid. + _, keyErr := parseUserKey(id.Bytes) + return keyErr +} + +func (id genGroupKey) IsEqual(otherResult Result[universe.Identifier]) error { + otherKey := otherResult.res.GroupKey + if otherKey == nil { + return fmt.Errorf("group key not unmarshalled: %v", id.Inner()) + } + + // Since we parse the provided key as Schnorr, we must drop the parity + // byte from the generated bytes before comparison. + otherKeyBytes := schnorr.SerializePubKey(otherKey) + idBytes := id.Inner() + if len(id.Inner()) == btcec.PubKeyBytesLenCompressed { + idBytes = idBytes[1:] + } + + if !bytes.Equal(idBytes, otherKeyBytes) { + return fmt.Errorf("group key mismatch: generated %x, "+ + "unmarshalled %x", id.Inner(), otherKeyBytes) + } + + return nil +} + +func (id genGroupKey) ValidInputErrorMsg(err error) error { + return fmt.Errorf("unmarshal group key bytes failed: %x, %w", + id.Inner(), err) +} + +func (id genGroupKey) InvalidInputErrorMsg(err error) error { + return fmt.Errorf("invalid group key bytes not rejected: %x, %w", + id.Inner(), id.IsValid()) +} + +var _ genUniIDField[[]byte, universe.Identifier] = (*genGroupKey)(nil) + +// genGroupKeyStr is generated data used to populate universerpc.ID_GroupKeyStr. +type genGroupKeyStr struct { + Str string +} + +func (id genGroupKeyStr) Inner() string { + return id.Str +} + +// NewGroupKeyStr creates a new genGroupKeyStr instance. +func NewGroupKeyStr(t *rapid.T) genGroupKeyStr { + var id genGroupKeyStr + id.Str = rapid.String().Draw(t, "Group key string") + + return id +} + +func (id genGroupKeyStr) IsValid() error { + idSize := len(id.Inner()) + if idSize == 0 { + return fmt.Errorf("group key string empty") + } + + // Invalid hex should be rejected. + _, hexErr := hex.DecodeString(id.Inner()) + if hexErr != nil { + return fmt.Errorf("non-hex group key string: %w", hexErr) + } + + // The only valid sizes for a group key string is 64 or 66 bytes. + if idSize != schnorr.PubKeyBytesLen*2 && + idSize != btcec.PubKeyBytesLenCompressed*2 { + + return fmt.Errorf("generated group key string invalid size: %d", + idSize) + } + + return nil +} + +func (id genGroupKeyStr) IsEqual( + otherResult Result[universe.Identifier]) error { + + otherKey := otherResult.res.GroupKey + if otherKey == nil { + return fmt.Errorf("group key string not unmarshalled: %v", + id.Inner()) + } + + // Since we parse the provided key as Schnorr, we must drop the parity + // byte from the generated string before comparison. + otherKeyStr := hex.EncodeToString(schnorr.SerializePubKey(otherKey)) + idStr := id.Inner() + if len(id.Inner()) == btcec.PubKeyBytesLenCompressed*2 { + idStr = idStr[2:] + } + + if idStr != otherKeyStr { + return fmt.Errorf("group key string mismatch: generated %s, "+ + "unmarshalled %s", id.Inner(), otherKeyStr) + } + + return nil +} + +func (id genGroupKeyStr) ValidInputErrorMsg(err error) error { + return fmt.Errorf("unmarshal group key string failed: %v, %w", + id.Inner(), err) +} + +func (id genGroupKeyStr) InvalidInputErrorMsg(err error) error { + return fmt.Errorf("invalid group key string not rejected: %v, %w", + id.Inner(), id.IsValid()) +} + +var _ genUniIDField[string, universe.Identifier] = (*genGroupKeyStr)(nil) + +// testUnmarshalUniId tests that UnmarshalUniID correctly unmarshals a +// well-formed rpc ID, and rejects an invalid ID. +func testUnmarshalUniId(t *rapid.T) { + KnownProofTypes := map[universerpc.ProofType]int32{ + universerpc.ProofType_PROOF_TYPE_UNSPECIFIED: 0, + universerpc.ProofType_PROOF_TYPE_ISSUANCE: 1, + universerpc.ProofType_PROOF_TYPE_TRANSFER: 2, + } + + IDBytes := NewAssetId(t) + IDStr := NewAssetIDStr(t) + IDGroupKeyBytes := NewGroupKey(t) + IDGroupKeyStr := NewGroupKeyStr(t) + + IDFieldSelector := rapid.ByteMax(0x5).Draw(t, "ID field selector") + proofType := rapid.Int32().Draw(t, "proofType") + rpcProofType := universerpc.ProofType(proofType) + + uniId := &universerpc.ID{ + ProofType: rpcProofType, + } + + // Set the ID to random data, of a random type. + switch IDFieldSelector { + case 0: + uniId.Id = &universerpc.ID_AssetId{ + AssetId: IDBytes.Inner(), + } + + case 1: + uniId.Id = &universerpc.ID_AssetIdStr{ + AssetIdStr: IDStr.Inner(), + } + + case 2: + uniId.Id = &universerpc.ID_GroupKey{ + GroupKey: IDGroupKeyBytes.Inner(), + } + + case 3: + uniId.Id = &universerpc.ID_GroupKeyStr{ + GroupKeyStr: IDGroupKeyStr.Inner(), + } + + // Empty ID field. + case 4: + + // Empty universe ID. + case 5: + uniId = nil + } + + nativeUniID, err := UnmarshalUniID(uniId) + unmarshalResult := Result[universe.Identifier]{ + res: nativeUniID, + err: err, + } + + // Unmarshalling an unknown proof type should fail. + _, knownProofType := KnownProofTypes[rpcProofType] + if !knownProofType { + if err == nil { + t.Fatalf("unknown proof type not rejected: %v", + rpcProofType) + } + + return + } + + switch IDFieldSelector { + case 0: + if cmpErr := Compare(IDBytes, unmarshalResult); cmpErr != nil { + t.Fatalf("%v", err) + } + + case 1: + if cmpErr := Compare(IDStr, unmarshalResult); cmpErr != nil { + t.Fatalf("%v", err) + } + + case 2: + cmpErr := Compare(IDGroupKeyBytes, unmarshalResult) + if cmpErr != nil { + t.Fatalf("%v", err) + } + + case 3: + cmpErr := Compare(IDGroupKeyStr, unmarshalResult) + if cmpErr != nil { + t.Fatalf("%v", err) + } + + case 4: + if err == nil { + t.Fatalf("unmarshal empty ID not rejected: %v", err) + } + + case 5: + if err == nil { + t.Fatalf("unmarshal ID with empty ID not rejected: %v", + err) + } + } + + // Check equality of the proof type. + if err == nil && int32(nativeUniID.ProofType) != proofType { + t.Fatalf("proof type mismatch: generated %v, unmarshalled %v", + proofType, nativeUniID.ProofType) + } +} + +func TestUnmarshalUniId(t *testing.T) { + rapid.Check(t, testUnmarshalUniId) +} + +func testUnmarshalAssetLeaf(t *rapid.T) { + // Don't check the unmarshal output, we are only testing if we can + // cause unmarshal to panic. + leaf := AssetLeafPtrGen.Draw(t, "Leaf") + _, _ = unmarshalAssetLeaf(leaf) +} + +func TestUnmarshalAssetLeaf(t *testing.T) { + rapid.Check(t, testUnmarshalAssetLeaf) +} diff --git a/testdata/rapid/TestUnmarshalAssetLeaf/TestUnmarshalAssetLeaf-20240816174811-4026466.fail b/testdata/rapid/TestUnmarshalAssetLeaf/TestUnmarshalAssetLeaf-20240816174811-4026466.fail new file mode 100644 index 000000000..2e4f71816 --- /dev/null +++ b/testdata/rapid/TestUnmarshalAssetLeaf/TestUnmarshalAssetLeaf-20240816174811-4026466.fail @@ -0,0 +1,4 @@ +# 2024/08/16 17:48:11.689274 [TestUnmarshalAssetLeaf] [rapid] draw Leaf: (*universerpc.AssetLeaf)(nil) +# +v0.4.8#7630617197267023936 +0x0 \ No newline at end of file diff --git a/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200615-3628702.fail b/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200615-3628702.fail new file mode 100644 index 000000000..ad03e9e49 --- /dev/null +++ b/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200615-3628702.fail @@ -0,0 +1,21 @@ +# 2024/08/15 20:06:15.870045 [TestUnmarshalUniId] [rapid] draw ID: []byte{0x0} +# 2024/08/15 20:06:15.870050 [TestUnmarshalUniId] [rapid] draw ID string: "" +# 2024/08/15 20:06:15.870052 [TestUnmarshalUniId] [rapid] draw Group key: []byte{} +# 2024/08/15 20:06:15.870053 [TestUnmarshalUniId] [rapid] draw Group key string: "" +# 2024/08/15 20:06:15.870053 [TestUnmarshalUniId] [rapid] draw ID field selector: 0x0 +# 2024/08/15 20:06:15.870055 [TestUnmarshalUniId] [rapid] draw proofType: 0 +# 2024/08/15 20:06:15.870056 [TestUnmarshalUniId] +# +v0.4.8#17568384081585189385 +0x5555555555555 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 \ No newline at end of file diff --git a/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200723-3629334.fail b/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200723-3629334.fail new file mode 100644 index 000000000..0ace329eb --- /dev/null +++ b/testdata/rapid/TestUnmarshalUniId/TestUnmarshalUniId-20240815200723-3629334.fail @@ -0,0 +1,26 @@ +# 2024/08/15 20:07:23.350129 [TestUnmarshalUniId] [rapid] draw ID: []byte{} +# 2024/08/15 20:07:23.350133 [TestUnmarshalUniId] [rapid] draw ID string: "AA" +# 2024/08/15 20:07:23.350135 [TestUnmarshalUniId] [rapid] draw Group key: []byte{} +# 2024/08/15 20:07:23.350136 [TestUnmarshalUniId] [rapid] draw Group key string: "" +# 2024/08/15 20:07:23.350137 [TestUnmarshalUniId] [rapid] draw ID field selector: 0x1 +# 2024/08/15 20:07:23.350138 [TestUnmarshalUniId] [rapid] draw proofType: 0 +# 2024/08/15 20:07:23.350140 [TestUnmarshalUniId] +# +v0.4.8#13202072726014832767 +0x0 +0x5555555555555 +0x0 +0x0 +0x0 +0x5555555555555 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x1 +0x0 +0x0 +0x0 \ No newline at end of file