package ots import ( "encoding/hex" "fmt" "slices" "strings" "git.intruders.space/public/opentimestamps/varn" ) // Header magic bytes // Designed to be give the user some information in a hexdump, while being identified as 'data' by the file utility. // \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94 var ( HeaderMagic = []byte{0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94} PendingMagic = []byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e} BitcoinMagic = []byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01} ) // A File represents the parsed content of an .ots file: it has an initial digest and // a series of sequences of instructions. Each sequence must be evaluated separately, applying the operations // on top of each other, starting with the .Digest until they end on an attestation. type File struct { Digest []byte Sequences []Sequence } func (ts File) GetPendingSequences() []Sequence { bitcoin := ts.GetBitcoinAttestedSequences() results := make([]Sequence, 0, len(ts.Sequences)) for _, seq := range ts.Sequences { if len(seq) > 0 && seq[len(seq)-1].Attestation != nil && seq[len(seq)-1].Attestation.CalendarServerURL != "" { // this is a calendar sequence, fine // now we check if this same sequence isn't contained in a bigger one that contains a bitcoin attestation cseq := seq for _, bseq := range bitcoin { if len(bseq) < len(cseq) { continue } if slices.EqualFunc(bseq[0:len(cseq)], cseq, func(a, b Instruction) bool { return CompareInstructions(a, b) == 0 }) { goto thisSequenceIsAlreadyConfirmed } } // sequence not confirmed, so add it to pending result results = append(results, seq) thisSequenceIsAlreadyConfirmed: // skip this continue } } return results } func (ts File) GetBitcoinAttestedSequences() []Sequence { results := make([]Sequence, 0, len(ts.Sequences)) for _, seq := range ts.Sequences { if len(seq) > 0 && seq[len(seq)-1].Attestation != nil && seq[len(seq)-1].Attestation.BitcoinBlockHeight > 0 { results = append(results, seq) } } return results } func (ts File) Human(withPartials bool) string { strs := make([]string, 0, 100) strs = append(strs, fmt.Sprintf("file digest: %x", ts.Digest)) strs = append(strs, fmt.Sprintf("hashed with: sha256")) strs = append(strs, "instruction sequences:") for _, seq := range ts.Sequences { curr := ts.Digest strs = append(strs, "~>") strs = append(strs, " start "+hex.EncodeToString(curr)) for _, inst := range seq { line := " " if inst.Operation != nil { line += inst.Operation.Name curr = inst.Operation.Apply(curr, inst.Argument) if inst.Operation.Binary { line += " " + hex.EncodeToString(inst.Argument) } if withPartials { line += " = " + hex.EncodeToString(curr) } } else if inst.Attestation != nil { line += inst.Attestation.Human() } else { panic(fmt.Sprintf("invalid instruction timestamp: %v", inst)) } strs = append(strs, line) } } return strings.Join(strs, "\n") } func (ts File) SerializeToFile() []byte { data := make([]byte, 0, 5050) data = append(data, HeaderMagic...) data = varn.AppendVarUint(data, 1) data = append(data, 0x08) // sha256 data = append(data, ts.Digest...) data = append(data, ts.SerializeInstructionSequences()...) return data } func (ts File) SerializeInstructionSequences() []byte { sequences := make([]Sequence, len(ts.Sequences)) copy(sequences, ts.Sequences) // first we sort everything so the checkpoint stuff makes sense slices.SortFunc(sequences, func(a, b Sequence) int { return slices.CompareFunc(a, b, CompareInstructions) }) // checkpoints we may leave to the next people sequenceCheckpoints := make([][]int, len(sequences)) for s1 := range sequences { // keep an ordered slice of all the checkpoints we will potentially leave during our write journey for this sequence checkpoints := make([]int, 0, len(sequences[s1])) for s2 := s1 + 1; s2 < len(sequences); s2++ { chp := GetCommonPrefixIndex(sequences[s1], sequences[s2]) if pos, found := slices.BinarySearch(checkpoints, chp); !found { checkpoints = append(checkpoints, -1) // make room copy(checkpoints[pos+1:], checkpoints[pos:]) // move elements to the right checkpoints[pos] = chp // insert this } } sequenceCheckpoints[s1] = checkpoints } // now actually go through the sequences writing them result := make([]byte, 0, 500) for s, seq := range sequences { startingAt := 0 if s > 0 { // we will always start at the last checkpoint left by the previous sequence startingAt = sequenceCheckpoints[s-1][len(sequenceCheckpoints[s-1])-1] } for i := startingAt; i < len(seq); i++ { // before writing anything, decide if we wanna leave a checkpoint here for _, chk := range sequenceCheckpoints[s] { if chk == i { // leave a checkpoint result = append(result, 0xff) } } inst := seq[i] if inst.Operation != nil { // write normal operation result = append(result, inst.Operation.Tag) if inst.Operation.Binary { result = varn.AppendVarBytes(result, inst.Argument) } } else if inst.Attestation != nil { // write attestation record result = append(result, 0x00) { // will use a new buffer for the actual attestation result abuf := make([]byte, 0, 100) if inst.BitcoinBlockHeight != 0 { result = append(result, BitcoinMagic...) // this goes in the main result buffer abuf = varn.AppendVarUint(abuf, inst.BitcoinBlockHeight) } else if inst.CalendarServerURL != "" { result = append(result, PendingMagic...) // this goes in the main result buffer abuf = varn.AppendVarBytes(abuf, []byte(inst.CalendarServerURL)) } else { panic(fmt.Sprintf("invalid attestation: %v", inst)) } result = varn.AppendVarBytes(result, abuf) // we append that result as varbytes } } else { panic(fmt.Sprintf("invalid instruction: %v", inst)) } } } return result } func ParseOTSFile(buf varn.Buffer) (*File, error) { // read magic // read version [1 byte] // read crypto operation for file digest [1 byte] // read file digest [32 byte (depends)] if magic, err := buf.ReadBytes(len(HeaderMagic)); err != nil || !slices.Equal(HeaderMagic, magic) { return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err) } if version, err := buf.ReadVarUint(); err != nil || version != 1 { return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err) } tag, err := buf.ReadByte() if err != nil { return nil, fmt.Errorf("failed to read operation byte: %w", err) } if op, err := ReadInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" { return nil, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err) } // if we got here assume the digest is sha256 digest, err := buf.ReadBytes(32) if err != nil { return nil, fmt.Errorf("failed to read 32-byte digest: %w", err) } ts := &File{ Digest: digest, } if seqs, err := ParseTimestamp(buf); err != nil { return nil, err } else { ts.Sequences = seqs } return ts, nil }