Skip to content

Commit edae5a6

Browse files
authored
Merge pull request #1839 from lightninglabs/wip/1833-extend-rpc-supply-leaves-with-block-data
Add block header support to supply RPCs and related integration tests
2 parents c2b0a37 + 770e2e2 commit edae5a6

File tree

12 files changed

+921
-404
lines changed

12 files changed

+921
-404
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@
179179
configured with public read access. This matches the behavior of the
180180
existing FetchSupplyCommit RPC endpoint.
181181

182+
- [PR#1839](https://github.com/lightninglabs/taproot-assets/pull/1839) The
183+
`FetchSupplyLeaves` and `FetchSupplyCommit` RPC endpoints now
184+
include a new `block_headers` field. This field is a map from block
185+
height to a `SupplyLeafBlockHeader` message, which provides the block
186+
header timestamp (in seconds since the Unix epoch) and the 32-byte
187+
block header hash. This allows clients to obtain block timing and hash
188+
information directly from the RPC response without performing separate
189+
blockchain queries.
190+
182191
## tapcli Additions
183192

184193
- [Rename](https://github.com/lightninglabs/taproot-assets/pull/1682) the mint

itest/assertions.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2882,3 +2882,20 @@ func WaitForSupplyCommit(t *testing.T, ctx context.Context,
28822882

28832883
return fetchResp, supplyCommitOutpoint
28842884
}
2885+
2886+
// AssertSupplyLeafBlockHeaders makes sure that the given block header exists
2887+
// in the map of block headers and that its contents matches the expected
2888+
// values.
2889+
func AssertSupplyLeafBlockHeaders(t *testing.T, expectedBlockHeight uint32,
2890+
expectedTimestamp int64, expectedBlockHash chainhash.Hash,
2891+
actualBlockHeaders map[uint32]*unirpc.SupplyLeafBlockHeader) {
2892+
2893+
actualBlockMeta, ok := actualBlockHeaders[expectedBlockHeight]
2894+
require.True(t, ok, "no block header for height %d",
2895+
expectedBlockHeight)
2896+
2897+
require.EqualValues(
2898+
t, fn.ByteSlice(expectedBlockHash), actualBlockMeta.Hash,
2899+
)
2900+
require.EqualValues(t, expectedTimestamp, actualBlockMeta.Timestamp)
2901+
}

itest/supply_commit_test.go

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightninglabs/taproot-assets/fn"
1818
"github.com/lightninglabs/taproot-assets/itest/rpcassert"
1919
"github.com/lightninglabs/taproot-assets/mssmt"
20+
"github.com/lightninglabs/taproot-assets/proof"
2021
"github.com/lightninglabs/taproot-assets/tapgarden"
2122
"github.com/lightninglabs/taproot-assets/taprpc"
2223
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
@@ -1044,7 +1045,9 @@ func testSupplyCommitMintBurn(t *harnessTest) {
10441045
// 4. Calling FetchSupplyLeaves to verify burn leaves are included.
10451046
// 5. Minting another tranche into the same group.
10461047
// 6. Calling FetchSupplyLeaves to verify all leaves are present.
1047-
// 7. Testing inclusion proof generation for various leaf types.
1048+
// 7. Ignoring an asset outpoint from the second mint.
1049+
// 8. Calling FetchSupplyLeaves to verify ignore leaves are included.
1050+
// 9. Testing inclusion proof generation for various leaf types.
10481051
func testFetchSupplyLeaves(t *harnessTest) {
10491052
ctxb := context.Background()
10501053

@@ -1105,6 +1108,30 @@ func testFetchSupplyLeaves(t *harnessTest) {
11051108
"issuance leaf amount mismatch",
11061109
)
11071110

1111+
actualMintEvent := new(supplycommit.NewMintEvent)
1112+
err = actualMintEvent.Decode(bytes.NewReader(issuanceLeaf1.RawLeaf))
1113+
require.NoError(t.t, err)
1114+
1115+
// Compare the issuance leaf proof block details with those given in
1116+
// *unirpc.SupplyLeafBlockHeader message field.
1117+
//
1118+
// Decode the block header from the issuance raw proof.
1119+
var actualBlockHeader wire.BlockHeader
1120+
err = proof.SparseDecode(
1121+
bytes.NewReader(actualMintEvent.IssuanceProof.RawProof),
1122+
proof.BlockHeaderRecord(&actualBlockHeader),
1123+
)
1124+
require.NoError(t.t, err)
1125+
1126+
proofBlockHeight := actualMintEvent.BlockHeight()
1127+
proofBlockTimestamp := actualBlockHeader.Timestamp.Unix()
1128+
proofBlockHash := actualBlockHeader.BlockHash()
1129+
1130+
AssertSupplyLeafBlockHeaders(
1131+
t.t, proofBlockHeight, proofBlockTimestamp, proofBlockHash,
1132+
leavesResp1.BlockHeaders,
1133+
)
1134+
11081135
t.Log("Burning portion of the asset")
11091136
const (
11101137
burnAmt = 1500
@@ -1177,6 +1204,24 @@ func testFetchSupplyLeaves(t *harnessTest) {
11771204
require.EqualValues(t.t, burnAmt, burnLeaf.LeafNode.RootSum,
11781205
"burn leaf amount mismatch")
11791206

1207+
// Compare the burn leaf proof block details with those given in
1208+
// *unirpc.SupplyLeafBlockHeader message field.
1209+
//
1210+
// Decode the burn raw proof.
1211+
actualBurnEvent := new(supplycommit.NewBurnEvent)
1212+
err = actualBurnEvent.Decode(bytes.NewReader(burnLeaf.RawLeaf))
1213+
require.NoError(t.t, err)
1214+
1215+
proofBlockHeight = actualBurnEvent.BurnProof.BlockHeight
1216+
proofBlockTimestamp =
1217+
actualBurnEvent.BurnProof.BlockHeader.Timestamp.Unix()
1218+
proofBlockHash = actualBurnEvent.BurnProof.BlockHeader.BlockHash()
1219+
1220+
AssertSupplyLeafBlockHeaders(
1221+
t.t, proofBlockHeight, proofBlockTimestamp, proofBlockHash,
1222+
leavesResp2.BlockHeaders,
1223+
)
1224+
11801225
t.Log("Minting second tranche into the same asset group")
11811226
secondMintReq := &mintrpc.MintAssetRequest{
11821227
Asset: &mintrpc.MintAsset{
@@ -1216,7 +1261,7 @@ func testFetchSupplyLeaves(t *harnessTest) {
12161261
expectedIssuanceTotal := int64(
12171262
mintReq.Asset.Amount + secondMintReq.Asset.Amount,
12181263
)
1219-
_, _ = WaitForSupplyCommit(
1264+
_, supplyOutpoint = WaitForSupplyCommit(
12201265
t.t, ctxb, t.tapd, groupKeyBytes, fn.Some(supplyOutpoint),
12211266
func(resp *unirpc.FetchSupplyCommitResponse) bool {
12221267
return resp.IssuanceSubtreeRoot != nil &&
@@ -1259,6 +1304,103 @@ func testFetchSupplyLeaves(t *harnessTest) {
12591304
"total issuance amount mismatch",
12601305
)
12611306

1307+
t.Log("Ignoring an asset outpoint from the second mint")
1308+
1309+
// Get the outpoint from the second minted asset to ignore it.
1310+
// We must ignore the entire asset at the outpoint, not just a portion.
1311+
ignoreAmount := rpcSecondAsset[0].Amount
1312+
ignoreAssetOutpoint := taprpc.AssetOutPoint{
1313+
AnchorOutPoint: rpcSecondAsset[0].ChainAnchor.AnchorOutpoint,
1314+
AssetId: rpcSecondAsset[0].AssetGenesis.AssetId,
1315+
ScriptKey: rpcSecondAsset[0].ScriptKey,
1316+
}
1317+
ignoreReq := &unirpc.IgnoreAssetOutPointRequest{
1318+
AssetOutPoint: &ignoreAssetOutpoint,
1319+
Amount: ignoreAmount,
1320+
}
1321+
1322+
respIgnore, err := t.tapd.IgnoreAssetOutPoint(ctxb, ignoreReq)
1323+
require.NoError(t.t, err)
1324+
require.NotNil(t.t, respIgnore)
1325+
require.EqualValues(t.t, ignoreAmount, respIgnore.Leaf.RootSum)
1326+
1327+
t.Log("Updating supply commitment after ignoring asset outpoint")
1328+
UpdateAndMineSupplyCommit(
1329+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
1330+
groupKeyBytes, 1,
1331+
)
1332+
1333+
t.Log("Wait for the supply commitment to include the ignored outpoint.")
1334+
_, _ = WaitForSupplyCommit(
1335+
t.t, ctxb, t.tapd, groupKeyBytes, fn.Some(supplyOutpoint),
1336+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
1337+
if resp.IgnoreSubtreeRoot == nil {
1338+
return false
1339+
}
1340+
1341+
return resp.IgnoreSubtreeRoot.RootNode.RootSum ==
1342+
int64(ignoreAmount)
1343+
},
1344+
)
1345+
1346+
t.Log("Fetching supply leaves after ignoring asset outpoint")
1347+
req = unirpc.FetchSupplyLeavesRequest{
1348+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
1349+
GroupKeyBytes: groupKeyBytes,
1350+
},
1351+
}
1352+
leavesResp4, err := t.tapd.FetchSupplyLeaves(ctxb, &req)
1353+
require.NoError(t.t, err)
1354+
require.NotNil(t.t, leavesResp4)
1355+
1356+
// Verify we have two issuance leaves, one burn leaf, and one ignore
1357+
// leaf.
1358+
require.Len(
1359+
t.t, leavesResp4.IssuanceLeaves, 2,
1360+
"expected 2 issuance leaves after ignore",
1361+
)
1362+
require.Len(
1363+
t.t, leavesResp4.BurnLeaves, 1,
1364+
"expected 1 burn leaf after ignore",
1365+
)
1366+
require.Len(
1367+
t.t, leavesResp4.IgnoreLeaves, 1,
1368+
"expected 1 ignore leaf after ignore",
1369+
)
1370+
1371+
// Verify the ignore leaf amount.
1372+
ignoreLeaf := leavesResp4.IgnoreLeaves[0]
1373+
require.EqualValues(
1374+
t.t, ignoreAmount, ignoreLeaf.LeafNode.RootSum,
1375+
"ignore leaf amount mismatch",
1376+
)
1377+
1378+
// Compare the ignore leaf block data with that given in
1379+
// *unirpc.SupplyLeafBlockHeader message field.
1380+
//
1381+
// TODO(ffranr): Extend t.lndHarness.Miner() with
1382+
// GetBlockHeaderByHeight and use here.
1383+
//
1384+
// We can't retrieve the block header from the miner based on block
1385+
// height, so we fetch it given the block hash in the field we are
1386+
// testing. This is not ideal, but it covers timestamp and height
1387+
// verification.
1388+
expectedBlockHeight := ignoreLeaf.BlockHeight
1389+
ignoreLeafBlockHeader :=
1390+
leavesResp4.BlockHeaders[expectedBlockHeight]
1391+
1392+
ignoreBlockHash, err := chainhash.NewHash(ignoreLeafBlockHeader.Hash)
1393+
require.NoError(t.t, err)
1394+
1395+
ignoreLeafBlock := t.lndHarness.Miner().GetBlock(ignoreBlockHash)
1396+
require.NotNil(t.t, ignoreLeafBlock)
1397+
expectedBlockTimestamp := ignoreLeafBlock.Header.Timestamp.Unix()
1398+
1399+
AssertSupplyLeafBlockHeaders(
1400+
t.t, expectedBlockHeight, expectedBlockTimestamp,
1401+
*ignoreBlockHash, leavesResp4.BlockHeaders,
1402+
)
1403+
12621404
t.Log("Testing inclusion proof generation for supply leaves")
12631405

12641406
// Collect leaf keys for inclusion proof request.

lndservices/chain_bridge.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,50 @@ func (l *LndRpcChainBridge) GetBlockHeader(ctx context.Context,
151151
)
152152
}
153153

154+
// GetBlockHeaderByHeight returns a block header given the block height.
155+
func (l *LndRpcChainBridge) GetBlockHeaderByHeight(ctx context.Context,
156+
blockHeight int64) (*wire.BlockHeader, error) {
157+
158+
// First, we need to resolve the block hash at the given height.
159+
blockHash, err := fn.RetryFuncN(
160+
ctx, l.retryConfig, func() (chainhash.Hash, error) {
161+
var zero chainhash.Hash
162+
163+
blockHash, err := l.lnd.ChainKit.GetBlockHash(
164+
ctx, blockHeight,
165+
)
166+
if err != nil {
167+
return zero, fmt.Errorf(
168+
"unable to retrieve block hash: %w",
169+
err,
170+
)
171+
}
172+
173+
return blockHash, nil
174+
},
175+
)
176+
if err != nil {
177+
return nil, fmt.Errorf("unable to retrieve block hash: %w", err)
178+
}
179+
180+
// Now that we have the block hash, we can fetch the block header.
181+
return fn.RetryFuncN(
182+
ctx, l.retryConfig, func() (*wire.BlockHeader, error) {
183+
header, err := l.lnd.ChainKit.GetBlockHeader(
184+
ctx, blockHash,
185+
)
186+
if err != nil {
187+
return nil, fmt.Errorf(
188+
"unable to retrieve block header: %w",
189+
err,
190+
)
191+
}
192+
193+
return header, nil
194+
},
195+
)
196+
}
197+
154198
// GetBlockHash returns the hash of the block in the best blockchain at the
155199
// given height.
156200
func (l *LndRpcChainBridge) GetBlockHash(ctx context.Context,

rpcserver.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4282,6 +4282,17 @@ func (r *rpcServer) FetchSupplyCommit(ctx context.Context,
42824282
"outstanding supply: %w", err)
42834283
}
42844284

4285+
// Extract block headers for all block heights that have supply leaves.
4286+
// And then marshal them into the RPC format.
4287+
heightHeaderMap, err := supplycommit.ExtractSupplyLeavesBlockHeaders(
4288+
ctx, r.cfg.ChainBridge, commit.Leaves,
4289+
)
4290+
if err != nil {
4291+
return nil, fmt.Errorf("failed to extract block headers for "+
4292+
"supply leaves: %w", err)
4293+
}
4294+
rpcHeightHeaderMap := marshalSupplyLeafBlockHeaders(heightHeaderMap)
4295+
42854296
return &unirpc.FetchSupplyCommitResponse{
42864297
ChainData: chainData,
42874298
TxChainFeesSats: commitBlock.ChainFees,
@@ -4296,6 +4307,8 @@ func (r *rpcServer) FetchSupplyCommit(ctx context.Context,
42964307

42974308
TotalOutstandingSupply: totalOutstandingSupply,
42984309
SpentCommitmentOutpoint: spentCommitmentOutpoint,
4310+
4311+
BlockHeaders: rpcHeightHeaderMap,
42994312
}, nil
43004313
}
43014314

@@ -4339,6 +4352,25 @@ func marshalSupplyLeaves(
43394352
return rpcIssuanceLeaves, rpcBurnLeaves, rpcIgnoreLeaves, nil
43404353
}
43414354

4355+
// marshalSupplyLeafBlockHeaders converts a map of block heights to block
4356+
// headers into a map of block heights to RPC SupplyLeafBlockHeader objects.
4357+
//
4358+
// nolint: lll
4359+
func marshalSupplyLeafBlockHeaders(
4360+
heightHeaderMap map[uint32]wire.BlockHeader) map[uint32]*unirpc.SupplyLeafBlockHeader {
4361+
4362+
rpcHeightHeader := make(map[uint32]*unirpc.SupplyLeafBlockHeader)
4363+
4364+
for height, header := range heightHeaderMap {
4365+
rpcHeightHeader[height] = &unirpc.SupplyLeafBlockHeader{
4366+
Timestamp: header.Timestamp.Unix(),
4367+
Hash: fn.ByteSlice(header.BlockHash()),
4368+
}
4369+
}
4370+
4371+
return rpcHeightHeader
4372+
}
4373+
43424374
// FetchSupplyLeaves fetches the supply leaves for a specific asset group
43434375
// within a specified block height range. The leaves include issuance, burn,
43444376
// and ignore leaves, which represent the supply changes for the asset group.
@@ -4433,13 +4465,25 @@ func (r *rpcServer) FetchSupplyLeaves(ctx context.Context,
44334465
}
44344466
}
44354467

4468+
// Extract block headers for all block heights that have supply leaves.
4469+
// And then marshal them into the RPC format.
4470+
heightHeaderMap, err := supplycommit.ExtractSupplyLeavesBlockHeaders(
4471+
ctx, r.cfg.ChainBridge, resp,
4472+
)
4473+
if err != nil {
4474+
return nil, fmt.Errorf("failed to extract block headers for "+
4475+
"supply leaves: %w", err)
4476+
}
4477+
rpcHeightHeaderMap := marshalSupplyLeafBlockHeaders(heightHeaderMap)
4478+
44364479
return &unirpc.FetchSupplyLeavesResponse{
44374480
IssuanceLeaves: issuanceLeaves,
44384481
BurnLeaves: burnLeaves,
44394482
IgnoreLeaves: ignoreLeaves,
44404483
IssuanceLeafInclusionProofs: issuanceInclusionProofs,
44414484
BurnLeafInclusionProofs: burnInclusionProofs,
44424485
IgnoreLeafInclusionProofs: ignoreInclusionProofs,
4486+
BlockHeaders: rpcHeightHeaderMap,
44434487
}, nil
44444488
}
44454489

tapgarden/interface.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ type ChainBridge interface {
350350
// height.
351351
GetBlockTimestamp(context.Context, uint32) int64
352352

353+
// GetBlockHeaderByHeight returns a block header given the block height.
354+
GetBlockHeaderByHeight(ctx context.Context,
355+
blockHeight int64) (*wire.BlockHeader, error)
356+
353357
// PublishTransaction attempts to publish a new transaction to the
354358
// network.
355359
PublishTransaction(context.Context, *wire.MsgTx, string) error

tapgarden/mock.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,13 @@ func (m *MockChainBridge) GetBlock(ctx context.Context,
688688
return block, nil
689689
}
690690

691+
// GetBlockHeaderByHeight returns a block header given the block height.
692+
func (m *MockChainBridge) GetBlockHeaderByHeight(ctx context.Context,
693+
blockHeight int64) (*wire.BlockHeader, error) {
694+
695+
return &wire.BlockHeader{}, nil
696+
}
697+
691698
// GetBlockHash returns the hash of the block in the best blockchain at the
692699
// given height.
693700
func (m *MockChainBridge) GetBlockHash(ctx context.Context,

0 commit comments

Comments
 (0)