12 Commits

Author SHA1 Message Date
fiatjaf
ba1196a962 File.Human() option to print partial results. 2024-10-27 09:37:34 -03:00
fiatjaf
a0aba28a2a return bitcoin transaction from sequence computation. 2024-10-27 09:37:17 -03:00
fiatjaf
4cb1ec89c0 use stdlib "slices". 2024-02-08 16:36:18 -03:00
fiatjaf
1b0ecd993e UpgradeSequence() instead of seq.Upgrade() and correct behavior. 2023-10-02 19:45:27 -03:00
fiatjaf
57291497e6 Sequence.GetAttestation() 2023-10-02 08:45:39 -03:00
fiatjaf
4f3422212a add license so pkg.go.dev works. 2023-09-30 14:52:54 -03:00
fiatjaf
dba106fc3e a readme. 2023-09-30 14:51:20 -03:00
fiatjaf
bbc37a4a1d rename struct field from .Instructions to .Sequences 2023-09-30 14:31:11 -03:00
fiatjaf
212b1c85b1 change sorting so bitcoin-attested sequences go after their counterparts that are just pending. 2023-09-30 14:12:27 -03:00
fiatjaf
cd3d6ee1a5 fix verifier condition. 2023-09-30 13:27:18 -03:00
fiatjaf
1b3227889e rename to nbd-wtf/opentimestamps. 2023-09-30 13:15:20 -03:00
fiatjaf
402291008e upgrade .SerializeInstructionSequences() to the crazy checkpointing scheme. 2023-09-30 13:14:54 -03:00
11 changed files with 284 additions and 102 deletions

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -2,6 +2,80 @@
Interact with calendar servers, create and verify OTS attestations.
# How to use
Full documentation at https://pkg.go.dev/github.com/nbd-wtf/opentimestamps. See some commented pseudocode below (you probably should not try to run it as it is).
```go
package main
import "github.com/nbd-wtf/opentimestamps"
func main () {
// create a timestamp at a specific calendar server
hash := sha256.Sum256([]byte{1,2,3,4,5,6})
seq, _ := opentimestamps.Stamp(context.Background(), "https://alice.btc.calendar.opentimestamps.org/", hash)
// you can just call UpgradeSequence() to get the upgraded sequence (or an error if not yet available)
upgradedSeq, err := opentimestamps.UpgradeSequence(context.Background(), seq, hash[:])
if err != nil {
fmt.Println("wait more")
}
// a File is a struct that represents the content of an .ots file, which contains the initial digest and any number of sequences
file := File{
Digest: hash,
Sequences: []Sequence{seq},
}
// it can be written to disk
os.WriteFile("file.ots", file.SerializeToFile(), 0644)
// or printed in human-readable format
fmt.Println(file.Human())
// sequences are always composed of a bunch of operation instructions -- these can be, for example, "append", "prepend", "sha256"
fmt.Println(seq[0].Operation.Name) // "append"
fmt.Println(seq[1].Operation.Name) // "sha256"
fmt.Println(seq[2].Operation.Name) // "prepend"
// "prepend" and "append" are "binary", i.e. they take an argument
fmt.Println(hex.EncodeToString(seq[2].Argument)) // "c40fe258f9b828a0b5a7"
// all these instructions can be executed in order, starting from the initial hash
result := seq.Compute(hash) // this is the value we send to the calendar server in order to get the upgraded sequence
finalResult := upgradedSeq.Compute(hash) // this should be the merkle root of a bitcoin block if this sequence is upgraded
// each sequence always ends in an "attestation"
// it can be either a pending attestation, i.e. a reference to a calendar server from which we will upgrade this sequence later
fmt.Println(seq[len(seq)-1].Attestation.CalendarServerURL) // "https://alice.btc.calendar.opentimestamps.org/"
// or it can be a reference to a bitcoin block, the merkle root of which we will check against the result of Compute() for verifying
fmt.Println(upgradedSeq[len(upgradedSeq)-1].Attestation.BitcoinBlockHeight) // 810041
// speaking of verifying, this is how we do it:
// first we need some source of bitcoin blocks,
var bitcoin opentimestamps.Bitcoin
if useLocallyRunningBitcoindNode {
// it can be either a locally running bitcoind node
bitcoin, _ = opentimestamps.NewBitcoindInterface(rpcclient.ConnConfig{
User: "nakamoto",
Pass: "mumbojumbo",
HTTPPostMode: true,
})
} else {
// or an esplora HTTP endpoint
bitcoin = opentimestamps.NewEsploraClient("https://blockstream.info/api")
}
// then we pass that to a sequence
if err := upgradedSeq.Verify(bitcoin, hash); err == nil {
fmt.Println("it works!")
}
}
```
You can also take a look at [`ots`](https://github.com/fiatjaf/ots), a simple CLI to OpenTimestamps which is basically a wrapper over this library.
# License
Public Domain

View File

@@ -9,9 +9,10 @@ import (
"strconv"
"strings"
"slices"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"golang.org/x/exp/slices"
)
func NewEsploraClient(url string) Bitcoin {

5
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/fiatjaf/opentimestamps
module github.com/nbd-wtf/opentimestamps
go 1.21
require (
github.com/btcsuite/btcd v0.23.4
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/crypto v0.13.0
)
require (
@@ -17,6 +17,5 @@ require (
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // 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
)

2
go.sum
View File

@@ -66,8 +66,6 @@ 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=

52
helpers.go Normal file
View File

@@ -0,0 +1,52 @@
package opentimestamps
import (
"strings"
"slices"
)
// CompareInstructions returns negative if a<b, 0 if a=b and positive if a>b.
// It considers an operation smaller than an attestation, a pending attestation smaller than a Bitcoin attestation.
// It orders operations by their tag byte and then by their argument.
func CompareInstructions(a, b Instruction) int {
if a.Operation != nil {
if b.Attestation != nil {
// a is an operation but b is an attestation, a is bigger
return +1
}
if a.Operation == b.Operation {
// if both are the same operation sort by the argument
return slices.Compare(a.Argument, b.Argument)
}
// sort by the operation
if a.Operation.Tag < b.Operation.Tag {
return -1
} else if a.Operation.Tag > b.Operation.Tag {
return 1
} else {
return 0
}
} else if a.Attestation != nil && b.Attestation == nil {
// a is an attestation but b is not, b is bigger
return -1
} else if a.Attestation != nil && b.Attestation != nil {
// both are attestations
if a.Attestation.BitcoinBlockHeight == 0 && b.Attestation.BitcoinBlockHeight == 0 {
// none are bitcoin attestations
return strings.Compare(a.Attestation.CalendarServerURL, b.Attestation.CalendarServerURL)
}
if a.Attestation.BitcoinBlockHeight != 0 && b.Attestation.BitcoinBlockHeight != 0 {
// both are bitcoin attestations
return int(b.Attestation.BitcoinBlockHeight - a.Attestation.BitcoinBlockHeight)
}
// one is bitcoin and the other is not -- compare by bitcoin block,
// but reverse the result since the one with 0 should not be considered bigger
return -1 * int(b.Attestation.BitcoinBlockHeight-a.Attestation.BitcoinBlockHeight)
} else {
// this shouldn't happen
return 0
}
}

164
ots.go
View File

@@ -1,12 +1,14 @@
package opentimestamps
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"slices"
"strings"
"golang.org/x/exp/slices"
"github.com/btcsuite/btcd/wire"
)
/*
@@ -52,12 +54,12 @@ var tags = map[byte]*Operation{
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 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 Timestamp struct {
Digest []byte
Instructions []Sequence
type File struct {
Digest []byte
Sequences []Sequence
}
// a Instruction can be an operation like "append" or "prepend" (this will be the case when .Operation != nil)
@@ -69,53 +71,49 @@ type Instruction struct {
*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 (seq Sequence) Compute(initial []byte) []byte {
func (seq Sequence) GetAttestation() Attestation {
if len(seq) == 0 {
return Attestation{}
}
att := seq[len(seq)-1]
if att.Attestation == nil {
return Attestation{}
}
return *att.Attestation
}
// Compute runs a sequence of operations on top of an initial digest and returns the result, which is often a
// Bitcoin block merkle root. It also tries to identify the point in the sequence in which an actual Bitcoin
// transaction is formed and parse that.
func (seq Sequence) Compute(initial []byte) (merkleRoot []byte, bitcoinTx *wire.MsgTx) {
current := initial
for _, inst := range seq {
for i, inst := range seq {
if inst.Operation == nil {
break
}
// the first time we do a double-sha256 that is likely a bitcoin transaction
if bitcoinTx == nil &&
inst.Operation.Name == "sha256" &&
len(seq) > i+1 && seq[i+1].Operation != nil &&
seq[i+1].Operation.Name == "sha256" {
tx := &wire.MsgTx{}
tx.Deserialize(bytes.NewReader(current))
bitcoinTx = tx
}
current = inst.Operation.Apply(current, inst.Argument)
}
return current
return current, bitcoinTx
}
func (ts Timestamp) GetPendingSequences() []Sequence {
func (ts File) GetPendingSequences() []Sequence {
bitcoin := ts.GetBitcoinAttestedSequences()
results := make([]Sequence, 0, len(ts.Instructions))
for _, seq := range ts.Instructions {
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
@@ -125,7 +123,7 @@ func (ts Timestamp) GetPendingSequences() []Sequence {
continue
}
if slices.EqualFunc(bseq[0:len(cseq)], cseq, func(a, b Instruction) bool { return a.Equal(b) }) {
if slices.EqualFunc(bseq[0:len(cseq)], cseq, func(a, b Instruction) bool { return CompareInstructions(a, b) == 0 }) {
goto thisSequenceIsAlreadyConfirmed
}
}
@@ -141,9 +139,9 @@ func (ts Timestamp) GetPendingSequences() []Sequence {
return results
}
func (ts Timestamp) GetBitcoinAttestedSequences() []Sequence {
results := make([]Sequence, 0, len(ts.Instructions))
for _, seq := range ts.Instructions {
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)
}
@@ -151,20 +149,26 @@ func (ts Timestamp) GetBitcoinAttestedSequences() []Sequence {
return results
}
func (ts Timestamp) Human() string {
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.Instructions {
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 {
@@ -176,7 +180,7 @@ func (ts Timestamp) Human() string {
return strings.Join(strs, "\n")
}
func (ts Timestamp) SerializeToFile() []byte {
func (ts File) SerializeToFile() []byte {
data := make([]byte, 0, 5050)
data = append(data, headerMagic...)
data = appendVarUint(data, 1)
@@ -186,43 +190,77 @@ func (ts Timestamp) SerializeToFile() []byte {
return data
}
func (ts Timestamp) SerializeInstructionSequences() []byte {
data := make([]byte, 0, 5000)
for i, seq := range ts.Instructions {
for _, inst := range seq {
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
data = append(data, inst.Operation.Tag)
result = append(result, inst.Operation.Tag)
if inst.Operation.Binary {
data = appendVarBytes(data, inst.Argument)
result = appendVarBytes(result, inst.Argument)
}
} else if inst.Attestation != nil {
// write attestation record
data = append(data, 0x00)
result = append(result, 0x00)
{
// will use a new buffer for the actual attestation data
// will use a new buffer for the actual attestation result
abuf := make([]byte, 0, 100)
if inst.BitcoinBlockHeight != 0 {
data = append(data, bitcoinMagic...) // this goes in the main data buffer
result = append(result, bitcoinMagic...) // this goes in the main result buffer
abuf = appendVarUint(abuf, inst.BitcoinBlockHeight)
} else if inst.CalendarServerURL != "" {
data = append(data, pendingMagic...) // this goes in the main data buffer
result = append(result, pendingMagic...) // this goes in the main result 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
result = appendVarBytes(result, abuf) // we append that result 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
return result
}
type Attestation struct {

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"io"
"golang.org/x/exp/slices"
"slices"
)
func parseCalendarServerResponse(buf Buffer) (Sequence, error) {
@@ -19,7 +19,7 @@ func parseCalendarServerResponse(buf Buffer) (Sequence, error) {
return seqs[0], nil
}
func parseOTSFile(buf Buffer) (*Timestamp, error) {
func parseOTSFile(buf Buffer) (*File, error) {
// read magic
// read version [1 byte]
// read crypto operation for file digest [1 byte]
@@ -47,14 +47,14 @@ func parseOTSFile(buf Buffer) (*Timestamp, error) {
return nil, fmt.Errorf("failed to read 32-byte digest: %w", err)
}
ts := &Timestamp{
ts := &File{
Digest: digest,
}
if seqs, err := parseTimestamp(buf); err != nil {
return nil, err
} else {
ts.Instructions = seqs
ts.Sequences = seqs
}
return ts, nil
@@ -102,7 +102,7 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
if err != nil {
return nil, fmt.Errorf("failed to read attestation bytes: %w", err)
}
abuf := NewBuffer(this)
abuf := newBuffer(this)
switch {
case slices.Equal(magic, pendingMagic):
@@ -131,17 +131,17 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
ncheckpoints := len(checkpoints)
if ncheckpoints > 0 {
// use this checkpoint as the starting point for the next block
cp := checkpoints[ncheckpoints-1]
chp := checkpoints[ncheckpoints-1]
checkpoints = checkpoints[0 : ncheckpoints-1] // remove this from the stack
seqs = append(seqs, cp)
seqs = append(seqs, chp)
currInstructionsBlock++
}
} else if tag == 0xff {
// pick up a checkpoint to be used later
currentBlock := seqs[currInstructionsBlock]
cp := make([]Instruction, len(currentBlock))
copy(cp, currentBlock)
checkpoints = append(checkpoints, cp)
chp := make([]Instruction, len(currentBlock))
copy(chp, currentBlock)
checkpoints = append(checkpoints, chp)
} else {
// a new operation in this block
inst, err := readInstruction(buf, tag)

View File

@@ -8,7 +8,7 @@ import (
"net/http"
)
func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (*Timestamp, error) {
func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (Sequence, error) {
body := bytes.NewBuffer(digest[:])
req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body)
if err != nil {
@@ -29,24 +29,21 @@ func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (*Timestamp
}
resp.Body.Close()
seq, err := parseCalendarServerResponse(NewBuffer(full))
seq, err := parseCalendarServerResponse(newBuffer(full))
if err != nil {
return nil, fmt.Errorf("failed to parse response from '%s': %w", calendarUrl, err)
}
return &Timestamp{
Digest: digest[:],
Instructions: []Sequence{seq},
}, nil
return seq, nil
}
func ReadFromFile(data []byte) (*Timestamp, error) {
return parseOTSFile(NewBuffer(data))
func ReadFromFile(data []byte) (*File, error) {
return parseOTSFile(newBuffer(data))
}
func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, error) {
result := seq.Compute(initial)
attestation := seq[len(seq)-1]
func UpgradeSequence(ctx context.Context, seq Sequence, initial []byte) (Sequence, error) {
result, _ := seq.Compute(initial)
attestation := seq.GetAttestation()
url := fmt.Sprintf("%s/timestamp/%x", normalizeUrl(attestation.CalendarServerURL), result)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@@ -72,10 +69,14 @@ func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, erro
}
resp.Body.Close()
newSeq, err := parseCalendarServerResponse(NewBuffer(body))
tail, err := parseCalendarServerResponse(newBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to parse response from '%s': %w", attestation.CalendarServerURL, err)
}
newSeq := make(Sequence, len(seq)+len(tail)-1)
copy(newSeq, seq[0:len(seq)-1])
copy(newSeq[len(seq)-1:], tail)
return newSeq, nil
}

View File

@@ -20,7 +20,7 @@ type Buffer struct {
buf []byte
}
func NewBuffer(buf []byte) Buffer {
func newBuffer(buf []byte) Buffer {
zero := 0
return Buffer{&zero, buf}
}
@@ -99,3 +99,13 @@ func appendVarBytes(buf []byte, value []byte) []byte {
buf = append(buf, value...)
return buf
}
func getCommonPrefixIndex(s1 []Instruction, s2 []Instruction) int {
n := min(len(s1), len(s2))
for i := 0; i < n; i++ {
if CompareInstructions(s1[i], s2[i]) != 0 {
return i
}
}
return n
}

View File

@@ -1,11 +1,11 @@
package opentimestamps
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"golang.org/x/exp/slices"
)
type Bitcoin interface {
@@ -13,33 +13,35 @@ type Bitcoin interface {
GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error)
}
func (seq Sequence) Verify(bitcoin Bitcoin, initial []byte) error {
// Verify validates sequence of operations that starts with digest and ends on a Bitcoin attestation against
// an actual Bitcoin block, as given by the provided Bitcoin interface.
func (seq Sequence) Verify(bitcoin Bitcoin, digest []byte) (*wire.MsgTx, error) {
if len(seq) == 0 {
return fmt.Errorf("empty sequence")
return nil, fmt.Errorf("empty sequence")
}
att := seq[len(seq)-1]
if att.Attestation == nil || att.BitcoinBlockHeight == 0 {
return fmt.Errorf("sequence doesn't include a bitcoin attestation")
return nil, fmt.Errorf("sequence doesn't include a bitcoin attestation")
}
blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight))
if err != nil {
return fmt.Errorf("failed to get block %d hash: %w", att.BitcoinBlockHeight, err)
return nil, fmt.Errorf("failed to get block %d hash: %w", att.BitcoinBlockHeight, err)
}
blockHeader, err := bitcoin.GetBlockHeader(blockHash)
if err != nil {
return fmt.Errorf("failed to get block %s header: %w", blockHash, err)
return nil, fmt.Errorf("failed to get block %s header: %w", blockHash, err)
}
merkleRoot := blockHeader.MerkleRoot[:]
result, tx := seq.Compute(digest)
result := seq.Compute(initial)
if slices.Equal(result, merkleRoot) {
return fmt.Errorf("sequence result '%x' doesn't match the bitcoin merkle root for block %d: %x",
if !bytes.Equal(result, merkleRoot) {
return nil, fmt.Errorf("sequence result '%x' doesn't match the bitcoin merkle root for block %d: %x",
result, att.BitcoinBlockHeight, merkleRoot)
}
return nil
return tx, nil
}