223 lines
7.2 KiB
Go
223 lines
7.2 KiB
Go
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
|
|
}
|