github.com/amazechain/amc@v0.1.3/internal/vm/absint_cfg_proof_gen.go (about) 1 package vm 2 3 import ( 4 "bufio" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "os" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/emicklei/dot" 15 "github.com/holiman/uint256" 16 ) 17 18 // //////////////////////////////////////////////// 19 type AbsValueKind int 20 21 ////////////////////////// 22 23 // stmt is the representation of an executable instruction - extension of an opcode 24 type Astmt struct { 25 inferredAsData bool 26 ends bool 27 isBlockEntry bool 28 isBlockExit bool 29 covered bool 30 opcode OpCode 31 operation *operation 32 pc int 33 numBytes int 34 value uint256.Int 35 } 36 37 func (stmt *Astmt) String() string { 38 ends := "" 39 if stmt.ends { 40 ends = "ends" 41 } 42 return fmt.Sprintf("%v %v %v", stmt.opcode, ends, stmt.operation.isPush) 43 } 44 45 type Program struct { 46 Code []byte 47 Stmts []*Astmt 48 Blocks []*Block 49 Entry2block map[int]*Block 50 Exit2block map[int]*Block 51 } 52 53 func (program *Program) GetCodeHex() string { 54 return hex.EncodeToString(program.Code) 55 } 56 57 func (program *Program) isJumpDest(value *uint256.Int) bool { 58 if !value.IsUint64() { 59 return false 60 } 61 62 pc := value.Uint64() 63 if pc >= uint64(len(program.Stmts)) { 64 return false 65 } 66 67 stmt := program.Stmts[pc] 68 if stmt == nil { 69 return false 70 } 71 72 return stmt.opcode == JUMPDEST 73 } 74 75 func toProgram(code []byte) *Program { 76 jt := newIstanbulInstructionSet() 77 78 program := &Program{Code: code} 79 80 codeLen := len(code) 81 inferIsData := make(map[int]bool) 82 for pc := 0; pc < codeLen; pc++ { 83 stmt := Astmt{} 84 stmt.pc = pc 85 stmt.inferredAsData = inferIsData[pc] 86 87 op := OpCode(code[pc]) 88 stmt.opcode = op 89 stmt.operation = jt[op] 90 stmt.ends = stmt.operation == nil 91 //fmt.Printf("%v %v %v", pc, stmt.opcode, stmt.operation.valid) 92 93 if op.IsPush() { 94 pushByteSize := stmt.operation.opNum 95 startMin := pc + 1 96 if startMin >= codeLen { 97 startMin = codeLen 98 } 99 endMin := startMin + pushByteSize 100 if startMin+pushByteSize >= codeLen { 101 endMin = codeLen 102 } 103 integer := new(uint256.Int) 104 integer.SetBytes(code[startMin:endMin]) 105 stmt.value = *integer 106 stmt.numBytes = pushByteSize + 1 107 108 if !stmt.inferredAsData { 109 for datapc := startMin; datapc < endMin; datapc++ { 110 inferIsData[datapc] = true 111 } 112 } 113 } else { 114 stmt.numBytes = 1 115 } 116 117 program.Stmts = append(program.Stmts, &stmt) 118 if pc != len(program.Stmts)-1 { 119 panic("Invalid length") 120 } 121 } 122 123 for pc, stmt := range program.Stmts { 124 if !stmt.inferredAsData { 125 if pc == 0 { 126 stmt.isBlockEntry = true 127 } else if stmt.opcode == JUMPDEST { 128 stmt.isBlockEntry = true 129 } else if stmt.opcode == JUMPI && pc < len(program.Stmts)-1 { 130 entry := program.Stmts[pc+1] 131 entry.isBlockEntry = true 132 } 133 134 if pc == len(program.Stmts)-1 { 135 stmt.isBlockExit = true 136 } else if stmt.opcode == JUMP || stmt.opcode == JUMPI { 137 stmt.isBlockExit = true 138 } 139 } 140 } 141 142 program.Entry2block = make(map[int]*Block) 143 program.Exit2block = make(map[int]*Block) 144 145 for entrypc, entry := range program.Stmts { 146 if entry.isBlockEntry { 147 block := &Block{Entrypc: entrypc, Stmts: make([]*Astmt, 0)} 148 program.Blocks = append(program.Blocks, block) 149 for i := entrypc; i < len(program.Stmts); i++ { 150 block.Stmts = append(block.Stmts, program.Stmts[i]) 151 if program.Stmts[i].isBlockExit { 152 break 153 } 154 } 155 156 if len(block.Stmts) > 0 { 157 block.Exitpc = block.Stmts[len(block.Stmts)-1].pc 158 } 159 160 program.Entry2block[block.Entrypc] = block 161 program.Exit2block[block.Exitpc] = block 162 } 163 } 164 165 /* 166 epilogue := []byte{0xa1, 0x65, 0x62, 0x7a, 0x7a, 0x72} 167 168 for i := 0; i < len(program.Stmts) - len(epilogue) + 1; i++ { 169 match := true 170 for e := 0; e < len(epilogue); e++ { 171 if byte(program.Stmts[i+e].opcode) != epilogue[e] { 172 match = false 173 break 174 } 175 } 176 if match { 177 for j := i; j < len(program.Stmts); j++ { 178 program.Stmts[j].inferredAsData = true 179 program.Stmts[j].epilogue = true 180 } 181 break 182 } 183 }*/ 184 185 return program 186 } 187 188 /* 189 func printStmts(stmts []*Astmt) { 190 for i, stmt := range stmts { 191 fmt.Printf("%v %v\n", i, stmt) 192 } 193 }*/ 194 195 //////////////////////// 196 197 type edge struct { 198 pc0 int 199 stmt *Astmt 200 pc1 int 201 isJump bool 202 } 203 204 func (e edge) String() string { 205 return fmt.Sprintf("%v %v %v", e.pc0, e.pc1, e.stmt.opcode) 206 } 207 208 // resolve analyses given executable instruction at given program counter in the context of given state 209 // It either concludes that the execution of the instruction may result in a jump to an unpredictable 210 // destination (in this case, attrubute resolved will be false), or returns one (for a non-JUMPI) or two (for JUMPI) 211 // edges that contain program counters of destinations where the executions can possibly come next 212 func resolve(cfg *Cfg, pc0 int) ([]edge, error) { 213 st0 := cfg.D[pc0] 214 215 stmt := cfg.Program.Stmts[pc0] 216 217 if stmt.ends { 218 return nil, nil 219 } 220 221 codeLen := len(cfg.Program.Code) 222 223 var edges []edge 224 isBadJump := false 225 226 //jump edges 227 for _, stack := range st0.stackset { 228 if stmt.opcode == JUMP || stmt.opcode == JUMPI { 229 if stack.hasIndices(0) { 230 jumpDest := stack.values[0] 231 if jumpDest.kind == InvalidValue { 232 //program terminates, don't add edges 233 } else if jumpDest.kind == TopValue { 234 isBadJump = true 235 cfg.Metrics.BadJumpImprecision = true 236 } else if jumpDest.kind == ConcreteValue { 237 if cfg.Program.isJumpDest(jumpDest.value) { 238 pc1 := int(jumpDest.value.Uint64()) 239 edges = append(edges, edge{pc0, stmt, pc1, true}) 240 } 241 } 242 } 243 } 244 } 245 246 //fall-thru edge 247 if stmt.opcode != JUMP { 248 if pc0 < codeLen-stmt.numBytes { 249 edges = append(edges, edge{pc0, stmt, pc0 + stmt.numBytes, false}) 250 } 251 } 252 253 for _, e := range edges { 254 if cfg.PrevEdgeMap[e.pc1] == nil { 255 cfg.PrevEdgeMap[e.pc1] = make(map[int]bool) 256 } 257 cfg.PrevEdgeMap[e.pc1][e.pc0] = true 258 259 cfg.Program.Stmts[e.pc0].covered = true 260 cfg.Program.Stmts[e.pc1].covered = true 261 } 262 263 if isBadJump { 264 cfg.BadJumps[stmt.pc] = true 265 cfg.Metrics.Unresolved = true 266 cfg.checkRep() 267 return nil, errors.New("unresolvable jumps found") 268 } 269 270 edges = sortAndUnique(edges) 271 272 cfg.checkRep() 273 return edges, nil 274 } 275 276 type edgeKey struct { 277 pc0 int 278 pc1 int 279 } 280 281 func sortAndUnique(edges []edge) []edge { 282 uedges := make([]edge, 0) 283 284 edgeMap := make(map[edgeKey]bool) 285 for i := 0; i < len(edges); i++ { 286 e := edges[i] 287 ek := edgeKey{e.pc0, e.pc1} 288 if edgeMap[ek] { 289 continue 290 } 291 292 edgeMap[ek] = true 293 uedges = append(uedges, e) 294 } 295 296 sort.SliceStable(uedges, func(i, j int) bool { 297 return uedges[i].pc0 < uedges[j].pc1 298 }) 299 300 return uedges 301 } 302 303 func post(cfg *Cfg, st0 *astate, edge edge, maxStackLen int) (*astate, error) { 304 st1 := emptyState() 305 stmt := edge.stmt 306 307 for _, stack0 := range st0.stackset { 308 if edge.isJump { 309 if !stack0.hasIndices(0) { 310 continue 311 } 312 313 elm0 := stack0.values[0] 314 if elm0.kind == ConcreteValue && elm0.value.IsUint64() && int(elm0.value.Uint64()) != edge.pc1 { 315 continue 316 } 317 } 318 319 stack1 := stack0.Copy() 320 321 if stmt.opcode.IsPush() { 322 if cfg.Program.isJumpDest(&stmt.value) || isFF(&stmt.value) { 323 stack1.Push(AbsValueConcrete(stmt.value)) 324 } else { 325 stack1.Push(AbsValueInvalid()) 326 } 327 } else if stmt.operation.isDup { 328 if !stack0.hasIndices(stmt.operation.opNum - 1) { 329 continue 330 } 331 332 value := stack1.values[stmt.operation.opNum-1] 333 stack1.Push(value) 334 } else if stmt.operation.isSwap { 335 opNum := stmt.operation.opNum 336 337 if !stack0.hasIndices(0, opNum) { 338 continue 339 } 340 341 a := stack1.values[0] 342 b := stack1.values[opNum] 343 stack1.values[0] = b 344 stack1.values[opNum] = a 345 346 } else if stmt.opcode == AND { 347 if !stack0.hasIndices(0, 1) { 348 continue 349 } 350 351 a := stack1.Pop(edge.pc0) 352 b := stack1.Pop(edge.pc0) 353 354 if a.kind == ConcreteValue && b.kind == ConcreteValue { 355 v := uint256.NewInt(0) 356 v.And(a.value, b.value) 357 stack1.Push(AbsValueConcrete(*v)) 358 } else { 359 stack1.Push(AbsValueTop(edge.pc0)) 360 } 361 } else if stmt.opcode == PC { 362 v := uint256.NewInt(0) 363 v.SetUint64(uint64(stmt.pc)) 364 stack1.Push(AbsValueConcrete(*v)) 365 } else { 366 if !stack0.hasIndices(stmt.operation.numPop - 1) { 367 continue 368 } 369 370 for i := 0; i < stmt.operation.numPop; i++ { 371 stack1.Pop(edge.pc0) 372 } 373 374 for i := 0; i < stmt.operation.numPush; i++ { 375 stack1.Push(AbsValueTop(edge.pc0)) 376 } 377 } 378 379 stack1.updateHash() 380 st1.Add(stack1) 381 } 382 383 for _, stack := range st1.stackset { 384 if len(stack.values) > maxStackLen { 385 cfg.Metrics.ShortStack = true 386 return nil, errors.New("max stack length reached") 387 } 388 } 389 390 return st1, nil 391 } 392 393 func isFF(u *uint256.Int) bool { 394 if u.IsUint64() && u.Uint64() == 4294967295 { 395 return true 396 } 397 return false 398 } 399 400 type Block struct { 401 Entrypc int 402 Exitpc int 403 Stmts []*Astmt 404 } 405 406 type CfgMetrics struct { 407 Valid bool 408 Panic bool 409 Unresolved bool 410 ShortStack bool 411 AnlyCounterLimit bool 412 LowCoverage bool 413 BadJumpImprecision bool 414 BadJumpInvalidOp bool 415 BadJumpInvalidJumpDest bool 416 StackCountLimitReached bool 417 OOM bool 418 Timeout bool 419 Checker bool 420 CheckerFailed bool 421 AnlyCounter int 422 NumStacks int 423 ProofSizeBytes int 424 Time time.Duration 425 MemUsedMBs uint64 426 } 427 428 type Cfg struct { 429 Program *Program 430 BadJumps map[int]bool 431 PrevEdgeMap map[int]map[int]bool 432 D map[int]*astate 433 Metrics *CfgMetrics 434 ProofSerialized []byte 435 } 436 437 type CfgCoverageStats struct { 438 Covered int 439 Uncovered int 440 Instructions int 441 Coverage int 442 Epilogue int 443 } 444 445 func (cfg *Cfg) Clear() { 446 cfg.D = nil 447 cfg.PrevEdgeMap = nil 448 cfg.BadJumps = nil 449 cfg.Program = nil 450 } 451 452 func (cfg *Cfg) checkRep() { 453 if true { 454 return 455 } 456 457 for pc1, pc0s := range cfg.PrevEdgeMap { 458 for pc0 := range pc0s { 459 s := cfg.Program.Stmts[pc0] 460 if s.ends { 461 msg := fmt.Sprintf("Halt has successor: %v -> %v", pc0, pc1) 462 panic(msg) 463 } 464 } 465 } 466 } 467 468 func (cfg *Cfg) GetCoverageStats() CfgCoverageStats { 469 stats := CfgCoverageStats{} 470 firstUncovered := -1 471 for pc, stmt := range cfg.Program.Stmts { 472 if !stmt.inferredAsData { 473 if stmt.covered { 474 stats.Covered++ 475 } else { 476 if firstUncovered < 0 { 477 firstUncovered = pc 478 } 479 } 480 stats.Instructions++ 481 } 482 } 483 484 stats.Epilogue = 0 485 for i := len(cfg.Program.Stmts) - 1; i >= 0; i-- { 486 stmt := cfg.Program.Stmts[i] 487 if !stmt.inferredAsData { 488 if cfg.Program.Stmts[i].covered { 489 break 490 } 491 stats.Epilogue++ 492 } 493 } 494 495 stats.Uncovered = stats.Instructions - stats.Covered 496 stats.Coverage = stats.Covered * 100 / stats.Instructions 497 return stats 498 } 499 500 func (cfg *Cfg) PrintAnlyState() { 501 // es := make([]edge, len(edges)) 502 // copy(es, edges) 503 // sortEdges(es) 504 505 var badJumpList []string 506 for pc, stmt := range cfg.Program.Stmts { 507 if stmt.inferredAsData { 508 //fmt.Printf("data: %v\n", stmt.inferredAsData) 509 continue 510 } 511 512 if stmt.opcode == JUMPDEST { 513 fmt.Println() 514 } 515 516 var valueStr string 517 if stmt.opcode.IsPush() { 518 valueStr = fmt.Sprintf("%v %v", stmt.opcode, stmt.value.Hex()) 519 } else { 520 valueStr = fmt.Sprintf("%v", stmt.opcode) 521 } 522 523 pc0s := make([]string, 0) 524 for pc0 := range cfg.PrevEdgeMap[pc] { 525 pc0s = append(pc0s, strconv.Itoa(pc0)) 526 } 527 528 if cfg.BadJumps[pc] { 529 out := fmt.Sprintf("[%5v] (w:%2v) %3v\t %-25v %-10v %v\n", cfg.D[pc].anlyCounter, cfg.D[pc].worklistLen, pc, valueStr, strings.Join(pc0s, ","), cfg.D[pc].String(false)) 530 fmt.Print(out) 531 badJumpList = append(badJumpList, out) 532 } else if cfg.PrevEdgeMap[pc] != nil { 533 fmt.Printf("[%5v] (w:%2v) %3v\t %-25v %-10v %v\n", cfg.D[pc].anlyCounter, cfg.D[pc].worklistLen, pc, valueStr, strings.Join(pc0s, ","), cfg.D[pc].String(true)) 534 } else { 535 fmt.Printf("[%5v] (w:%2v) %3v\t %-25v\n", cfg.D[pc].anlyCounter, cfg.D[pc].worklistLen, pc, valueStr) 536 } 537 } 538 539 print("\nFull list of bad jumps:\n") 540 for _, badJump := range badJumpList { 541 fmt.Println(badJump) 542 } 543 544 g := dot.NewGraph(dot.Directed) 545 block2node := make(map[*Block]*dot.Node) 546 for _, block := range cfg.Program.Blocks { 547 n := g.Node(fmt.Sprintf("%v\n%v", block.Entrypc, block.Exitpc)).Box() 548 block2node[block] = &n 549 } 550 551 for pc1 := range cfg.PrevEdgeMap { 552 for pc0 := range cfg.PrevEdgeMap[pc1] { 553 block1 := cfg.Program.Entry2block[pc1] 554 555 if block1 == nil { 556 continue 557 } 558 559 block0 := cfg.Program.Exit2block[pc0] 560 if block0 == nil { 561 continue 562 } 563 564 n0 := block2node[block0] 565 n1 := block2node[block1] 566 g.Edge(*n0, *n1) 567 } 568 } 569 570 path := "cfg.dot" 571 _ = os.Remove(path) 572 573 f, errcr := os.Create(path) 574 if errcr != nil { 575 panic(errcr) 576 } 577 defer f.Close() 578 579 w := bufio.NewWriter(f) 580 _, errwr := w.WriteString(g.String()) 581 if errwr != nil { 582 panic(errwr) 583 } 584 _ = w.Flush() 585 } 586 587 func (metrics *CfgMetrics) GetBadJumpReason() string { 588 if metrics.Valid { 589 return "" 590 } 591 592 if metrics.ShortStack { 593 return "ShortStack" 594 } 595 596 if metrics.AnlyCounterLimit { 597 return "AnlyCounterLimit" 598 } 599 600 if metrics.BadJumpImprecision { 601 return "Imprecision" 602 } 603 604 if metrics.BadJumpInvalidJumpDest { 605 return "InvalidJumpDest" 606 } 607 608 if metrics.BadJumpInvalidOp { 609 return "InvalidOpcode" 610 } 611 612 if metrics.Panic { 613 return "Panic" 614 } 615 616 return "Unknown" 617 } 618 619 func pushNewEdges(workList []edge, edges []edge) []edge { 620 for _, e := range edges { 621 inWorkList := false 622 for _, w := range workList { 623 if w.pc0 == e.pc0 && w.pc1 == e.pc1 { 624 inWorkList = true 625 } 626 } 627 if !inWorkList { 628 head := []edge{e} 629 workList = append(head, workList...) 630 } 631 } 632 return workList 633 } 634 635 func GenCfg(code []byte, anlyCounterLimit int, maxStackLen int, maxStackCount int, metrics *CfgMetrics) (cfg *Cfg, err error) { 636 program := toProgram(code) 637 cfg = &Cfg{Metrics: metrics} 638 cfg.BadJumps = make(map[int]bool) 639 cfg.Metrics = metrics 640 cfg.Program = program 641 cfg.PrevEdgeMap = make(map[int]map[int]bool) 642 643 startPC := 0 644 codeLen := len(program.Code) 645 cfg.D = make(map[int]*astate) 646 for pc := 0; pc < codeLen; pc++ { 647 cfg.D[pc] = emptyState() 648 } 649 cfg.D[startPC] = botState() 650 651 var workList []edge 652 653 edgesR1, errR1 := resolve(cfg, startPC) 654 if errR1 != nil { 655 return cfg, errR1 656 } 657 workList = pushNewEdges(workList, edgesR1) 658 659 for len(workList) > 0 { 660 if anlyCounterLimit > 0 && cfg.Metrics.AnlyCounter > anlyCounterLimit { 661 cfg.Metrics.AnlyCounterLimit = true 662 return cfg, errors.New("reached analysis counter limit") 663 } 664 665 var e edge 666 e, workList = workList[0], workList[1:] 667 668 preDpc0 := cfg.D[e.pc0] 669 preDpc1 := cfg.D[e.pc1] 670 671 post1, err := post(cfg, preDpc0, e, maxStackLen) 672 if err != nil { 673 return cfg, err 674 } 675 676 if !Leq(post1, preDpc1) { 677 postDpc1 := Lub(post1, preDpc1) 678 cfg.D[e.pc1] = postDpc1 679 680 if len(postDpc1.stackset) > maxStackCount { 681 //fmt.Printf("stacklen: %v %v\n", len(postDpc1.stackset), maxStackCount) 682 //fmt.Println(postDpc1.String(false)) 683 //for _, stack := range postDpc1.stackset { 684 // fmt.Printf("%v\n", stack.String(false)) 685 //} 686 cfg.Metrics.StackCountLimitReached = true 687 return cfg, errors.New("stack count limit reach") 688 } 689 690 edgesR2, errR2 := resolve(cfg, e.pc1) 691 if errR2 != nil { 692 return cfg, errR2 693 } 694 workList = pushNewEdges(workList, edgesR2) 695 } 696 697 decp1Copy := cfg.D[e.pc1] 698 decp1Copy.anlyCounter = cfg.Metrics.AnlyCounter 699 decp1Copy.worklistLen = len(workList) 700 cfg.D[e.pc1] = decp1Copy 701 cfg.Metrics.AnlyCounter++ 702 703 cfg.checkRep() 704 } 705 706 if len(cfg.BadJumps) > 0 { 707 cfg.Metrics.Unresolved = true 708 return cfg, errors.New("unresolvable jumps found") 709 } 710 711 cov := cfg.GetCoverageStats() 712 if cov.Uncovered > cov.Epilogue { 713 cfg.Metrics.LowCoverage = true 714 //return cfg, errors.New("low coverage") 715 } 716 717 cfg.Metrics.Valid = true 718 719 return cfg, nil 720 } 721 722 func (cfg *Cfg) GenerateProof() *CfgProof { 723 succEdgeMap := make(map[int][]int) 724 proof := CfgProof{} 725 entries := make(map[int]bool) 726 exits := make(map[int]bool) 727 728 pcs := make(map[int]bool) 729 for pc1, pc0s := range cfg.PrevEdgeMap { 730 for pc0 := range pc0s { 731 succEdgeMap[pc0] = append(succEdgeMap[pc0], pc1) 732 pcs[pc0] = true 733 pcs[pc1] = true 734 } 735 } 736 737 entries[0] = true 738 for pc := range pcs { 739 if pc == 0 || len(cfg.PrevEdgeMap[pc]) > 1 { 740 entries[pc] = true 741 } 742 for pc0 := range cfg.PrevEdgeMap[pc] { 743 if len(succEdgeMap[pc0]) > 1 { 744 entries[pc] = true 745 break 746 } 747 } 748 opcode := OpCode(cfg.Program.Code[pc]) 749 if opcode == JUMPDEST { 750 entries[pc] = true 751 } 752 if opcode == JUMPI { 753 if pc < len(cfg.Program.Code)-1 { 754 entries[pc+1] = true 755 } 756 } 757 } 758 759 for pc0 := range pcs { 760 for _, pc1 := range succEdgeMap[pc0] { 761 if entries[pc1] { 762 exits[pc0] = true 763 break 764 } 765 } 766 767 if len(succEdgeMap[pc0]) == 0 || len(succEdgeMap[pc0]) > 1 { 768 exits[pc0] = true 769 } 770 771 opcode := OpCode(cfg.Program.Code[pc0]) 772 if opcode == JUMP || opcode == JUMPI || 773 opcode == RETURN || opcode == REVERT { 774 exits[pc0] = true 775 } 776 } 777 778 entriesList := make([]int, 0) 779 for pc := range entries { 780 entriesList = append(entriesList, pc) 781 } 782 sort.Ints(entriesList) 783 for _, pc0 := range entriesList { 784 pc1 := pc0 785 for !exits[pc1] { 786 if len(succEdgeMap[pc1]) != 1 { 787 panic("Inconsistent successors") 788 } 789 pc1 = succEdgeMap[pc1][0] 790 } 791 792 block := CfgProofBlock{} 793 block.Entry = &CfgProofState{pc0, StringifyAState(cfg.D[pc0])} 794 block.Exit = &CfgProofState{pc1, StringifyAState(cfg.D[pc1])} 795 proof.Blocks = append(proof.Blocks, &block) 796 } 797 798 for _, predBlock := range proof.Blocks { 799 for _, succBlock := range proof.Blocks { 800 if cfg.PrevEdgeMap[succBlock.Entry.Pc][predBlock.Exit.Pc] { 801 predBlock.Succs = append(predBlock.Succs, succBlock.Entry.Pc) 802 succBlock.Preds = append(succBlock.Preds, predBlock.Exit.Pc) 803 } 804 } 805 } 806 807 return &proof 808 }