diff --git a/esplora.go b/esplora.go index 1180f96..410820b 100644 --- a/esplora.go +++ b/esplora.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "golang.org/x/exp/slices" ) func NewEsploraClient(url string) Bitcoin { @@ -32,16 +33,23 @@ func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) { if err != nil { return nil, err } - if _, err := hex.Decode(hexb, hexb); err != nil || len(hexb) != chainhash.HashSize { + + hash, err := hex.DecodeString(string(hexb)) + if err != nil { return nil, err } + if len(hash) != chainhash.HashSize { + return nil, fmt.Errorf("got block hash (%x) of invalid size (expected %d)", hash, chainhash.HashSize) + } + + slices.Reverse(hash) var chash chainhash.Hash - copy(chash[:], hexb) + copy(chash[:], hash) return &chash, nil } func (e esplora) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) { - resp, err := http.Get(fmt.Sprintf("%s/block/%x/header", e.baseurl, hash)) + resp, err := http.Get(fmt.Sprintf("%s/block/%s/header", e.baseurl, hash.String())) if err != nil { return nil, err } @@ -50,12 +58,14 @@ func (e esplora) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) if err != nil { return nil, err } - if _, err := hex.Decode(hexb, hexb); err != nil { + + headerHash, err := hex.DecodeString(string(hexb)) + if err != nil { return nil, err } header := &wire.BlockHeader{} - if err := header.BtcDecode(bytes.NewBuffer(hexb), 0, 0); err != nil { + if err := header.BtcDecode(bytes.NewBuffer(headerHash), 0, 0); err != nil { return nil, err } diff --git a/ots.go b/ots.go index 7204fdb..c0eb09d 100644 --- a/ots.go +++ b/ots.go @@ -100,6 +100,17 @@ func (a Instruction) Equal(b Instruction) bool { type Sequence []Instruction +func (seq Sequence) Compute(initial []byte) []byte { + current := initial + for _, inst := range seq { + if inst.Operation == nil { + break + } + current = inst.Operation.Apply(current, inst.Argument) + } + return current +} + func (ts Timestamp) GetPendingSequences() []Sequence { bitcoin := ts.GetBitcoinAttestedSequences() @@ -238,14 +249,3 @@ func (att Attestation) Human() string { 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 -} diff --git a/stamp.go b/stamp.go index 0b20a1d..037f5c4 100644 --- a/stamp.go +++ b/stamp.go @@ -45,7 +45,7 @@ func ReadFromFile(data []byte) (*Timestamp, error) { } func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, error) { - result := ComputeSequence(initial, seq) + result := seq.Compute(initial) attestation := seq[len(seq)-1] url := fmt.Sprintf("%s/timestamp/%x", normalizeUrl(attestation.CalendarServerURL), result) diff --git a/verifier.go b/verifier.go index d9d8f6a..f8affe0 100644 --- a/verifier.go +++ b/verifier.go @@ -1,11 +1,45 @@ package opentimestamps import ( + "fmt" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "golang.org/x/exp/slices" ) type Bitcoin interface { GetBlockHash(height int64) (*chainhash.Hash, error) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) } + +func (seq Sequence) Verify(bitcoin Bitcoin, initial []byte) error { + if len(seq) == 0 { + return 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") + } + + blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) + if err != nil { + return 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) + } + + merkleRoot := blockHeader.MerkleRoot[:] + + 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", + result, att.BitcoinBlockHeight, merkleRoot) + } + + return nil +}