diff --git a/schema/.gitignore b/schema/.gitignore new file mode 100644 index 0000000..be2499a --- /dev/null +++ b/schema/.gitignore @@ -0,0 +1,4 @@ +.idea/ +lbryschema-cli +lbryschema-python-binding.h +lbryschema-python-binding.so diff --git a/schema/.travis.yml b/schema/.travis.yml new file mode 100644 index 0000000..7a538d2 --- /dev/null +++ b/schema/.travis.yml @@ -0,0 +1,9 @@ +os: linux +dist: trusty +language: go + +go: + - 1.10.3 + +script: + - ./build_and_test.sh \ No newline at end of file diff --git a/schema/address/address_test.go b/schema/address/address_test.go new file mode 100644 index 0000000..5b631a5 --- /dev/null +++ b/schema/address/address_test.go @@ -0,0 +1,29 @@ +package address + +import "testing" + +func TestDecodeAddressLBRYCrdMain(t *testing.T) { + addr := "bUc9gyCJPKu2CBYpTvJ98MdmsLb68utjP6" + correct := [25]byte{85, 174, 41, 64, 245, 110, 91, 239, 43, 208, 32, 73, 115, 20, 70, 204, 83, 199, 3, + 206, 210, 176, 194, 188, 193} + result, err := DecodeAddress(addr, "lbrycrd_main") + if err != nil { + t.Error(err) + } + if result != correct { + t.Error("Mismatch") + } +} + +func TestEncodeAddressLBRYCrdMain(t *testing.T) { + addr := [25]byte{85, 174, 41, 64, 245, 110, 91, 239, 43, 208, 32, 73, 115, 20, 70, 204, 83, 199, 3, + 206, 210, 176, 194, 188, 193} + correct := "bUc9gyCJPKu2CBYpTvJ98MdmsLb68utjP6" + result, err := EncodeAddress(addr, "lbrycrd_main") + if err != nil { + t.Error(err) + } + if result != correct { + t.Error("Mismatch") + } +} diff --git a/schema/address/base58/base58_test.go b/schema/address/base58/base58_test.go new file mode 100644 index 0000000..5297064 --- /dev/null +++ b/schema/address/base58/base58_test.go @@ -0,0 +1 @@ +package base58 diff --git a/schema/address/base58/character.go b/schema/address/base58/character.go new file mode 100644 index 0000000..dd709b0 --- /dev/null +++ b/schema/address/base58/character.go @@ -0,0 +1,21 @@ +package base58 + +import ( + "errors" + "math/big" +) + +var b58Characters = [58]byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x48, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x6b, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a} + +func CharacterIndex(character byte) (*big.Int, error) { + for i := 0; i < 58; i++ { + if b58Characters[i] == character { + return big.NewInt(int64(i)), nil + } + } + return nil, errors.New("invalid character") +} diff --git a/schema/address/base58/checksum.go b/schema/address/base58/checksum.go new file mode 100644 index 0000000..48971f3 --- /dev/null +++ b/schema/address/base58/checksum.go @@ -0,0 +1,20 @@ +package base58 + +import "crypto/sha256" + +const checksumLength = 4 + +func VerifyBase58Checksum(v []byte) bool { + checksum := [checksumLength]byte{} + for i := range checksum { + checksum[i] = v[len(v)-checksumLength+i] + } + real_checksum := sha256.Sum256(v[:len(v)-checksumLength]) + real_checksum = sha256.Sum256(real_checksum[:]) + for i, c := range checksum { + if c != real_checksum[i] { + return false + } + } + return true +} diff --git a/schema/address/base58/decode.go b/schema/address/base58/decode.go new file mode 100644 index 0000000..44a277e --- /dev/null +++ b/schema/address/base58/decode.go @@ -0,0 +1,39 @@ +package base58 + +import ( + "errors" + "math/big" +) + +func DecodeBase58(value string, size int64) ([]byte, error) { + buf := []byte(value) + longValue := big.NewInt(0) + result := make([]byte, size) + for i := int64(len(buf) - 1); i >= 0; i-- { + to_add := big.NewInt(0) + to_add = to_add.Exp(big.NewInt(58), big.NewInt(i), to_add) + c, err := CharacterIndex(buf[int64(len(buf))-i-1]) + if err != nil { + return result, err + } + to_add = to_add.Mul(c, to_add) + longValue = longValue.Add(to_add, longValue) + } + for i := size - 1; i >= 0; i-- { + m := big.NewInt(0) + longValue, m = longValue.DivMod(longValue, big.NewInt(256), m) + bs := m.Bytes() + if len(bs) == 0 { + bs = append(bs, 0x00) + } + b := byte(bs[0]) + result[i] = b + } + if longValue.Int64() != 0 { + return result, errors.New("cannot decode to the given size") + } + if size != int64(len(result)) { + return result, errors.New("length mismatch") + } + return result, nil +} diff --git a/schema/address/base58/encode.go b/schema/address/base58/encode.go new file mode 100644 index 0000000..0922fdc --- /dev/null +++ b/schema/address/base58/encode.go @@ -0,0 +1,32 @@ +package base58 + +import ( + "math/big" +) + +func EncodeBase58(data []byte) string { + longValue := big.NewInt(0) + result := "" + for i := 0; i < len(data); i++ { + to_add := big.NewInt(0) + to_add = to_add.Exp(big.NewInt(256), big.NewInt(int64(i)), to_add) + to_add = to_add.Mul(big.NewInt(int64(data[24-i])), to_add) + longValue = longValue.Add(to_add, longValue) + } + i := 0 + for { + m := big.NewInt(0) + longValue, m = longValue.DivMod(longValue, big.NewInt(58), m) + bs := m.Bytes() + if len(bs) == 0 { + bs = append(bs, 0x00) + } + b := b58Characters[bs[0]] + result = string(b) + result + if longValue.Int64() == 0 { + break + } + i += 1 + } + return result +} diff --git a/schema/address/decode.go b/schema/address/decode.go new file mode 100644 index 0000000..0e3de50 --- /dev/null +++ b/schema/address/decode.go @@ -0,0 +1,19 @@ +package address + +import ( + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbryschema.go/address/base58" +) + +func DecodeAddress(address string, blockchainName string) ([addressLength]byte, error) { + decoded, err := base58.DecodeBase58(address, addressLength) + if err != nil { + return [addressLength]byte{}, errors.Err("failed to decode") + } + buf := [addressLength]byte{} + for i, b := range decoded { + buf[i] = b + } + + return ValidateAddress(buf, blockchainName) +} diff --git a/schema/address/encode.go b/schema/address/encode.go new file mode 100644 index 0000000..48e2697 --- /dev/null +++ b/schema/address/encode.go @@ -0,0 +1,13 @@ +package address + +import ( + "github.com/lbryio/lbryschema.go/address/base58" +) + +func EncodeAddress(address [addressLength]byte, blockchainName string) (string, error) { + buf, err := ValidateAddress(address, blockchainName) + if err != nil { + return "", err + } + return base58.EncodeBase58(buf[:]), nil +} diff --git a/schema/address/validate.go b/schema/address/validate.go new file mode 100644 index 0000000..74d8d05 --- /dev/null +++ b/schema/address/validate.go @@ -0,0 +1,69 @@ +package address + +import ( + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbryschema.go/address/base58" +) + +const lbrycrdMainPubkeyPrefix = byte(85) +const lbrycrdMainScriptPrefix = byte(122) + +const lbrycrdTestnetPubkeyPrefix = byte(111) +const lbrycrdTestnetScriptPrefix = byte(196) + +const lbrycrdRegtestPubkeyPrefix = byte(111) +const lbrycrdRegtestScriptPrefix = byte(196) + +const prefixLength = 1 +const pubkeyLength = 20 +const checksumLength = 4 +const addressLength = prefixLength + pubkeyLength + checksumLength + +const lbrycrdMain = "lbrycrd_main" +const lbrycrdTestnet = "lbrycrd_testnet" +const lbrycrdRegtest = "lbrycrd_regtest" + +var addressPrefixes = map[string][2]byte{ + lbrycrdMain: [2]byte{lbrycrdMainPubkeyPrefix, lbrycrdMainScriptPrefix}, + lbrycrdTestnet: [2]byte{lbrycrdTestnetPubkeyPrefix, lbrycrdTestnetScriptPrefix}, + lbrycrdRegtest: [2]byte{lbrycrdRegtestPubkeyPrefix, lbrycrdRegtestScriptPrefix}, +} + +func PrefixIsValid(address [addressLength]byte, blockchainName string) bool { + prefix := address[0] + for _, addrPrefix := range addressPrefixes[blockchainName] { + if addrPrefix == prefix { + return true + } + } + return false +} + +func PubKeyIsValid(address [addressLength]byte) bool { + pubkey := address[prefixLength : pubkeyLength+prefixLength] + // TODO: validate this for real + if len(pubkey) != pubkeyLength { + return false + } + return true +} + +func ChecksumIsValid(address [addressLength]byte) bool { + return base58.VerifyBase58Checksum(address[:]) +} + +func ValidateAddress(address [addressLength]byte, blockchainName string) ([addressLength]byte, error) { + if blockchainName != lbrycrdMain && blockchainName != lbrycrdTestnet && blockchainName != lbrycrdRegtest { + return address, errors.Err("invalid blockchain name") + } + if !PrefixIsValid(address, blockchainName) { + return address, errors.Err("invalid prefix") + } + if !PubKeyIsValid(address) { + return address, errors.Err("invalid pubkey") + } + if !ChecksumIsValid(address) { + return address, errors.Err("invalid address checksum") + } + return address, nil +} diff --git a/schema/build.sh b/schema/build.sh new file mode 100755 index 0000000..de13e86 --- /dev/null +++ b/schema/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euxo pipefail +go build ./... +go build ./cli/lbryschema-cli.go diff --git a/schema/build_and_test.sh b/schema/build_and_test.sh new file mode 100755 index 0000000..2458d78 --- /dev/null +++ b/schema/build_and_test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euxo pipefail +./build.sh +./test.sh diff --git a/schema/claim/claim.go b/schema/claim/claim.go new file mode 100644 index 0000000..8ef378a --- /dev/null +++ b/schema/claim/claim.go @@ -0,0 +1,249 @@ +package claim + +import ( + "encoding/hex" + "strconv" + + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbryschema.go/address" + legacy_pb "github.com/lbryio/types/v1/go" + pb "github.com/lbryio/types/v2/go" + + "github.com/golang/protobuf/proto" +) + +type version byte + +func (v version) byte() byte { + return byte(v) +} + +const ( + NoSig = version(byte(0)) + //Signature using ECDSA SECP256k1 key and SHA-256 hash. + WithSig = version(byte(1)) + UNKNOWN = version(byte(2)) +) + +type ClaimHelper struct { + *pb.Claim + LegacyClaim *legacy_pb.Claim + ClaimID []byte + Version version + Signature []byte + Payload []byte +} + +const migrationErrorMessage = "migration from v1 to v2 protobuf failed with: " + +func (c *ClaimHelper) ValidateAddresses(blockchainName string) error { + if c.Claim != nil { // V2 + // check the validity of a fee address + if c.Claim.GetStream() != nil { + fee := c.GetStream().GetFee() + if fee != nil { + return validateAddress(fee.GetAddress(), blockchainName) + } else { + return nil + } + } else if c.GetChannel() != nil { + return nil + } + } + + return errors.Err("claim helper created with migrated v2 protobuf claim 'invalid state'") +} + +func validateAddress(tmp_addr []byte, blockchainName string) error { + if len(tmp_addr) != 25 { + return errors.Err("invalid address length: " + strconv.FormatInt(int64(len(tmp_addr)), 10) + "!") + } + addr := [25]byte{} + for i := range addr { + addr[i] = tmp_addr[i] + } + _, err := address.EncodeAddress(addr, blockchainName) + if err != nil { + return errors.Err(err) + } + + return nil +} + +func getVersionFromByte(versionByte byte) version { + if versionByte == byte(0) { + return NoSig + } else if versionByte == byte(1) { + return WithSig + } + + return UNKNOWN +} + +func (c *ClaimHelper) ValidateCertificate() error { + if c.GetChannel() == nil { + return nil + } + _, err := c.GetPublicKey() + if err != nil { + return errors.Err(err) + } + return nil +} + +func (c *ClaimHelper) LoadFromBytes(raw_claim []byte, blockchainName string) error { + if c.String() != "" { + return errors.Err("already initialized") + } + if len(raw_claim) < 1 { + return errors.Err("there is nothing to decode") + } + + var claim_pb *pb.Claim + var legacy_claim_pb *legacy_pb.Claim + + version := getVersionFromByte(raw_claim[0]) //First byte = version + pbPayload := raw_claim[1:] + var claimID []byte + var signature []byte + if version == WithSig { + if len(raw_claim) < 86 { + return errors.Err("signature version indicated by 1st byte but not enough bytes for valid format") + } + claimID = raw_claim[1:21] // channel claimid = next 20 bytes + signature = raw_claim[21:85] // signature = next 64 bytes + pbPayload = raw_claim[85:] // protobuf payload = remaining bytes + } + + claim_pb = &pb.Claim{} + err := proto.Unmarshal(pbPayload, claim_pb) + if err != nil { + legacy_claim_pb = &legacy_pb.Claim{} + legacyErr := proto.Unmarshal(raw_claim, legacy_claim_pb) + if legacyErr == nil { + claim_pb, err = migrateV1PBClaim(*legacy_claim_pb) + if err != nil { + return errors.Prefix(migrationErrorMessage, err) + } + if legacy_claim_pb.GetPublisherSignature() != nil { + version = WithSig + claimID = legacy_claim_pb.GetPublisherSignature().GetCertificateId() + signature = legacy_claim_pb.GetPublisherSignature().GetSignature() + } + if legacy_claim_pb.GetCertificate() != nil { + version = NoSig + } + } else { + return err + } + } + + *c = ClaimHelper{ + Claim: claim_pb, + LegacyClaim: legacy_claim_pb, + ClaimID: claimID, + Version: version, + Signature: signature, + Payload: pbPayload, + } + + // Commenting out because of a bug in SDK release allowing empty addresses. + //err = c.ValidateAddresses(blockchainName) + //if err != nil { + // return err + //} + + err = c.ValidateCertificate() + if err != nil { + return err + } + + return nil +} + +func (c *ClaimHelper) LoadFromHexString(claim_hex string, blockchainName string) error { + buf, err := hex.DecodeString(claim_hex) + if err != nil { + return err + } + return c.LoadFromBytes(buf, blockchainName) +} + +func DecodeClaimProtoBytes(serialized []byte, blockchainName string) (*ClaimHelper, error) { + claim := &ClaimHelper{&pb.Claim{}, nil, nil, NoSig, nil, nil} + err := claim.LoadFromBytes(serialized, blockchainName) + if err != nil { + return nil, err + } + return claim, nil +} + +func DecodeClaimHex(serialized string, blockchainName string) (*ClaimHelper, error) { + claim_bytes, err := hex.DecodeString(serialized) + if err != nil { + return nil, errors.Err(err) + } + return DecodeClaimBytes(claim_bytes, blockchainName) +} + +// DecodeClaimBytes take a byte array and tries to decode it to a protobuf claim or migrate it from either json v1,2,3 or pb v1 +func DecodeClaimBytes(serialized []byte, blockchainName string) (*ClaimHelper, error) { + helper, err := DecodeClaimProtoBytes(serialized, blockchainName) + if err == nil { + return helper, nil + } + helper = &ClaimHelper{} + //If protobuf fails, try json versions before returning an error. + v1Claim := new(V1Claim) + err = v1Claim.Unmarshal(serialized) + if err != nil { + v2Claim := new(V2Claim) + err := v2Claim.Unmarshal(serialized) + if err != nil { + v3Claim := new(V3Claim) + err := v3Claim.Unmarshal(serialized) + if err != nil { + return nil, errors.Prefix("Claim value has no matching version", err) + } + helper.Claim, err = migrateV3Claim(*v3Claim) + if err != nil { + return nil, errors.Prefix("V3 Metadata Migration Error", err) + } + return helper, nil + } + helper.Claim, err = migrateV2Claim(*v2Claim) + if err != nil { + return nil, errors.Prefix("V2 Metadata Migration Error ", err) + } + return helper, nil + } + + helper.Claim, err = migrateV1Claim(*v1Claim) + if err != nil { + return nil, errors.Prefix("V1 Metadata Migration Error ", err) + } + return helper, nil +} + +func (c *ClaimHelper) GetStream() *pb.Stream { + if c != nil { + return c.Claim.GetStream() + } + return nil +} + +func (c *ClaimHelper) CompileValue() ([]byte, error) { + payload, err := c.serialized() + if err != nil { + return nil, err + } + var value []byte + value = append(value, c.Version.byte()) + if c.Version == WithSig { + value = append(value, c.ClaimID...) + value = append(value, c.Signature...) + } + value = append(value, payload...) + + return value, nil +} diff --git a/schema/claim/claim_test.go b/schema/claim/claim_test.go new file mode 100644 index 0000000..493a5df --- /dev/null +++ b/schema/claim/claim_test.go @@ -0,0 +1,33 @@ +package claim + +import "testing" + +func TestClaimHelper(t *testing.T) { + for _, rawClaim := range raw_claims { + helper, err := DecodeClaimHex(rawClaim, "lbrycrd_main") + if err != nil { + t.Error(err) + } + _, err = helper.RenderJSON() + if err != nil { + t.Error(err) + } + + _, err = helper.serialized() + if err != nil { + t.Error(err) + } + _, err = helper.serializedHexString() + if err != nil { + t.Error(err) + } + _, err = helper.serializedNoSignature() + if err != nil { + t.Error(err) + } + err = helper.ValidateAddresses("lbrycrd_main") + if err != nil { + t.Error(err) + } + } +} diff --git a/schema/claim/decode_test.go b/schema/claim/decode_test.go new file mode 100644 index 0000000..101aa82 --- /dev/null +++ b/schema/claim/decode_test.go @@ -0,0 +1,103 @@ +package claim + +import ( + "encoding/hex" + "testing" + + pb "github.com/lbryio/types/v2/go" + + "github.com/btcsuite/btcd/btcec" +) + +type rawClaim struct { + Hex string + ClaimID string +} + +var raw_claims = []string{ + "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367", + "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65", + "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f676966", + "080110011af901080112b101080410011a1c43414e47474948207c204b412044494c554152204e4547455249207c222c0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d5f5470313577746e7753732a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f5f5470313577746e77537352005a001a41080110011a304616bfdbb6fcb870d4c235443f9261d289ee8edbd4a51b8c6e3e95a34baeebbbb08978a7c5f9bf9a36245513b450943b2209766964656f2f6d70342a5c080110031a40f94a9db9c70217e4f17f9d38f08770096e7ce94a86b742b972e07c62c9606459c6ad735cd517175cf76ad6ea9eb16ca8198a17e2d31dc3ac53413005b5ca2a3a221402b1839207e2a706f0ba73dec0ce6b719043293d", + "080110011aa510080112dd0f080410011a4b4953545249204d4142554b204e41494b204b412021207b547269707d205369646f61726a6f202d2042616e797577616e67692042617275204e61696b204b41205372692054616e6a756e6722a80e4b657265746120617069205372692054616e6a756e67206164616c61682072616e676b6169616e206b657265746120617069206b656c617320656b6f6e6f6d69204143206a6172616b206a617568206d696c696b205054204b65726574612041706920496e646f6e6573696120285065727365726f292079616e67206d656c6179616e6920727574652042616e797577616e676920426172752d4c656d707579616e67616e2c2070702e204b65726574612061706920696e692064696f7065726173696b616e206f6c656820446165726168204f706572617369204958204a656d6265722c2079616e67206469616d62696c2064617269205372692054616e6a756e672c206e616d6120746f6b6f682064616c616d206365726974612072616b7961742042616e797577616e67692e0a0a4b65726574612061706920696e6920626572616e676b617420646172692042616e797577616e67692070756b756c2030362e3330205749422074696261206469204c656d707579616e67616e2070756b756c2031392e3330205749422c20736564616e676b616e20626572616e676b61742064617269204c656d707579616e67616e2070756b756c2030372e31352057494220746962612064692042616e797577616e67692070756b756c2032312e3135205749422e204b65726574612061706920696e69206d656d62617761207361747520676572626f6e6720616c696e672d616c696e6720626572757061206b65726574612070656d62616e676b6974202862696173616e7961204b5033292c20656e616d206b65726574612070656e756d70616e67206b656c617320656b6f6e6f6d692c2073617475206b6572657461206d616b616e2070656d62616e676b6974206b656c617320656b6f6e6f6d692c2064616e2068616d7069722073656c616c75206d656d626177612073617475206b65726574612062616761736920756e696b2079616e67206265727761726e61206269727520706f6c6f732e204e616d756e2073656972696e672064656e67616e2070656d62617275616e20696d616765205054204b41492c207365636172612062657274616861702073656d756120676572626f6e67206d656e6767756e616b616e206c697665727920224b65736570616b6174616e22206d656e67696b757469206b657265746120617069206c61696e6e79612e0a0a44616c616d207065726a616c616e616e6e79612c206b65726574612061706920696e692062657268656e7469206469205374617369756e2042616e797577616e676920426172752c204b6172616e676173656d2c20526f676f6a616d70692c2054656d7567757275682c204b616c6973657461696c2c2053756d626572776164756e672c20476c656e6d6f72652c204b616c69626172752c204b616c697361742c204a656d6265722c2052616d626970756a692c2054616e6767756c2c2050726f626f6c696e67676f2c20506173757275616e2c2042616e67696c2c205369646f61726a6f2c20537572616261796120477562656e672c20576f6e6f6b726f6d6f2c204d6f6a6f6b6572746f2c204a6f6d62616e672c204b6572746f736f6e6f2c204e67616e6a756b2c204361727562616e2c204d616469756e2c2042617261742c205061726f6e2c2057616c696b756b756e2c2053726167656e2c20507572776f736172692c204b6c6174656e2c2064616e204c656d707579616e67616e2c2064656e67616e20746f74616c2077616b74752074656d7075682073656b697461722031332d3134206a616d2e0a0a4b687573757320756e74756b206b652061726168205374617369756e2042616e797577616e6769204261727520284b41203139342f313935292c206b657265746120696e692064617061742062657268656e7469206469205374617369756e205361726164616e2028756e74756b2070657273696c616e67616e292c204261726f6e2c2053756d6f6269746f2028756e74756b2070657273696c616e67616e292c2064616e20536570616e6a616e672e204469205374617369756e20537572616261796120477562656e672c2064696c616b756b616e2070656d696e646168616e20706f73697369206c6f6b6f6d6f7469662e0a0a5061646120746168756e2032303136207461726966204b41205372692054616e6a756e67206b656d62616c69206469737562736964692070656d6572696e7461682e20506164612074616e6767616c2031204a616e756172692068696e676761203331204d617265742074617269666e7961206164616c6168205270203130302e3030302c30302c206d756c6169203120417072696c2074617269666e7961206164616c61682052702039362e3030302c30302c2064616e206d756c61692031204a756c692074617269666e7961206164616c61682052702039342e3030302c30302e202857696b697065646961290a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d754750344b5857614536512a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f754750344b58576145365152005a001a41080110011a30d3d1d49ce3268e3dcf318ebbb6f4bfd454995d6b772bd5e27630743c0fb1d66387bf84b51afe28733812c5495b837b8f2209766964656f2f6d70342a5c080110031a40a47aa2d45ec15d1e578b91e5c8c76ee8a82e55af37da4873a7703795889ee7400967cf41e903788bcf0510d7c06976c99983fa01e702e1fb6d518b0646b0d565221402b1839207e2a706f0ba73dec0ce6b719043293d", + "080110011aa30d080112db0c080410011a3b5354415349554e2042414e595557414e47492042415255207c2050656e6767616e7469205374617369756e2042616e797577616e6769204c616d6122b60b5374617369756e2042616e797577616e676920426172752028425729206164616c6168207374617369756e206b657265746120617069206b656c61732062657361722079616e6720626572616461206469204b65746170616e672c204b616c697075726f2c2042616e797577616e67692e205374617369756e2079616e67207465726c6574616b2070616461206b6574696e676769616e202b37206d20696e69206d65727570616b616e207374617369756e2079616e67206c6574616b6e79612070616c696e672074696d757220646920446165726168204f706572617369204958204a656d6265722e205374617369756e20696e692062657261646120646920756a756e672070616c696e672074696d75722050756c6175204a6177612064616e2068616e7961206265726a6172616b20313030206d6574657220646172692050656c61627568616e2046657269204b65746170616e6720736568696e676761207374617369756e20696e69206a75676120736572696e672064697365627574205374617369756e204b65746170616e672e205374617369756e20696e69206a756761206d65727570616b616e207374617369756e206b65726574612061706920616b7469662079616e67206265726c6f6b6173692070616c696e672074696d75722064692042616e797577616e67692c204a6177612054696d75722c2064616e20496e646f6e657369612e205374617369756e20696e6920646962616e67756e2062657273616d61616e2064656e67616e2070656d62616e67756e616e206a616c757220626172752064617269207374617369756e206e6f6e20616b746966204b61626174206d656e756a752070656c61627568616e207465727365627574207061646120746168756e20313938353b20646966756e6773696b616e20756e74756b206d656e6767616e74696b616e205374617369756e2042616e797577616e6769204c616d612079616e67206164612064692077696c61796168206b6f74612042616e797577616e67692e205374617369756e2042616e797577616e67692042617275207465726c6574616b203130206b6d20646172692077696c61796168206b6f7461206b6520617261682075746172613b20646962616e67756e20756e74756b206d656d656e756869206b656275747568616e207472616e73706f72746173692079616e672073696e657267697320616e74617261206b6572657461206170692064656e67616e206b6170616c20666572692064692070656e7965626572616e67616e204b65746170616e672e205374617369756e20696e69206d656d696c696b6920656e616d206a616c75722064656e67616e206a616c757220322073656261676169207365707572206c757275732e0a5374617369756e20696e692064696c656e676b6170692064656e67616e20737562206469706f206c6f6b6f6d6f7469662064616e206469706f206b65726574612020756e74756b206d656e79696d70616e2c206d6572617761742061726d616461206b657265746120617069206b68757375736e7961206d696c696b2044616f70204958206974752073656e646972692c206a756761206d656d70756e79616920207475726e7461626c652042616c6c6f6f6e204c6f6f702079616e67207465726c6574616b20646920736562656c61682075746172612e0a5374617369756e20696e69206a756761206d656c6179616e6920616e676b7574616e20626172616e672c207961697475206b6572657461206170692053656d656e205469676120526f64612079616e67206469626572616e676b61746b616e2064617269205374617369756e204e616d626f2064616e206d656e6a616469204b412079616e67206d656e656d707568206a6172616b2070616c696e67206a61756820646920496e646f6e657369612e5b77696b6970656469615d0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d475473535a5a30794a53452a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f475473535a5a30794a534552005a001a41080110011a3048b3efe92661810e11118c9f8c0b4b4d1bca195eb6f74c8325070d97c699f4fb7ecc9ac90b3decc4feeb2ea0431e65922209766964656f2f6d70342a5c080110031a408e9fc836cad00c52ec7cdc95c11fc5369874948891df2187faaee212ca0925fc1058df5339c153dc00f055a8a21b853fb449a8ccb25ea52a98ba5645b22bdbfb221402b1839207e2a706f0ba73dec0ce6b719043293d", + "080110011aa40b080112dc0a080410011a465354415349554e2054454d504548202854504529207c504155442d4b422053454b4152204152554d7c204a616c7572204d617469204c756d616a616e672d506173697269616e22ac095374617369756e2054656d706568202854504529206b6574696e676769616e202b3933206d206d65727570616b616e207374617369756e206b657265746120617069206d61746920286e6f6e20616b746966292079616e67207465726c6574616b20646920447573756e2054756c757372656a6f20492054656d706568204c6f72204b6563616d6174616e2054656d7065682c204b616275706174656e204c756d616a616e672064656e67616e204b6f6f7264696e6174203a203038c2b03131e280b235372e32e280b34c532c313133c2b03130e280b232392e38e280b342542e205374617369756e20696e69206d65727570616b616e2073616c61682073617475207374617369756e2070616461206a616c7572206b657265746120617069204c756d616a616e672d506173697269616e2079616e67206d756c616920646967756e616b616e2074616e6767616c203136204d65692031383936202064616e2074656c616820646974757475702073656d656e6a616b203120466562727561726920313938382e2050616461206d6173612070656e6a616a6168616e2042656c616e64612c206a616c75722d6a616c757220696e69206265726164612064692062617761682070656e67656c6f6c61616e2053746161747373706f6f722d20656e205472616d776567656e20284f6f737465726c696a6e656e29202853532d4f4c292e0a5361617420696e69206b6f6e64697369205374617369756e2054656d7065682074696e6767616c2062616e67756e616e207574616d612c2079616e672064696a6164696b616e20736562616761692074656d706174206265726d61696e206b616e616b2d6b616e616b205041554420e28093204b422053454b4152204152554d2e0a4a616c7572206b65726574612061706920696e692070616461206d617361206c616c75206d65727570616b616e206a616c75722079616e672063756b757020736962756b2c2064656e67616e205374617369756e204c756d616a616e67202d79616e67207465726265736172206469206a616c757220696e69206d656c6179616e692068616d706972203330302e3030302070656e756d70616e6720706572746168756e2064616e20626172616e672068696e676761203233207269627520746f6e206c6562696820646920616e7461726120746168756e20313935302d31393533202e204a756d6c61682070656e756d70616e672079616e67206e61696b2064617269205374617369756e2054656d7065682068616d70697220736574656e6761682062616e79616b6e79612079616e67206e61696b2064617269205374617369756e204c756d616a616e672e0a53656d656e74617261206a6172696e67616e2072656c2062657365727461206b656c656e676b6170616e20776573656c2064616e2070657273696e79616c616e6e79612074656c61682068616269732074616b20626572736973612e2042656b6173206a616c75722072656c6e7961206b696e69206d656e6a616469206a616c616e206b6563696c202867616e672920616e746172206b616d70756e672e202857696b697065646961290a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d43704e33485f6f67695f6f2a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f43704e33485f6f67695f6f52005a001a41080110011a30c11a6c72dc5cf5bb9b80bb58e760893984010a219702062234ef6eb9ec9572353a3c1b5b4da91a57057ee671b454f3c22209766964656f2f6d70342a5c080110031a40a8134e7e6e123c0b9ca568d95d8804da8a70877bdc47ca9b1c536db3a0e35a0de213ee66e3df77d42fb0c47cbd32c901b344b3e017f355169d7f85722a124dc9221402b1839207e2a706f0ba73dec0ce6b719043293d", + "080110011a8307080112bb06080410011a484b4120534552415955202032313520444154414e47204449205354415349554e2042414e4a415220204d454d424157412050554c414e47207c4a75727573616e205057542d505345228905536161742073656c657361692070656e656c75737572616e206a616c7572206e6f6e20616b7469662042616e6a61722c2050616e67616e646172616e2073616d7061692043696a756c616e67206d61756e79612074656d75616e206469207374617369756e2042616e6a617220756e74756b206d656e67756361706b616e2073616c616d207065727069736168616e2064656e67616e204f6d204d6179626920507261626f776f2073616e67206d617374657220626c7573756b616e206a616c7572206e6f6e20616b7469662c207465726e79617461204b412079616e6720616b616e206d656d626177612070756c616e672062616c696b206b65204a616b6172746120646174616e672064756c75616e206461726920507572776f6b6572746f2064616e20736179612073656e64697269206d617369682062657261646120646961746173206a656d626174616e2f4f7665727061732079616e672062657261646120646920736562656c6168207374617369756e2042616e6a61722061726168204a616b617274612c20616b6869726e79612073616c616d207065727069736168616e2068616e79612064656e67616e206d656d766964696f6b616e206b65726574612079616e67206d656d626177616e79612070756c616e672062616c696b206b65204a616b6172746120646172692061746173204a656d626174616e2f4f7665727061732e0a536179612073656e646972692062616c696b206b652053757261626179612064656e67616e204b4120506173756e64616e2c20476f6f64627965206d7920667269656e6420746f206d65657420616761696e2e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d596c6f594d3353447430452a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f596c6f594d33534474304552005a001a41080110011a308c591efe76bd6d31b39c553996f925b3002b6fc150116f0e8d6bf7654e6674c5b3a59baef24c50fa908580a02dd90ded2209766964656f2f6d70342a5c080110031a40cbef89584d26bbf2695e039a10f2b34749843d827323c530e63b0472407fc7b184d174634d91f05efee9b90c1706e319bd9641226728524952e2b9004400684d221402b1839207e2a706f0ba73dec0ce6b719043293d", + "080110011aed03080112a503080410011a3b4b41205345524159552020323136204449204a504c203432362d41205354415349554e2042414e4a4152207c4a75727573616e205053452d505754228002496e696c6168204b4120536572617975203231362079616e67206d656d62617761206d617374657220626c7573756b616e206a616c7572206e6f6e20616b746966204f6d204d6179626920507261626f776f202068747470733a2f2f7777772e796f75747562652e636f6d2f6368616e6e656c2f55435076355953496f59716f38364a525f4871626b437251202e0a5361617420696e6920616b616e206d656e656c7573757269206a616c7572206e6f6e20616b7469662042616e6a61722d50616e67616e646172616e2d43696a756c616e672e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4a6b4347615473774c35632a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f4a6b4347615473774c356352005a001a41080110011a302ed97c79df5eccb145f8f8e1e866be1a392004a6794347c08c7e851c5f00b1504092a9f3c0674c78805a73a33c8b1bf32209766964656f2f6d70342a5c080110031a40cbcec20908e60b5f6198aecc192d2a9e4b069aa58d9238cb7154e37c4d04f268feefe92c2705c14009acf32e7e876df180cff3afdea6c989e75b4861150d1644221402b1839207e2a706f0ba73dec0ce6b719043293d", +} + +func TestDecodeClaim(t *testing.T) { + claimHex := "000aa4010a8a010a30f1303989f58396694b0c5982c97f7e9d9435841d92aa13f4b80f671c27110c469babc4fbf4bd764155eaac089cfc49e8121454554d205045204d45524e45204c41472e6d703418cad0c8012209766964656f2f6d70343230c2c9389731e2a9568f66c78d703736a8c341015ada2e46f5dcc87aa6f08ab17c02df2121d9f6ef74055827a29dfc75801a044e6f6e6532040803180a5a0908b001109001188102421054554d205045204d45524e45204c41474a0944657369206c6f636b62020801" + claim, err := DecodeClaimHex(claimHex, "lbrycrd_main") + if err != nil { + t.Error(err, claim.ClaimID) + } +} + +func TestDecodeClaims(t *testing.T) { + for _, claim_hex := range raw_claims { + claim, err := DecodeClaimHex(claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + serializedHex, err := claim.serializedHexString() + if err != nil { + t.Error(err) + } + if serializedHex != claim_hex { + t.Error("failed to re-serialize") + } + + } +} + +func TestStripSignature(t *testing.T) { + claimHex := raw_claims[1] + claim, err := DecodeClaimHex(claimHex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + noSig, err := claim.serializedNoSignature() + if err != nil { + t.Error(err) + } + if hex.EncodeToString(noSig) != raw_claims[2] { + t.Error("failed to remove signature") + } +} + +func TestCreateChannelClaim(t *testing.T) { + private, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Error(err) + } + pubKeyBytes, err := PublicKeyToDER(private.PubKey()) + if err != nil { + t.Error(err) + } + claim := &ClaimHelper{Claim: newChannelClaim(), Version: NoSig} + claim.GetChannel().PublicKey = pubKeyBytes + claim.Title = "Test Channel Title" + claim.Description = "Test Channel Description" + claim.GetChannel().Cover = &pb.Source{Url: "http://testcoverurl.com"} + claim.Tags = []string{"TagA", "TagB", "TagC"} + claim.Languages = []*pb.Language{{Language: pb.Language_en}, {Language: pb.Language_es}} + claim.Thumbnail = &pb.Source{Url: "http://thumbnailurl.com"} + claim.GetChannel().WebsiteUrl = "http://homepageurl.com" + claim.Locations = []*pb.Location{{Country: pb.Location_AD}, {Country: pb.Location_US, State: "NJ", City: "some city"}} + + rawClaim, err := claim.CompileValue() + if err != nil { + t.Error(err) + } + + claim, err = DecodeClaimBytes(rawClaim, "lbrycrd_main") + if err != nil { + t.Error(err) + } + + if bytes, err := claim.CompileValue(); err != nil || len(bytes) != len(rawClaim) { + t.Error("decoded claim does not match original") + } + +} diff --git a/schema/claim/keys.go b/schema/claim/keys.go new file mode 100644 index 0000000..cb3301e --- /dev/null +++ b/schema/claim/keys.go @@ -0,0 +1,50 @@ +package claim + +import ( + "crypto/elliptic" + "crypto/x509/pkix" + "encoding/asn1" + + "github.com/lbryio/lbry.go/extras/errors" + + "github.com/btcsuite/btcd/btcec" +) + +func PublicKeyToDER(publicKey *btcec.PublicKey) ([]byte, error) { + var publicKeyBytes []byte + var publicKeyAlgorithm pkix.AlgorithmIdentifier + var err error + pub := publicKey.ToECDSA() + publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) + //ans1 encoding oid for ecdsa public key https://github.com/golang/go/blob/release-branch.go1.12/src/crypto/x509/x509.go#L457 + publicKeyAlgorithm.Algorithm = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + //asn1 encoding oid for secp256k1 https://github.com/bitpay/bitpay-go/blob/v2.2.2/key_utils/key_utils.go#L30 + paramBytes, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 132, 0, 10}) + if err != nil { + return nil, errors.Err(err) + } + publicKeyAlgorithm.Parameters.FullBytes = paramBytes + + return asn1.Marshal(publicKeyInfo{ + Algorithm: publicKeyAlgorithm, + PublicKey: asn1.BitString{ + Bytes: publicKeyBytes, + BitLength: 8 * len(publicKeyBytes), + }, + }) + +} + +func (c *ClaimHelper) GetPublicKey() (*btcec.PublicKey, error) { + if c.GetChannel() == nil { + return nil, errors.Err("claim is not of type channel, so there is no public key to get") + } + return getPublicKeyFromBytes(c.GetChannel().PublicKey) +} + +func getPublicKeyFromBytes(pubKeyBytes []byte) (*btcec.PublicKey, error) { + PKInfo := publicKeyInfo{} + asn1.Unmarshal(pubKeyBytes, &PKInfo) + pubkeyBytes1 := []byte(PKInfo.PublicKey.Bytes) + return btcec.ParsePubKey(pubkeyBytes1, btcec.S256()) +} diff --git a/schema/claim/keys_test.go b/schema/claim/keys_test.go new file mode 100644 index 0000000..0a5e4f6 --- /dev/null +++ b/schema/claim/keys_test.go @@ -0,0 +1,36 @@ +package claim + +import ( + "testing" + + "gotest.tools/assert" +) + +// The purpose of this test, is to make sure the function converts btcec.PublicKey to DER format the same way +// lbry SDK does as this is the bytes that are put into protobuf and the same bytes are used for verify signatures. +// Making sure these +func TestPublicKeyToDER(t *testing.T) { + cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367" + cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + p1, err := getPublicKeyFromBytes(cert_claim.Claim.GetChannel().PublicKey) + if err != nil { + t.Error(err) + } + + pubkeyBytes2, err := PublicKeyToDER(p1) + if err != nil { + t.Error(err) + } + for i, b := range cert_claim.Claim.GetChannel().PublicKey { + assert.Assert(t, b == pubkeyBytes2[i], "DER format in bytes must match!") + } + + p2, err := getPublicKeyFromBytes(pubkeyBytes2) + if err != nil { + t.Error(err) + } + assert.Assert(t, p1.IsEqual(p2), "The keys produced must be the same key!") +} diff --git a/schema/claim/migration.go b/schema/claim/migration.go new file mode 100644 index 0000000..9e74f2f --- /dev/null +++ b/schema/claim/migration.go @@ -0,0 +1,197 @@ +package claim + +import ( + "encoding/hex" + + "github.com/lbryio/lbry.go/extras/errors" + v1pb "github.com/lbryio/types/v1/go" + pb "github.com/lbryio/types/v2/go" + + "github.com/btcsuite/btcutil/base58" +) + +const lbrySDHash = "lbry_sd_hash" + +func newStreamClaim() *pb.Claim { + claimStream := new(pb.Claim_Stream) + stream := new(pb.Stream) + + pbClaim := new(pb.Claim) + pbClaim.Type = claimStream + claimStream.Stream = stream + + return pbClaim +} + +func newChannelClaim() *pb.Claim { + claimChannel := new(pb.Claim_Channel) + channel := new(pb.Channel) + + pbClaim := new(pb.Claim) + pbClaim.Type = claimChannel + claimChannel.Channel = channel + + return pbClaim +} + +func setMetaData(claim *pb.Claim, author string, description string, language pb.Language_Language, license string, + licenseURL *string, title string, thumbnail *string, nsfw bool) { + claim.Title = title + claim.Description = description + + claim.GetStream().Author = author + claim.Languages = []*pb.Language{{Language: language}} + + if thumbnail != nil { + source := new(pb.Source) + source.Url = *thumbnail + claim.Thumbnail = source + } + if nsfw { + claim.Tags = []string{"mature"} + } + claim.GetStream().License = license + if licenseURL != nil { + claim.GetStream().LicenseUrl = *licenseURL + } +} + +func migrateV1PBClaim(vClaim v1pb.Claim) (*pb.Claim, error) { + if *vClaim.ClaimType == v1pb.Claim_streamType { + return migrateV1PBStream(vClaim) + } + if *vClaim.ClaimType == v1pb.Claim_certificateType { + return migrateV1PBChannel(vClaim) + } + return nil, errors.Err("Could not migrate v1 protobuf claim due to unknown type '%s'.", vClaim.ClaimType.String()) +} + +func migrateV1PBStream(vClaim v1pb.Claim) (*pb.Claim, error) { + claim := newStreamClaim() + source := new(pb.Source) + source.MediaType = vClaim.GetStream().GetSource().GetContentType() + source.SdHash = vClaim.GetStream().GetSource().GetSource() + claim.GetStream().Source = source + md := vClaim.GetStream().GetMetadata() + if md.GetFee() != nil { + claim.GetStream().Fee = new(pb.Fee) + claim.GetStream().GetFee().Amount = uint64(*md.GetFee().Amount * 100000000) + claim.GetStream().GetFee().Address = md.GetFee().GetAddress() + claim.GetStream().GetFee().Currency = pb.Fee_Currency(pb.Fee_Currency_value[md.GetFee().GetCurrency().String()]) + } + if vClaim.GetStream().GetMetadata().GetNsfw() { + claim.Tags = []string{"mature"} + } + thumbnailSource := new(pb.Source) + thumbnailSource.Url = md.GetThumbnail() + claim.Thumbnail = thumbnailSource + language := pb.Language_Language(pb.Language_Language_value[md.GetLanguage().String()]) + claim.Languages = []*pb.Language{{Language: language}} + claim.GetStream().LicenseUrl = md.GetLicenseUrl() + claim.GetStream().License = md.GetLicense() + claim.Title = md.GetTitle() + claim.Description = md.GetDescription() + claim.GetStream().Author = md.GetAuthor() + + return claim, nil +} + +func migrateV1PBChannel(vClaim v1pb.Claim) (*pb.Claim, error) { + claim := newChannelClaim() + claim.GetChannel().PublicKey = vClaim.GetCertificate().PublicKey + + return claim, nil +} + +func migrateV1Claim(vClaim V1Claim) (*pb.Claim, error) { + pbClaim := newStreamClaim() + //Stream + // -->Universal + setFee(vClaim.Fee, pbClaim) + // -->MetaData + language := pb.Language_Language(pb.Language_Language_value[vClaim.Language]) + setMetaData(pbClaim, vClaim.Author, vClaim.Description, language, + vClaim.License, nil, vClaim.Title, vClaim.Thumbnail, false) + // -->Source + source := new(pb.Source) + source.MediaType = vClaim.ContentType + + src, err := hex.DecodeString(vClaim.Sources.LbrySDHash) + if err != nil { + return nil, errors.Err(err) + } + source.SdHash = src + pbClaim.GetStream().Source = source + + return pbClaim, nil +} + +func migrateV2Claim(vClaim V2Claim) (*pb.Claim, error) { + pbClaim := newStreamClaim() + //Stream + // -->Fee + setFee(vClaim.Fee, pbClaim) + // -->MetaData + language := pb.Language_Language(pb.Language_Language_value[vClaim.Language]) + setMetaData(pbClaim, vClaim.Author, vClaim.Description, language, + vClaim.License, vClaim.LicenseURL, vClaim.Title, vClaim.Thumbnail, vClaim.NSFW) + // -->Source + source := new(pb.Source) + source.MediaType = vClaim.ContentType + src, err := hex.DecodeString(vClaim.Sources.LbrySDHash) + if err != nil { + return nil, errors.Err(err) + } + source.SdHash = src + pbClaim.GetStream().Source = source + + return pbClaim, nil +} + +func migrateV3Claim(vClaim V3Claim) (*pb.Claim, error) { + pbClaim := newStreamClaim() + //Stream + // -->Fee + setFee(vClaim.Fee, pbClaim) + // -->MetaData + language := pb.Language_Language(pb.Language_Language_value[vClaim.Language]) + setMetaData(pbClaim, vClaim.Author, vClaim.Description, language, + vClaim.License, vClaim.LicenseURL, vClaim.Title, vClaim.Thumbnail, vClaim.NSFW) + // -->Source + source := new(pb.Source) + source.MediaType = vClaim.ContentType + src, err := hex.DecodeString(vClaim.Sources.LbrySDHash) + if err != nil { + return nil, errors.Err(err) + } + source.SdHash = src + pbClaim.GetStream().Source = source + + return pbClaim, nil +} + +func setFee(fee *Fee, pbClaim *pb.Claim) { + if fee != nil { + amount := float32(0.0) + currency := pb.Fee_LBC + address := "" + if fee.BTC != nil { + amount = fee.BTC.Amount + currency = pb.Fee_BTC + address = fee.BTC.Address + } else if fee.LBC != nil { + amount = fee.LBC.Amount + currency = pb.Fee_LBC + address = fee.LBC.Address + } else if fee.USD != nil { + amount = fee.USD.Amount + currency = pb.Fee_USD + address = fee.USD.Address + } + pbClaim.GetStream().Fee = new(pb.Fee) + //Fee Settings + pbClaim.GetStream().GetFee().Amount = uint64(amount * 100000000) + pbClaim.GetStream().GetFee().Currency = currency + pbClaim.GetStream().GetFee().Address = base58.Decode(address) + } +} diff --git a/schema/claim/migration_test.go b/schema/claim/migration_test.go new file mode 100644 index 0000000..e59a9cf --- /dev/null +++ b/schema/claim/migration_test.go @@ -0,0 +1,282 @@ +package claim + +import ( + "bytes" + "encoding/hex" + "fmt" + "testing" + + "github.com/lbryio/lbryschema.go/address" + + "github.com/btcsuite/btcutil/base58" + "gotest.tools/assert" +) + +type valueTestPair struct { + ValueAsHex string + Claim claimResult +} + +type claimResult struct { + Version string + Author string + Title string + Description string + License string + LicenseURL string + FeeAmount float32 + FeeCurrency string + FeeAddress string + ContentType string + Language string + LbrySDHash string + Thumbnail string + NSFW bool +} + +var badJsonVersionTests = []string{ + "7b22666565223a20352c2022766572223a2022302e302e33222c20226c6963656e7365223a202247504c20332e30222c20226c616e6775616765223a2022656e222c2022617574686f72223a2022576f6e64657220576f6d616e2031393933222c20227469746c65223a2022576f6e64657220576f6d616e2031393933222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022323338663832363932356462323235353863646335636430373436303637626666393964636263333834333832666661303236646463373230373261366362323461393332346461373961626136643232373437313164653035353063396534227d2c20226e736677223a20747275652c2022636f6e74656e745f74797065223a2022696d6167652f6a706567222c20227468756d626e61696c223a2022687474703a2f2f692e696d6775722e636f6d2f4568344a4658732e6a7067222c20226465736372697074696f6e223a2022466f7220746865206c6f766521227d", + "7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c2022617574686f72223a20225061756c204b6176616e616768222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022393962383766363064643136643730316538613562666238353130633938343239623866633563656538623764663333643665666135386464313133313261333665616437303638643133636364636331383563376465613730643930393261227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d", + "080110011ac3bd01080112c2b501080410011a0b57696e205465737420313122065465737420322a0444617665322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e697465642053746174657338004224080110031a19556bc2a9c28bc2b057c3aec2901d5445c3b17cc384c3bd29c3b72b5f74101cc2aec2aac2a1250000c2b2424a0052005a3868747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f64651a41080110011a302a1ec2a42cc383223f7cc2b4c291353d0f687675c289c3a34cc3a73942c383c3abc3b7330dc3912d162ac388c3bec3a5c2aec293c2b1c38f68c3a2c2bfc2907a04c3b8c39dc2b1742209696d6167652f706e67", + "7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c2022617574686f72223a20225061756c204b6176616e616768222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022393962383766363064643136643730316538613562666238353130633938343239623866633563656538623764663333643665666135386464313133313261333665616437303638643133636364636331383563376465613730643930393261227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d", + "7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c2022617574686f72223a20225061756c204b6176616e616768222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022626639663033333464623237393634303730663035636637356262376131626664663431363864356565376130633866373563663661623536653965633363643039633534636636383262353366316531353030363930616165353530663134227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d", + "30383031313030313161356330383031313231343038303431303030316130303232303032613030333230303338303034613030353230303561303031613432303830313130303131613330333933333030616561386262623636613238613966633031316231666238373634636364386338323633656233343532623733303832363233656534356431363866646530343836346435303563376535396539353430636263643766623336323230613639366436313637363532663661373036353637", +} + +var jsonVersionTests = []valueTestPair{ + {"7b22666565223a207b224c4243223a207b22616d6f756e74223a20312e302c202261646472657373223a2022625077474139683775696a6f79357541767a565051773951794c6f595a6568484a6f227d7d2c20226465736372697074696f6e223a2022313030304d4220746573742066696c6520746f206d65617375726520646f776e6c6f6164207370656564206f6e204c627279207032702d6e6574776f726b2e222c20226c6963656e7365223a20224e6f6e65222c2022617574686f72223a2022726f6f74222c20226c616e6775616765223a2022456e676c697368222c20227469746c65223a2022313030304d4220737065656420746573742066696c65222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022626439343033336431336634663339303837303837303163616635363562666130396366616466326633346661646634613733666238366232393564316232316137653634383035393934653435623566626336353066333062616334383734227d2c2022636f6e74656e742d74797065223a20226170706c69636174696f6e2f6f637465742d73747265616d222c20227468756d626e61696c223a20222f686f6d65726f626572742f6c6272792f73706565642e6a7067227d", + claimResult{"0.0.1", + "root", + "1000MB speed test file", + "1000MB test file to measure download speed on Lbry p2p-network.", + "None", + "", + 1, + "LBC", + "bPwGA9h7uijoy5uAvzVPQw9QyLoYZehHJo", + "application/octet-stream", + "", //"English" is not supported for conversion. + "bd94033d13f4f3908708701caf565bfa09cfadf2f34fadf4a73fb86b295d1b21a7e64805994e45b5fbc650f30bac4874", + "/homerobert/lbry/speed.jpg", + false, + }, + }, + {"7b22666565223a207b224c4243223a207b22616d6f756e74223a2035302e302c202261646472657373223a2022624c5673336966507275795a6e70596d46665432544c416d68715a76676a70514461227d7d2c20226465736372697074696f6e223a2022466f757220636f75706c6573206d65657420666f722053756e646179206272756e6368206f6e6c7920746f20646973636f76657220746865792061726520737475636b20696e206120686f75736520746f6765746865722061732074686520776f726c64206d61792062652061626f757420746f20656e642e222c20226c6963656e7365223a20224f7363696c6c6f73636f7065204c61626f7261746f72696573222c2022617574686f72223a20225772697474656e20616e6420646972656374656420627920546f646420426572676572222c20226c616e6775616765223a2022656e222c20227469746c65223a2022497427732061204469736173746572222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022646363316266323838393361353033376561623965346139636437613462666536663736616436633231393730656134636565653031323266353032656630373936353764346463613435366234626533323439383439633465313836386238227d2c2022636f6e74656e742d74797065223a2022766964656f2f717569636b74696d65222c20227468756d626e61696c223a2022687474703a2f2f69612e6d656469612d696d64622e636f6d2f696d616765732f4d2f4d5635424d5451774e6a597a4d5451304d6c35424d6c3542616e426e586b46745a5463774e44557a4f444d354e7740402e5f56315f5359313030305f4352302c302c3637332c313030305f414c5f2e6a7067227d", + claimResult{"0.0.1", + "Written and directed by Todd Berger", + "It's a Disaster", + "Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.", + "Oscilloscope Laboratories", + "", + 50, + "LBC", + "bLVs3ifPruyZnpYmFfT2TLAmhqZvgjpQDa", + "video/quicktime", + "language:en ", + "dcc1bf28893a5037eab9e4a9cd7a4bfe6f76ad6c21970ea4ceee0122f502ef079657d4dca456b4be3249849c4e1868b8", + "http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg", + false, + }, + }, + {"7b226465736372697074696f6e223a202241647669736f7220416c6578205461626172726f6b206769766573206869732074616b65206f6e204c4252592e222c20226c6963656e7365223a20224c4252592c20496e632e222c2022617574686f72223a202253616d75656c20427279616e222c20226c616e6775616765223a2022656e222c20227469746c65223a20224d65657420746865205465616d20457069736f64652031222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022373939613164653933623865353536643766363638313033613666666334386163356664363830316464346438396361653637373363383064383163323833373130666434666432356564363864306462656565323638663832393134313435227d2c2022636f6e74656e742d74797065223a2022766964656f2f6d7034227d", + claimResult{"0.0.1", + "Samuel Bryan", + "Meet the Team Episode 1", + "Advisor Alex Tabarrok gives his take on LBRY.", + "LBRY, Inc.", + "", + 0, + "UNKNOWN_CURRENCY", + "", + "video/mp4", + "language:en ", + "799a1de93b8e556d7f668103a6ffc48ac5fd6801dd4d89cae6773c80d81c283710fd4fd25ed68d0dbeee268f82914145", + "http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg", + false, + }, + }, + {"7b226c616e6775616765223a2022656e222c2022666565223a207b22555344223a207b22616d6f756e74223a20302e30312c202261646472657373223a2022624d486d5a4b5a6250713662504245514663384d5870694468463966374d56784d52227d7d2c2022736f7572636573223a207b226c6272795f73645f68617368223a2022326264386439646431613231386337663536373137653533666135313065666435613863303839656431663236373561306638643062356238626233633165643338336362396633616562396238393137383937363133303532393339373961227d2c20226465736372697074696f6e223a2022636c6f756473222c20226c6963656e7365223a2022637265617469766520636f6d6d6f6e73222c2022617574686f72223a202268747470733a2f2f7777772e76696465657a792e636f6d2f636c6f7564732f323637362d6461726b2d73746f726d2d636c6f7564732d726f79616c74792d667265652d68642d73746f636b2d766964656f222c20226e736677223a2066616c73652c20227469746c65223a2022636c6f756473222c2022636f6e74656e742d74797065223a2022766964656f2f6d7034222c2022766572223a2022302e302e32227d", + claimResult{"0.0.2", + "https://www.videezy.com/clouds/2676-dark-storm-clouds-royalty-free-hd-stock-video", + "clouds", + "clouds", + "creative commons", + "", + 0.01, + "USD", + "bMHmZKZbPq6bPBEQFc8MXpiDhF9f7MVxMR", + "video/mp4", + "language:en ", + "2bd8d9dd1a218c7f56717e53fa510efd5a8c089ed1f2675a0f8d0b5b8bb3c1ed383cb9f3aeb9b891789761305293979a", + "", + false, + }, + }, + {"7b226c6963656e7365223a2022437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e6974656420537461746573222c2022666565223a207b224c4243223a207b22616d6f756e74223a20312c202261646472657373223a20226256507157775966766a424859426f7575766b6e62514d58555a46764c644573354d227d7d2c2022766572223a2022302e302e32222c20226465736372697074696f6e223a20227a222c20226c616e6775616765223a2022656e222c2022617574686f72223a202279222c20227469746c65223a202278222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022376332316565323337333234653561353061333432353632306665366363343030643363636363303535313938363763646131623963313061393737313934653331323030343134643837313436626666343730626162376637643735343738227d2c20226e736677223a2066616c73652c20226c6963656e73655f75726c223a202268747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f6465222c2022636f6e74656e742d74797065223a2022746578742f706c61696e227d", + claimResult{"0.0.2", + "y", + "x", + "z", + "Creative Commons Attribution 3.0 United States", + "https://creativecommons.org/licenses/by/3.0/us/legalcode", + 1, + "LBC", + "bVPqWwYfvjBHYBouuvknbQMXUZFvLdEs5M", + "text/plain", + "language:en ", + "7c21ee237324e5a50a3425620fe6cc400d3cccc05519867cda1b9c10a977194e31200414d87146bff470bab7f7d75478", + "", + false, + }, + }, + {"7b22666565223a207b22555344223a207b22616d6f756e74223a20302e342c202261646472657373223a202262485365334b417674565352346d365331317a6475754639584874777363446a6f45227d7d2c2022766572223a2022302e302e33222c20226c6963656e7365223a2022437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e6974656420537461746573222c20226c616e6775616765223a2022656e222c20227469746c65223a2022526561647920506c61796572204f6e652028417564696f626f6f6b2031206f66203229222c2022617574686f72223a202245726e65737420436c696e65222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022333430653164646130653834313463323166616662623166323866326338623338343832316665306431653261373134336134383130353435653634653236613431303530343264346434376463393735346236313865636466653064313931227d2c20226e736677223a2066616c73652c2022636f6e74656e745f74797065223a2022617564696f2f6d706567222c20226c6963656e73655f75726c223a202268747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f6465222c20227468756d626e61696c223a2022687474703a2f2f692e696d6775722e636f6d2f6c794b45485a632e6a7067222c20226465736372697074696f6e223a2022496e20746865207965617220323034342c2074686520776f726c64206973206772697070656420627920616e20656e65726779206372697369732063617573696e67207769646573707265616420736f6369616c2070726f626c656d7320616e642065636f6e6f6d696320737461676e6174696f6e2e20546865207072696d6172792065736361706520666f72206d6f73742070656f706c6520697320746865204f415349532c2061207669727475616c20756e6976657273652c20616363657373656420776974682061207669736f7220616e642068617074696320676c6f7665732e2049742066756e6374696f6e7320626f746820617320616e204d4d4f52504720616e642061732061207669727475616c20736f63696574792c2077697468206974732063757272656e6379206265696e6720746865206d6f737420737461626c652063757272656e637920696e2074686520776f726c642e204974207761732063726561746564206279204a616d65732048616c6c696461792c2077686f73652077696c6c206c656674206120736572696573206f6620636c75657320746f776172647320616e20456173746572204567672077697468696e20746865204f41534953207468617420776f756c64206772616e742077686f6576657220666f756e6420697420626f74682068697320666f7274756e6520616e6420636f6e74726f6c206f6620746865204f4153495320697473656c662e205468697320686173206c656420746f20616e20696e74656e736520696e74657265737420696e20616c6c2061737065637473206f662038307320706f702063756c747572652c2077686963682048616c6c69646179206d61646520636c65617220776f756c6420626520657373656e7469616c20746f2066696e64696e6720686973206567672e227d", + claimResult{"0.0.3", + "Ernest Cline", + "Ready Player One (Audiobook 1 of 2)", + "In the year 2044, the world is gripped by an energy crisis causing widespread social problems and economic stagnation. The primary escape for most people is the OASIS, a virtual universe, accessed with a visor and haptic gloves. It functions both as an MMORPG and as a virtual society, with its currency being the most stable currency in the world. It was created by James Halliday, whose will left a series of clues towards an Easter Egg within the OASIS that would grant whoever found it both his fortune and control of the OASIS itself. This has led to an intense interest in all aspects of 80s pop culture, which Halliday made clear would be essential to finding his egg.", + "Creative Commons Attribution 3.0 United States", + "https://creativecommons.org/licenses/by/3.0/us/legalcode", + 0.4, + "USD", + "bHSe3KAvtVSR4m6S11zduuF9XHtwscDjoE", + "audio/mpeg", + "language:en ", + "340e1dda0e8414c21fafbb1f28f2c8b384821fe0d1e2a7143a4810545e64e26a4105042d4d47dc9754b618ecdfe0d191", + "http://i.imgur.com/lyKEHZc.jpg", + false, + }, + }, + {"7b22666565223a207b224c4243223a207b22616d6f756e74223a20312c202261646472657373223a202262525478744355706a3654764a48675763527347634861467972524c6b6b69586747227d7d2c2022766572223a2022302e302e33222c20226c6963656e7365223a2022436f707972696768742032303136206272617a7a657273222c20226c616e6775616765223a2022656e222c20227469746c65223a2022686f7420706f726e222c2022617574686f72223a20226272617a7a657273222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022666330646163356366633532363335343936336666313736396636653733396334653432613037393034323066666162396661336236343031653933616535613035313565666639363066386339633237323930376564613666636261323534227d2c20226e736677223a20747275652c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226465736372697074696f6e223a2022686f7420706f726e5c6e6272617a7a657273227d", + claimResult{"0.0.3", + "brazzers", + "hot porn", + "hot porn\nbrazzers", + "Copyright 2016 brazzers", + "", + 1, + "LBC", + "bRTxtCUpj6TvJHgWcRsGcHaFyrRLkkiXgG", + "video/mp4", + "language:en ", + "fc0dac5cfc526354963ff1769f6e739c4e42a0790420ffab9fa3b6401e93ae5a0515eff960f8c9c272907eda6fcba254", + "", + true, + }, + }, +} + +func TestBadMigrationFromJSON(t *testing.T) { + for _, hexStr := range badJsonVersionTests { + valueBytes, err := hex.DecodeString(hexStr) + if err != nil { + t.Error(err) + } + _, err = DecodeClaimBytes(valueBytes, "lbrycrd_main") + if err == nil { + t.Error(fmt.Sprintf("Decode error: The raw claim '%s' should have failed decoding.", hexStr)) + } + } +} + +func TestMigrationFromJSON(t *testing.T) { + for _, pair := range jsonVersionTests { + valueBytes, err := hex.DecodeString(pair.ValueAsHex) + if err != nil { + t.Error(err) + } + helper, err := DecodeClaimBytes(valueBytes, "lbrycrd_main") + if err != nil { + t.Error("Decode error: ", err) + } + if helper.Claim.GetStream().GetAuthor() != pair.Claim.Author { + t.Error("Author mismatch: expected", pair.Claim.Author, "got", helper.Claim.GetStream().GetAuthor()) + } + if helper.Claim.GetTitle() != pair.Claim.Title { + t.Error("Title mismatch: expected", pair.Claim.Title, "got '", helper.Claim.GetTitle(), "'") + } + if helper.Claim.GetDescription() != pair.Claim.Description { + t.Error("Description mismatch: expected", pair.Claim.Description, "got '", helper.Claim.GetDescription(), "'") + } + if helper.Claim.GetStream().GetLicense() != pair.Claim.License { + t.Error("License mismatch: expected", pair.Claim.License, "got", helper.Claim.GetStream().GetLicense()) + } + if helper.Claim.GetStream().GetLicenseUrl() != pair.Claim.LicenseURL { + t.Error("LicenseURL mismatch: expected", pair.Claim.LicenseURL, "got", helper.Claim.GetStream().GetLicenseUrl()) + } + if helper.Claim.GetStream().GetFee().GetAmount() != uint64(pair.Claim.FeeAmount*100000000) { + t.Error("Fee Amount mismatch: expected", pair.Claim.FeeAmount, "got", helper.Claim.GetStream().GetFee().GetAmount()) + } + if helper.Claim.GetStream().GetFee().GetCurrency().String() != pair.Claim.FeeCurrency { + t.Error("Fee Currency mismatch: expected", pair.Claim.FeeCurrency, "got", helper.Claim.GetStream().GetFee().GetCurrency()) + } + hexaddress := base58.Encode(helper.Claim.GetStream().GetFee().GetAddress()) + if hexaddress != pair.Claim.FeeAddress { + t.Error("Fee Address mismatch: expected", pair.Claim.FeeAddress, "got", hexaddress) + } + if helper.Claim.GetStream().GetSource().GetMediaType() != pair.Claim.ContentType { + t.Error("ContentType mismatch: expected", pair.Claim.ContentType, "got", helper.Claim.GetStream().GetSource().GetMediaType()) + } + if helper.Claim.GetLanguages()[0].String() != pair.Claim.Language { + t.Error("Language mismatch: expected ", pair.Claim.Language, " got ", helper.Claim.GetLanguages()[0].String()) + } + content := hex.EncodeToString(helper.Claim.GetStream().GetSource().GetSdHash()) + if content != pair.Claim.LbrySDHash { + t.Error("Source mismatch: expected", pair.Claim.LbrySDHash, "got", content) + } + } +} + +func TestMigrationFromV1YTSync(t *testing.T) { + claimHex := "080110011aee04080112a604080410011a2b4865726520617265203520526561736f6e73204920e29da4efb88f204e657874636c6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e657874636c6f75643a2068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e2066696e64206d65206f6e20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a2f2f666f72756d2e6865617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733a2f2f6f6666746f706963616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f70617472656f6e2e636f6d2f7468656c696e757867616d65720a202a204d657263683a2068747470733a2f2f746565737072696e672e636f6d2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a205477697463683a2068747470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723a2068747470733a2f2f747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0f546865204c696e75782047616d6572321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f4672546442434f535f666352005a001a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a4062b2dd4c45e364030fbfad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c0b68498382b2701b22c03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b51" + claim, err := DecodeClaimHex(claimHex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + assert.Assert(t, claim.GetTitle() == "Here are 5 Reasons I ❤️ Nextcloud | TLG") + assert.Assert(t, claim.GetDescription() == "Find out more about Nextcloud: https://nextcloud.com/\n\nYou can find me on these socials:\n * Forums: https://forum.heavyelement.io/\n * Podcast: https://offtopical.net\n * Patreon: https://patreon.com/thelinuxgamer\n * Merch: https://teespring.com/stores/official-linux-gamer\n * Twitch: https://twitch.tv/xondak\n * Twitter: https://twitter.com/thelinuxgamer\n\n...\nhttps://www.youtube.com/watch?v=FrTdBCOS_fc") + assert.Assert(t, claim.GetStream().GetLicense() == "Copyrighted (contact author)") + assert.Assert(t, claim.GetStream().GetAuthor() == "The Linux Gamer") + //?assert.Assert(t, claim.GetStream().GetLanguages()[0]) + assert.Assert(t, claim.GetStream().GetSource().GetMediaType() == "video/mp4") + assert.Assert(t, claim.GetThumbnail().GetUrl() == "https://berk.ninja/thumbnails/FrTdBCOS_fc") + sdHashBytes, err := hex.DecodeString("040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f3") + if err != nil { + t.Error(err) + } + assert.Assert(t, bytes.Equal(claim.GetStream().GetSource().GetSdHash(), sdHashBytes)) + + channelHex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae473f17" + channel, err := DecodeClaimHex(channelHex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + pubKeyBytes, err := hex.DecodeString("3056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae473f17") + if err != nil { + t.Error(err) + } + assert.Assert(t, bytes.Equal(pubKeyBytes, channel.GetChannel().GetPublicKey())) +} + +func TestMigrationFromV1UnsignedWithFee(t *testing.T) { + claimHex := "080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a19553f00bc139bbf40de425f94d51fffb34c1bea6d9171cd374c25000070414a0052005a001a54080110011a301f41eb0312aa7e8a5ce49349bc77d811da975833719d751523b19f123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3221c6170706c69636174696f6e2f782d7a69702d636f6d70726573736564" + claim, err := DecodeClaimHex(claimHex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + assert.Assert(t, claim.GetTitle() == "rpg midi") + assert.Assert(t, claim.GetDescription() == "midi") + assert.Assert(t, claim.GetStream().GetLicense() == "Creative Commons Attribution 4.0 International") + assert.Assert(t, claim.GetStream().GetAuthor() == "rpg midi") + //assert.Assert(t, claim.GetStream().GetLanguage() == "en") + assert.Assert(t, claim.GetStream().GetSource().GetMediaType() == "application/x-zip-compressed") + sdHashBytes, err := hex.DecodeString("1f41eb0312aa7e8a5ce49349bc77d811da975833719d751523b19f123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3") + if err != nil { + t.Error(err) + } + assert.Assert(t, bytes.Equal(claim.GetStream().GetSource().GetSdHash(), sdHashBytes)) + feeAddressBytes, err := address.DecodeAddress("bJUQ9MxS9N6M29zsA5GTpVSDzsnPjMBBX9", "lbrycrd_main") + assert.Assert(t, bytes.Equal(claim.GetStream().GetFee().GetAddress(), feeAddressBytes[:])) + assert.Assert(t, claim.GetStream().GetFee().GetAmount() == 1500000000) + assert.Assert(t, claim.GetStream().GetFee().GetCurrency().String() == "LBC") + +} diff --git a/schema/claim/pretty.go b/schema/claim/pretty.go new file mode 100644 index 0000000..a2d4155 --- /dev/null +++ b/schema/claim/pretty.go @@ -0,0 +1,28 @@ +package claim + +import ( + "encoding/json" + "fmt" + "github.com/golang/protobuf/jsonpb" +) + +func marshalToString(c *ClaimHelper) (string, error) { + m_pb := &jsonpb.Marshaler{} + return m_pb.MarshalToString(c) +} + +func (c *ClaimHelper) RenderJSON() (string, error) { + r, err := marshalToString(c) + if err != nil { + fmt.Println("err") + return "", err + } + var dat map[string]interface{} + err = json.Unmarshal([]byte(r), &dat) + if err != nil { + return "", err + } + return r, nil +} + +//TODO: encode byte arrays with b58 for addresses and b16 for source hashes instead of the default of b64 diff --git a/schema/claim/schema.go b/schema/claim/schema.go new file mode 100644 index 0000000..ef4b374 --- /dev/null +++ b/schema/claim/schema.go @@ -0,0 +1,125 @@ +package claim + +import ( + "encoding/json" + + "github.com/lbryio/lbry.go/extras/errors" +) + +// V1Claim is the first version of claim metadata used by lbry. +type V1Claim struct { + Version string `json:"ver,omitempty"` + Title string `json:"title"` //Required + Description string `json:"description"` //Required + Author string `json:"author"` //Required + Language string `json:"language"` //Required + License string `json:"license"` //Required + Sources Sources `json:"sources"` //Required + ContentType string `json:"content-type"` //Required + Thumbnail *string `json:"thumbnail,omitempty"` + Fee *Fee `json:"fee,omitempty"` + Contact *int `json:"contact,omitempty"` + PubKey *string `json:"pubkey,omitempty"` +} + +// V2Claim is the second version of claim metadata used by lbry. +type V2Claim struct { + Version string `json:"ver"` //Required + Title string `json:"title"` //Required + Description string `json:"description"` //Required + Author string `json:"author"` //Required + Language string `json:"language"` //Required + License string `json:"license"` //Required + Sources Sources `json:"sources"` //Required + ContentType string `json:"content-type"` //Required + Thumbnail *string `json:"thumbnail,omitempty"` + Fee *Fee `json:"fee,omitempty"` + Contact *int `json:"contact,omitempty"` + PubKey *string `json:"pubkey,omitempty"` + LicenseURL *string `json:"license_url,omitempty"` + NSFW bool `json:"nsfw"` //Required + +} + +// V3Claim is the third version of claim metadata used by lbry. +type V3Claim struct { + Version string `json:"ver"` //Required + Title string `json:"title"` //Required + Description string `json:"description"` //Required + Author string `json:"author"` //Required + Language string `json:"language"` //Required + License string `json:"license"` //Required + Sources Sources `json:"sources"` //Required + ContentType string `json:"content_type"` //Required + Thumbnail *string `json:"thumbnail,omitempty"` + Fee *Fee `json:"fee,omitempty"` + Contact *int `json:"contact,omitempty"` + PubKey *string `json:"pubkey,omitempty"` + LicenseURL *string `json:"license_url,omitempty"` + NSFW bool `json:"nsfw"` //Required + Sig *string `json:"sig"` +} + +// FeeInfo is the structure of fee information used by lbry. +type FeeInfo struct { + Amount float32 `json:"amount"` //Required + Address string `json:"address"` //Required +} + +// Sources is the structure of Sources that can be used for a claim. Sources mainly include lbrysdhash but could be from +// elsewhere in the future. +type Sources struct { + LbrySDHash string `json:"lbry_sd_hash"` //Required + BTIH string `json:"btih"` //Required + URL string `json:"url"` //Required +} + +// Fee is the structure used for different currencies allowed for claims. +type Fee struct { + LBC *FeeInfo `json:"LBC,omitempty"` + BTC *FeeInfo `json:"BTC,omitempty"` + USD *FeeInfo `json:"USD,omitempty"` +} + +// Unmarshal is an implementation to unmarshal the V1 claim from json. Main addition is to check the version. +func (c *V1Claim) Unmarshal(value []byte) error { + err := json.Unmarshal(value, c) + if err != nil { + return err + } //Version can be blank for version 1 + if c.Version != "" && c.Version != "0.0.1" { + err = errors.Base("Incorrect version, expected 0.0.1 found " + c.Version) + return err + } + //ToDo - restrict to required fields? + + return nil +} + +// Unmarshal is an implementation to unmarshal the V2 claim from json. Main addition is to check the version. +func (c *V2Claim) Unmarshal(value []byte) error { + err := json.Unmarshal(value, c) + if err != nil { + return err + } + if c.Version != "0.0.2" { + err = errors.Base("Incorrect version, expected 0.0.2 found " + c.Version) + return err + } + + return nil +} + +// Unmarshal is an implementation to unmarshal the V3 claim from json. Main addition is to check the version. +func (c *V3Claim) Unmarshal(value []byte) error { + err := json.Unmarshal(value, c) + if err != nil { + return err + } + if c.Version != "0.0.3" { + err = errors.Base("Incorrect version, expected 0.0.3 found " + c.Version) + return err + } + + return nil +} diff --git a/schema/claim/serialization.go b/schema/claim/serialization.go new file mode 100644 index 0000000..43d38d6 --- /dev/null +++ b/schema/claim/serialization.go @@ -0,0 +1,90 @@ +package claim + +import ( + "encoding/hex" + + "github.com/lbryio/lbry.go/extras/errors" + legacy "github.com/lbryio/types/v1/go" + pb "github.com/lbryio/types/v2/go" + + "github.com/golang/protobuf/proto" +) + +func (c *ClaimHelper) serialized() ([]byte, error) { + serialized := c.String() + if serialized == "" { + return nil, errors.Err("not initialized") + } + + if c.LegacyClaim != nil { + return proto.Marshal(c.getLegacyProtobuf()) + } + + return proto.Marshal(c.getProtobuf()) +} + +func (c *ClaimHelper) getProtobuf() *pb.Claim { + claim := &pb.Claim{ + Title: c.GetTitle(), + Description: c.GetDescription(), + Thumbnail: c.GetThumbnail(), + Tags: c.GetTags(), + Languages: c.GetLanguages(), + Locations: c.GetLocations(), + } + if c.GetChannel() != nil { + claim.Type = &pb.Claim_Channel{Channel: c.GetChannel()} + } else if c.GetStream() != nil { + claim.Type = &pb.Claim_Stream{Stream: c.GetStream()} + } else if c.GetCollection() != nil { + claim.Type = &pb.Claim_Collection{Collection: c.GetCollection()} + } else if c.GetRepost() != nil { + claim.Type = &pb.Claim_Repost{Repost: c.GetRepost()} + } + + return claim +} + +func (c *ClaimHelper) getLegacyProtobuf() *legacy.Claim { + v := c.LegacyClaim.GetVersion() + t := c.LegacyClaim.GetClaimType() + return &legacy.Claim{ + Version: &v, + ClaimType: &t, + Stream: c.LegacyClaim.GetStream(), + Certificate: c.LegacyClaim.GetCertificate(), + PublisherSignature: c.LegacyClaim.GetPublisherSignature()} +} + +func (c *ClaimHelper) serializedHexString() (string, error) { + serialized, err := c.serialized() + if err != nil { + return "", err + } + serialized_hex := hex.EncodeToString(serialized) + return serialized_hex, nil +} + +func (c *ClaimHelper) serializedNoSignature() ([]byte, error) { + if c.String() == "" { + return nil, errors.Err("not initialized") + } + if c.Signature == nil { + serialized, err := c.serialized() + if err != nil { + return nil, err + } + return serialized, nil + } else { + if c.LegacyClaim != nil { + clone := &legacy.Claim{} + proto.Merge(clone, c.getLegacyProtobuf()) + proto.ClearAllExtensions(clone.PublisherSignature) + clone.PublisherSignature = nil + return proto.Marshal(clone) + } + clone := &pb.Claim{} + proto.Merge(clone, c.getProtobuf()) + return proto.Marshal(clone) + } +} diff --git a/schema/claim/sign.go b/schema/claim/sign.go new file mode 100644 index 0000000..18a2a2f --- /dev/null +++ b/schema/claim/sign.go @@ -0,0 +1,112 @@ +package claim + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbryschema.go/address" + + "github.com/btcsuite/btcd/btcec" +) + +func Sign(privKey btcec.PrivateKey, channel ClaimHelper, claim ClaimHelper, k string) (*Signature, error) { + if channel.GetChannel() == nil { + return nil, errors.Err("claim as channel is not of type channel") + } + if claim.LegacyClaim != nil { + return claim.signV1(privKey, channel, k) + } + + return claim.sign(privKey, channel, k) +} + +func (c *ClaimHelper) sign(privKey btcec.PrivateKey, channel ClaimHelper, firstInputTxID string) (*Signature, error) { + + txidBytes, err := hex.DecodeString(firstInputTxID) + if err != nil { + return nil, errors.Err(err) + } + + metadataBytes, err := c.serialized() + if err != nil { + return nil, errors.Err(err) + } + + var digest []byte + digest = append(digest, txidBytes...) + digest = append(digest, c.ClaimID...) + digest = append(digest, metadataBytes...) + hash := sha256.Sum256(digest) + hashBytes := make([]byte, len(hash)) + for i, b := range hash { + hashBytes[i] = b + } + + sig, err := privKey.Sign(hashBytes) + if err != nil { + return nil, errors.Err(err) + } + + return &Signature{*sig}, nil + +} + +func (c *ClaimHelper) signV1(privKey btcec.PrivateKey, channel ClaimHelper, claimAddress string) (*Signature, error) { + metadataBytes, err := c.serializedNoSignature() + if err != nil { + return nil, errors.Err(err) + } + + addressBytes, err := address.DecodeAddress(claimAddress, "lbrycrd_main") + if err != nil { + return nil, errors.Prefix("V1 signing requires claim address and the decode failed with: ", err) + } + + var digest []byte + + address := make([]byte, len(addressBytes)) + for i, b := range addressBytes { + address[i] = b + } + + digest = append(digest, address...) + digest = append(digest, metadataBytes...) + digest = append(digest, channel.ClaimID...) + + hash := sha256.Sum256(digest) + hashBytes := make([]byte, len(hash)) + for i, b := range hash { + hashBytes[i] = b + } + + sig, err := privKey.Sign(hashBytes) + if err != nil { + return nil, errors.Err(err) + } + + return &Signature{*sig}, nil +} + +type Signature struct { + btcec.Signature +} + +func (s *Signature) LBRYSDKEncode() ([]byte, error) { + if s.R == nil || s.S == nil { + return nil, errors.Err("invalid signature, both S & R are nil") + } + rBytes := s.R.Bytes() + sBytes := s.S.Bytes() + + return append(rBytes, sBytes...), nil +} + +// rev reverses a byte slice. useful for switching endian-ness +func reverseBytes(b []byte) []byte { + r := make([]byte, len(b)) + for left, right := 0, len(b)-1; left < right; left, right = left+1, right-1 { + r[left], r[right] = b[right], b[left] + } + return r +} diff --git a/schema/claim/sign_test.go b/schema/claim/sign_test.go new file mode 100644 index 0000000..e813f07 --- /dev/null +++ b/schema/claim/sign_test.go @@ -0,0 +1,137 @@ +package claim + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec" + "gotest.tools/assert" +) + +func TestSign(t *testing.T) { + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Error(err) + return + } + channel := &ClaimHelper{newChannelClaim(), nil, nil, NoSig, nil, nil} + pubkeyBytes, err := PublicKeyToDER(privateKey.PubKey()) + if err != nil { + t.Error(err) + return + } + channel.GetChannel().PublicKey = pubkeyBytes + claimID := "cf3f7c898af87cc69b06a6ac7899efb9a4878fdb" //Fake + txid := "4c1df9e022e396859175f9bfa69b38e444db10fb53355fa99a0989a83bcdb82f" //Fake + claimIDHexBytes, err := hex.DecodeString(claimID) + if err != nil { + t.Error(err) + return + } + + claim := &ClaimHelper{newStreamClaim(), nil, reverseBytes(claimIDHexBytes), WithSig, nil, nil} + claim.Claim.Title = "Test title" + claim.Claim.Description = "Test description" + sig, err := Sign(*privateKey, *channel, *claim, txid) + if err != nil { + t.Error(err) + return + } + + signatureBytes, err := sig.LBRYSDKEncode() + if err != nil { + t.Error(err) + return + } + + claim.Signature = signatureBytes + + rawChannel, err := channel.CompileValue() + if err != nil { + t.Error(err) + return + } + rawClaim, err := claim.CompileValue() + if err != nil { + t.Error(err) + return + } + + channel, err = DecodeClaimBytes(rawChannel, "lbrycrd_main") + if err != nil { + t.Error(err) + return + } + claim, err = DecodeClaimBytes(rawClaim, "lbrycrd_main") + if err != nil { + t.Error(err) + return + } + + valid, err := claim.ValidateClaimSignature(channel, txid, claimID, "lbrycrd_main") + if err != nil { + t.Error(err) + return + } + + assert.Assert(t, valid, "could not verify signature") + +} + +func TestSignWithV1Channel(t *testing.T) { + cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367" + channel, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Error(err) + return + } + pubkeyBytes, err := PublicKeyToDER(privateKey.PubKey()) + if err != nil { + t.Error(err) + return + } + channel.GetChannel().PublicKey = pubkeyBytes + + claimID := "251305ca93d4dbedb50dceb282ebcb7b07b7ac64" + txid := "4c1df9e022e396859175f9bfa69b38e444db10fb53355fa99a0989a83bcdb82f" //Fake + claimIDHexBytes, err := hex.DecodeString(claimID) + if err != nil { + t.Error(err) + return + } + + claim := &ClaimHelper{newStreamClaim(), nil, reverseBytes(claimIDHexBytes), WithSig, nil, nil} + claim.Claim.Title = "Test title" + claim.Claim.Description = "Test description" + sig, err := Sign(*privateKey, *channel, *claim, txid) + if err != nil { + t.Error(err) + return + } + + signatureBytes, err := sig.LBRYSDKEncode() + if err != nil { + t.Error(err) + return + } + + claim.Signature = signatureBytes + compiledClaim, err := claim.CompileValue() + if err != nil { + t.Error(err) + } + claim, err = DecodeClaimBytes(compiledClaim, "lbrycrd_main") + + valid, err := claim.ValidateClaimSignature(channel, txid, claimID, "lbrycrd_main") + if err != nil { + t.Error(err) + return + } + + assert.Assert(t, valid, "could not verify signature") + +} diff --git a/schema/claim/validator.go b/schema/claim/validator.go new file mode 100644 index 0000000..4c243af --- /dev/null +++ b/schema/claim/validator.go @@ -0,0 +1,128 @@ +package claim + +import ( + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/binary" + "encoding/hex" + "math/big" + + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbryschema.go/address" +) + +type publicKeyInfo struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +const SECP256k1 = "SECP256k1" + +//const NIST256p = "NIST256p" +//const NIST384p = "NIST384p" + +func getClaimSignatureDigest(bytes ...[]byte) [32]byte { + + var combined []byte + for _, b := range bytes { + combined = append(combined, b...) + } + digest := sha256.Sum256(combined) + return [32]byte(digest) +} + +func (c *ClaimHelper) VerifyDigest(certificate *ClaimHelper, signature [64]byte, digest [32]byte) bool { + if certificate == nil { + return false + } + + R := &big.Int{} + S := &big.Int{} + R.SetBytes(signature[0:32]) + S.SetBytes(signature[32:64]) + pk, err := certificate.GetPublicKey() + if err != nil { + return false + } + return ecdsa.Verify(pk.ToECDSA(), digest[:], R, S) +} + +func (c *ClaimHelper) ValidateClaimSignature(certificate *ClaimHelper, k string, certificateId string, blockchainName string) (bool, error) { + if c.LegacyClaim != nil { + return c.validateV1ClaimSignature(certificate, k, certificateId, blockchainName) + } + + return c.validateClaimSignature(certificate, k, certificateId, blockchainName) +} + +func (c *ClaimHelper) validateClaimSignature(certificate *ClaimHelper, firstInputTxHash, certificateId string, blockchainName string) (bool, error) { + certificateIdSlice, err := hex.DecodeString(certificateId) + if err != nil { + return false, errors.Err(err) + } + certificateIdSlice = reverseBytes(certificateIdSlice) + firstInputTxIDBytes, err := hex.DecodeString(firstInputTxHash) + if err != nil { + return false, errors.Err(err) + } + + signature := c.Signature + if signature == nil { + return false, errors.Err("claim does not have a signature") + } + signatureBytes := [64]byte{} + for i, b := range signature { + signatureBytes[i] = b + } + + claimDigest := getClaimSignatureDigest(firstInputTxIDBytes, certificateIdSlice, c.Payload) + return c.VerifyDigest(certificate, signatureBytes, claimDigest), nil +} + +func (c *ClaimHelper) validateV1ClaimSignature(certificate *ClaimHelper, claimAddy string, certificateId string, blockchainName string) (bool, error) { + addressBytes, err := address.DecodeAddress(claimAddy, blockchainName) + if err != nil { + return false, err + } + //For V1 claim_id was incorrectly stored for claim signing. + // So the bytes are not reversed like they are supposed to be (Endianess) + certificateIdSlice, err := hex.DecodeString(certificateId) + if err != nil { + return false, err + } + + signature := c.Signature + if signature == nil { + return false, errors.Err("claim does not have a signature") + } + signatureBytes := [64]byte{} + for i := range signatureBytes { + signatureBytes[i] = signature[i] + } + + claimAddress, err := address.ValidateAddress(addressBytes, blockchainName) + if err != nil { + return false, errors.Err("invalid address") + } + + serializedNoSig, err := c.serializedNoSignature() + if err != nil { + return false, errors.Err("serialization error") + } + + claimDigest := getClaimSignatureDigest(claimAddress[:], serializedNoSig, certificateIdSlice) + return c.VerifyDigest(certificate, signatureBytes, claimDigest), nil +} + +func GetOutpointHash(txid string, vout uint32) (string, error) { + txidBytes, err := hex.DecodeString(txid) + if err != nil { + return "", errors.Err(err) + } + var voutBytes = make([]byte, 4) + binary.LittleEndian.PutUint32(voutBytes, vout) + return hex.EncodeToString(append(reverseBytes(txidBytes), voutBytes...)), nil +} diff --git a/schema/claim/validator_test.go b/schema/claim/validator_test.go new file mode 100644 index 0000000..ef88a4d --- /dev/null +++ b/schema/claim/validator_test.go @@ -0,0 +1,94 @@ +package claim + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestV1ValidateClaimSignature(t *testing.T) { + cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367" + signed_claim_hex := "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65" + + signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + + claim_addr := "bSkUov7HMWpYBiXackDwRnR5ishhGHvtJt" + cert_id := "251305ca93d4dbedb50dceb282ebcb7b07b7ac65" + + result, err := signed_claim.ValidateClaimSignature(cert_claim, claim_addr, cert_id, "lbrycrd_main") + if err != nil { + t.Error(err) + } + if result != true { + t.Error("failed to validate signature:", result) + } +} + +func TestV1FailToValidateClaimSignature(t *testing.T) { + cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367" + signed_claim_hex := "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65" + + signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + + claim_addr := "bSkUov7HMWpYBiXackDwRnR5ishhGHvtJt" + cert_id := "251305ca93d4dbedb50dceb282ebcb7b07b7ac64" + + result, err := signed_claim.ValidateClaimSignature(cert_claim, claim_addr, cert_id, "lbrycrd_main") + if err != nil { + t.Error(err) + } + if result != false { + t.Error("failed to validate signature:", result) + } +} + +func TestV2ValidateClaimSignature(t *testing.T) { + cert_claim_hex := "00125a0a583056301006072a8648ce3d020106052b8104000a034200045a0343c155302280da01ae0001b7295241eb03c42a837acf92ccb9680892f7db50fd1d3c14b28bb594e304f05fc4ae7c1f222a85d1d1a3461b3cfb9906f66cb5" + signed_claim_hex := "015cb78e424a34fbf79b67f9107430427aa62373e69b4998a29ecec8f14a9e0a213a043ced8064c069d7e464b5fd3ccb92b45bd59b15c0e1bb27e3c366d43f86a9a6b5ad42647a1aad69a73ac50b19ae3ec978c2c70aa2010a99010a301c662f19abc461e7eddecf165adfa7fca569e209773f3db31241c1e297f0a8d5b3e4768828b065fbeb1d6776f61073f6121b3031202d20556e6d6173746572656420496d70756c7365732e377a187a22146170706c69636174696f6e2f782d6578742d377a32302eb61ea475017e28c013616a56c1219ba90dc35fffff453d9675146f648f66634e0d1516528d37aba9f5801229d9f2181a044e6f6e6542087465737420707562520062020801" + + signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main") + if err != nil { + t.Error(err) + } + + firstInputTxHash, err := GetOutpointHash("becb96a4a2e66bd24f083772fe9da904654ea9b5f07cc5bfbee233355911ddb1", uint32(0)) + if err != nil { + t.Error(err) + } + cert_id := "e67323a67a42307410f9679bf7fb344a428eb75c" + + result, err := signed_claim.ValidateClaimSignature(cert_claim, firstInputTxHash, cert_id, "lbrycrd_main") + if err != nil { + t.Error(err) + } + if result != true { + t.Error("failed to validate signature:", result) + } + +} + +func TestGetOutpointHash(t *testing.T) { + hash, err := GetOutpointHash("dc3dcf2f94d3c91e454ac2474802e20f26b30705372dda43890c811d918aef64", 1) + if err != nil { + t.Error(err) + } + assert.Assert(t, hash == "64ef8a911d810c8943da2d370507b3260fe2024847c24a451ec9d3942fcf3ddc01000000", uint(1)) +} diff --git a/schema/cli/lbryschema-cli.go b/schema/cli/lbryschema-cli.go new file mode 100644 index 0000000..37452cb --- /dev/null +++ b/schema/cli/lbryschema-cli.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "github.com/lbryio/lbryschema.go/claim" + "os" +) + +func main() { + args := os.Args[1:] + if len(args) == 1 { + claimBytes := []byte(args[0]) + decoded, err := claim.DecodeClaimBytes(claimBytes, "lbrycrd_main") + if err != nil { + fmt.Println("Decoding error:", err) + return + } + text, err := decoded.RenderJSON() + if err != nil { + fmt.Println("Decoding error:", err) + return + } + fmt.Println(text) + return + } else if (len(args) == 2) && (args[1] == "--decode_hex") { + claimHex := args[0] + decoded, err := claim.DecodeClaimHex(claimHex, "lbrycrd_main") + if err != nil { + fmt.Println("Decoding error:", err) + return + } + text, err := decoded.RenderJSON() + if err != nil { + fmt.Println("Decoding error:", err) + return + } + fmt.Println(text) + return + } else { + fmt.Println("encountered an error\nusage: \n\tlbryschema-cli [--decode_hex]") + return + } +} diff --git a/schema/test.sh b/schema/test.sh new file mode 100755 index 0000000..8091974 --- /dev/null +++ b/schema/test.sh @@ -0,0 +1 @@ +go test ./... -v