github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/execution/storehouse/in_memory_register_store_test.go (about) 1 package storehouse 2 3 import ( 4 "fmt" 5 "math/rand" 6 "sync" 7 "testing" 8 9 "github.com/stretchr/testify/require" 10 11 "github.com/onflow/flow-go/model/flow" 12 "github.com/onflow/flow-go/utils/unittest" 13 ) 14 15 // 1. SaveRegisters should fail if height is below or equal to pruned height 16 func TestInMemoryRegisterStore(t *testing.T) { 17 t.Run("FailBelowOrEqualPrunedHeight", func(t *testing.T) { 18 t.Parallel() 19 // 1. 20 pruned := uint64(10) 21 lastID := unittest.IdentifierFixture() 22 store := NewInMemoryRegisterStore(pruned, lastID) 23 err := store.SaveRegisters( 24 pruned-1, // below pruned pruned, will fail 25 unittest.IdentifierFixture(), 26 unittest.IdentifierFixture(), 27 flow.RegisterEntries{}, 28 ) 29 require.Error(t, err) 30 require.Contains(t, err.Error(), "<= pruned height") 31 32 err = store.SaveRegisters( 33 pruned, // equal to pruned height, will fail 34 lastID, 35 unittest.IdentifierFixture(), 36 flow.RegisterEntries{}, 37 ) 38 require.Error(t, err) 39 require.Contains(t, err.Error(), "<= pruned height") 40 }) 41 42 // 2. SaveRegisters should fail if its parent block doesn't exist and it is not the pruned block 43 // SaveRegisters should succeed if height is above pruned height and block is not saved, 44 // the updates can be retrieved by GetUpdatedRegisters 45 // GetRegister should return PrunedError if the queried key is not updated since pruned height 46 // GetRegister should return PrunedError if the queried height is below pruned height 47 // GetRegister should return ErrNotExecuted if the block is unknown 48 t.Run("FailParentNotExist", func(t *testing.T) { 49 t.Parallel() 50 pruned := uint64(10) 51 lastID := unittest.IdentifierFixture() 52 store := NewInMemoryRegisterStore(pruned, lastID) 53 54 height := pruned + 1 // above the pruned pruned 55 blockID := unittest.IdentifierFixture() 56 notExistParent := unittest.IdentifierFixture() 57 reg := unittest.RegisterEntryFixture() 58 err := store.SaveRegisters( 59 height, 60 blockID, 61 notExistParent, // should fail because parent doesn't exist 62 flow.RegisterEntries{reg}, 63 ) 64 require.Error(t, err) 65 require.Contains(t, err.Error(), "but its parent") 66 }) 67 68 t.Run("StoreOK", func(t *testing.T) { 69 t.Parallel() 70 // 3. 71 pruned := uint64(10) 72 lastID := unittest.IdentifierFixture() 73 store := NewInMemoryRegisterStore(pruned, lastID) 74 75 height := pruned + 1 // above the pruned pruned 76 blockID := unittest.IdentifierFixture() 77 reg := unittest.RegisterEntryFixture() 78 err := store.SaveRegisters( 79 height, 80 blockID, 81 lastID, 82 flow.RegisterEntries{reg}, 83 ) 84 require.NoError(t, err) 85 86 val, err := store.GetRegister(height, blockID, reg.Key) 87 require.NoError(t, err) 88 require.Equal(t, reg.Value, val) 89 90 // unknown key 91 _, err = store.GetRegister(height, blockID, unknownKey) 92 require.Error(t, err) 93 pe, ok := IsPrunedError(err) 94 require.True(t, ok) 95 require.Equal(t, pe.PrunedHeight, pruned) 96 require.Equal(t, pe.Height, height) 97 98 // unknown block with unknown height 99 _, err = store.GetRegister(height+1, unknownBlock, reg.Key) 100 require.Error(t, err) 101 require.ErrorIs(t, err, ErrNotExecuted) 102 103 // unknown block with known height 104 _, err = store.GetRegister(height, unknownBlock, reg.Key) 105 require.Error(t, err) 106 require.ErrorIs(t, err, ErrNotExecuted) 107 108 // too low height 109 _, err = store.GetRegister(height-1, unknownBlock, reg.Key) 110 require.Error(t, err) 111 pe, ok = IsPrunedError(err) 112 require.True(t, ok) 113 require.Equal(t, pe.PrunedHeight, pruned) 114 require.Equal(t, pe.Height, height-1) 115 }) 116 117 // 3. SaveRegisters should fail if the block is already saved 118 t.Run("StoreFailAlreadyExist", func(t *testing.T) { 119 t.Parallel() 120 pruned := uint64(10) 121 lastID := unittest.IdentifierFixture() 122 store := NewInMemoryRegisterStore(pruned, lastID) 123 124 height := pruned + 1 // above the pruned pruned 125 blockID := unittest.IdentifierFixture() 126 reg := unittest.RegisterEntryFixture() 127 err := store.SaveRegisters( 128 height, 129 blockID, 130 lastID, 131 flow.RegisterEntries{reg}, 132 ) 133 require.NoError(t, err) 134 135 // saving again should fail 136 err = store.SaveRegisters( 137 height, 138 blockID, 139 lastID, 140 flow.RegisterEntries{reg}, 141 ) 142 require.Error(t, err) 143 require.Contains(t, err.Error(), "already exists") 144 }) 145 146 // 4. SaveRegisters should succeed if a different block at the same height was saved before, 147 // updates for different blocks can be retrieved by their blockID 148 t.Run("StoreOKDifferentBlockSameParent", func(t *testing.T) { 149 t.Parallel() 150 pruned := uint64(10) 151 lastID := unittest.IdentifierFixture() 152 store := NewInMemoryRegisterStore(pruned, lastID) 153 154 // 10 <- A 155 // ^- B 156 height := pruned + 1 // above the pruned pruned 157 blockA := unittest.IdentifierFixture() 158 regA := unittest.RegisterEntryFixture() 159 err := store.SaveRegisters( 160 height, 161 blockA, 162 lastID, 163 flow.RegisterEntries{regA}, 164 ) 165 require.NoError(t, err) 166 167 blockB := unittest.IdentifierFixture() 168 regB := unittest.RegisterEntryFixture() 169 err = store.SaveRegisters( 170 height, 171 blockB, // different block 172 lastID, // same parent 173 flow.RegisterEntries{regB}, 174 ) 175 require.NoError(t, err) 176 177 valA, err := store.GetRegister(height, blockA, regA.Key) 178 require.NoError(t, err) 179 require.Equal(t, regA.Value, valA) 180 181 valB, err := store.GetRegister(height, blockB, regB.Key) 182 require.NoError(t, err) 183 require.Equal(t, regB.Value, valB) 184 }) 185 186 t.Run("IsBlockExecuted", func(t *testing.T) { 187 t.Parallel() 188 pruned := uint64(10) 189 lastID := unittest.IdentifierFixture() 190 store := NewInMemoryRegisterStore(pruned, lastID) 191 192 height := pruned + 1 // above the pruned pruned 193 blockID := unittest.IdentifierFixture() 194 reg := unittest.RegisterEntryFixture() 195 err := store.SaveRegisters( 196 height, 197 blockID, 198 lastID, 199 flow.RegisterEntries{reg}, 200 ) 201 require.NoError(t, err) 202 203 // above the pruned height and is executed 204 executed, err := store.IsBlockExecuted(height, blockID) 205 require.NoError(t, err) 206 require.True(t, executed) 207 208 // above the pruned height, and is not executed 209 executed, err = store.IsBlockExecuted(pruned+1, unittest.IdentifierFixture()) 210 require.NoError(t, err) 211 require.False(t, executed) 212 213 executed, err = store.IsBlockExecuted(pruned+2, unittest.IdentifierFixture()) 214 require.NoError(t, err) 215 require.False(t, executed) 216 217 // below the pruned height 218 _, err = store.IsBlockExecuted(pruned-1, unittest.IdentifierFixture()) 219 require.Error(t, err) 220 221 // equal to the pruned height and is the pruned block 222 executed, err = store.IsBlockExecuted(pruned, lastID) 223 require.NoError(t, err) 224 require.True(t, executed) 225 226 // equal to the pruned height, but is not the pruned block 227 executed, err = store.IsBlockExecuted(pruned, unittest.IdentifierFixture()) 228 require.NoError(t, err) 229 require.False(t, executed) 230 231 // prune a new block 232 require.NoError(t, store.Prune(height, blockID)) 233 // equal to the pruned height and is the pruned block 234 executed, err = store.IsBlockExecuted(height, blockID) 235 require.NoError(t, err) 236 require.True(t, executed) 237 238 // equal to the pruned height, but is not the pruned block 239 executed, err = store.IsBlockExecuted(height, unittest.IdentifierFixture()) 240 require.NoError(t, err) 241 require.False(t, executed) 242 243 // below the pruned height 244 _, err = store.IsBlockExecuted(pruned, lastID) 245 require.Error(t, err) 246 }) 247 248 // 5. Given A(X: 1, Y: 2), GetRegister(A, X) should return 1, GetRegister(A, X) should return 2 249 t.Run("GetRegistersOK", func(t *testing.T) { 250 t.Parallel() 251 pruned := uint64(10) 252 lastID := unittest.IdentifierFixture() 253 store := NewInMemoryRegisterStore(pruned, lastID) 254 255 // 10 <- A (X: 1, Y: 2) 256 height := pruned + 1 // above the pruned pruned 257 blockA := unittest.IdentifierFixture() 258 regX := makeReg("X", "1") 259 regY := makeReg("Y", "2") 260 err := store.SaveRegisters( 261 height, 262 blockA, 263 lastID, 264 flow.RegisterEntries{regX, regY}, 265 ) 266 require.NoError(t, err) 267 268 valX, err := store.GetRegister(height, blockA, regX.Key) 269 require.NoError(t, err) 270 require.Equal(t, regX.Value, valX) 271 272 valY, err := store.GetRegister(height, blockA, regY.Key) 273 require.NoError(t, err) 274 require.Equal(t, regY.Value, valY) 275 }) 276 277 // 6. Given A(X: 1, Y: 2) <- B(Y: 3), 278 // GetRegister(B, X) should return 1, because X is not updated in B 279 // GetRegister(B, Y) should return 3, because Y is updated in B 280 // GetRegister(A, Y) should return 2, because the query queries the value at A, not B 281 // GetRegister(B, Z) should return PrunedError, because register is unknown 282 // GetRegister(C, X) should return BlockNotExecuted, because block is not executed (unexecuted) 283 t.Run("GetLatestValueOK", func(t *testing.T) { 284 t.Parallel() 285 pruned := uint64(10) 286 lastID := unittest.IdentifierFixture() 287 store := NewInMemoryRegisterStore(pruned, lastID) 288 289 // 10 <- A (X: 1, Y: 2) <- B (Y: 3) 290 blockA := unittest.IdentifierFixture() 291 regX := makeReg("X", "1") 292 regY := makeReg("Y", "2") 293 err := store.SaveRegisters( 294 pruned+1, 295 blockA, 296 lastID, 297 flow.RegisterEntries{regX, regY}, 298 ) 299 require.NoError(t, err) 300 301 blockB := unittest.IdentifierFixture() 302 regY3 := makeReg("Y", "3") 303 err = store.SaveRegisters( 304 pruned+2, 305 blockB, 306 blockA, 307 flow.RegisterEntries{regY3}, 308 ) 309 require.NoError(t, err) 310 311 val, err := store.GetRegister(pruned+2, blockB, regX.Key) 312 require.NoError(t, err) 313 require.Equal(t, regX.Value, val) // X is not updated in B 314 315 val, err = store.GetRegister(pruned+2, blockB, regY.Key) 316 require.NoError(t, err) 317 require.Equal(t, regY3.Value, val) // Y is updated in B 318 319 val, err = store.GetRegister(pruned+1, blockA, regY.Key) 320 require.NoError(t, err) 321 require.Equal(t, regY.Value, val) // Y's old value at A 322 323 _, err = store.GetRegister(pruned+2, blockB, unknownKey) 324 require.Error(t, err) 325 pe, ok := IsPrunedError(err) 326 require.True(t, ok) 327 require.Equal(t, pe.PrunedHeight, pruned) 328 require.Equal(t, pe.Height, pruned+2) 329 330 _, err = store.GetRegister(pruned+3, unittest.IdentifierFixture(), regX.Key) 331 require.Error(t, err) 332 require.ErrorIs(t, err, ErrNotExecuted) // unknown block 333 }) 334 335 // 7. Given the following tree: 336 // Pruned <- A(X:1) <- B(Y:2) 337 // .......^- C(X:3) <- D(Y:4) 338 // GetRegister(D, X) should return 3 339 t.Run("StoreMultiForkOK", func(t *testing.T) { 340 t.Parallel() 341 pruned := uint64(10) 342 lastID := unittest.IdentifierFixture() 343 store := NewInMemoryRegisterStore(pruned, lastID) 344 345 // 10 <- A (X: 1) <- B (Y: 2) 346 // ^- C (X: 3) <- D (Y: 4) 347 blockA := unittest.IdentifierFixture() 348 blockB := unittest.IdentifierFixture() 349 blockC := unittest.IdentifierFixture() 350 blockD := unittest.IdentifierFixture() 351 352 require.NoError(t, store.SaveRegisters( 353 pruned+1, 354 blockA, 355 lastID, 356 flow.RegisterEntries{makeReg("X", "1")}, 357 )) 358 359 require.NoError(t, store.SaveRegisters( 360 pruned+2, 361 blockB, 362 blockA, 363 flow.RegisterEntries{makeReg("Y", "2")}, 364 )) 365 366 require.NoError(t, store.SaveRegisters( 367 pruned+1, 368 blockC, 369 lastID, 370 flow.RegisterEntries{makeReg("X", "3")}, 371 )) 372 373 require.NoError(t, store.SaveRegisters( 374 pruned+2, 375 blockD, 376 blockC, 377 flow.RegisterEntries{makeReg("Y", "4")}, 378 )) 379 380 reg := makeReg("X", "3") 381 val, err := store.GetRegister(pruned+2, blockD, reg.Key) 382 require.NoError(t, err) 383 require.Equal(t, reg.Value, val) 384 }) 385 386 // 8. Given the following tree: 387 // Pruned <- A(X:1) <- B(Y:2), B is not executed 388 // GetUpdatedRegisters(B) should return ErrNotExecuted 389 t.Run("GetUpdatedRegisters", func(t *testing.T) { 390 t.Parallel() 391 pruned := uint64(10) 392 lastID := unittest.IdentifierFixture() 393 store := NewInMemoryRegisterStore(pruned, lastID) 394 395 // 10 <- A (X: 1) <- B (Y: 2) 396 blockA := unittest.IdentifierFixture() 397 blockB := unittest.IdentifierFixture() 398 399 require.NoError(t, store.SaveRegisters( 400 pruned+1, 401 blockA, 402 lastID, 403 flow.RegisterEntries{makeReg("X", "1")}, 404 )) 405 406 reg, err := store.GetUpdatedRegisters(pruned+1, blockA) 407 require.NoError(t, err) 408 require.Equal(t, flow.RegisterEntries{makeReg("X", "1")}, reg) 409 410 _, err = store.GetUpdatedRegisters(pruned+2, blockB) 411 require.Error(t, err) 412 require.ErrorIs(t, err, ErrNotExecuted) 413 }) 414 415 // 9. Prune should fail if the block is unknown 416 // Prune should succeed if the block is known, and GetUpdatedRegisters should return err 417 // Prune should prune up to the pruned height. 418 // Given Pruned <- A(X:1) <- B(X:2) <- C(X:3) <- D(X:4) 419 // after Prune(B), GetRegister(C, X) should return 3, GetRegister(B, X) should return err 420 t.Run("StorePrune", func(t *testing.T) { 421 t.Parallel() 422 pruned := uint64(10) 423 lastID := unittest.IdentifierFixture() 424 store := NewInMemoryRegisterStore(pruned, lastID) 425 426 blockA := unittest.IdentifierFixture() 427 blockB := unittest.IdentifierFixture() 428 blockC := unittest.IdentifierFixture() 429 blockD := unittest.IdentifierFixture() 430 431 require.NoError(t, store.SaveRegisters( 432 pruned+1, 433 blockA, 434 lastID, 435 flow.RegisterEntries{makeReg("X", "1")}, 436 )) 437 438 require.NoError(t, store.SaveRegisters( 439 pruned+2, 440 blockB, 441 blockA, 442 flow.RegisterEntries{makeReg("X", "2")}, 443 )) 444 445 require.NoError(t, store.SaveRegisters( 446 pruned+3, 447 blockC, 448 blockB, 449 flow.RegisterEntries{makeReg("X", "3")}, 450 )) 451 452 require.NoError(t, store.SaveRegisters( 453 pruned+4, 454 blockD, 455 blockC, 456 flow.RegisterEntries{makeReg("X", "4")}, 457 )) 458 459 err := store.Prune(pruned+1, unknownBlock) // block is unknown 460 require.Error(t, err) 461 462 err = store.Prune(pruned+1, blockB) // block is known, but height is wrong 463 require.Error(t, err) 464 465 err = store.Prune(pruned+4, unknownBlock) // height is unknown 466 require.Error(t, err) 467 468 err = store.Prune(pruned+1, blockA) // prune next block 469 require.NoError(t, err) 470 471 require.Equal(t, pruned+1, store.PrunedHeight()) 472 473 reg := makeReg("X", "3") 474 val, err := store.GetRegister(pruned+3, blockC, reg.Key) 475 require.NoError(t, err) 476 require.Equal(t, reg.Value, val) 477 478 _, err = store.GetRegister(pruned+1, blockA, reg.Key) // A is pruned 479 require.Error(t, err) 480 pe, ok := IsPrunedError(err) 481 require.True(t, ok) 482 require.Equal(t, pe.PrunedHeight, pruned+1) 483 require.Equal(t, pe.Height, pruned+1) 484 485 err = store.Prune(pruned+3, blockC) // prune both B and C 486 require.NoError(t, err) 487 488 require.Equal(t, pruned+3, store.PrunedHeight()) 489 490 reg = makeReg("X", "4") 491 val, err = store.GetRegister(pruned+4, blockD, reg.Key) // can still get X at block D 492 require.NoError(t, err) 493 require.Equal(t, reg.Value, val) 494 }) 495 496 // 10. Prune should prune conflicting forks 497 // Given Pruned <- A(X:1) <- B(X:2) 498 // .................. ^----- E(X:5) 499 // ............ ^- C(X:3) <- D(X:4) 500 // Prune(A) should prune C and D, and GetUpdatedRegisters(C) should return out of range error, 501 // GetUpdatedRegisters(D) should return NotFound 502 t.Run("PruneConflictingForks", func(t *testing.T) { 503 t.Parallel() 504 pruned := uint64(10) 505 lastID := unittest.IdentifierFixture() 506 store := NewInMemoryRegisterStore(pruned, lastID) 507 508 blockA := unittest.IdentifierFixture() 509 blockB := unittest.IdentifierFixture() 510 blockC := unittest.IdentifierFixture() 511 blockD := unittest.IdentifierFixture() 512 blockE := unittest.IdentifierFixture() 513 514 require.NoError(t, store.SaveRegisters( 515 pruned+1, 516 blockA, 517 lastID, 518 flow.RegisterEntries{makeReg("X", "1")}, 519 )) 520 521 require.NoError(t, store.SaveRegisters( 522 pruned+2, 523 blockB, 524 blockA, 525 flow.RegisterEntries{makeReg("X", "2")}, 526 )) 527 528 require.NoError(t, store.SaveRegisters( 529 pruned+1, 530 blockC, 531 lastID, 532 flow.RegisterEntries{makeReg("X", "3")}, 533 )) 534 535 require.NoError(t, store.SaveRegisters( 536 pruned+2, 537 blockD, 538 blockC, 539 flow.RegisterEntries{makeReg("X", "4")}, 540 )) 541 542 require.NoError(t, store.SaveRegisters( 543 pruned+2, 544 blockE, 545 blockA, 546 flow.RegisterEntries{makeReg("X", "5")}, 547 )) 548 549 err := store.Prune(pruned+1, blockA) // prune A should prune C and D 550 require.NoError(t, err) 551 552 _, err = store.GetUpdatedRegisters(pruned+2, blockD) 553 require.Error(t, err) 554 require.Contains(t, err.Error(), "not found") 555 556 _, err = store.GetUpdatedRegisters(pruned+2, blockE) 557 require.NoError(t, err) 558 }) 559 560 // 11. Concurrency: SaveRegisters can happen concurrently with GetUpdatedRegisters, and GetRegister 561 t.Run("ConcurrentSaveAndGet", func(t *testing.T) { 562 t.Parallel() 563 pruned := uint64(10) 564 lastID := unittest.IdentifierFixture() 565 store := NewInMemoryRegisterStore(pruned, lastID) 566 567 // prepare a chain of 101 blocks with the first as lastID 568 count := 100 569 blocks := make(map[uint64]flow.Identifier, count) 570 blocks[pruned] = lastID 571 for i := 1; i < count; i++ { 572 block := unittest.IdentifierFixture() 573 blocks[pruned+uint64(i)] = block 574 } 575 576 reg := makeReg("X", "0") 577 578 var wg sync.WaitGroup 579 for i := 1; i < count; i++ { 580 height := pruned + uint64(i) 581 require.NoError(t, store.SaveRegisters( 582 height, 583 blocks[height], 584 blocks[height-1], 585 flow.RegisterEntries{makeReg("X", fmt.Sprintf("%v", height))}, 586 )) 587 588 // concurrently query get registers for past registers 589 wg.Add(1) 590 go func(i int) { 591 defer wg.Done() 592 593 rdHeight := randBetween(pruned+1, pruned+uint64(i)+1) 594 val, err := store.GetRegister(rdHeight, blocks[rdHeight], reg.Key) 595 require.NoError(t, err) 596 r := makeReg("X", fmt.Sprintf("%v", rdHeight)) 597 require.Equal(t, r.Value, val) 598 }(i) 599 600 // concurrently query updated registers 601 wg.Add(1) 602 go func(i int) { 603 defer wg.Done() 604 605 rdHeight := randBetween(pruned+1, pruned+uint64(i)+1) 606 vals, err := store.GetUpdatedRegisters(rdHeight, blocks[rdHeight]) 607 require.NoError(t, err) 608 r := makeReg("X", fmt.Sprintf("%v", rdHeight)) 609 require.Equal(t, flow.RegisterEntries{r}, vals) 610 }(i) 611 } 612 613 wg.Wait() 614 }) 615 616 // 12. Concurrency: Prune can happen concurrently with GetUpdatedRegisters, and GetRegister 617 t.Run("ConcurrentSaveAndPrune", func(t *testing.T) { 618 t.Parallel() 619 pruned := uint64(10) 620 lastID := unittest.IdentifierFixture() 621 store := NewInMemoryRegisterStore(pruned, lastID) 622 623 // prepare a chain of 101 blocks with the first as lastID 624 count := 100 625 blocks := make(map[uint64]flow.Identifier, count) 626 blocks[pruned] = lastID 627 for i := 1; i < count; i++ { 628 block := unittest.IdentifierFixture() 629 blocks[pruned+uint64(i)] = block 630 } 631 632 var wg sync.WaitGroup 633 savedHeights := make(chan uint64, 100) 634 635 wg.Add(1) 636 go func() { 637 defer wg.Done() 638 639 lastPrunedHeight := pruned 640 for savedHeight := range savedHeights { 641 if savedHeight%10 != 0 { 642 continue 643 } 644 rdHeight := randBetween(lastPrunedHeight+1, savedHeight+1) 645 err := store.Prune(rdHeight, blocks[rdHeight]) 646 require.NoError(t, err) 647 lastPrunedHeight = rdHeight 648 } 649 }() 650 651 // save 100 blocks 652 for i := 1; i < count; i++ { 653 height := pruned + uint64(i) 654 require.NoError(t, store.SaveRegisters( 655 height, 656 blocks[height], 657 blocks[height-1], 658 flow.RegisterEntries{makeReg("X", fmt.Sprintf("%v", i))}, 659 )) 660 savedHeights <- height 661 } 662 663 close(savedHeights) 664 665 wg.Wait() 666 }) 667 668 t.Run("PrunedError", func(t *testing.T) { 669 e := NewPrunedError(1, 2, unittest.IdentifierFixture()) 670 pe, ok := IsPrunedError(e) 671 require.True(t, ok) 672 require.Equal(t, uint64(1), pe.Height) 673 require.Equal(t, uint64(2), pe.PrunedHeight) 674 }) 675 } 676 677 func randBetween(min, max uint64) uint64 { 678 return uint64(rand.Intn(int(max)-int(min))) + min 679 } 680 681 func makeReg(key string, value string) flow.RegisterEntry { 682 return unittest.MakeOwnerReg(key, value) 683 } 684 685 var unknownBlock = unittest.IdentifierFixture() 686 var unknownKey = flow.RegisterID{ 687 Owner: "unknown", 688 Key: "unknown", 689 }