parsing from file fixes + pretty printing.

This commit is contained in:
fiatjaf
2023-09-24 22:05:54 -03:00
parent 619f2cb453
commit 3c38206ce3
3 changed files with 180 additions and 29 deletions

139
ots.go
View File

@@ -2,8 +2,10 @@ package opentimestamps
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"strings"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@@ -53,50 +55,77 @@ type Instruction struct {
} }
type Attestation struct { type Attestation struct {
Name string
BitcoinBlockHeight uint64 BitcoinBlockHeight uint64
CalendarServerURL string CalendarServerURL string
} }
func parseCalendarServerResponse(buf Buffer, digest []byte) (Timestamp, error) { func (att Attestation) Name() string {
ts := Timestamp{ if att.BitcoinBlockHeight != 0 {
return "bitcoin"
} else if att.CalendarServerURL != "" {
return "pending"
} else {
return "unknown/broken"
}
}
func (att Attestation) String() string {
if att.BitcoinBlockHeight != 0 {
return fmt.Sprintf("bitcoin(%d)", att.BitcoinBlockHeight)
} else if att.CalendarServerURL != "" {
return fmt.Sprintf("pending(%s)", att.CalendarServerURL)
} else {
return "unknown/broken"
}
}
func parseCalendarServerResponse(buf Buffer, digest []byte) (*Timestamp, error) {
ts := &Timestamp{
Digest: digest, Digest: digest,
} }
err := parseTimestamp(buf, &ts) err := parseTimestamp(buf, ts)
if err != nil { if err != nil {
return ts, err return nil, err
} }
return ts, nil return ts, nil
} }
func parseOTSFile(buf Buffer) (Timestamp, error) { func parseOTSFile(buf Buffer) (*Timestamp, error) {
ts := Timestamp{}
// read magic // read magic
// read version [1 byte] // read version [1 byte]
// read crypto operation for file digest [1 byte] // read crypto operation for file digest [1 byte]
// read file digest [32 byte (depends)] // read file digest [32 byte (depends)]
if magic, err := buf.readBytes(len(headerMagic)); err != nil || slices.Equal(headerMagic, magic) { if magic, err := buf.readBytes(len(headerMagic)); err != nil || !slices.Equal(headerMagic, magic) {
return ts, fmt.Errorf("invalid ots file header '%s': %w", magic, err) return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err)
} }
if version, err := buf.readByte(); err != nil || version != '1' { if version, err := buf.readVarUint(); err != nil || version != 1 {
return ts, fmt.Errorf("invalid ots file version '%v': %w", version, err) return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err)
} }
tag, err := buf.readByte() tag, err := buf.readByte()
if err != nil { if err != nil {
return ts, fmt.Errorf("failed to read operation byte: %w", err) return nil, fmt.Errorf("failed to read operation byte: %w", err)
} }
if op, err := readInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" { if op, err := readInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" {
return ts, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err) return nil, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err)
} }
if err := parseTimestamp(buf, &ts); err != nil { // if we got here assume the digest is sha256
return ts, err digest, err := buf.readBytes(32)
if err != nil {
return nil, fmt.Errorf("failed to read 32-byte digest: %w", err)
}
ts := &Timestamp{
Digest: digest,
}
if err := parseTimestamp(buf, ts); err != nil {
return nil, err
} }
return ts, nil return ts, nil
@@ -150,7 +179,7 @@ func parseTimestamp(buf Buffer, ts *Timestamp) error {
} }
ts.Instructions[currInstructionsBlock] = append( ts.Instructions[currInstructionsBlock] = append(
ts.Instructions[currInstructionsBlock], ts.Instructions[currInstructionsBlock],
Instruction{Attestation: &Attestation{Name: "pending", CalendarServerURL: string(val)}}, Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}},
) )
continue continue
case slices.Equal(magic, bitcoinMagic): case slices.Equal(magic, bitcoinMagic):
@@ -160,7 +189,7 @@ func parseTimestamp(buf Buffer, ts *Timestamp) error {
} }
ts.Instructions[currInstructionsBlock] = append( ts.Instructions[currInstructionsBlock] = append(
ts.Instructions[currInstructionsBlock], ts.Instructions[currInstructionsBlock],
Instruction{Attestation: &Attestation{Name: "bitcoin", BitcoinBlockHeight: val}}, Instruction{Attestation: &Attestation{BitcoinBlockHeight: val}},
) )
continue continue
default: default:
@@ -209,3 +238,77 @@ func readInstruction(buf Buffer, tag byte) (*Instruction, error) {
return &inst, nil return &inst, nil
} }
func (ts Timestamp) String() 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, "sets:")
for _, set := range ts.Instructions {
strs = append(strs, "~> instruction set")
for _, inst := range set {
line := " "
if inst.Operation != nil {
line += inst.Operation.Name
if inst.Operation.Binary {
line += " " + hex.EncodeToString(inst.Argument)
}
} else if inst.Attestation != nil {
line += inst.Attestation.String()
} else {
panic(fmt.Sprintf("invalid instruction timestamp: %v", inst))
}
strs = append(strs, line)
}
}
return strings.Join(strs, "\n")
}
func (ts Timestamp) SerializeToFile() []byte {
data := make([]byte, 0, 5050)
data = append(data, headerMagic...)
data = appendVarUint(data, 1)
data = append(data, 0x08) // sha256
data = append(data, ts.Digest...)
data = append(data, ts.Serialize()...)
return data
}
func (ts Timestamp) Serialize() []byte {
data := make([]byte, 0, 5000)
for i, set := range ts.Instructions {
for _, inst := range set {
if inst.Operation != nil {
// write normal operation
data = append(data, inst.Operation.Tag)
if inst.Operation.Binary {
data = appendVarBytes(data, inst.Argument)
}
} else if inst.Attestation != nil {
// write attestation record
data = append(data, 0x00)
{
// will use a new buffer for the actual attestation data
abuf := make([]byte, 0, 100)
if inst.BitcoinBlockHeight != 0 {
data = append(data, bitcoinMagic...) // this goes in the main data buffer
abuf = appendVarUint(abuf, inst.BitcoinBlockHeight)
} else if inst.CalendarServerURL != "" {
data = append(data, pendingMagic...) // this goes in the main data buffer
abuf = appendVarBytes(abuf, []byte(inst.CalendarServerURL))
} else {
panic(fmt.Sprintf("invalid attestation: %v", inst))
}
data = appendVarBytes(data, abuf) // we append that data as varbytes
}
} else {
panic(fmt.Sprintf("invalid instruction: %v", inst))
}
}
if i+1 < len(ts.Instructions) {
// write separator and start a new set of instructions
data = append(data, 0xff)
}
}
return data
}

View File

@@ -3,17 +3,16 @@ package opentimestamps
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
) )
func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) error { func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (*Timestamp, error) {
body := bytes.NewBuffer(digest[:]) body := bytes.NewBuffer(digest[:])
req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body) req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body)
if err != nil { if err != nil {
return err return nil, err
} }
req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps") req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps")
@@ -21,19 +20,42 @@ func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) error {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("'%s' request failed: %w", calendarUrl, err) return nil, fmt.Errorf("'%s' request failed: %w", calendarUrl, err)
} }
full, err := io.ReadAll(resp.Body) full, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read response from '%s': %w", calendarUrl, err) return nil, fmt.Errorf("failed to read response from '%s': %w", calendarUrl, err)
} }
resp.Body.Close() resp.Body.Close()
fmt.Println("full", hex.EncodeToString(full)) return parseCalendarServerResponse(NewBuffer(full), digest[:])
v, err := parseCalendarServerResponse(NewBuffer(full), digest[:])
fmt.Println(err)
fmt.Println(v)
return nil
} }
func ReadFromFile(data []byte) (*Timestamp, error) {
return parseOTSFile(NewBuffer(data))
}
// func Upgrade(ctx context.Context, calendarUrl string) (*Timestamp, error) {
// body := bytes.NewBuffer(digest[:])
// req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/timestamp/" +, nil)
// if err != nil {
// return nil, err
// }
//
// req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps")
// req.Header.Add("Accept", "application/vnd.opentimestamps.v1")
// req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// resp, err := http.DefaultClient.Do(req)
// if err != nil {
// return nil, fmt.Errorf("'%s' request failed: %w", calendarUrl, err)
// }
//
// full, err := io.ReadAll(resp.Body)
// if err != nil {
// return nil, fmt.Errorf("failed to read response from '%s': %w", calendarUrl, err)
// }
// resp.Body.Close()
//
// return parseCalendarServerResponse(NewBuffer(full), digest[:])
// }

View File

@@ -88,3 +88,29 @@ func (buf Buffer) readVarBytes() ([]byte, error) {
fmt.Println("->", hex.EncodeToString(b)) fmt.Println("->", hex.EncodeToString(b))
return b, nil return b, nil
} }
func appendVarUint(buf []byte, value uint64) []byte {
if value == 0 {
buf = append(buf, 0)
} else {
for value != 0 {
b := byte(value & 0b01111111)
if value > 0b01111111 {
b |= 0b10000000
}
buf = append(buf, b)
if value <= 0b01111111 {
break
}
value >>= 7
}
}
return buf
}
func appendVarBytes(buf []byte, value []byte) []byte {
buf = appendVarUint(buf, uint64(len(value)))
buf = append(buf, value...)
return buf
}