Skip to content

Commit c2b0a37

Browse files
authored
Merge pull request #1829 from lightninglabs/wip/supplycommit/public-rpc-fetchsupplyleaves
taprpc+itest: whitelist `FetchSupplyLeaves` RPC and add integration test
2 parents 614acdc + 8124851 commit c2b0a37

File tree

5 files changed

+367
-2
lines changed

5 files changed

+367
-2
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@
174174
- The `AddrReceives` RPC now supports timestamp filtering with
175175
[new `StartTimestamp` and `EndTimestamp` fields](https://github.com/lightninglabs/taproot-assets/pull/1794).
176176

177+
- The [FetchSupplyLeaves RPC endpoint](https://github.com/lightninglabs/taproot-assets/pull/1829)
178+
is now accessible without authentication when the universe server is
179+
configured with public read access. This matches the behavior of the
180+
existing FetchSupplyCommit RPC endpoint.
181+
177182
## tapcli Additions
178183

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

itest/supply_commit_test.go

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ import (
55
"context"
66
"fmt"
77
"strings"
8+
"testing"
89
"time"
910

1011
"github.com/btcsuite/btcd/btcec/v2"
12+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1113
"github.com/btcsuite/btcd/chaincfg/chainhash"
1214
"github.com/btcsuite/btcd/wire"
1315
taprootassets "github.com/lightninglabs/taproot-assets"
16+
"github.com/lightninglabs/taproot-assets/asset"
1417
"github.com/lightninglabs/taproot-assets/fn"
1518
"github.com/lightninglabs/taproot-assets/itest/rpcassert"
1619
"github.com/lightninglabs/taproot-assets/mssmt"
1720
"github.com/lightninglabs/taproot-assets/tapgarden"
1821
"github.com/lightninglabs/taproot-assets/taprpc"
1922
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
2023
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
24+
"github.com/lightninglabs/taproot-assets/universe"
2125
"github.com/lightninglabs/taproot-assets/universe/supplycommit"
2226
"github.com/stretchr/testify/require"
2327
)
@@ -1032,6 +1036,356 @@ func testSupplyCommitMintBurn(t *harnessTest) {
10321036
t.Log("Supply commit mint and burn test completed successfully")
10331037
}
10341038

1039+
// testFetchSupplyLeaves tests the FetchSupplyLeaves RPC endpoint by:
1040+
//
1041+
// 1. Minting an asset group with supply commitments enabled.
1042+
// 2. Calling FetchSupplyLeaves to verify initial mint leaves.
1043+
// 3. Burning some of the asset and updating the supply commit.
1044+
// 4. Calling FetchSupplyLeaves to verify burn leaves are included.
1045+
// 5. Minting another tranche into the same group.
1046+
// 6. Calling FetchSupplyLeaves to verify all leaves are present.
1047+
// 7. Testing inclusion proof generation for various leaf types.
1048+
func testFetchSupplyLeaves(t *harnessTest) {
1049+
ctxb := context.Background()
1050+
1051+
t.Log("Minting initial asset group with supply commitments enabled")
1052+
mintReq := CopyRequest(issuableAssets[0])
1053+
mintReq.Asset.Amount = 8000
1054+
1055+
rpcFirstAsset, _ := MintAssetWithSupplyCommit(
1056+
t, mintReq, fn.None[btcec.PublicKey](),
1057+
)
1058+
1059+
groupKeyBytes := rpcFirstAsset.AssetGroup.TweakedGroupKey
1060+
require.NotNil(t.t, groupKeyBytes)
1061+
1062+
t.Log("Creating first supply commitment transaction")
1063+
UpdateAndMineSupplyCommit(
1064+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
1065+
groupKeyBytes, 1,
1066+
)
1067+
1068+
t.Log("Waiting for first supply commitment to be mined")
1069+
_, supplyOutpoint := WaitForSupplyCommit(
1070+
t.t, ctxb, t.tapd, groupKeyBytes, fn.None[wire.OutPoint](),
1071+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
1072+
return resp.ChainData.BlockHeight > 0 &&
1073+
len(resp.ChainData.BlockHash) > 0
1074+
},
1075+
)
1076+
1077+
t.Log("Fetching supply leaves after initial mint")
1078+
req := unirpc.FetchSupplyLeavesRequest{
1079+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
1080+
GroupKeyBytes: groupKeyBytes,
1081+
},
1082+
}
1083+
leavesResp1, err := t.tapd.FetchSupplyLeaves(ctxb, &req)
1084+
require.NoError(t.t, err)
1085+
require.NotNil(t.t, leavesResp1)
1086+
1087+
// Verify we have one issuance leaf and no burn/ignore leaves.
1088+
require.Len(
1089+
t.t, leavesResp1.IssuanceLeaves, 1,
1090+
"expected 1 issuance leaf after first mint",
1091+
)
1092+
require.Len(
1093+
t.t, leavesResp1.BurnLeaves, 0,
1094+
"expected 0 burn leaves after first mint",
1095+
)
1096+
require.Len(
1097+
t.t, leavesResp1.IgnoreLeaves, 0,
1098+
"expected 0 ignore leaves after first mint",
1099+
)
1100+
1101+
// Verify the issuance leaf amount.
1102+
issuanceLeaf1 := leavesResp1.IssuanceLeaves[0]
1103+
require.EqualValues(
1104+
t.t, mintReq.Asset.Amount, issuanceLeaf1.LeafNode.RootSum,
1105+
"issuance leaf amount mismatch",
1106+
)
1107+
1108+
t.Log("Burning portion of the asset")
1109+
const (
1110+
burnAmt = 1500
1111+
burnNote = "FetchSupplyLeaves burn test"
1112+
)
1113+
1114+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
1115+
Asset: &taprpc.BurnAssetRequest_AssetId{
1116+
AssetId: rpcFirstAsset.AssetGenesis.AssetId,
1117+
},
1118+
AmountToBurn: burnAmt,
1119+
Note: burnNote,
1120+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
1121+
})
1122+
require.NoError(t.t, err)
1123+
require.NotNil(t.t, burnResp)
1124+
1125+
t.Log("Confirming burn transaction")
1126+
AssertAssetOutboundTransferWithOutputs(
1127+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
1128+
[][]byte{rpcFirstAsset.AssetGenesis.AssetId},
1129+
[]uint64{mintReq.Asset.Amount - burnAmt, burnAmt},
1130+
0, 1, 2, true,
1131+
)
1132+
1133+
t.Log("Updating supply commitment after burn")
1134+
UpdateAndMineSupplyCommit(
1135+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
1136+
groupKeyBytes, 1,
1137+
)
1138+
1139+
// Wait for the supply commitment to include the burn.
1140+
_, supplyOutpoint = WaitForSupplyCommit(
1141+
t.t, ctxb, t.tapd, groupKeyBytes, fn.Some(supplyOutpoint),
1142+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
1143+
if resp.BurnSubtreeRoot == nil {
1144+
return false
1145+
}
1146+
1147+
actualBurnSum := resp.BurnSubtreeRoot.RootNode.RootSum
1148+
return actualBurnSum == int64(burnAmt)
1149+
},
1150+
)
1151+
1152+
t.Log("Fetching supply leaves after burn")
1153+
req = unirpc.FetchSupplyLeavesRequest{
1154+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
1155+
GroupKeyBytes: groupKeyBytes,
1156+
},
1157+
}
1158+
leavesResp2, err := t.tapd.FetchSupplyLeaves(ctxb, &req)
1159+
require.NoError(t.t, err)
1160+
require.NotNil(t.t, leavesResp2)
1161+
1162+
// Verify we have one issuance leaf and one burn leaf.
1163+
require.Len(
1164+
t.t, leavesResp2.IssuanceLeaves, 1,
1165+
"expected 1 issuance leaf after burn",
1166+
)
1167+
require.Len(
1168+
t.t, leavesResp2.BurnLeaves, 1,
1169+
"expected 1 burn leaf after burn",
1170+
)
1171+
require.Len(
1172+
t.t, leavesResp2.IgnoreLeaves, 0,
1173+
"expected 0 ignore leaves after burn",
1174+
)
1175+
1176+
burnLeaf := leavesResp2.BurnLeaves[0]
1177+
require.EqualValues(t.t, burnAmt, burnLeaf.LeafNode.RootSum,
1178+
"burn leaf amount mismatch")
1179+
1180+
t.Log("Minting second tranche into the same asset group")
1181+
secondMintReq := &mintrpc.MintAssetRequest{
1182+
Asset: &mintrpc.MintAsset{
1183+
AssetType: taprpc.AssetType_NORMAL,
1184+
Name: "itestbuxx-fetchsupplyleaves-tranche-2",
1185+
AssetMeta: &taprpc.AssetMeta{
1186+
Data: []byte("second tranche for " +
1187+
"FetchSupplyLeaves test"),
1188+
},
1189+
Amount: 3500,
1190+
AssetVersion: taprpc.AssetVersion_ASSET_VERSION_V1,
1191+
NewGroupedAsset: false,
1192+
GroupedAsset: true,
1193+
GroupKey: groupKeyBytes,
1194+
1195+
EnableSupplyCommitments: true,
1196+
},
1197+
}
1198+
1199+
rpcSecondAsset := MintAssetsConfirmBatch(
1200+
t.t, t.lndHarness.Miner().Client, t.tapd,
1201+
[]*mintrpc.MintAssetRequest{secondMintReq},
1202+
)
1203+
require.Len(t.t, rpcSecondAsset, 1, "expected one minted asset")
1204+
require.EqualValues(
1205+
t.t, groupKeyBytes,
1206+
rpcSecondAsset[0].AssetGroup.TweakedGroupKey,
1207+
)
1208+
1209+
t.Log("Updating supply commitment after second mint")
1210+
UpdateAndMineSupplyCommit(
1211+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
1212+
groupKeyBytes, 1,
1213+
)
1214+
1215+
// Wait for the supply commitment to include both mints.
1216+
expectedIssuanceTotal := int64(
1217+
mintReq.Asset.Amount + secondMintReq.Asset.Amount,
1218+
)
1219+
_, _ = WaitForSupplyCommit(
1220+
t.t, ctxb, t.tapd, groupKeyBytes, fn.Some(supplyOutpoint),
1221+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
1222+
return resp.IssuanceSubtreeRoot != nil &&
1223+
resp.IssuanceSubtreeRoot.RootNode.RootSum ==
1224+
expectedIssuanceTotal
1225+
},
1226+
)
1227+
1228+
t.Log("Fetching supply leaves after second mint")
1229+
req = unirpc.FetchSupplyLeavesRequest{
1230+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
1231+
GroupKeyBytes: groupKeyBytes,
1232+
},
1233+
}
1234+
leavesResp3, err := t.tapd.FetchSupplyLeaves(ctxb, &req)
1235+
require.NoError(t.t, err)
1236+
require.NotNil(t.t, leavesResp3)
1237+
1238+
// Verify we have two issuance leaves and one burn leaf.
1239+
require.Len(
1240+
t.t, leavesResp3.IssuanceLeaves, 2,
1241+
"expected 2 issuance leaves after second mint",
1242+
)
1243+
require.Len(
1244+
t.t, leavesResp3.BurnLeaves, 1,
1245+
"expected 1 burn leaf after second mint",
1246+
)
1247+
require.Len(
1248+
t.t, leavesResp3.IgnoreLeaves, 0,
1249+
"expected 0 ignore leaves after second mint",
1250+
)
1251+
1252+
// Verify the total issuance amount across both leaves.
1253+
totalIssuanceAmount := int64(0)
1254+
for _, leaf := range leavesResp3.IssuanceLeaves {
1255+
totalIssuanceAmount += leaf.LeafNode.RootSum
1256+
}
1257+
require.EqualValues(
1258+
t.t, expectedIssuanceTotal, totalIssuanceAmount,
1259+
"total issuance amount mismatch",
1260+
)
1261+
1262+
t.Log("Testing inclusion proof generation for supply leaves")
1263+
1264+
// Collect leaf keys for inclusion proof request.
1265+
var issuanceLeafKeys [][]byte
1266+
var burnLeafKeys [][]byte
1267+
for _, leaf := range leavesResp3.IssuanceLeaves {
1268+
issuanceLeafKeys = append(
1269+
issuanceLeafKeys,
1270+
unmarshalRPCSupplyLeafKey(t.t, leaf.LeafKey),
1271+
)
1272+
}
1273+
for _, leaf := range leavesResp3.BurnLeaves {
1274+
burnLeafKeys = append(
1275+
burnLeafKeys,
1276+
unmarshalRPCSupplyLeafKey(t.t, leaf.LeafKey),
1277+
)
1278+
}
1279+
1280+
// Request supply leaves with inclusion proofs.
1281+
req = unirpc.FetchSupplyLeavesRequest{
1282+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
1283+
GroupKeyBytes: groupKeyBytes,
1284+
},
1285+
IssuanceLeafKeys: issuanceLeafKeys,
1286+
BurnLeafKeys: burnLeafKeys,
1287+
}
1288+
leavesRespWithProofs, err := t.tapd.FetchSupplyLeaves(ctxb, &req)
1289+
require.NoError(t.t, err)
1290+
require.NotNil(t.t, leavesRespWithProofs)
1291+
1292+
// Verify that inclusion proofs are provided.
1293+
require.Len(
1294+
t.t, leavesRespWithProofs.IssuanceLeafInclusionProofs,
1295+
len(issuanceLeafKeys),
1296+
"expected inclusion proofs for all issuance leaf keys",
1297+
)
1298+
require.Len(
1299+
t.t, leavesRespWithProofs.BurnLeafInclusionProofs,
1300+
len(burnLeafKeys),
1301+
"expected inclusion proofs for all burn leaf keys",
1302+
)
1303+
1304+
t.Log("Verifying inclusion proof validity")
1305+
1306+
// Fetch the current supply commitment to get the subtree roots.
1307+
reqFetchCommit := unirpc.FetchSupplyCommitRequest{
1308+
GroupKey: &unirpc.FetchSupplyCommitRequest_GroupKeyBytes{
1309+
GroupKeyBytes: groupKeyBytes,
1310+
},
1311+
Locator: &unirpc.FetchSupplyCommitRequest_Latest{
1312+
Latest: true,
1313+
},
1314+
}
1315+
fetchResp, err := t.tapd.FetchSupplyCommit(ctxb, &reqFetchCommit)
1316+
require.NoError(t.t, err)
1317+
require.NotNil(t.t, fetchResp)
1318+
1319+
// Verify issuance leaf inclusion proofs.
1320+
inclusionProofs := leavesRespWithProofs.IssuanceLeafInclusionProofs
1321+
for i, proofBytes := range inclusionProofs {
1322+
leafKey := fn.ToArray[[32]byte](issuanceLeafKeys[i])
1323+
leafNode := unmarshalMerkleSumNode(
1324+
leavesRespWithProofs.IssuanceLeaves[i].LeafNode,
1325+
)
1326+
1327+
expectedSubtreeRootHash := fn.ToArray[[32]byte](
1328+
fetchResp.IssuanceSubtreeRoot.RootNode.RootHash,
1329+
)
1330+
1331+
AssertInclusionProof(
1332+
t, expectedSubtreeRootHash, proofBytes,
1333+
leafKey, leafNode,
1334+
)
1335+
}
1336+
1337+
// Verify burn leaf inclusion proofs.
1338+
inclusionProofs = leavesRespWithProofs.BurnLeafInclusionProofs
1339+
for i, proofBytes := range inclusionProofs {
1340+
leafKey := fn.ToArray[[32]byte](burnLeafKeys[i])
1341+
leafNode := unmarshalMerkleSumNode(
1342+
leavesRespWithProofs.BurnLeaves[i].LeafNode,
1343+
)
1344+
1345+
expectedSubtreeRootHash := fn.ToArray[[32]byte](
1346+
fetchResp.BurnSubtreeRoot.RootNode.RootHash,
1347+
)
1348+
1349+
AssertInclusionProof(
1350+
t, expectedSubtreeRootHash, proofBytes,
1351+
leafKey, leafNode,
1352+
)
1353+
}
1354+
}
1355+
1356+
// unmarshalRPCSupplyLeafKey converts a *unirpc.SupplyLeafKey to a byte slice
1357+
// using the same method as the universe key serialization.
1358+
func unmarshalRPCSupplyLeafKey(t *testing.T,
1359+
leafKey *unirpc.SupplyLeafKey) []byte {
1360+
1361+
t.Helper()
1362+
1363+
hash, err := chainhash.NewHashFromStr(leafKey.Outpoint.HashStr)
1364+
require.NoError(t, err)
1365+
1366+
outpoint := wire.OutPoint{
1367+
Hash: *hash,
1368+
Index: uint32(leafKey.Outpoint.Index),
1369+
}
1370+
1371+
scriptKeyPubKey, err := schnorr.ParsePubKey(leafKey.ScriptKey)
1372+
require.NoError(t, err)
1373+
1374+
scriptKey := asset.NewScriptKey(scriptKeyPubKey)
1375+
1376+
assetID := fn.ToArray[[32]byte](leafKey.AssetId)
1377+
assetLeafKey := universe.AssetLeafKey{
1378+
BaseLeafKey: universe.BaseLeafKey{
1379+
OutPoint: outpoint,
1380+
ScriptKey: &scriptKey,
1381+
},
1382+
AssetID: assetID,
1383+
}
1384+
1385+
universeKey := assetLeafKey.UniverseKey()
1386+
return universeKey[:]
1387+
}
1388+
10351389
// testSupplyVerifyPeerNode verifies that a secondary node can sync and fetch
10361390
// multiple supply commitments published by the primary node. It:
10371391
//

0 commit comments

Comments
 (0)