simplify codebase.

This commit is contained in:
fiatjaf
2023-09-19 21:18:40 -03:00
parent 5afda80c1a
commit cd227e6986
14 changed files with 111 additions and 527 deletions

View File

@@ -97,10 +97,7 @@ func (b *BitcoinAttestation) encode(ctx *serializationContext) error {
const hashMerkleRootSize = 32
//
func (b *BitcoinAttestation) VerifyAgainstBlockHash(
digest, blockHash []byte,
) error {
func (b *BitcoinAttestation) VerifyAgainstBlockHash(digest, blockHash []byte) error {
if len(digest) != hashMerkleRootSize {
return fmt.Errorf("invalid digest size %d", len(digest))
}

7
bitcoind.go Normal file
View File

@@ -0,0 +1,7 @@
package opentimestamps
import "github.com/btcsuite/btcd/rpcclient"
func NewBitcoindInterface(config rpcclient.ConnConfig) (Bitcoin, error) {
return rpcclient.New(&config, nil)
}

View File

@@ -1,83 +0,0 @@
package client
import (
"fmt"
"net/url"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/rpcclient"
"github.com/fiatjaf/opentimestamps"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const envvarRPCURL = "GOTS_TEST_BITCOIN_RPC"
func newTestBTCConn() (*rpcclient.Client, error) {
val := os.Getenv(envvarRPCURL)
if val == "" {
return nil, fmt.Errorf("envvar %q unset", envvarRPCURL)
}
connData, err := url.Parse(val)
if err != nil {
return nil, fmt.Errorf(
"could not parse %q=%q: %v", envvarRPCURL, val, err,
)
}
host := connData.Host
if connData.User == nil {
return nil, fmt.Errorf("no Userinfo in parsed url")
}
username := connData.User.Username()
password, ok := connData.User.Password()
if !ok {
return nil, fmt.Errorf("no password given in RPC URL")
}
connCfg := &rpcclient.ConnConfig{
Host: host,
User: username,
Pass: password,
HTTPPostMode: true,
DisableTLS: true,
}
return rpcclient.New(connCfg, nil)
}
func TestVerifyHelloWorld(t *testing.T) {
if os.Getenv(envvarRPCURL) == "" {
t.Skipf("envvar %s unset, skipping", envvarRPCURL)
}
// Format RFC3339
expectedTime := "2015-05-28T15:41:18Z"
helloWorld, err := opentimestamps.NewDetachedTimestampFromPath(
"../examples/hello-world.txt.ots",
)
require.NoError(t, err)
ts := helloWorld.Timestamp
btcConn, err := newTestBTCConn()
require.NoError(t, err)
verifier := BitcoinAttestationVerifier{btcConn}
// using BitcoinVerifications()
results := verifier.BitcoinVerifications(ts)
assert.Equal(t, 1, len(results))
result0 := results[0]
require.NoError(t, result0.Error)
assert.Equal(
t, expectedTime, result0.AttestationTime.Format(time.RFC3339),
)
// using Verify()
verifiedTime, err := verifier.Verify(ts)
require.NoError(t, err)
require.NotNil(t, verifiedTime)
assert.Equal(t, expectedTime, verifiedTime.Format(time.RFC3339))
}

View File

@@ -1,26 +0,0 @@
package opentimestamps
import (
"crypto/sha256"
"io"
"os"
)
func CreateDetachedTimestampForFile(
path string, cal *RemoteCalendar,
) (*DetachedTimestamp, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
hasher := sha256.New()
if _, err := io.Copy(hasher, f); err != nil {
return nil, err
}
digest := hasher.Sum([]byte{})
ts, err := cal.Submit(digest)
if err != nil {
return nil, err
}
return NewDetachedTimestamp(*opSHA256, digest, ts)
}

View File

@@ -1,100 +0,0 @@
package opentimestamps
import (
"bytes"
"fmt"
"io"
"os"
)
var fileHeaderMagic = []byte(
"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94",
)
const (
minFileDigestLength = 20
maxFileDigestLength = 32
fileMajorVersion = 1
)
type DetachedTimestamp struct {
HashOp cryptOp
FileHash []byte
Timestamp *Timestamp
}
func (d *DetachedTimestamp) Dump() string {
w := &bytes.Buffer{}
fmt.Fprintf(
w, "File %s hash: %x\n", d.HashOp.name, d.Timestamp.Message,
)
fmt.Fprint(w, d.Timestamp.Dump())
return w.String()
}
func (d *DetachedTimestamp) encode(ctx *serializationContext) error {
if err := ctx.writeBytes(fileHeaderMagic); err != nil {
return err
}
if err := ctx.writeVarUint(fileMajorVersion); err != nil {
return err
}
if err := d.HashOp.encode(ctx); err != nil {
return err
}
if err := ctx.writeBytes(d.FileHash); err != nil {
return err
}
return d.Timestamp.encode(ctx)
}
func (d *DetachedTimestamp) WriteToStream(w io.Writer) error {
return d.encode(&serializationContext{w})
}
func NewDetachedTimestamp(
hashOp cryptOp, fileHash []byte, ts *Timestamp,
) (*DetachedTimestamp, error) {
if len(fileHash) != hashOp.digestLength {
return nil, fmt.Errorf(
"op %v expects %d byte digest, got %d",
hashOp, hashOp.digestLength, len(fileHash),
)
}
return &DetachedTimestamp{hashOp, fileHash, ts}, nil
}
func NewDetachedTimestampFromReader(r io.Reader) (*DetachedTimestamp, error) {
ctx := newDeserializationContext(r)
if err := ctx.assertMagic([]byte(fileHeaderMagic)); err != nil {
return nil, err
}
major, err := ctx.readVarUint()
if err != nil {
return nil, err
}
if major != uint64(fileMajorVersion) {
return nil, fmt.Errorf("unexpected major version %d", major)
}
fileHashOp, err := parseCryptOp(ctx)
if err != nil {
return nil, err
}
fileHash, err := ctx.readBytes(fileHashOp.digestLength)
if err != nil {
return nil, err
}
ts, err := newTimestampFromContext(ctx, fileHash)
if err != nil {
return nil, err
}
return &DetachedTimestamp{*fileHashOp, fileHash, ts}, nil
}
func NewDetachedTimestampFromPath(p string) (*DetachedTimestamp, error) {
f, err := os.Open(p)
if err != nil {
return nil, err
}
return NewDetachedTimestampFromReader(f)
}

View File

@@ -1,110 +0,0 @@
package opentimestamps
import (
"bytes"
"encoding/hex"
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func examplePaths() []string {
matches, err := filepath.Glob("./examples/*ots")
if err != nil {
panic(err)
}
return matches
}
func containsUnknownAttestation(ts *Timestamp) (res bool) {
ts.Walk(func(subTs *Timestamp) {
for _, att := range subTs.Attestations {
if _, ok := att.(unknownAttestation); ok {
res = true
}
}
})
return
}
func TestDecodeHelloWorld(t *testing.T) {
dts, err := NewDetachedTimestampFromPath(
"./examples/hello-world.txt.ots",
)
assert.NoError(t, err)
attCount := 0
checkAttestation := func(ts *Timestamp, att Attestation) {
assert.Equal(t, 0, attCount)
expectedAtt := newBitcoinAttestation()
expectedAtt.Height = 358391
assert.Equal(t, expectedAtt, att)
// If ts.Message is correct, opcode parsing and execution should
// have succeeded.
assert.Equal(t,
"007ee445d23ad061af4a36b809501fab1ac4f2d7e7a739817dd0cbb7ec661b8a",
hex.EncodeToString(ts.Message),
)
attCount += 1
}
dts.Timestamp.Walk(func(ts *Timestamp) {
for _, att := range ts.Attestations {
// this should be called exactly once
checkAttestation(ts, att)
}
})
assert.Equal(t, 1, attCount)
}
func TestDecodeEncodeAll(t *testing.T) {
for _, path := range examplePaths() {
t.Log(path)
dts, err := NewDetachedTimestampFromPath(path)
assert.NoError(t, err, path)
if containsUnknownAttestation(dts.Timestamp) {
t.Logf("skipping encode cycle: unknownAttestation")
continue
}
buf := &bytes.Buffer{}
err = dts.Timestamp.encode(&serializationContext{buf})
if !assert.NoError(t, err, path) {
continue
}
buf = bytes.NewBuffer(buf.Bytes())
ts1, err := NewTimestampFromReader(buf, dts.Timestamp.Message)
if !assert.NoError(t, err, path) {
continue
}
dts1, err := NewDetachedTimestamp(
dts.HashOp, dts.FileHash, ts1,
)
if !assert.NoError(t, err) {
continue
}
dts1Target := &bytes.Buffer{}
err = dts1.WriteToStream(dts1Target)
if !assert.NoError(t, err) {
continue
}
orgBytes, err := ioutil.ReadFile(path)
if !assert.NoError(t, err) {
continue
}
assert.Equal(t, orgBytes, dts1Target.Bytes())
t.Log("encode cycle success")
}
}

63
esplora.go Normal file
View File

@@ -0,0 +1,63 @@
package opentimestamps
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
func NewEsploraClient(url string) Bitcoin {
if strings.HasSuffix(url, "/") {
url = url[0 : len(url)-1]
}
return esplora{url}
}
type esplora struct{ baseurl string }
func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) {
resp, err := http.Get(e.baseurl + "/block-height/" + strconv.FormatInt(height, 10))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hexb, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if _, err := hex.Decode(hexb, hexb); err != nil || len(hexb) != chainhash.HashSize {
return nil, err
}
var chash chainhash.Hash
copy(chash[:], hexb)
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))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hexb, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if _, err := hex.Decode(hexb, hexb); err != nil {
return nil, err
}
header := &wire.BlockHeader{}
if err := header.BtcDecode(bytes.NewBuffer(hexb), 0, 0); err != nil {
return nil, err
}
return header, nil
}

4
go.mod
View File

@@ -4,15 +4,14 @@ 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/crypto v0.13.0
)
require (
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/btcsuite/btcd/btcutil v1.1.0 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
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
@@ -20,6 +19,7 @@ require (
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
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,12 +1,9 @@
package opentimestamps
import (
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
"golang.org/x/crypto/ripemd160"
)
const maxResultLength = 4096
@@ -49,25 +46,6 @@ func msgHexlify(msg []byte) ([]byte, error) {
return []byte(hex.EncodeToString(msg)), nil
}
func msgSHA1(msg []byte) ([]byte, error) {
res := sha1.Sum(msg)
return res[:], nil
}
func msgRIPEMD160(msg []byte) ([]byte, error) {
h := ripemd160.New()
_, err := h.Write(msg)
if err != nil {
return nil, err
}
return h.Sum([]byte{}), nil
}
func msgSHA256(msg []byte) ([]byte, error) {
res := sha256.Sum256(msg)
return res[:], nil
}
type opCode interface {
match(byte) bool
decode(*deserializationContext) (opCode, error)
@@ -110,30 +88,6 @@ func (u *unaryOp) apply(message []byte) ([]byte, error) {
return u.msgOp(message)
}
// Crypto operations
// These are hash ops that define a digest length
type cryptOp struct {
unaryOp
digestLength int
}
func newCryptOp(
tag byte, name string, msgOp unaryMsgOp, digestLength int,
) *cryptOp {
return &cryptOp{
unaryOp: *newUnaryOp(tag, name, msgOp),
digestLength: digestLength,
}
}
func (c *cryptOp) decode(ctx *deserializationContext) (opCode, error) {
u, err := c.unaryOp.decode(ctx)
if err != nil {
return nil, err
}
return &cryptOp{*u.(*unaryOp), c.digestLength}, nil
}
// Binary operations
// We decode an extra varbyte argument and use it in apply()
@@ -179,20 +133,20 @@ func (b *binaryOp) String() string {
return fmt.Sprintf("%s %x", b.name, b.argument)
}
func msgSHA256(msg []byte) ([]byte, error) {
res := sha256.Sum256(msg)
return res[:], nil
}
var (
opAppend = newBinaryOp(0xf0, "APPEND", msgAppend)
opPrepend = newBinaryOp(0xf1, "PREPEND", msgPrepend)
opReverse = newUnaryOp(0xf2, "REVERSE", msgReverse)
opHexlify = newUnaryOp(0xf3, "HEXLIFY", msgHexlify)
opSHA1 = newCryptOp(0x02, "SHA1", msgSHA1, 20)
opRIPEMD160 = newCryptOp(0x03, "RIPEMD160", msgRIPEMD160, 20)
opSHA256 = newCryptOp(0x08, "SHA256", msgSHA256, 32)
opAppend = newBinaryOp(0xf0, "APPEND", msgAppend)
opPrepend = newBinaryOp(0xf1, "PREPEND", msgPrepend)
opReverse = newUnaryOp(0xf2, "REVERSE", msgReverse)
opHexlify = newUnaryOp(0xf3, "HEXLIFY", msgHexlify)
opSHA256 = newUnaryOp(0x08, "SHA256", msgSHA256)
)
var opCodes []opCode = []opCode{
opAppend, opPrepend, opReverse, opHexlify, opSHA1, opRIPEMD160,
opSHA256,
}
var opCodes []opCode = []opCode{opAppend, opPrepend, opReverse, opHexlify, opSHA256}
func parseOp(ctx *deserializationContext, tag byte) (opCode, error) {
for _, op := range opCodes {
@@ -202,19 +156,3 @@ func parseOp(ctx *deserializationContext, tag byte) (opCode, error) {
}
return nil, fmt.Errorf("could not decode tag %02x", tag)
}
func parseCryptOp(ctx *deserializationContext) (*cryptOp, error) {
tag, err := ctx.readByte()
if err != nil {
return nil, err
}
op, err := parseOp(ctx, tag)
if err != nil {
return nil, err
}
if cryptOp, ok := op.(*cryptOp); ok {
return cryptOp, nil
} else {
return nil, fmt.Errorf("expected cryptOp, got %#v", op)
}
}

View File

@@ -1,78 +0,0 @@
package opentimestamps
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMsgAppend(t *testing.T) {
msg := []byte("123")
res, err := msgAppend(msg, []byte("456"))
assert.NoError(t, err)
assert.Equal(t, "123456", string(res))
// make sure changes to input msg don't affect output
msg[0] = byte('0')
assert.Equal(t, "123456", string(res))
}
func TestMsgPrepend(t *testing.T) {
msg := []byte("123")
res, err := msgPrepend(msg, []byte("abc"))
assert.NoError(t, err)
assert.Equal(t, "abc123", string(res))
// make sure changes to input msg don't affect output
msg[0] = byte('0')
assert.Equal(t, "abc123", string(res))
}
func TestMsgReverse(t *testing.T) {
_, err := msgReverse([]byte{})
assert.Error(t, err)
res, err := msgReverse([]byte{1, 2, 3})
assert.NoError(t, err)
assert.Equal(t, []byte{3, 2, 1}, res)
}
func TestMsgHexlify(t *testing.T) {
_, err := msgHexlify([]byte{})
assert.Error(t, err)
res, err := msgHexlify([]byte{1, 2, 3, 0xff})
assert.NoError(t, err)
assert.Equal(t, []byte("010203ff"), res)
}
func TestMsgSHA1(t *testing.T) {
out, err := msgSHA1([]byte{})
assert.NoError(t, err)
assert.Equal(t,
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
hex.EncodeToString(out),
)
}
func TestMsgSHA256(t *testing.T) {
out, err := msgSHA256([]byte{})
assert.NoError(t, err)
assert.Equal(t,
"e3b0c44298fc1c149afbf4c8996fb924"+
"27ae41e4649b934ca495991b7852b855",
hex.EncodeToString(out),
)
}
func TestRIPEMD160(t *testing.T) {
out, err := msgRIPEMD160([]byte{})
assert.Equal(t,
"9c1185a5c5e9fc54612808977ee8f548b2258d31",
hex.EncodeToString(out),
)
out, err = msgRIPEMD160(out)
assert.NoError(t, err)
assert.Equal(t,
"38bbc57e4cbe8b6a1d2c999ef62503e0a6e58109",
hex.EncodeToString(out),
)
}

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/hex"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/http/httputil"
"strings"
@@ -49,7 +49,7 @@ func checkStatusOK(resp *http.Response) error {
return fmt.Errorf("%s (body=nil)", errMsg)
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%s (bodyErr=%v)", errMsg, err)
} else {
@@ -80,8 +80,8 @@ func (c *RemoteCalendar) url(path string) string {
return c.baseURL + path
}
func (c *RemoteCalendar) Submit(digest []byte) (*Timestamp, error) {
body := bytes.NewBuffer(digest)
func (c *RemoteCalendar) Submit(digest [32]byte) (*Timestamp, error) {
body := bytes.NewBuffer(digest[:])
req, err := http.NewRequest("POST", c.url("digest"), body)
if err != nil {
return nil, err
@@ -96,7 +96,7 @@ func (c *RemoteCalendar) Submit(digest []byte) (*Timestamp, error) {
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected 200, got %v", resp.Status)
}
return NewTimestampFromReader(resp.Body, digest)
return NewTimestampFromReader(resp.Body, digest[:])
}
func (c *RemoteCalendar) GetTimestamp(commitment []byte) (*Timestamp, error) {

View File

@@ -27,9 +27,8 @@ func newTestCalendar(url string) *RemoteCalendar {
return cal
}
func newTestDigest(in string) []byte {
hash := sha256.Sum256([]byte(in))
return hash[:]
func newTestDigest(in string) [32]byte {
return sha256.Sum256([]byte(in))
}
func TestRemoteCalendarExample(t *testing.T) {

View File

@@ -114,13 +114,7 @@ func (t *Timestamp) Dump() string {
return t.DumpWithConfig(defaultDumpConfig)
}
func parseTagOrAttestation(
ts *Timestamp,
ctx *deserializationContext,
tag byte,
message []byte,
limit int,
) error {
func parseTagOrAttestation(ts *Timestamp, ctx *deserializationContext, tag byte, message []byte, limit int) error {
if tag == 0x00 {
a, err := ParseAttestation(ctx)
if err != nil {
@@ -147,9 +141,7 @@ func parseTagOrAttestation(
return nil
}
func parse(
ts *Timestamp, ctx *deserializationContext, message []byte, limit int,
) error {
func parse(ts *Timestamp, ctx *deserializationContext, message []byte, limit int) error {
if limit == 0 {
return fmt.Errorf("recursion limit")
}
@@ -176,9 +168,7 @@ func parse(
return parseTagOrAttestation(ts, ctx, tag, message, limit)
}
func newTimestampFromContext(
ctx *deserializationContext, message []byte,
) (*Timestamp, error) {
func newTimestampFromContext(ctx *deserializationContext, message []byte) (*Timestamp, error) {
recursionLimit := 1000
ts := &Timestamp{Message: message}
err := parse(ts, ctx, message, recursionLimit)

View File

@@ -1,40 +1,31 @@
package client
package opentimestamps
import (
"fmt"
"math"
"time"
"github.com/fiatjaf/opentimestamps"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
// A BitcoinAttestationVerifier uses a bitcoin RPC connection to verify bitcoin
// headers.
type BitcoinAttestationVerifier struct {
btcrpcClient *rpcclient.Client
}
func NewBitcoinAttestationVerifier(
c *rpcclient.Client,
) *BitcoinAttestationVerifier {
return &BitcoinAttestationVerifier{c}
type Bitcoin interface {
GetBlockHash(height int64) (*chainhash.Hash, error)
GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error)
}
// VerifyAttestation checks a BitcoinAttestation using a given hash digest. It
// returns the time of the block if the verification succeeds, an error
// otherwise.
func (v *BitcoinAttestationVerifier) VerifyAttestation(
digest []byte, a *opentimestamps.BitcoinAttestation,
) (*time.Time, error) {
func VerifyAttestation(bitcoinInterface Bitcoin, digest []byte, a *BitcoinAttestation) (*time.Time, error) {
if a.Height > math.MaxInt64 {
return nil, fmt.Errorf("illegal block height")
}
blockHash, err := v.btcrpcClient.GetBlockHash(int64(a.Height))
blockHash, err := bitcoinInterface.GetBlockHash(int64(a.Height))
if err != nil {
return nil, err
}
h, err := v.btcrpcClient.GetBlockHeader(blockHash)
h, err := bitcoinInterface.GetBlockHeader(blockHash)
if err != nil {
return nil, err
}
@@ -51,24 +42,22 @@ func (v *BitcoinAttestationVerifier) VerifyAttestation(
// A BitcoinVerification is the result of verifying a BitcoinAttestation
type BitcoinVerification struct {
Timestamp *opentimestamps.Timestamp
Attestation *opentimestamps.BitcoinAttestation
Timestamp *Timestamp
Attestation *BitcoinAttestation
AttestationTime *time.Time
Error error
}
// BitcoinVerifications returns the all bitcoin attestation results for the
// timestamp.
func (v *BitcoinAttestationVerifier) BitcoinVerifications(
t *opentimestamps.Timestamp,
) (res []BitcoinVerification) {
t.Walk(func(ts *opentimestamps.Timestamp) {
func BitcoinVerifications(bitcoinInterface Bitcoin, t *Timestamp) (res []BitcoinVerification) {
t.Walk(func(ts *Timestamp) {
for _, att := range ts.Attestations {
btcAtt, ok := att.(*opentimestamps.BitcoinAttestation)
btcAtt, ok := att.(*BitcoinAttestation)
if !ok {
continue
}
attTime, err := v.VerifyAttestation(ts.Message, btcAtt)
attTime, err := VerifyAttestation(bitcoinInterface, ts.Message, btcAtt)
res = append(res, BitcoinVerification{
Timestamp: ts,
Attestation: btcAtt,
@@ -82,10 +71,8 @@ func (v *BitcoinAttestationVerifier) BitcoinVerifications(
// Verify returns the earliest bitcoin-attested time, or nil if none can be
// found or verified successfully.
func (v *BitcoinAttestationVerifier) Verify(
t *opentimestamps.Timestamp,
) (ret *time.Time, err error) {
res := v.BitcoinVerifications(t)
func Verify(bitcoinInterface Bitcoin, t *Timestamp) (ret *time.Time, err error) {
res := BitcoinVerifications(bitcoinInterface, t)
for _, r := range res {
if r.Error != nil {
err = r.Error