252 lines
7.5 KiB
Go
252 lines
7.5 KiB
Go
package opentimestamps
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
/*
|
|
* 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}
|
|
|
|
var (
|
|
pendingMagic = []byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e}
|
|
bitcoinMagic = []byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01}
|
|
)
|
|
|
|
type Operation struct {
|
|
Name string
|
|
Tag byte
|
|
Binary bool // it's an operation that takes one argument, otherwise takes none
|
|
Apply func(curr []byte, arg []byte) []byte
|
|
}
|
|
|
|
var tags = map[byte]*Operation{
|
|
0xf0: {"append", 0xf0, true, func(curr []byte, arg []byte) []byte {
|
|
result := make([]byte, len(curr)+len(arg))
|
|
copy(result[0:], curr)
|
|
copy(result[len(curr):], arg)
|
|
return result
|
|
}},
|
|
0xf1: {"prepend", 0xf1, true, func(curr []byte, arg []byte) []byte {
|
|
result := make([]byte, len(curr)+len(arg))
|
|
copy(result[0:], arg)
|
|
copy(result[len(arg):], curr)
|
|
return result
|
|
}},
|
|
0xf2: {"reverse", 0xf2, false, func(curr []byte, arg []byte) []byte { panic("reverse not implemented") }},
|
|
0xf3: {"hexlify", 0xf3, false, func(curr []byte, arg []byte) []byte { panic("hexlify not implemented") }},
|
|
0x02: {"sha1", 0x02, false, func(curr []byte, arg []byte) []byte { panic("sha1 not implemented") }},
|
|
0x03: {"ripemd160", 0x03, false, func(curr []byte, arg []byte) []byte { panic("ripemd160 not implemented") }},
|
|
0x08: {"sha256", 0x08, false, func(curr []byte, arg []byte) []byte {
|
|
v := sha256.Sum256(curr)
|
|
return v[:]
|
|
}},
|
|
0x67: {"keccak256", 0x67, false, func(curr []byte, arg []byte) []byte { panic("keccak256 not implemented") }},
|
|
}
|
|
|
|
// A Timestamp is basically the 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 Timestamp struct {
|
|
Digest []byte
|
|
Instructions []Sequence
|
|
}
|
|
|
|
// a Instruction can be an operation like "append" or "prepend" (this will be the case when .Operation != nil)
|
|
// or an attestation (when .Attestation != nil).
|
|
// It will have a non-nil .Argument whenever the operation requires an argument.
|
|
type Instruction struct {
|
|
*Operation
|
|
Argument []byte
|
|
*Attestation
|
|
}
|
|
|
|
func (a Instruction) Equal(b Instruction) bool {
|
|
if a.Operation != nil {
|
|
if a.Operation == b.Operation && slices.Equal(a.Argument, b.Argument) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
} else if a.Attestation != nil {
|
|
if b.Attestation == nil {
|
|
return false
|
|
}
|
|
if a.Attestation.BitcoinBlockHeight != 0 &&
|
|
a.Attestation.BitcoinBlockHeight == b.Attestation.BitcoinBlockHeight {
|
|
return true
|
|
}
|
|
if a.Attestation.CalendarServerURL != "" &&
|
|
a.Attestation.CalendarServerURL == b.Attestation.CalendarServerURL {
|
|
return true
|
|
}
|
|
return false
|
|
} else {
|
|
// a is nil -- this is already broken but whatever
|
|
if b.Attestation == nil && b.Operation == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
type Sequence []Instruction
|
|
|
|
func (ts Timestamp) GetPendingSequences() []Sequence {
|
|
bitcoin := ts.GetBitcoinAttestedSequences()
|
|
|
|
results := make([]Sequence, 0, len(ts.Instructions))
|
|
for _, seq := range ts.Instructions {
|
|
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 a.Equal(b) }) {
|
|
goto thisSequenceIsAlreadyConfirmed
|
|
}
|
|
}
|
|
|
|
// sequence not confirmed, so add it to pending result
|
|
results = append(results, seq)
|
|
|
|
thisSequenceIsAlreadyConfirmed:
|
|
// skip this
|
|
continue
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
func (ts Timestamp) GetBitcoinAttestedSequences() []Sequence {
|
|
results := make([]Sequence, 0, len(ts.Instructions))
|
|
for _, seq := range ts.Instructions {
|
|
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 Timestamp) Human() 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.Instructions {
|
|
strs = append(strs, "~>")
|
|
for _, inst := range seq {
|
|
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.Human()
|
|
} 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.SerializeInstructionSequences()...)
|
|
return data
|
|
}
|
|
|
|
func (ts Timestamp) SerializeInstructionSequences() []byte {
|
|
data := make([]byte, 0, 5000)
|
|
for i, seq := range ts.Instructions {
|
|
for _, inst := range seq {
|
|
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 sequence of instructions
|
|
data = append(data, 0xff)
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
type Attestation struct {
|
|
BitcoinBlockHeight uint64
|
|
CalendarServerURL string
|
|
}
|
|
|
|
func (att Attestation) Name() string {
|
|
if att.BitcoinBlockHeight != 0 {
|
|
return "bitcoin"
|
|
} else if att.CalendarServerURL != "" {
|
|
return "pending"
|
|
} else {
|
|
return "unknown/broken"
|
|
}
|
|
}
|
|
|
|
func (att Attestation) Human() 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 ComputeSequence(initial []byte, seq []Instruction) []byte {
|
|
current := initial
|
|
for _, inst := range seq {
|
|
if inst.Operation == nil {
|
|
break
|
|
}
|
|
current = inst.Operation.Apply(current, inst.Argument)
|
|
}
|
|
return current
|
|
}
|