Skip to content

Commit

Permalink
feat: block body verification functions (#617)
Browse files Browse the repository at this point in the history
Copy the block body verification functions from
https://github.com/cardano-foundation/cardano-ibc-incubator with minimal
modifications.

Signed-off-by: Chris Gianelloni <[email protected]>
  • Loading branch information
wolf31o2 authored May 13, 2024
1 parent d57ae3d commit c65efd3
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 10 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.21.5
require (
filippo.io/edwards25519 v1.1.0
github.com/blinklabs-io/ouroboros-mock v0.3.0
github.com/cosmos/cosmos-sdk v0.50.6
github.com/fxamacker/cbor/v2 v2.6.0
github.com/jinzhu/copier v0.4.0
github.com/utxorpc/go-codegen v0.5.1
Expand All @@ -15,6 +16,7 @@ require (
)

require (
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
Expand Down
22 changes: 12 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/blinklabs-io/ouroboros-mock v0.3.0 h1:6VRWyhAv0k7nQEgzFpuqhS/n8OM+OAaLN/sCT5K2Hbc=
github.com/blinklabs-io/ouroboros-mock v0.3.0/go.mod h1:0dzTNEk/Kvqa7qYHDy7/Nn3OTt+EOosMknB37FRzI1k=
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/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk=
github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis=
github.com/cosmos/cosmos-sdk v0.50.6 h1:efR3MsvMHX5sxS3be+hOobGk87IzlZbSpsI2x/Vw3hk=
github.com/cosmos/cosmos-sdk v0.50.6/go.mod h1:lVkRY6cdMJ0fG3gp8y4hFrsKZqF4z7y0M2UXFb9Yt40=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
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/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/utxorpc/go-codegen v0.5.1 h1:Xhq3CdWAQEJgi46Naq7epeO4G5EyGApP38yNxMCqCbY=
github.com/utxorpc/go-codegen v0.5.1/go.mod h1:sEfglXN19j3cq0qQvb2NS4IcxUSrK1crYXbsVf11SGM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand All @@ -24,8 +28,6 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
306 changes: 306 additions & 0 deletions ledger/verify_block_body.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// Copyright 2024 Cardano Foundation
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This file is taken almost verbatim (including comments) from
// https://github.com/cardano-foundation/cardano-ibc-incubator

package ledger

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"

"github.com/blinklabs-io/gouroboros/cbor"
"github.com/cosmos/cosmos-sdk/types/bech32"
"golang.org/x/crypto/blake2b"
)

const (
LOVELACE_TOKEN = "lovelace"
BLOCK_BODY_HASH_ZERO_TX_HEX = "29571d16f081709b3c48651860077bebf9340abb3fc7133443c54f1f5a5edcf1"
)

func VerifyBlockBody(data string, blockBodyHash string) (bool, error) {
rawDataBytes, _ := hex.DecodeString(data)
var txsRaw [][]string
_, err := cbor.Decode(rawDataBytes, &txsRaw)
if err != nil {
return false, fmt.Errorf("VerifyBlockBody: txs decode error, %v", err.Error())
}

blockBodyHashByte, decodeBBHError := hex.DecodeString(blockBodyHash)
if decodeBBHError != nil {
return false, fmt.Errorf("VerifyBlockBody: blockBodyHashByte decode error, %v", decodeBBHError.Error())
}

var calculateBlockBodyHashByte [32]byte
if len(txsRaw) == 0 {
zeroTxHash, decodeZTHError := hex.DecodeString(BLOCK_BODY_HASH_ZERO_TX_HEX)
if err != nil {
return false, fmt.Errorf("VerifyBlockBody: zeroTxHash decode error, %v", decodeZTHError.Error())
}
copy(calculateBlockBodyHashByte[:], zeroTxHash[:32])
} else {
calculateBlockBodyHash, calculateHashError := CalculateBlockBodyHash(txsRaw)
if calculateHashError != nil {
return false, fmt.Errorf("VerifyBlockBody: CalculateBlockBodyHash error, %v", calculateHashError.Error())
}
calculateBlockBodyHashByte = blake2b.Sum256(calculateBlockBodyHash)
}
return bytes.Equal(calculateBlockBodyHashByte[:32], blockBodyHashByte), nil
}

func CalculateBlockBodyHash(txsRaw [][]string) ([]byte, error) {
var txSeqBody []cbor.RawMessage
var txSeqWit []cbor.RawMessage
txSeqMetadata := map[uint64]cbor.RawTag{}
txSeqNonValid := []uint{}
for index, tx := range txsRaw {
if len(tx) != 3 {
return nil, fmt.Errorf("CalculateBlockBodyHash: tx len error, tx index %v length = %v", index, len(tx))
}
bodyTmpHex := tx[0]
bodyTmpBytes, bodyTmpBytesError := hex.DecodeString(bodyTmpHex)
if bodyTmpBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: decode body tx[%v] error, %v", index, bodyTmpBytesError.Error())
}
txSeqBody = append(txSeqBody, bodyTmpBytes)

witTmpHex := tx[1]
witTmpBytes, witTmpBytesError := hex.DecodeString(witTmpHex)
if witTmpBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: decode wit tx[%v] error, %v", index, witTmpBytesError.Error())
}

txSeqWit = append(txSeqWit, witTmpBytes)

auxTmpHex := tx[2]
if auxTmpHex != "" {
auxBytes, auxBytesError := hex.DecodeString(auxTmpHex)
if auxBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: decode aux tx[%v] error, %v", index, auxBytesError.Error())
}
// Cardano use Tag 259 for this when encCbor
// Ref: https://github.com/IntersectMBO/cardano-ledger/blob/master/eras/alonzo/impl/src/Cardano/Ledger/Alonzo/TxAuxData.hs#L125
txSeqMetadata[uint64(index)] = cbor.RawTag{
Number: 259, Content: auxBytes,
}
}
// TODO: should form nonValid TX here
}
txSeqBodyBytes, txSeqBodyBytesError := cbor.Encode(txSeqBody)
if txSeqBodyBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: encode txSeqBody error, %v", txSeqBodyBytesError.Error())
}

txSeqBodySum32Bytes := blake2b.Sum256(txSeqBodyBytes)
txSeqBodySumBytes := txSeqBodySum32Bytes[:]

txSeqWitsBytes, txSeqWitsBytesError := cbor.Encode(txSeqWit)
if txSeqWitsBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: encode txSeqWit error, %v", txSeqWitsBytesError.Error())
}
txSeqWitsSum32Bytes := blake2b.Sum256(txSeqWitsBytes)
txSeqWitsSumBytes := txSeqWitsSum32Bytes[:]

txSeqMetadataBytes, txSeqMetadataBytesError := cbor.Encode(txSeqMetadata)
if txSeqMetadataBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: encode txSeqMetadata error, %v", txSeqMetadataBytesError.Error())
}
txSeqMetadataSum32Bytes := blake2b.Sum256(txSeqMetadataBytes)
txSeqMetadataSumBytes := txSeqMetadataSum32Bytes[:]

txSeqNonValidBytes, txSeqNonValidBytesError := cbor.Encode(txSeqNonValid)
if txSeqNonValidBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: encode txSeqNonValid error, %v", txSeqNonValidBytesError.Error())
}
txSeqIsValidSum32Bytes := blake2b.Sum256(txSeqNonValidBytes)
txSeqIsValidSumBytes := txSeqIsValidSum32Bytes[:]

var serializeBytes []byte
// Ref: https://github.com/IntersectMBO/cardano-ledger/blob/9cc766a31ad6fb31f88e25a770c902d24fa32499/eras/alonzo/impl/src/Cardano/Ledger/Alonzo/TxSeq.hs#L183
serializeBytes = append(serializeBytes, txSeqBodySumBytes...)
serializeBytes = append(serializeBytes, txSeqWitsSumBytes...)
serializeBytes = append(serializeBytes, txSeqMetadataSumBytes...)
serializeBytes = append(serializeBytes, txSeqIsValidSumBytes...)

return serializeBytes, nil
}

func GetTxBodies(txsRaw [][]string) ([]BabbageTransactionBody, error) {
var bodies []BabbageTransactionBody
for index, tx := range txsRaw {
var tmp BabbageTransactionBody
bodyTmpHex := tx[0]
bodyTmpBytes, bodyTmpBytesError := hex.DecodeString(bodyTmpHex)
if bodyTmpBytesError != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: decode bodyTmpBytesError, index %v, error, %v", index, bodyTmpBytesError.Error())
}
_, err := cbor.Decode(bodyTmpBytes, &tmp)
if err != nil {
return nil, fmt.Errorf("CalculateBlockBodyHash: decode bodyTmpBytes, index %v, error, %v", index, err.Error())
}
bodies = append(bodies, tmp)
}
return bodies, nil
}

func GetBlockOutput(txBodies []BabbageTransactionBody) ([]UTXOOutput, []RegisCert, []DeRegisCert, error) {
var outputs []UTXOOutput
var regisCerts []RegisCert
var deRegisCerts []DeRegisCert
for txIndex, tx := range txBodies {
txHash := tx.Hash()
txOutputs := tx.Outputs()
for outputIndex, txOutput := range txOutputs {
cborDatum := []byte{}
if txOutput.Datum() != nil {
cborDatum = txOutput.Datum().Cbor()
}
cborDatumHex := hex.EncodeToString(cborDatum)
tokens, extractTokensError := ExtractTokens(txOutput)
if extractTokensError != nil {
return nil, nil, nil, fmt.Errorf("GetBlockOutput: ExtractTokens error, tx index %v, outputIndex %v, error, %v", txIndex, outputIndex, extractTokensError.Error())
}
tmpOutput := UTXOOutput{
TxHash: txHash,
OutputIndex: strconv.Itoa(outputIndex),
Tokens: tokens,
DatumHex: cborDatumHex,
}
outputs = append(outputs, tmpOutput)
}

// Ref: https://github.com/IntersectMBO/cardano-ledger/blob/master/eras/babbage/impl/cddl-files/babbage.cddl#L193
// We will only focus on:
// pool_registration = (3, pool_params)
// pool_retirement = (4, pool_keyhash, epoch)
txCertsBytes := tx.Certificates
if txCertsBytes != nil {
var certs []interface{}
_, err := cbor.Decode(txCertsBytes, &certs)
if err != nil {
return nil, nil, nil, fmt.Errorf("GetBlockOutput: decode txCertsBytes, tx index %v, error, %v", txIndex, err.Error())
}
for certIndex, cert := range certs {
certBytes := cert.([]interface{})
// For type like this, Haskell cbor will have an int flag at first byte, to detect which struct to be used
flagByte := certBytes[0].(uint64)
if flagByte == 3 {
poolIdBytes := certBytes[1].([]byte)
vrfKeyHashBytes := certBytes[2].([]byte)
vrfKeyHashHex := hex.EncodeToString(vrfKeyHashBytes)
poolId, poolIdError := PoolIdToBech32(poolIdBytes)
if poolIdError != nil {
return nil, nil, nil, fmt.Errorf("GetBlockOutput: RegisSPO => PoolIdToBech32 , tx index %v, cert index %v, error, %v", txIndex, certIndex, poolIdError.Error())
}
regisCerts = append(regisCerts, RegisCert{
RegisPoolId: poolId,
RegisPoolVrf: vrfKeyHashHex,
TxIndex: txIndex,
})
} else if flagByte == 4 {
// pool_retirement
poolIdBytes := certBytes[1].([]byte)
poolId, poolIdError := PoolIdToBech32(poolIdBytes)
if poolIdError != nil {
return nil, nil, nil, fmt.Errorf("GetBlockOutput: RetireSPO => PoolIdToBech32, tx index %v, cert index %v, error, %v", txIndex, certIndex, poolIdError.Error())
}
retireEpoch := certBytes[2].(uint64)
deRegisCerts = append(deRegisCerts, DeRegisCert{
DeRegisPoolId: poolId,
DeRegisEpoch: strconv.FormatUint(retireEpoch, 10),
TxIndex: txIndex,
})
}
}
}

}

return outputs, regisCerts, deRegisCerts, nil
}

func PoolIdToBech32(data []byte) (string, error) {
pool, err := bech32.ConvertAndEncode("pool", data)
if err != nil {
return "", fmt.Errorf("PoolIdToBech32: ConvertAndEncode error, %v", err.Error())
}
return pool, nil
}

func ExtractTokens(output TransactionOutput) ([]UTXOOutputToken, error) {
var outputTokens []UTXOOutputToken
// append lovelace first
outputTokens = append(outputTokens, UTXOOutputToken{
TokenAssetName: LOVELACE_TOKEN,
TokenValue: strconv.FormatUint(output.Amount(), 10),
})
if output.Assets() != nil {
assetsBytes, assetsBytesError := output.Assets().MarshalJSON()
if assetsBytesError != nil {
return nil, fmt.Errorf("ExtractTokens: MarshalJSON assets error, %v", assetsBytesError.Error())
}
var assets []multiAssetJson[MultiAssetTypeOutput]
err := json.Unmarshal(assetsBytes, &assets)
if err != nil {
return nil, fmt.Errorf("ExtractTokens: json.Unmarshal error, %v", err.Error())
}
for _, asset := range assets {
outputTokens = append(outputTokens, UTXOOutputToken{
TokenAssetName: asset.PolicyId + asset.NameHex,
TokenValue: strconv.FormatUint(asset.Amount, 10),
})
}
}
return outputTokens, nil
}

// These are copied from types.go

type UTXOOutputToken struct {
_ struct{} `cbor:",toarray"`
Flag int
TokenAssetName string
TokenValue string
}

type UTXOOutput struct {
_ struct{} `cbor:",toarray"`
Flag int
TxHash string
OutputIndex string
Tokens []UTXOOutputToken
DatumHex string
}

type RegisCert struct {
_ struct{} `cbor:",toarray"`
Flag int
RegisPoolId string
RegisPoolVrf string
TxIndex int
}

type DeRegisCert struct {
_ struct{} `cbor:",toarray"`
Flag int
DeRegisPoolId string
DeRegisEpoch string
TxIndex int
}

0 comments on commit c65efd3

Please sign in to comment.