an atproto pds written in F# (.NET 9) 🦒
pds fsharp giraffe dotnet atproto

feat: add Merkle Search Tree (MST) core impl

Changed files
+1085 -7
PDSharp.Core
PDSharp.Tests
+812
PDSharp.Core/Mst.fs
··· 1 + namespace PDSharp.Core 2 + 3 + open System 4 + open System.Collections.Generic 5 + open System.Formats.Cbor 6 + 7 + module Mst = 8 + type MstEntry = { 9 + PrefixLen : int 10 + KeySuffix : string 11 + Value : Cid 12 + Tree : Cid option 13 + } 14 + 15 + type MstNode = { Left : Cid option; Entries : MstEntry list } 16 + 17 + /// Layer Calculation 18 + let layer (key : string) : int = 19 + let hash = Crypto.sha256Str key 20 + let mutable zeros = 0 21 + let mutable i = 0 22 + let mutable found = false 23 + 24 + while i < hash.Length && not found do 25 + let b = hash.[i] 26 + 27 + if b = 0uy then 28 + zeros <- zeros + 8 29 + i <- i + 1 30 + else 31 + let mutable mask = 0x80uy 32 + 33 + while b &&& mask = 0uy do 34 + zeros <- zeros + 1 35 + mask <- mask >>> 1 36 + 37 + found <- true 38 + 39 + zeros / 2 40 + 41 + let entryToCborObj (e : MstEntry) : obj = 42 + let arr = Array.zeroCreate<obj> 4 43 + arr.[0] <- e.PrefixLen :> obj 44 + arr.[1] <- e.KeySuffix :> obj 45 + arr.[2] <- e.Value :> obj 46 + 47 + arr.[3] <- 48 + match e.Tree with 49 + | Some c -> c :> obj 50 + | None -> null 51 + 52 + arr :> obj 53 + 54 + let nodeToCborObj (node : MstNode) : obj = 55 + let arr = Array.zeroCreate<obj> 2 56 + 57 + arr.[0] <- 58 + match node.Left with 59 + | Some c -> c :> obj 60 + | None -> null 61 + 62 + let entriesArr = node.Entries |> List.map entryToCborObj |> Seq.toArray 63 + arr.[1] <- entriesArr :> obj 64 + arr :> obj 65 + 66 + let serialize (node : MstNode) : byte[] = nodeToCborObj node |> DagCbor.encode 67 + 68 + let readCid (reader : CborReader) = 69 + let tag = reader.ReadTag() 70 + let tag42 = LanguagePrimitives.EnumOfValue<uint64, CborTag>(42UL) 71 + 72 + if tag <> tag42 then 73 + failwith "Expected CID tag 42" 74 + 75 + let bytes = reader.ReadByteString() 76 + 77 + if bytes.Length > 0 && bytes.[0] = 0x00uy then 78 + let raw = Array.zeroCreate<byte> (bytes.Length - 1) 79 + Array.Copy(bytes, 1, raw, 0, raw.Length) 80 + Cid raw 81 + else 82 + Cid bytes 83 + 84 + let deserialize (data : byte[]) : MstNode = 85 + let reader = new CborReader(data.AsMemory(), CborConformanceMode.Strict) 86 + let len = reader.ReadStartArray() 87 + 88 + if len.HasValue && len.Value <> 2 then 89 + failwith "MST node must be array of length 2" 90 + 91 + let left = 92 + match reader.PeekState() with 93 + | CborReaderState.Null -> 94 + reader.ReadNull() 95 + None 96 + | _ -> Some(readCid reader) 97 + 98 + let entriesLen = reader.ReadStartArray() 99 + let mutable entries = [] 100 + 101 + let count = 102 + if entriesLen.HasValue then 103 + entriesLen.Value 104 + else 105 + Int32.MaxValue 106 + 107 + let mutable i = 0 108 + 109 + while i < count && reader.PeekState() <> CborReaderState.EndArray do 110 + let entryLen = reader.ReadStartArray() 111 + 112 + if entryLen.HasValue && entryLen.Value <> 4 then 113 + failwith "MST entry must be array of length 4" 114 + 115 + let p = reader.ReadInt32() 116 + let k = reader.ReadTextString() 117 + let v = readCid reader 118 + 119 + let t = 120 + match reader.PeekState() with 121 + | CborReaderState.Null -> 122 + reader.ReadNull() 123 + None 124 + | _ -> Some(readCid reader) 125 + 126 + reader.ReadEndArray() 127 + 128 + entries <- entries @ [ { PrefixLen = p; KeySuffix = k; Value = v; Tree = t } ] 129 + i <- i + 1 130 + 131 + reader.ReadEndArray() 132 + reader.ReadEndArray() 133 + { Left = left; Entries = entries } 134 + 135 + let compareKeys (a : string) (b : string) : int = 136 + let bytesA = System.Text.Encoding.UTF8.GetBytes(a) 137 + let bytesB = System.Text.Encoding.UTF8.GetBytes(b) 138 + let len = min bytesA.Length bytesB.Length 139 + let mutable res = 0 140 + let mutable i = 0 141 + 142 + while res = 0 && i < len do 143 + res <- bytesA.[i].CompareTo(bytesB.[i]) 144 + i <- i + 1 145 + 146 + if res <> 0 then 147 + res 148 + else 149 + bytesA.Length.CompareTo bytesB.Length 150 + 151 + type NodeLoader = Cid -> Async<MstNode option> 152 + type NodePersister = MstNode -> Async<Cid> 153 + 154 + let storeNode (persister : NodePersister) (node : MstNode) : Async<Cid> = persister node 155 + 156 + let rec get (loader : NodeLoader) (node : MstNode) (key : string) (prevKey : string) : Async<Cid option> = async { 157 + let mutable currentKey = prevKey 158 + let mutable foundEntry : MstEntry option = None 159 + let mutable nextTree : Cid option = node.Left 160 + let mutable stop = false 161 + let mutable i = 0 162 + 163 + while not stop && i < node.Entries.Length do 164 + let e = node.Entries.[i] 165 + 166 + let prefix = 167 + if e.PrefixLen > currentKey.Length then 168 + currentKey 169 + else 170 + currentKey.Substring(0, e.PrefixLen) 171 + 172 + let fullKey = prefix + e.KeySuffix 173 + let cmp = compareKeys key fullKey 174 + 175 + if cmp = 0 then 176 + foundEntry <- Some e 177 + stop <- true 178 + elif cmp < 0 then 179 + stop <- true 180 + else 181 + nextTree <- e.Tree 182 + currentKey <- fullKey 183 + i <- i + 1 184 + 185 + match foundEntry with 186 + | Some e -> return Some e.Value 187 + | None -> 188 + match nextTree with 189 + | None -> return None 190 + | Some cid -> 191 + let! child = loader cid 192 + 193 + match child with 194 + | Some cNode -> return! get loader cNode key currentKey 195 + | None -> return None 196 + } 197 + 198 + let sharedPrefixLen (a : string) (b : string) = 199 + let len = min a.Length b.Length 200 + let mutable i = 0 201 + 202 + while i < len && a.[i] = b.[i] do 203 + i <- i + 1 204 + 205 + i 206 + 207 + let nodeLayer (node : MstNode) (prevKey : string) = 208 + match node.Entries with 209 + | [] -> -1 210 + | first :: _ -> 211 + let prefix = 212 + if first.PrefixLen > prevKey.Length then 213 + prevKey 214 + else 215 + prevKey.Substring(0, first.PrefixLen) 216 + 217 + let fullKey = prefix + first.KeySuffix 218 + layer fullKey 219 + 220 + /// Splits a node around a key (which is assumed to be higher layer than any in the node). 221 + /// 222 + /// Returns (LeftNode, RightNode). 223 + let rec split 224 + (loader : NodeLoader) 225 + (persister : NodePersister) 226 + (node : MstNode) 227 + (key : string) 228 + (prevKey : string) 229 + : Async<MstNode * MstNode> = 230 + async { 231 + let mutable splitIdx = -1 232 + let mutable found = false 233 + let mutable currentKey = prevKey 234 + let mutable i = 0 235 + let entries = node.Entries 236 + let mutable splitPrevKey = prevKey 237 + 238 + while i < entries.Length && not found do 239 + let e = entries.[i] 240 + 241 + let prefix = 242 + if e.PrefixLen > currentKey.Length then 243 + currentKey 244 + else 245 + currentKey.Substring(0, e.PrefixLen) 246 + 247 + let fullKey = prefix + e.KeySuffix 248 + 249 + if compareKeys key fullKey < 0 then 250 + splitIdx <- i 251 + found <- true 252 + else 253 + currentKey <- fullKey 254 + splitPrevKey <- currentKey 255 + i <- i + 1 256 + 257 + let childCidToSplit = 258 + if found then 259 + if splitIdx = 0 then 260 + node.Left 261 + else 262 + entries.[splitIdx - 1].Tree 263 + else if entries.Length = 0 then 264 + node.Left 265 + else 266 + entries.[entries.Length - 1].Tree 267 + 268 + let! (lChild, rChild) = async { 269 + match childCidToSplit with 270 + | None -> return ({ Left = None; Entries = [] }, { Left = None; Entries = [] }) 271 + | Some cid -> 272 + let! childNodeOpt = loader cid 273 + 274 + match childNodeOpt with 275 + | None -> return ({ Left = None; Entries = [] }, { Left = None; Entries = [] }) 276 + | Some childNode -> return! split loader persister childNode key currentKey 277 + } 278 + 279 + let persistOrNone (n : MstNode) = async { 280 + if n.Entries.IsEmpty && n.Left.IsNone then 281 + return None 282 + else 283 + let! c = persister n 284 + return Some c 285 + } 286 + 287 + let! lCid = persistOrNone lChild 288 + let! rCid = persistOrNone rChild 289 + let leftEntries = if found then entries |> List.take splitIdx else entries 290 + 291 + let newLeftEntries = 292 + if leftEntries.IsEmpty then 293 + [] 294 + else 295 + let last = leftEntries.[leftEntries.Length - 1] 296 + let newLast = { last with Tree = lCid } 297 + (leftEntries |> List.take (leftEntries.Length - 1)) @ [ newLast ] 298 + 299 + let leftNode = 300 + if leftEntries.IsEmpty then 301 + { Left = lCid; Entries = [] } 302 + else 303 + { Left = node.Left; Entries = newLeftEntries } 304 + 305 + let rightEntries = if found then entries |> List.skip splitIdx else [] 306 + 307 + let newRightEntries = 308 + match rightEntries with 309 + | [] -> [] 310 + | first :: rest -> 311 + let firstFullKey = 312 + let prefix = 313 + if first.PrefixLen > currentKey.Length then 314 + currentKey 315 + else 316 + currentKey.Substring(0, first.PrefixLen) 317 + 318 + prefix + first.KeySuffix 319 + 320 + let newP = sharedPrefixLen key firstFullKey 321 + let newSuffix = firstFullKey.Substring(newP) 322 + let newFirst = { first with PrefixLen = newP; KeySuffix = newSuffix } 323 + newFirst :: rest 324 + 325 + let rightNode = { Left = rCid; Entries = newRightEntries } 326 + return (leftNode, rightNode) 327 + } 328 + 329 + let rec put 330 + (loader : NodeLoader) 331 + (persister : NodePersister) 332 + (node : MstNode) 333 + (key : string) 334 + (value : Cid) 335 + (prevKey : string) 336 + : Async<MstNode> = 337 + async { 338 + let kLayer = layer key 339 + 340 + let nLayer = 341 + match node.Entries with 342 + | [] -> -1 343 + | first :: _ -> 344 + let prefix = 345 + if first.PrefixLen > prevKey.Length then 346 + prevKey 347 + else 348 + prevKey.Substring(0, first.PrefixLen) 349 + 350 + let fullKey = prefix + first.KeySuffix 351 + layer fullKey 352 + 353 + if kLayer > nLayer then 354 + let! (lNode, rNode) = split loader persister node key prevKey 355 + 356 + let persistOrNone (n : MstNode) = async { 357 + if n.Entries.IsEmpty && n.Left.IsNone then 358 + return None 359 + else 360 + let! c = persister n 361 + return Some c 362 + } 363 + 364 + let! lCid = persistOrNone lNode 365 + let! rCid = persistOrNone rNode 366 + 367 + let p = sharedPrefixLen prevKey key 368 + let suffix = key.Substring(p) 369 + 370 + return { 371 + Left = lCid 372 + Entries = [ 373 + { 374 + PrefixLen = p 375 + KeySuffix = suffix 376 + Value = value 377 + Tree = rCid 378 + } 379 + ] 380 + } 381 + 382 + elif kLayer < nLayer then 383 + let mutable nextCid = node.Left 384 + let mutable currentKey = prevKey 385 + let mutable found = false 386 + let mutable i = 0 387 + 388 + let entries = node.Entries 389 + let mutable childIdx = -1 390 + 391 + while i < entries.Length && not found do 392 + let e = entries.[i] 393 + 394 + let prefix = 395 + if e.PrefixLen > currentKey.Length then 396 + currentKey 397 + else 398 + currentKey.Substring(0, e.PrefixLen) 399 + 400 + let fullKey = prefix + e.KeySuffix 401 + 402 + if compareKeys key fullKey < 0 then 403 + found <- true 404 + else 405 + childIdx <- i 406 + nextCid <- e.Tree 407 + currentKey <- fullKey 408 + i <- i + 1 409 + 410 + let! childNode = 411 + match nextCid with 412 + | Some c -> loader c 413 + | None -> async { return Some { Left = None; Entries = [] } } 414 + 415 + match childNode with 416 + | None -> return failwith "Failed to load child node" 417 + | Some cn -> 418 + let! newChildNode = put loader persister cn key value currentKey 419 + let! newChildCid = persister newChildNode 420 + 421 + if childIdx = -1 then 422 + return { node with Left = Some newChildCid } 423 + else 424 + return { 425 + node with 426 + Entries = 427 + entries 428 + |> List.mapi (fun idx x -> 429 + if idx = childIdx then 430 + { entries.[childIdx] with Tree = Some newChildCid } 431 + else 432 + x) 433 + } 434 + 435 + else 436 + let mutable insertIdx = -1 437 + let mutable found = false 438 + let mutable currentKey = prevKey 439 + let mutable i = 0 440 + let mutable targetChildCid = None 441 + let mutable childPrevKey = prevKey 442 + 443 + let fullKeysCache = new List<string>() 444 + 445 + while i < node.Entries.Length && not found do 446 + let e = node.Entries.[i] 447 + 448 + let prefix = 449 + if e.PrefixLen > currentKey.Length then 450 + currentKey 451 + else 452 + currentKey.Substring(0, e.PrefixLen) 453 + 454 + let fullKey = prefix + e.KeySuffix 455 + fullKeysCache.Add(fullKey) 456 + 457 + let cmp = compareKeys key fullKey 458 + 459 + if cmp = 0 then 460 + insertIdx <- i 461 + found <- true 462 + elif cmp < 0 then 463 + insertIdx <- i 464 + targetChildCid <- if i = 0 then node.Left else node.Entries.[i - 1].Tree 465 + childPrevKey <- currentKey 466 + found <- true 467 + else 468 + currentKey <- fullKey 469 + i <- i + 1 470 + 471 + if not found then 472 + insertIdx <- node.Entries.Length 473 + 474 + targetChildCid <- 475 + if node.Entries.Length = 0 then 476 + node.Left 477 + else 478 + node.Entries.[node.Entries.Length - 1].Tree 479 + 480 + childPrevKey <- currentKey 481 + 482 + let entries = node.Entries 483 + 484 + if found && compareKeys key fullKeysCache.[insertIdx] = 0 then 485 + let e = entries.[insertIdx] 486 + let newE = { e with Value = value } 487 + 488 + let newEntries = 489 + entries |> List.mapi (fun idx x -> if idx = insertIdx then newE else x) 490 + 491 + return { node with Entries = newEntries } 492 + else 493 + let! (lChild, rChild) = async { 494 + match targetChildCid with 495 + | None -> return { Left = None; Entries = [] }, { Left = None; Entries = [] } 496 + | Some cid -> 497 + let! cOpt = loader cid 498 + 499 + match cOpt with 500 + | None -> return { Left = None; Entries = [] }, { Left = None; Entries = [] } 501 + | Some c -> return! split loader persister c key childPrevKey 502 + } 503 + 504 + let persistOrNone (n : MstNode) = async { 505 + if n.Entries.IsEmpty && n.Left.IsNone then 506 + return None 507 + else 508 + let! c = persister n 509 + return Some c 510 + } 511 + 512 + let! lCid = persistOrNone lChild 513 + let! rCid = persistOrNone rChild 514 + 515 + let beforeEntries = 516 + if insertIdx = 0 then 517 + [] 518 + else 519 + let prevE = entries.[insertIdx - 1] 520 + let newPrevE = { prevE with Tree = lCid } 521 + (entries |> List.take (insertIdx - 1)) @ [ newPrevE ] 522 + 523 + let newLeft = if insertIdx = 0 then lCid else node.Left 524 + let getFullKey idx = fullKeysCache.[idx] 525 + let p = sharedPrefixLen childPrevKey key 526 + let suffix = key.Substring(p) 527 + 528 + let newEntry = { 529 + PrefixLen = p 530 + KeySuffix = suffix 531 + Value = value 532 + Tree = rCid 533 + } 534 + 535 + let afterEntries = 536 + if insertIdx >= entries.Length then 537 + [] 538 + else 539 + let first = entries.[insertIdx] 540 + let firstFullKey = getFullKey insertIdx 541 + 542 + let newP = sharedPrefixLen key firstFullKey 543 + let newS = firstFullKey.Substring(newP) 544 + let newFirst = { first with PrefixLen = newP; KeySuffix = newS } 545 + 546 + [ newFirst ] @ (entries |> List.skip (insertIdx + 1)) 547 + 548 + let newEntries = beforeEntries @ [ newEntry ] @ afterEntries 549 + 550 + return { Left = newLeft; Entries = newEntries } 551 + } 552 + 553 + // --- Merge Operation --- 554 + let rec merge 555 + (loader : NodeLoader) 556 + (persister : NodePersister) 557 + (leftCid : Cid option) 558 + (rightCid : Cid option) 559 + (prevKey : string) 560 + : Async<MstNode> = 561 + async { 562 + match leftCid, rightCid with 563 + | None, None -> return { Left = None; Entries = [] } 564 + | Some l, None -> 565 + let! n = loader l 566 + return n |> Option.defaultValue { Left = None; Entries = [] } 567 + | None, Some r -> 568 + let! n = loader r 569 + return n |> Option.defaultValue { Left = None; Entries = [] } 570 + | Some l, Some r -> 571 + let! lNodeOpt = loader l 572 + let! rNodeOpt = loader r 573 + let lNode = lNodeOpt |> Option.defaultValue { Left = None; Entries = [] } 574 + let rNode = rNodeOpt |> Option.defaultValue { Left = None; Entries = [] } 575 + 576 + let lLayer = nodeLayer lNode prevKey 577 + let mutable current = prevKey 578 + 579 + for e in lNode.Entries do 580 + let p = 581 + if e.PrefixLen > current.Length then 582 + current 583 + else 584 + current.Substring(0, e.PrefixLen) 585 + 586 + current <- p + e.KeySuffix 587 + 588 + let rightPrevKey = current 589 + 590 + let realLLayer = nodeLayer lNode prevKey 591 + let realRLayer = nodeLayer rNode rightPrevKey 592 + 593 + if realLLayer > realRLayer then 594 + match lNode.Entries with 595 + | [] -> return! merge loader persister lNode.Left rightCid prevKey 596 + | entries -> 597 + let lastIdx = entries.Length - 1 598 + let lastEntry = entries.[lastIdx] 599 + 600 + let! mergedChild = merge loader persister lastEntry.Tree rightCid rightPrevKey 601 + let! mergedCid = persister mergedChild 602 + 603 + let newEntry = { lastEntry with Tree = Some mergedCid } 604 + let newEntries = (entries |> List.take lastIdx) @ [ newEntry ] 605 + return { lNode with Entries = newEntries } 606 + 607 + elif realRLayer > realLLayer then 608 + match rNode.Entries with 609 + | [] -> return! merge loader persister leftCid rNode.Left prevKey 610 + | _ -> 611 + let! mergedChild = merge loader persister leftCid rNode.Left prevKey 612 + let! mergedCid = persister mergedChild 613 + 614 + return { rNode with Left = Some mergedCid } 615 + 616 + else 617 + let boundaryL = 618 + match lNode.Entries with 619 + | [] -> lNode.Left 620 + | es -> es.[es.Length - 1].Tree 621 + 622 + let boundaryR = rNode.Left 623 + 624 + let! mergedBoundaryNode = merge loader persister boundaryL boundaryR rightPrevKey 625 + let! mergedBoundaryCid = persister mergedBoundaryNode 626 + 627 + let newEntries = 628 + match lNode.Entries with 629 + | [] -> rNode.Entries 630 + | lEntries -> 631 + let lastIdx = lEntries.Length - 1 632 + let lastE = lEntries.[lastIdx] 633 + let newLastE = { lastE with Tree = Some mergedBoundaryCid } 634 + (lEntries |> List.take lastIdx) @ [ newLastE ] @ rNode.Entries 635 + 636 + let newLeft = 637 + if lNode.Entries.IsEmpty then 638 + Some mergedBoundaryCid 639 + else 640 + lNode.Left 641 + 642 + return { Left = newLeft; Entries = newEntries } 643 + } 644 + 645 + let rec delete 646 + (loader : NodeLoader) 647 + (persister : NodePersister) 648 + (node : MstNode) 649 + (key : string) 650 + (prevKey : string) 651 + : Async<MstNode option> = 652 + async { 653 + let mutable currentKey = prevKey 654 + let mutable foundIdx = -1 655 + let mutable nextTreeIdx = -1 656 + let mutable i = 0 657 + let mutable found = false 658 + 659 + while i < node.Entries.Length && not found do 660 + let e = node.Entries.[i] 661 + 662 + let prefix = 663 + if e.PrefixLen > currentKey.Length then 664 + currentKey 665 + else 666 + currentKey.Substring(0, e.PrefixLen) 667 + 668 + let fullKey = prefix + e.KeySuffix 669 + 670 + let cmp = compareKeys key fullKey 671 + 672 + if cmp = 0 then 673 + foundIdx <- i 674 + found <- true 675 + elif cmp < 0 then 676 + found <- true 677 + nextTreeIdx <- i - 1 678 + else 679 + currentKey <- fullKey 680 + i <- i + 1 681 + 682 + if not found then 683 + nextTreeIdx <- node.Entries.Length - 1 684 + 685 + if foundIdx <> -1 then 686 + let e = node.Entries.[foundIdx] 687 + 688 + let leftChildCid = 689 + if foundIdx = 0 then 690 + node.Left 691 + else 692 + node.Entries.[foundIdx - 1].Tree 693 + 694 + let rightChildCid = e.Tree 695 + 696 + let mergePrevKey = if foundIdx = 0 then prevKey else currentKey 697 + 698 + let! mergedChildNode = merge loader persister leftChildCid rightChildCid mergePrevKey 699 + let! mergedChildCid = persister mergedChildNode 700 + 701 + let newEntries = 702 + if foundIdx = 0 then 703 + let rest = node.Entries |> List.skip 1 704 + 705 + match rest with 706 + | [] -> [] 707 + | first :: rs -> 708 + let firstFullKey = 709 + let oldP = 710 + if first.PrefixLen > key.Length then 711 + key 712 + else 713 + key.Substring(0, first.PrefixLen) 714 + 715 + oldP + first.KeySuffix 716 + 717 + let newP = sharedPrefixLen prevKey firstFullKey 718 + let newSuffix = firstFullKey.Substring(newP) 719 + let newFirst = { first with PrefixLen = newP; KeySuffix = newSuffix } 720 + newFirst :: rs 721 + else 722 + let before = node.Entries |> List.take (foundIdx - 1) 723 + let prevE = node.Entries.[foundIdx - 1] 724 + let newPrevE = { prevE with Tree = Some mergedChildCid } 725 + 726 + let rest = node.Entries |> List.skip (foundIdx + 1) 727 + 728 + let newRest = 729 + match rest with 730 + | [] -> [] 731 + | first :: rs -> 732 + let firstFullKey = 733 + let oldP = 734 + if first.PrefixLen > key.Length then 735 + key 736 + else 737 + key.Substring(0, first.PrefixLen) 738 + 739 + oldP + first.KeySuffix 740 + 741 + let newP = sharedPrefixLen mergePrevKey firstFullKey 742 + let newSuffix = firstFullKey.Substring(newP) 743 + let newFirst = { first with PrefixLen = newP; KeySuffix = newSuffix } 744 + newFirst :: rs 745 + 746 + before @ [ newPrevE ] @ newRest 747 + 748 + let newLeft = if foundIdx = 0 then Some mergedChildCid else node.Left 749 + 750 + let newNode = { Left = newLeft; Entries = newEntries } 751 + 752 + if newNode.Entries.IsEmpty && newNode.Left.IsNone then 753 + return None 754 + else 755 + return Some newNode 756 + 757 + else 758 + let childCid = 759 + if nextTreeIdx = -1 then 760 + node.Left 761 + else 762 + node.Entries.[nextTreeIdx].Tree 763 + 764 + match childCid with 765 + | None -> return Some node 766 + | Some cid -> 767 + let! childNodeOpt = loader cid 768 + 769 + match childNodeOpt with 770 + | None -> return Some node 771 + | Some childNode -> 772 + let! newChildOpt = delete loader persister childNode key currentKey 773 + 774 + match newChildOpt with 775 + | None -> 776 + if nextTreeIdx = -1 then 777 + return Some { node with Left = None } 778 + else 779 + let prevE = node.Entries.[nextTreeIdx] 780 + let newPrevE = { prevE with Tree = None } 781 + 782 + let newEntries = 783 + node.Entries 784 + |> List.mapi (fun idx x -> if idx = nextTreeIdx then newPrevE else x) 785 + 786 + return Some { node with Entries = newEntries } 787 + | Some newChild -> 788 + let! newChildCid = persister newChild 789 + 790 + if nextTreeIdx = -1 then 791 + return Some { node with Left = Some newChildCid } 792 + else 793 + let prevE = node.Entries.[nextTreeIdx] 794 + let newPrevE = { prevE with Tree = Some newChildCid } 795 + 796 + let newEntries = 797 + node.Entries 798 + |> List.mapi (fun idx x -> if idx = nextTreeIdx then newPrevE else x) 799 + 800 + return Some { node with Entries = newEntries } 801 + } 802 + 803 + let fromEntries (loader : NodeLoader) (persister : NodePersister) (entries : (string * Cid) list) : Async<MstNode> = async { 804 + let mutable root = { Left = None; Entries = [] } 805 + let sortedEntries = entries |> List.sortBy fst 806 + 807 + for k, v in sortedEntries do 808 + let! newRoot = put loader persister root k v "" 809 + root <- newRoot 810 + 811 + return root 812 + }
+1
PDSharp.Core/PDSharp.Core.fsproj
··· 9 9 <Compile Include="Cid.fs" /> 10 10 <Compile Include="DagCbor.fs" /> 11 11 <Compile Include="Crypto.fs" /> 12 + <Compile Include="Mst.fs" /> 12 13 <Compile Include="DidResolver.fs" /> 13 14 <Compile Include="Library.fs" /> 14 15 </ItemGroup>
+264
PDSharp.Tests/Mst.Tests.fs
··· 1 + module MstTests 2 + 3 + open Xunit 4 + open PDSharp.Core 5 + open PDSharp.Core.Mst 6 + 7 + [<Fact>] 8 + let ``Serialization Roundtrip`` () = 9 + let cid1 = Cid(Crypto.sha256Str "val1") 10 + 11 + let e1 = { 12 + PrefixLen = 0 13 + KeySuffix = "apple" 14 + Value = cid1 15 + Tree = None 16 + } 17 + 18 + let e2 = { 19 + PrefixLen = 2 20 + KeySuffix = "ricot" 21 + Value = cid1 22 + Tree = None 23 + } 24 + 25 + let node = { Left = None; Entries = [ e1; e2 ] } 26 + 27 + let bytes = Mst.serialize node 28 + let node2 = Mst.deserialize bytes 29 + 30 + Assert.Equal(node.Entries.Length, node2.Entries.Length) 31 + Assert.Equal("apple", node2.Entries.[0].KeySuffix) 32 + Assert.Equal("ricot", node2.Entries.[1].KeySuffix) 33 + Assert.Equal(2, node2.Entries.[1].PrefixLen) 34 + 35 + [<Fact>] 36 + let ``Get Operation Linear Scan`` () = 37 + let cid1 = Cid(Crypto.sha256Str "val1") 38 + let cid2 = Cid(Crypto.sha256Str "val2") 39 + 40 + let e1 = { 41 + PrefixLen = 0 42 + KeySuffix = "apple" 43 + Value = cid1 44 + Tree = None 45 + } 46 + 47 + let e2 = { 48 + PrefixLen = 0 49 + KeySuffix = "banana" 50 + Value = cid2 51 + Tree = None 52 + } 53 + 54 + let node = { Left = None; Entries = [ e1; e2 ] } 55 + 56 + let loader (c : Cid) = async { return None } 57 + 58 + let res1 = Mst.get loader node "apple" "" |> Async.RunSynchronously 59 + Assert.Equal(Some cid1, res1) 60 + 61 + let res2 = Mst.get loader node "banana" "" |> Async.RunSynchronously 62 + Assert.Equal(Some cid2, res2) 63 + 64 + let res3 = Mst.get loader node "cherry" "" |> Async.RunSynchronously 65 + Assert.True(Option.isNone res3) 66 + 67 + [<Fact>] 68 + let ``Get Operation With Prefix Compression`` () = 69 + let cid1 = Cid(Crypto.sha256Str "val1") 70 + let cid2 = Cid(Crypto.sha256Str "val2") 71 + 72 + let e1 = { 73 + PrefixLen = 0 74 + KeySuffix = "apple" 75 + Value = cid1 76 + Tree = None 77 + } 78 + 79 + let e2 = { 80 + PrefixLen = 2 81 + KeySuffix = "ricot" 82 + Value = cid2 83 + Tree = None 84 + } 85 + 86 + let node = { Left = None; Entries = [ e1; e2 ] } 87 + let loader (c : Cid) = async { return None } 88 + 89 + let res1 = Mst.get loader node "apricot" "" |> Async.RunSynchronously 90 + Assert.Equal(Some cid2, res1) 91 + 92 + [<Fact>] 93 + let ``Put Operation Simple Insert`` () = 94 + let store = System.Collections.Concurrent.ConcurrentDictionary<string, MstNode>() 95 + 96 + let loader (c : Cid) = async { 97 + let key = System.Convert.ToBase64String(c.Bytes) 98 + let success, node = store.TryGetValue(key) 99 + return if success then Some node else None 100 + } 101 + 102 + let persister (n : MstNode) = async { 103 + let bytes = Mst.serialize n 104 + let cid = Cid(Crypto.sha256 bytes) 105 + let key = System.Convert.ToBase64String(cid.Bytes) 106 + store.[key] <- n 107 + return cid 108 + } 109 + 110 + let node = { Left = None; Entries = [] } 111 + let cid1 = Cid(Crypto.sha256Str "v1") 112 + let node2 = Mst.put loader persister node "apple" cid1 "" |> Async.RunSynchronously 113 + 114 + Assert.Equal(1, node2.Entries.Length) 115 + Assert.Equal("apple", node2.Entries.[0].KeySuffix) 116 + Assert.Equal(0, node2.Entries.[0].PrefixLen) 117 + Assert.Equal(cid1, node2.Entries.[0].Value) 118 + 119 + let res = Mst.get loader node2 "apple" "" |> Async.RunSynchronously 120 + Assert.Equal(Some cid1, res) 121 + 122 + [<Fact>] 123 + let ``Put Operation Multiple Sorted`` () = 124 + let store = System.Collections.Concurrent.ConcurrentDictionary<string, MstNode>() 125 + 126 + let loader (c : Cid) = async { 127 + let key = System.Convert.ToBase64String(c.Bytes) 128 + let success, node = store.TryGetValue(key) 129 + return if success then Some node else None 130 + } 131 + 132 + let persister (n : MstNode) = async { 133 + let bytes = Mst.serialize n 134 + let cid = Cid(Crypto.sha256 bytes) 135 + let key = System.Convert.ToBase64String(cid.Bytes) 136 + store.[key] <- n 137 + return cid 138 + } 139 + 140 + let mutable node = { Left = None; Entries = [] } 141 + 142 + let k1, v1 = "apple", Cid(Crypto.sha256Str "1") 143 + let k2, v2 = "banana", Cid(Crypto.sha256Str "2") 144 + let k3, v3 = "cherry", Cid(Crypto.sha256Str "3") 145 + 146 + node <- Mst.put loader persister node k1 v1 "" |> Async.RunSynchronously 147 + node <- Mst.put loader persister node k2 v2 "" |> Async.RunSynchronously 148 + node <- Mst.put loader persister node k3 v3 "" |> Async.RunSynchronously 149 + 150 + let g1 = Mst.get loader node "apple" "" |> Async.RunSynchronously 151 + let g2 = Mst.get loader node "banana" "" |> Async.RunSynchronously 152 + let g3 = Mst.get loader node "cherry" "" |> Async.RunSynchronously 153 + 154 + Assert.Equal(Some v1, g1) 155 + Assert.Equal(Some v2, g2) 156 + Assert.Equal(Some v3, g3) 157 + 158 + [<Fact>] 159 + let ``Put Operation Multiple Reverse`` () = 160 + let store = System.Collections.Concurrent.ConcurrentDictionary<string, MstNode>() 161 + 162 + let loader (c : Cid) = async { 163 + let key = System.Convert.ToBase64String(c.Bytes) 164 + let success, node = store.TryGetValue(key) 165 + return if success then Some node else None 166 + } 167 + 168 + let persister (n : MstNode) = async { 169 + let bytes = Mst.serialize n 170 + let cid = Cid(Crypto.sha256 bytes) 171 + let key = System.Convert.ToBase64String(cid.Bytes) 172 + store.[key] <- n 173 + return cid 174 + } 175 + 176 + let mutable node = { Left = None; Entries = [] } 177 + 178 + let data = [ "zebra"; "yak"; "xylophone" ] 179 + 180 + for k in data do 181 + let v = Cid(Crypto.sha256Str k) 182 + node <- Mst.put loader persister node k v "" |> Async.RunSynchronously 183 + 184 + for k in data do 185 + let expected = Cid(Crypto.sha256Str k) 186 + let actual = Mst.get loader node k "" |> Async.RunSynchronously 187 + Assert.Equal(Some expected, actual) 188 + 189 + [<Fact>] 190 + let ``Delete Operation Simple`` () = 191 + let store = System.Collections.Concurrent.ConcurrentDictionary<string, MstNode>() 192 + 193 + let loader (c : Cid) = async { 194 + let key = System.Convert.ToBase64String(c.Bytes) 195 + let success, node = store.TryGetValue(key) 196 + return if success then Some node else None 197 + } 198 + 199 + let persister (n : MstNode) = async { 200 + let bytes = Mst.serialize n 201 + let cid = Cid(Crypto.sha256 bytes) 202 + let key = System.Convert.ToBase64String(cid.Bytes) 203 + store.[key] <- n 204 + return cid 205 + } 206 + 207 + let mutable node = { Left = None; Entries = [] } 208 + let cid1 = Cid(Crypto.sha256Str "val1") 209 + 210 + node <- Mst.put loader persister node "apple" cid1 "" |> Async.RunSynchronously 211 + 212 + let res1 = Mst.get loader node "apple" "" |> Async.RunSynchronously 213 + Assert.Equal(Some cid1, res1) 214 + 215 + // Delete 216 + let nodeOpt = Mst.delete loader persister node "apple" "" |> Async.RunSynchronously 217 + 218 + match nodeOpt with 219 + | None -> () 220 + | Some n -> 221 + let res2 = Mst.get loader n "apple" "" |> Async.RunSynchronously 222 + Assert.True(Option.isNone res2) 223 + 224 + [<Fact>] 225 + let ``Determinism From Entries`` () = 226 + let store = System.Collections.Concurrent.ConcurrentDictionary<string, MstNode>() 227 + 228 + let loader (c : Cid) = async { 229 + let key = System.Convert.ToBase64String(c.Bytes) 230 + let success, node = store.TryGetValue(key) 231 + return if success then Some node else None 232 + } 233 + 234 + let persister (n : MstNode) = async { 235 + let bytes = Mst.serialize n 236 + let cid = Cid(Crypto.sha256 bytes) 237 + let key = System.Convert.ToBase64String(cid.Bytes) 238 + store.[key] <- n 239 + return cid 240 + } 241 + 242 + let data = [ 243 + "apple", Cid(Crypto.sha256Str "1") 244 + "banana", Cid(Crypto.sha256Str "2") 245 + "cherry", Cid(Crypto.sha256Str "3") 246 + "date", Cid(Crypto.sha256Str "4") 247 + "elderberry", Cid(Crypto.sha256Str "5") 248 + ] 249 + 250 + 251 + let node1 = Mst.fromEntries loader persister data |> Async.RunSynchronously 252 + let cid1 = persister node1 |> Async.RunSynchronously 253 + 254 + let node2 = 255 + Mst.fromEntries loader persister (List.rev data) |> Async.RunSynchronously 256 + 257 + let cid2 = persister node2 |> Async.RunSynchronously 258 + let data3 = [ data.[2]; data.[0]; data.[4]; data.[1]; data.[3] ] 259 + let node3 = Mst.fromEntries loader persister data3 |> Async.RunSynchronously 260 + let cid3 = persister node3 |> Async.RunSynchronously 261 + 262 + Assert.Equal(cid1, cid2) 263 + Assert.Equal(cid1, cid3) 264 + ()
+4 -3
PDSharp.Tests/PDSharp.Tests.fsproj
··· 8 8 9 9 <ItemGroup> 10 10 <Compile Include="Tests.fs" /> 11 + <Compile Include="Mst.Tests.fs" /> 11 12 <Compile Include="Program.fs" /> 12 13 </ItemGroup> 13 14 ··· 18 19 <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> 19 20 </ItemGroup> 20 21 21 - <ItemGroup> 22 - <ProjectReference Include="..\PDSharp\PDSharp.fsproj" /> 23 - <ProjectReference Include="..\PDSharp.Core\PDSharp.Core.fsproj" /> 22 + <ItemGroup> 23 + <ProjectReference Include="..\PDSharp\PDSharp.fsproj" /> 24 + <ProjectReference Include="..\PDSharp.Core\PDSharp.Core.fsproj" /> 24 25 </ItemGroup> 25 26 26 27 </Project>
+4 -4
roadmap.txt
··· 23 23 -------------------------------------------------------------------------------- 24 24 Milestone D: MST Implementation 25 25 -------------------------------------------------------------------------------- 26 - - Merkle Search Tree per repository spec 27 - - Key depth = leading zero bits in SHA-256(key) counted in 2-bit chunks 28 - - Node encoding: (l, e[p,k,v,t]) with key prefix compression 26 + - [x] Merkle Search Tree per repository spec 27 + - [x] Key depth = leading zero bits in SHA-256(key) counted in 2-bit chunks 28 + - [x] Node encoding: (l, e[p,k,v,t]) with key prefix compression 29 29 DoD: Insert/update/delete yields reproducible root CID 30 30 -------------------------------------------------------------------------------- 31 31 Milestone E: Commit + BlockStore + putRecord ··· 117 117 [ ] describeServer endpoint working 118 118 [ ] Crypto primitives (sha256, ECDSA p256/k256, low-S) 119 119 [ ] DAG-CBOR + CID generation correct 120 - [ ] MST producing deterministic root CIDs 120 + [x] MST producing deterministic root CIDs 121 121 [ ] putRecord + blockstore operational 122 122 [ ] CAR export + sync endpoints 123 123 [ ] subscribeRepos firehose