diff --git a/go.mod b/go.mod index 4d0409a..bcc2177 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,7 @@ go 1.21 require ( github.com/btcsuite/btcd v0.23.4 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 - github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 ) require ( @@ -15,11 +14,9 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/sys v0.12.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 659ef97..ee537ef 100644 --- a/go.sum +++ b/go.sum @@ -24,7 +24,6 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= @@ -60,10 +59,6 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -71,6 +66,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -86,7 +83,6 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -101,13 +97,11 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ots.go b/ots.go new file mode 100644 index 0000000..4fbe941 --- /dev/null +++ b/ots.go @@ -0,0 +1,211 @@ +package opentimestamps + +import ( + "crypto/sha256" + "fmt" + "io" + + "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 { return append(curr, arg...) }}, + 0xf1: {"prepend", 0xf1, true, func(curr []byte, arg []byte) []byte { return append(arg, curr...) }}, + 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") }}, +} + +type Timestamp struct { + Digest []byte + Instructions [][]Instruction +} + +type Instruction struct { + *Operation + Argument []byte + *Attestation +} + +type Attestation struct { + Name string + BitcoinBlockHeight uint64 + CalendarServerURL string +} + +func parseCalendarServerResponse(buf Buffer, digest []byte) (Timestamp, error) { + ts := Timestamp{ + Digest: digest, + } + + err := parseTimestamp(buf, &ts) + if err != nil { + return ts, err + } + + return ts, nil +} + +func parseOTSFile(buf Buffer) (Timestamp, error) { + ts := Timestamp{} + + // 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 ts, fmt.Errorf("invalid ots file header '%s': %w", magic, err) + } + + if version, err := buf.readByte(); err != nil || version != '1' { + return ts, fmt.Errorf("invalid ots file version '%v': %w", version, err) + } + + tag, err := buf.readByte() + if err != nil { + return ts, fmt.Errorf("failed to read operation byte: %w", err) + } + + 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) + } + + if err := parseTimestamp(buf, &ts); err != nil { + return ts, err + } + + return ts, nil +} + +func parseTimestamp(buf Buffer, ts *Timestamp) error { + // read instructions + // if operation = push + // if 0x00 = attestation + // read tag [8 bytes] + // readvarbytes + // interpret these depending on the type of attestation + // if bitcoin: readvaruint as the block height + // if pending from calendar: readvarbytes as the utf-8 calendar url + // if 0xff = skip and start reading a new block of instructions? + + currInstructionsBlock := 0 + ts.Instructions = make([][]Instruction, 0, 5) + + // start first instruction block + ts.Instructions = append(ts.Instructions, make([]Instruction, 0, 30)) + + // go read these tags + for { + tag, err := buf.readByte() + if err != nil { + if err == io.EOF { + return nil + } + return fmt.Errorf("failed to read operation byte: %w", err) + } + + if tag == 0x00 { + // enter an attestation context + magic, err := buf.readBytes(8) + if err != nil { + return fmt.Errorf("failed to read attestion magic bytes: %w", err) + } + + this, err := buf.readVarBytes() + if err != nil { + return fmt.Errorf("failed to read attestation bytes: %w", err) + } + abuf := NewBuffer(this) + + switch { + case slices.Equal(magic, pendingMagic): + val, err := abuf.readVarBytes() + if err != nil { + return fmt.Errorf("failed reading calendar server url: %w", err) + } + ts.Instructions[currInstructionsBlock] = append( + ts.Instructions[currInstructionsBlock], + Instruction{Attestation: &Attestation{Name: "pending", CalendarServerURL: string(val)}}, + ) + continue + case slices.Equal(magic, bitcoinMagic): + val, err := abuf.readVarUint() + if err != nil { + return fmt.Errorf("failed reading bitcoin block number: %w", err) + } + ts.Instructions[currInstructionsBlock] = append( + ts.Instructions[currInstructionsBlock], + Instruction{Attestation: &Attestation{Name: "bitcoin", BitcoinBlockHeight: val}}, + ) + continue + default: + return fmt.Errorf("unsupported attestation type %v", magic) + } + } else if tag == 0xff { + // another block of instructions + ts.Instructions = append(ts.Instructions, make([]Instruction, 0, 30)) + currInstructionsBlock++ + tag, err = buf.readByte() + if err != nil { + if err == io.EOF { + return nil + } + return fmt.Errorf("failed to read operation byte when starting a new block of instructions: %w", err) + } + } + + // a new operation in this block + inst, err := readInstruction(buf, tag) + if err != nil { + return fmt.Errorf("failed to read instruction: %w", err) + } + + ts.Instructions[currInstructionsBlock] = append(ts.Instructions[currInstructionsBlock], *inst) + } +} + +func readInstruction(buf Buffer, tag byte) (*Instruction, error) { + op, ok := tags[tag] + if !ok { + return nil, fmt.Errorf("unknown tag %v", tag) + } + + inst := Instruction{ + Operation: op, + } + + if op.Binary { + val, err := buf.readVarBytes() + if err != nil { + return nil, fmt.Errorf("error reading argument: %w", err) + } + inst.Argument = val + } + + return &inst, nil +} diff --git a/stamp.go b/stamp.go new file mode 100644 index 0000000..293d66c --- /dev/null +++ b/stamp.go @@ -0,0 +1,39 @@ +package opentimestamps + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "net/http" +) + +func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) error { + body := bytes.NewBuffer(digest[:]) + req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body) + if err != nil { + return 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 fmt.Errorf("'%s' request failed: %w", calendarUrl, err) + } + + full, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response from '%s': %w", calendarUrl, err) + } + resp.Body.Close() + + fmt.Println("full", hex.EncodeToString(full)) + v, err := parseCalendarServerResponse(NewBuffer(full), digest[:]) + fmt.Println(err) + fmt.Println(v) + + return nil +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..69f8baa --- /dev/null +++ b/utils.go @@ -0,0 +1,90 @@ +package opentimestamps + +import ( + "encoding/hex" + "fmt" + "io" + "strings" +) + +func normalizeUrl(u string) string { + if strings.HasSuffix(u, "/") { + u = u[0 : len(u)-1] + } + if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") { + u = "http://" + u + } + return u +} + +type Buffer struct { + pos *int + buf []byte +} + +func NewBuffer(buf []byte) Buffer { + zero := 0 + return Buffer{&zero, buf} +} + +func (buf Buffer) readBytes(n int) ([]byte, error) { + fmt.Println("reading", n, "bytes") + + if *buf.pos >= len(buf.buf) { + return nil, io.EOF + } + res := buf.buf[*buf.pos : *buf.pos+n] + *buf.pos = *buf.pos + n + fmt.Println("->", hex.EncodeToString(res)) + return res, nil +} + +func (buf Buffer) readByte() (byte, error) { + fmt.Println("reading byte") + + b, err := buf.readBytes(1) + if err != nil { + return 0, err + } + fmt.Println("->", hex.EncodeToString(b)) + return b[0], nil +} + +func (buf Buffer) readVarUint() (uint64, error) { + fmt.Println("reading varuint") + + var value uint64 = 0 + var shift uint64 = 0 + + for { + b, err := buf.readByte() + if err != nil { + return 0, err + } + value |= (uint64(b) & 0b01111111) << shift + shift += 7 + if b&0b10000000 == 0 { + break + } + } + + fmt.Println("->", value, "(num)") + return value, nil +} + +func (buf Buffer) readVarBytes() ([]byte, error) { + fmt.Println("reading varbytes") + + v, err := buf.readVarUint() + if err != nil { + return nil, err + } + + b, err := buf.readBytes(int(v)) + if err != nil { + return nil, err + } + + fmt.Println("->", hex.EncodeToString(b)) + return b, nil +} diff --git a/verifier.go b/verifier.go new file mode 100644 index 0000000..d9d8f6a --- /dev/null +++ b/verifier.go @@ -0,0 +1,11 @@ +package opentimestamps + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +type Bitcoin interface { + GetBlockHash(height int64) (*chainhash.Hash, error) + GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) +}