access_test.mx raw

   1  package protocol
   2  
   3  import "testing"
   4  
   5  const (
   6  	mintURL  = "https://mint.example.com"
   7  	hostPub  = "9999999999999999999999999999999999999999999999999999999999999999"
   8  	retailPk = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
   9  	filtHash = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
  10  	aggID    = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
  11  	aggID2   = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
  12  )
  13  
  14  // The access filter example from FEATURES_ACCESS.md / PROTOCOL_FLOWS A1.
  15  func TestAccessFilterRoundTrip(t *testing.T) {
  16  	asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}
  17  	var want [][]string
  18  	want = appendKV(want, TagD, FanFilterD(asset))
  19  	want = appendKV(want, TagFilter, FilterBloom)
  20  	want = appendKV(want, TagFp, "1e-9")
  21  	want = appendKV(want, TagN, "1234")
  22  	want = appendKV(want, TagK, "30")
  23  	want = appendKV(want, TagX, filtHash)
  24  	want = appendKV(want, TagServer, "https://blossom.musiquay.com")
  25  	want = appendKV(want, TagMint, mintURL)
  26  	want = appendKV(want, TagRetailer, retailPk)
  27  	want = appendKV(want, TagExpiration, "1799999999")
  28  
  29  	f, err := ParseAccessFilter(want, "")
  30  	if err != nil {
  31  		t.Fatalf("parse: %s", err.Error())
  32  	}
  33  	if verr := f.Validate(); verr != nil {
  34  		t.Fatalf("validate: %s", verr.Error())
  35  	}
  36  	if f.D != "fan:32210:"|ownerPub|":track-d" {
  37  		t.Fatalf("d = %q", f.D)
  38  	}
  39  	if len(f.Mints) != 1 || f.Mints[0] != mintURL || len(f.Retailers) != 1 {
  40  		t.Fatalf("mints=%v retailers=%v", f.Mints, f.Retailers)
  41  	}
  42  	if got := f.Tags(); !sameTags(got, want) {
  43  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
  44  	}
  45  }
  46  
  47  func TestFilterTierForms(t *testing.T) {
  48  	asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}
  49  
  50  	fan, err := ParseFilterD(FanFilterD(asset))
  51  	if err != nil {
  52  		t.Fatalf("fan: %s", err.Error())
  53  	}
  54  	if fan.Tier != TierFan || fan.Asset == nil || fan.Asset.D != "track-d" {
  55  		t.Fatalf("fan tier = %+v", fan)
  56  	}
  57  	if fan.String() != FanFilterD(asset) {
  58  		t.Fatalf("fan d round trip = %q", fan.String())
  59  	}
  60  
  61  	retail, err := ParseFilterD(RetailFilterD(asset))
  62  	if err != nil {
  63  		t.Fatalf("retail: %s", err.Error())
  64  	}
  65  	if retail.Tier != TierRetail || retail.Asset == nil {
  66  		t.Fatalf("retail tier = %+v", retail)
  67  	}
  68  
  69  	subD := SubFilterD(retailPk, "channel-one")
  70  	sub, err := ParseFilterD(subD)
  71  	if err != nil {
  72  		t.Fatalf("sub: %s", err.Error())
  73  	}
  74  	if sub.Tier != TierSub || sub.Retailer != retailPk || sub.Channel != "channel-one" {
  75  		t.Fatalf("sub tier = %+v", sub)
  76  	}
  77  	if sub.String() != subD {
  78  		t.Fatalf("sub d round trip = %q", sub.String())
  79  	}
  80  
  81  	for _, bad := range []string{
  82  		"",
  83  		"fan",
  84  		"owner:" | ownerPub | ":track-d",
  85  		"fan:32210:short:track-d",
  86  		"sub:" | retailPk,
  87  		"sub:short:chan",
  88  		"sub:" | retailPk | ":",
  89  	} {
  90  		if _, berr := ParseFilterD(bad); berr == nil {
  91  			t.Errorf("ParseFilterD(%q) accepted a bad value", bad)
  92  		}
  93  	}
  94  }
  95  
  96  // A subscription filter with no catalog coverage is what the spec leaves open:
  97  // it warns rather than failing, and the open decision is visible.
  98  func TestSubscriptionFilterCatalog(t *testing.T) {
  99  	asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}
 100  	f := &AccessFilter{
 101  		D: SubFilterD(retailPk, "chan"), Filter: FilterBloom,
 102  		Fp: "1e-9", N: "10", K: "30", X: filtHash,
 103  	}
 104  	if verr := f.Validate(); verr != nil {
 105  		t.Fatalf("validate: %s", verr.Error())
 106  	}
 107  	w := f.Warnings()
 108  	found := false
 109  	for i := range w {
 110  		if w[i] == "subscription filter with no catalog coverage" {
 111  			found = true
 112  		}
 113  	}
 114  	if !found {
 115  		t.Fatalf("warnings = %s", joinWarnings(w))
 116  	}
 117  	f.Catalog = push(f.Catalog, asset.String())
 118  	tags := f.Tags()
 119  	if len(tags) != 7 {
 120  		t.Fatalf("tag count with catalog = %d", len(tags))
 121  	}
 122  	var hasCatalog bool = false
 123  	for i := range tags {
 124  		if tags[i][0] == TagCatalog && tags[i][1] == asset.String() {
 125  			hasCatalog = true
 126  		}
 127  	}
 128  	if !hasCatalog {
 129  		t.Fatal("catalog coverage was not emitted")
 130  	}
 131  }
 132  
 133  func TestAccessFilterValidateRejects(t *testing.T) {
 134  	asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}
 135  	base := func() (f *AccessFilter) {
 136  		return &AccessFilter{
 137  			D: FanFilterD(asset), Filter: FilterBloom,
 138  			Fp: "1e-9", N: "1", K: "30", X: filtHash,
 139  		}
 140  	}
 141  	f := base()
 142  	f.X = "nothash"
 143  	if verr := f.Validate(); verr == nil {
 144  		t.Fatal("a bad blob hash was accepted")
 145  	}
 146  	f2 := base()
 147  	f2.Filter = "cuckoo"
 148  	if verr := f2.Validate(); verr == nil {
 149  		t.Fatal("an unknown filter structure was accepted")
 150  	}
 151  	f3 := base()
 152  	f3.Fp = ""
 153  	if verr := f3.Validate(); verr == nil {
 154  		t.Fatal("a filter with no rate was accepted")
 155  	}
 156  	f4 := base()
 157  	f4.Retailers = push(f4.Retailers, "short")
 158  	if verr := f4.Validate(); verr == nil {
 159  		t.Fatal("a short retailer pubkey was accepted")
 160  	}
 161  	f5 := base()
 162  	f5.D = "owner:" | ownerPub | ":" | asset.String()
 163  	if verr := f5.Validate(); verr == nil {
 164  		t.Fatal("a filter with a bad tier was accepted")
 165  	}
 166  }
 167  
 168  // The purchase order from PROTOCOL_FLOWS A4.
 169  func TestPurchaseOrderRoundTrip(t *testing.T) {
 170  	var want [][]string
 171  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d", Relay: "wss://relay.musiquay.com"}).Tag())
 172  	want = appendKV(want, TagP, ownerPub)
 173  	want = appendKV(want, TagTier, TierFan)
 174  	want = appendTag(want, (&Price{Amount: "500", Unit: "sat"}).Tag())
 175  	want = appendKV(want, TagPayment, PaymentCashu)
 176  	want = appendKV(want, TagMint, mintURL)
 177  	want = appendKV(want, TagToken, "cashuBtoken")
 178  
 179  	o, err := ParsePurchaseOrder(want, "")
 180  	if err != nil {
 181  		t.Fatalf("parse: %s", err.Error())
 182  	}
 183  	if verr := o.Validate(); verr != nil {
 184  		t.Fatalf("validate: %s", verr.Error())
 185  	}
 186  	if o.Asset == nil || o.Asset.Kind != KindTrack {
 187  		t.Fatalf("asset = %v", o.Asset)
 188  	}
 189  	if o.Price == nil || o.Price.Amount != "500" || o.Price.Unit != "sat" {
 190  		t.Fatalf("price = %v", o.Price)
 191  	}
 192  	if got := o.Tags(); !sameTags(got, want) {
 193  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 194  	}
 195  }
 196  
 197  func TestPurchaseOrderValidate(t *testing.T) {
 198  	asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}
 199  	base := func() (o *PurchaseOrder) {
 200  		return &PurchaseOrder{
 201  			Asset: asset, Seller: ownerPub, Tier: TierFan,
 202  			Price: &Price{Amount: "500", Unit: "sat"},
 203  			Payment: PaymentCashu, Mint: mintURL, Token: "cashuBx",
 204  		}
 205  	}
 206  	if verr := base().Validate(); verr != nil {
 207  		t.Fatalf("valid order rejected: %s", verr.Error())
 208  	}
 209  	o := base()
 210  	o.Seller = ""
 211  	if verr := o.Validate(); verr == nil {
 212  		t.Fatal("an order with no seller was accepted")
 213  	}
 214  	o2 := base()
 215  	o2.Payment = ""
 216  	if verr := o2.Validate(); verr == nil {
 217  		t.Fatal("an order with no payment leg was accepted")
 218  	}
 219  	o3 := base()
 220  	o3.Token = ""
 221  	if verr := o3.Validate(); verr == nil {
 222  		t.Fatal("a cashu order with no token was accepted")
 223  	}
 224  	o4 := base()
 225  	o4.Tier = TierSub
 226  	if verr := o4.Validate(); verr == nil {
 227  		t.Fatal("a subscription order with no channel or period was accepted")
 228  	}
 229  	o4.Channel = "chan"
 230  	o4.Period = "2592000"
 231  	if verr := o4.Validate(); verr != nil {
 232  		t.Fatalf("a complete subscription order was rejected: %s", verr.Error())
 233  	}
 234  	o5 := base()
 235  	o5.Payment = PaymentLightning
 236  	o5.Token = ""
 237  	o5.Mint = ""
 238  	if verr := o5.Validate(); verr != nil {
 239  		t.Fatalf("a lightning order was rejected: %s", verr.Error())
 240  	}
 241  	// The order is private: it must never be published to a relay.
 242  	if !IsPrivate(KindPurchaseOrder) {
 243  		t.Fatal("the order kind is not private")
 244  	}
 245  }
 246  
 247  // The delivery receipt from PROTOCOL_FLOWS D1.
 248  func TestDeliveryReceiptRoundTrip(t *testing.T) {
 249  	var want [][]string
 250  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag())
 251  	want = appendKV(want, TagP, buyerPub)
 252  	want = appendKV(want, TagX, mp3Hash)
 253  	want = appendKV(want, TagServer, "https://blossom.musiquay.com")
 254  	want = appendKV(want, TagMode, ModeDownload)
 255  	want = appendKV(want, TagStart, "1758130000")
 256  	want = appendKV(want, TagEnd, "1758130000")
 257  	want = appendKV(want, TagBytes, "48216387")
 258  	want = appendKV(want, TagAmountSats, "0")
 259  	want = appendKV(want, TagPaid, "tokenhash")
 260  
 261  	r, err := ParseDeliveryReceipt(want, "")
 262  	if err != nil {
 263  		t.Fatalf("parse: %s", err.Error())
 264  	}
 265  	if verr := r.Validate(); verr != nil {
 266  		t.Fatalf("validate: %s", verr.Error())
 267  	}
 268  	if r.Mode != ModeDownload || r.AmountSats != "0" || r.TokenHashesUsed() != 1 {
 269  		t.Fatalf("receipt = %+v", r)
 270  	}
 271  	if got := r.Tags(); !sameTags(got, want) {
 272  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 273  	}
 274  	// A stream receipt with a settled amount.
 275  	r2 := &DeliveryReceipt{
 276  		Asset: r.Asset, Buyer: buyerPub, Blob: mp3Hash,
 277  		Mode: ModeStream, Start: "1", End: "601", Bytes: "100", AmountSats: "10",
 278  	}
 279  	if verr := r2.Validate(); verr != nil {
 280  		t.Fatalf("stream receipt rejected: %s", verr.Error())
 281  	}
 282  	r3 := &DeliveryReceipt{Asset: r.Asset, Buyer: buyerPub, Blob: mp3Hash, Mode: "burn", AmountSats: "0"}
 283  	if verr := r3.Validate(); verr == nil {
 284  		t.Fatal("an unknown mode was accepted")
 285  	}
 286  }
 287  
 288  // The host aggregate from PROTOCOL_FLOWS D2: listeners is marked approximate.
 289  func TestHostAggregateRoundTrip(t *testing.T) {
 290  	var want [][]string
 291  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag())
 292  	want = appendKV(want, TagStart, "1758130000")
 293  	want = appendKV(want, TagEnd, "1760808400")
 294  	want = appendKV(want, TagPlays, "48213")
 295  	want = appendKV(want, TagDownloads, "3921")
 296  	want = appendKV(want, TagBytes, "21474836480")
 297  	want = appendKV(want, TagSats, "1510000")
 298  	lst := []string{:3}
 299  	lst[0] = TagListeners
 300  	lst[1] = "11054"
 301  	lst[2] = Approx
 302  	want = appendTag(want, lst)
 303  
 304  	a, err := ParseHostAggregate(want, "")
 305  	if err != nil {
 306  		t.Fatalf("parse: %s", err.Error())
 307  	}
 308  	if verr := a.Validate(); verr != nil {
 309  		t.Fatalf("validate: %s", verr.Error())
 310  	}
 311  	if !a.Approx || a.Listeners != "11054" {
 312  		t.Fatalf("aggregate = %+v", a)
 313  	}
 314  	if got := a.Tags(); !sameTags(got, want) {
 315  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 316  	}
 317  	// A bare listener count reads as exact and is rejected.
 318  	a2 := &HostAggregate{Asset: a.Asset, Start: "1", End: "2", Listeners: "10"}
 319  	if verr := a2.Validate(); verr == nil {
 320  		t.Fatal("an unmarked listener count was accepted")
 321  	}
 322  }
 323  
 324  // The publisher rollup from PROTOCOL_FLOWS D3: the e tags carry the host
 325  // pubkey at index 3 so the authenticity chain is verifiable.
 326  func TestPublisherRollupRoundTrip(t *testing.T) {
 327  	var want [][]string
 328  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag())
 329  	want = appendKV(want, TagStart, "1758130000")
 330  	want = appendKV(want, TagEnd, "1760808400")
 331  	want = appendTag(want, (&AggregateRef{ID: aggID, Host: hostPub}).Tag())
 332  	want = appendTag(want, (&AggregateRef{ID: aggID2, Host: retailPk}).Tag())
 333  	want = appendKV(want, TagReceipts, "52134")
 334  	want = appendKV(want, TagPlays, "55123")
 335  	want = appendKV(want, TagDownloads, "4120")
 336  	want = appendKV(want, TagBytes, "22817013760")
 337  	want = appendKV(want, TagSats, "1730000")
 338  
 339  	r, err := ParsePublisherRollup(want, "")
 340  	if err != nil {
 341  		t.Fatalf("parse: %s", err.Error())
 342  	}
 343  	if verr := r.Validate(); verr != nil {
 344  		t.Fatalf("validate: %s", verr.Error())
 345  	}
 346  	if len(r.Aggregates) != 2 || r.Aggregates[0].Host != hostPub {
 347  		t.Fatalf("aggregates = %v", r.Aggregates)
 348  	}
 349  	if ref := (&AggregateRef{ID: aggID, Host: hostPub}).Tag(); len(ref) != 4 || ref[2] != "" {
 350  		t.Fatalf("aggregate ref tag = %s", joinTags([][]string{ref}))
 351  	}
 352  	if got := r.Tags(); !sameTags(got, want) {
 353  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 354  	}
 355  	r2 := &PublisherRollup{Asset: r.Asset, Start: "1", End: "2"}
 356  	r2.Aggregates = push(r2.Aggregates, AggregateRef{ID: "short", Host: hostPub})
 357  	if verr := r2.Validate(); verr == nil {
 358  		t.Fatal("a short aggregate id was accepted")
 359  	}
 360  }
 361